diff --git a/.eslintignore b/.eslintignore index 9464cfa..b0a5c34 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1,2 @@ -webpack.config.js -/dist/ \ No newline at end of file +/node_modules/ +/dist/ diff --git a/.eslintrc.js b/.eslintrc.js index bfa0292..0c15db2 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,22 +1,49 @@ +const utils = require('./build/utils'); + module.exports = { root: true, + + env: { + browser: true, + es6: true, + node: true, + }, + + parser: 'vue-eslint-parser', + parserOptions: { parser: '@babel/eslint-parser', + ecmaVersion: 2020, + extraFileExtensions: ['.vue'], sourceType: 'module', }, + + plugins: ['@kiwiirc', 'jsdoc'], + extends: [ 'plugin:vue/recommended', + 'eslint:recommended', '@vue/airbnb', 'standard', ], - env: { - browser: true, + + settings: { + 'import/resolver': { + alias: { + map: [ + ['@', utils.pathResolve('src')], + ], + extensions: ['.js', '.vue', '.json'], + }, + }, }, - plugins: [ - 'vue', - ], - // add your custom rules here + rules: { + 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'warn', + 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'warn', + + '@kiwiirc/class-name-prefix': 'warn', + 'class-methods-use-this': 0, 'comma-dangle': ['error', { arrays: 'always-multiline', @@ -26,32 +53,51 @@ module.exports = { functions: 'ignore', }], 'import/extensions': 0, - 'import/no-extraneous-dependencies': 0, - 'import/no-unresolved': 0, + 'import/no-cycle': 0, 'import/prefer-default-export': 0, 'indent': ['error', 4], + // 'max-len': ['error', { code: 120 }], + 'max-classes-per-file': 0, 'no-continue': 0, - 'no-control-regex': 0, + 'no-else-return': 0, 'no-multi-assign': 0, 'no-param-reassign': ['error', { props: false }], 'no-plusplus': 0, 'no-prototype-builtins': 0, - 'prefer-promise-reject-errors': 0, - 'quote-props': ['error', 'consistent-as-needed'], + 'no-control-regex': 0, 'object-shorthand': 0, 'operator-linebreak': 0, - 'prefer-const': 0, 'prefer-destructuring': 0, 'prefer-object-spread': 0, + 'prefer-promise-reject-errors': 0, 'prefer-template': 0, + 'quote-props': ['error', 'consistent-as-needed'], 'semi': ['error', 'always'], - 'space-before-function-paren': ['error', 'never'], - 'vue/html-closing-bracket-spacing': 0, + 'space-before-function-paren': ['error', { + anonymous: 'always', + named: 'never', + asyncArrow: 'always', + }], 'vue/html-indent': ['error', 4], + 'vue/max-len': [ + 'error', + { + code: 120, + template: 120, + tabWidth: 4, + comments: 120, + ignoreComments: true, + }, + ], 'vue/max-attributes-per-line': 0, + 'vue/multi-word-component-names': 0, 'vue/multiline-html-element-content-newline': 0, - 'vue/no-mutating-props': 0, + 'vue/no-mutating-props': ['error', { + shallowOnly: true, + }], 'vue/no-v-html': 0, + 'vue/prefer-template': 0, + 'vue/require-default-prop': 0, 'vue/require-prop-types': 0, 'vue/singleline-html-element-content-newline': 0, 'vuejs-accessibility/anchor-has-content': 0, @@ -61,5 +107,24 @@ module.exports = { 'vuejs-accessibility/interactive-supports-focus': 0, 'vuejs-accessibility/label-has-for': 0, 'vuejs-accessibility/mouse-events-have-key-events': 0, + 'vuejs-accessibility/media-has-caption': 0, }, + overrides: [ + { + files: [ + '**/__tests__/*.{j,t}s?(x)', + '**/tests/unit/**/*.spec.{j,t}s?(x)', + ], + env: { + jest: true, + }, + }, + { + files: ['webpack.config.js', 'build/**/*.js'], + rules: { + 'import/no-extraneous-dependencies': 0, + 'no-console': 0, + }, + }, + ], }; diff --git a/.gitignore b/.gitignore index 4d0f2a8..f10b9af 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .DS_Store +.yarn/ dist/ node_modules/ @@ -13,7 +14,6 @@ yarn-error.log* # Editor directories and files .idea -.vscode *.suo *.ntvs* *.njsproj diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 0000000..64d5827 --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,9 @@ +module.exports = { + printWidth: 120, + quoteProps: 'consistent', + semi: true, + singleQuote: true, + trailingComma: 'es5', + tabWidth: 4, + jsdocVerticalAlignment: true, +}; diff --git a/.stylelintrc.js b/.stylelintrc.js index bcc35e6..130e0b8 100644 --- a/.stylelintrc.js +++ b/.stylelintrc.js @@ -1,21 +1,62 @@ module.exports = { - extends: 'stylelint-config-standard', + plugins: ['@stylistic/stylelint-plugin'], + extends: [ + 'stylelint-config-standard', + 'stylelint-config-recommended', + 'stylelint-config-recommended-vue', + 'stylelint-config-standard-scss', + 'stylelint-config-recommended-scss', + 'stylelint-config-recess-order', + ], overrides: [ { - files: ['*.vue', '**/*.vue'], + files: ['**/*.vue', '**/*.html'], customSyntax: 'postcss-html', }, ], rules: { 'alpha-value-notation': null, 'color-function-notation': null, + 'declaration-block-no-redundant-longhand-properties': null, 'declaration-no-important': true, - 'indentation': 4, + 'media-feature-range-notation': null, 'no-descending-specificity': null, - 'no-empty-first-line': null, + 'number-max-precision': null, + 'order/properties-order': null, 'property-no-vendor-prefix': null, + 'scss/at-rule-no-unknown': [ + true, + { + ignoreAtRules: [ + 'each', + 'else', + 'extends', + 'for', + 'function', + 'if', + 'ignores', + 'include', + 'media', + 'mixin', + 'return', + 'use', + + // Font Awesome 4 + 'fa-font-path', + ], + }, + ], + 'scss/double-slash-comment-empty-line-before': null, + 'scss/double-slash-comment-whitespace-inside': null, 'selector-class-pattern': null, 'shorthand-property-no-redundant-values': null, - 'string-quotes': 'single', + + '@stylistic/color-hex-case': 'lower', + '@stylistic/indentation': 4, + // '@stylistic/no-empty-first-line': true, + '@stylistic/number-leading-zero': 'always', + '@stylistic/property-case': 'lower', + '@stylistic/string-quotes': 'single', + '@stylistic/unit-case': 'lower', }, }; diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..db7ae10 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,8 @@ +{ + "recommendations": [ + "vue.volar", + "dbaeumer.vscode-eslint", + "stylelint.vscode-stylelint", + "rvest.vs-code-prettier-eslint" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..eb6a994 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,23 @@ +{ + "files.eol": "\n", + "files.insertFinalNewline": true, + "files.trimFinalNewlines": true, + "files.trimTrailingWhitespace": true, + "stylelint.validate": [ + "vue", + "css", + "less", + "sass", + "scss", + "postcss" + ], + "[javascript]": { + "editor.defaultFormatter": "rvest.vs-code-prettier-eslint" + }, + "[vue]": { + "editor.defaultFormatter": "rvest.vs-code-prettier-eslint" + }, + "[css]": { + "editor.defaultFormatter": "rvest.vs-code-prettier-eslint" + }, +} diff --git a/.yarnrc.yml b/.yarnrc.yml new file mode 100644 index 0000000..3186f3f --- /dev/null +++ b/.yarnrc.yml @@ -0,0 +1 @@ +nodeLinker: node-modules diff --git a/README.md b/README.md index e6f2c5e..16e7084 100644 --- a/README.md +++ b/README.md @@ -42,21 +42,23 @@ Note that the "secure" option enables JWT authentication, but will not work on J Jitsi Meet supports extra configuration to customise its interface and functions. You can configure these via the optional `interfaceConfigOverwrite` and `configOverwrite` config options. The defaults are: -~~~json -"conference": { +~~~json5 +"plugin-conference": { "secure": false, "server": "meet.jit.si", "queries": true, "channels": true, + "closeOnLeave": true, "buttonIcon": "fa-phone", "viewHeight": "40%", - "enabledInChannels": ["*"], + "enabledInChannels": [], + "disabledInChannels": [], "groupInvitesTTL": 30000, "maxParticipantsLength": 60, - "participantsMore": "more...", + // These messages are sent to irc for users not using the plugin + // Users with the plugin will see the translations "inviteText": "{{ nick }} is inviting you to a private call.", "joinText": "{{ nick }} has joined the conference.", - "joinButtonText": "Join now!", "showLink": false, "useLinkShortener": false, "linkShortenerURL": "https://x0.no/api/?{{ link }}", @@ -65,9 +67,10 @@ The defaults are: "SHOW_JITSI_WATERMARK": false, "SHOW_WATERMARK_FOR_GUESTS": false, "TOOLBAR_BUTTONS": [ - "microphone", "camera", "fullscreen", "hangup", - "settings", "videoquality", "filmstrip", "fodeviceselection", - "stats", "shortcuts", + "camera", "closedcaptions", "desktop", "etherpad", "filmstrip", + "fullscreen", "hangup", "help", "highlight", "livestreaming", + "microphone", "noisesuppression", "raisehand", "select-background", + "settings", "shortcuts", "stats", "tileview", "toggle-camera", ], }, "configOverwrite": { @@ -96,7 +99,7 @@ More info about Jitsi's options can be found in these files: * https://github.com/jitsi/jitsi-meet/blob/master/interface_config.js * https://github.com/jitsi/jitsi-meet/blob/master/config.js -You may also choose to hide the conference call icon in either channels or private messages: +You may also choose to hide the conference call icon in either channels or direct messages: ```json { "channels": false, @@ -108,4 +111,4 @@ Running your own conference server allows you to secure your conference rooms. W ## License -[ Licensed under the Apache License, Version 2.0](LICENSE). +[Licensed under the Apache License, Version 2.0](LICENSE). diff --git a/build/commands/build.js b/build/commands/build.js new file mode 100644 index 0000000..2f9eef3 --- /dev/null +++ b/build/commands/build.js @@ -0,0 +1,135 @@ +const webpack = require('webpack'); +const minimist = require('minimist'); +const ora = require('ora'); +const chalk = require('chalk'); +const cliui = require('cliui'); +const { rimraf } = require('rimraf'); + +const utils = require('../utils'); +const webpackConfigFunc = require('../../webpack.config'); + +const argv = minimist(process.argv.slice(2)); +const spinner = ora(); + +(async () => { + const webpackConfig = await webpackConfigFunc({}, argv); + + console.log(); + spinner.text = `Building for ${webpackConfig.mode}...`; + spinner.start(); + + rimraf(utils.pathResolve('dist') + '/*', { glob: true }).then(() => { + webpack(webpackConfig, (wpErr, stats) => { + spinner.stop(); + console.log(); + + if (wpErr) { + console.error(wpErr); + console.log(); + process.exit(1); + } + + if (stats.hasErrors()) { + process.exit(1); + } + + const getCompressedAsset = (asset, type) => { + if (!Array.isArray(asset.related)) { + return undefined; + } + return asset.related.find((relAsset) => relAsset.type === type); + }; + + const isJS = (val) => /\.js$/.test(val); + const isCSS = (val) => /\.css$/.test(val); + const assetSorter = (a, b) => { + if (isJS(a.name) && isCSS(b.name)) { + return -1; + } + if (isCSS(a.name) && isJS(b.name)) { + return 1; + } + return b.size - a.size; + }; + + const data = stats.toJson(); + const files = Object.values(data.assetsByChunkName).flat(); + + const out = [ + // Column headers + ['File', 'Size', 'Gzip', 'Brotli'], + ]; + const totals = { + size: 0, + gzip: 0, + brotli: 0, + }; + + data.assets.sort(assetSorter).forEach((asset) => { + if (!asset.emitted) { + return; + } + + const gzipAsset = getCompressedAsset(asset, 'gzipped'); + const brotliAsset = getCompressedAsset(asset, 'brotliCompressed'); + + totals.size += asset.size; + totals.gzip += gzipAsset ? gzipAsset.size : asset.size; + totals.brotli += brotliAsset ? brotliAsset.size : asset.size; + + if (files.includes(asset.name)) { + out.push([ + asset.name, + utils.formatSize(asset.size), + gzipAsset ? utils.formatSize(gzipAsset.size) : '', + brotliAsset ? utils.formatSize(brotliAsset.size) : '', + ]); + } + }); + + out.push([ + 'Totals (including assets)', + utils.formatSize(totals.size), + utils.formatSize(totals.gzip), + utils.formatSize(totals.brotli), + ]); + + const colWidths = out.reduce((acc, row) => { + row.forEach((col, idx) => { + acc[idx] = Math.max(acc[idx] || 0, col.length + 4); + }); + return acc; + }, []); + + const table = cliui(); + out.forEach((row, rowIdx) => { + table.div( + ...row.map((col, colIdx) => ({ + text: (rowIdx === 0 || (rowIdx === out.length - 1 && colIdx === 0)) + ? chalk.cyan.bold(col) + : col, + width: colWidths[colIdx], + padding: (rowIdx === 0 || rowIdx === out.length - 2) + ? [0, 0, 1, 3] + : [0, 0, 0, 3], + })) + ); + }); + + console.log(table.toString()); + console.log(); + console.log(); + console.log( + chalk.bgGreen.black(' DONE '), + `Build Complete. The ${chalk.cyan('dist')} directory is ready to be deployed` + ); + console.log(); + }); + }).catch((rmErr) => { + spinner.stop(); + console.log(); + console.error(rmErr); + console.log(); + process.exit(1); + }); +})(); diff --git a/build/commands/dev.js b/build/commands/dev.js new file mode 100644 index 0000000..e119256 --- /dev/null +++ b/build/commands/dev.js @@ -0,0 +1,63 @@ +const ora = require('ora'); +const chalk = require('chalk'); +const minimist = require('minimist'); +const portfinder = require('portfinder'); +const webpack = require('webpack'); +const WebpackDevServer = require('webpack-dev-server'); + +const utils = require('../utils'); +const webpackConfigFunc = require('../../webpack.config'); + +const argv = minimist(process.argv.slice(2)); +const spinner = ora(); + +(async () => { + const webpackConfig = await webpackConfigFunc({ WEBPACK_SERVE: true }, argv); + + console.log(); + spinner.text = 'Starting development server...'; + spinner.start(); + + const devServerOptions = webpackConfig.devServer; + + const protocol = devServerOptions.https ? 'https' : 'http'; + const host = devServerOptions.host || '0.0.0.0'; + const port = await portfinder.getPortPromise({ + port: devServerOptions.port || 8080, + host, + }); + + Object.assign(devServerOptions, { host, port }); + + const compiler = webpack(webpackConfig); + const server = new WebpackDevServer(devServerOptions, compiler); + + compiler.hooks.done.tap('dev', (stats) => { + spinner.stop(); + + if (stats.hasErrors()) { + return; + } + + console.log(' App running at:'); + if (host === 'localhost' || host.substring(0, 4) === '127.' || host === '0.0.0.0') { + const hostText = (host === '127.0.0.1') ? 'localhost' : host; + console.log(` - Local: ${chalk.cyan(`${protocol}://${hostText}:${port}`)}`); + + if (host !== '0.0.0.0') { + console.log(` - Network: ${chalk.grey('use "--host" to expose')}`); + } + } + if (host !== 'localhost' && host.substring(0, 4) !== '127.') { + const networkIPs = utils.getNetworkIPs(); + networkIPs.forEach((ip) => { + console.log(` - Network: ${chalk.cyan(`${protocol}://${ip}:${port}`)}`); + }); + } + console.log(); + console.log(' Note that the development build is not optimized.'); + console.log(` To create a production build, run ${chalk.cyan('yarn build')}.`); + }); + + await server.start(); +})(); diff --git a/build/configs/base.js b/build/configs/base.js new file mode 100644 index 0000000..d880fea --- /dev/null +++ b/build/configs/base.js @@ -0,0 +1,102 @@ +const { merge } = require('webpack-merge'); + +const ESLintPlugin = require('eslint-webpack-plugin'); +const ESLintFormatter = require('eslint-formatter-friendly'); +const { VueLoaderPlugin } = require('vue-loader'); +const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin'); +const FriendlyErrorsWebpackPlugin = require('@soda/friendly-errors-webpack-plugin'); + +const utils = require('../utils'); +const pkg = require('../../package.json'); + +const cssConfig = require('./css'); + +module.exports = (env, argv, config) => { + let sourceMap; + if (config.mode === 'development') { + sourceMap = env.WEBPACK_SERVE ? 'eval-source-map' : 'source-map'; + } else if (argv.srcmap) { + sourceMap = 'source-map'; + } + + const baseConfig = merge(config, { + context: process.cwd(), + + entry: { + app: './src/plugin.js', + }, + + devtool: sourceMap, + + output: { + path: utils.pathResolve('dist'), + publicPath: 'auto', + filename: pkg.name.replace(/^kiwiirc-/, '') + '.js', + }, + + resolve: { + alias: { + '@': utils.pathResolve('src'), + }, + extensions: ['.js', '.jsx', '.vue', '.json'], + }, + + externals: { + vue: 'kiwi.Vue', + }, + + performance: { + maxEntrypointSize: 512 * utils.KiB, // 0.5MiB + maxAssetSize: 512 * utils.KiB, // 0.5MiB + }, + + plugins: [ + new ESLintPlugin({ + emitError: true, + emitWarning: true, + extensions: ['.ts', '.tsx', '.js', '.jsx', '.vue'], + formatter: ESLintFormatter, + }), + new VueLoaderPlugin(), + new CaseSensitivePathsPlugin(), + new FriendlyErrorsWebpackPlugin(), + ], + + module: { + rules: [ + { + test: /\.vue$/, + use: [ + { + loader: 'vue-loader', + options: { + transformAssetUrls: { + // Defaults + video: ['src', 'poster'], + source: 'src', + img: 'src', + image: ['xlink:href', 'href'], + use: ['xlink:href', 'href'], + + // Object can be used for svg files + object: 'data', + }, + compilerOptions: { + comments: false, + }, + }, + }, + ], + }, + + { + test: /\.js$/, + exclude: (file) => /node_modules/.test(file), + use: ['babel-loader'], + }, + ], + }, + }); + + return cssConfig(env, argv, baseConfig); +}; diff --git a/build/configs/css.js b/build/configs/css.js new file mode 100644 index 0000000..4ca6adb --- /dev/null +++ b/build/configs/css.js @@ -0,0 +1,86 @@ +const Autoprefixer = require('autoprefixer'); +const { merge } = require('webpack-merge'); + +const cssRules = [ + { + test: /\.css$/, + use: [ + { + loader: 'vue-style-loader', + }, + { + loader: 'css-loader', + options: { + importLoaders: 2, + esModule: false, + }, + }, + { + loader: 'postcss-loader', + options: { + postcssOptions: { + plugins: [Autoprefixer], + }, + }, + }, + ], + }, + { + test: /\.less$/, + use: [ + { + loader: 'vue-style-loader', + }, + { + loader: 'css-loader', + options: { + importLoaders: 2, + esModule: false, + }, + }, + { + loader: 'postcss-loader', + options: { + postcssOptions: { + plugins: [Autoprefixer], + }, + }, + }, + { + loader: 'less-loader', + }, + ], + }, + { + test: /\.s[ac]ss$/, + use: [ + { + loader: 'vue-style-loader', + }, + { + loader: 'css-loader', + options: { + importLoaders: 2, + esModule: false, + }, + }, + { + loader: 'postcss-loader', + options: { + postcssOptions: { + plugins: [Autoprefixer], + }, + }, + }, + { + loader: 'sass-loader', + }, + ], + }, +]; + +module.exports = (env, argv, config) => merge(config, { + module: { + rules: cssRules, + }, +}); diff --git a/build/configs/dev.js b/build/configs/dev.js new file mode 100644 index 0000000..a273d0a --- /dev/null +++ b/build/configs/dev.js @@ -0,0 +1,60 @@ +const { merge } = require('webpack-merge'); +const murmurhash3 = require('murmurhash3js'); +const utils = require('../utils'); +const pkg = require('../../package.json'); + +const baseConfig = require('./base'); + +module.exports = (env, argv, config) => { + const pluginNumber = Math.abs(murmurhash3.x86.hash32(pkg.name)) % 1000; + const portNumber = utils.mapRange(pluginNumber, 0, 999, 9000, 9999); + + const devConfig = { + plugins: [], + + devServer: { + devMiddleware: { + publicPath: 'auto', + }, + open: false, + host: '127.0.0.1', + port: portNumber, + headers: { + 'Access-Control-Allow-Origin': '*', + }, + static: [ + { + directory: utils.pathResolve('static'), + publicPath: 'static', + }, + ], + client: { + logging: 'info', + overlay: { + runtimeErrors: true, + errors: true, + warnings: false, + }, + }, + }, + + infrastructureLogging: { + level: 'warn', + }, + + stats: { + all: false, + loggingDebug: ['sass-loader'], + }, + }; + + if (argv.host) { + devConfig.devServer.host = argv.host === true ? '0.0.0.0' : argv.host; + } + + if (argv.port) { + devConfig.devServer.port = argv.port; + } + + return merge(baseConfig(env, argv, config), devConfig); +}; diff --git a/build/configs/prod.js b/build/configs/prod.js new file mode 100644 index 0000000..52de7e0 --- /dev/null +++ b/build/configs/prod.js @@ -0,0 +1,44 @@ +const CompressionPlugin = require('compression-webpack-plugin'); +const TerserPlugin = require('terser-webpack-plugin'); +const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); +const { merge } = require('webpack-merge'); +const zlib = require('zlib'); + +const baseConfig = require('./base'); +const terserOptions = require('./terser'); + +module.exports = (env, argv, config) => { + const compressionTest = /\.(js|css|js.map|css.map|svg|json|ttf|eot|woff2?)(\?.*)?$/; + + const prodConfig = { + plugins: [ + new CompressionPlugin({ + filename: '[path][base].gz', + algorithm: 'gzip', + test: compressionTest, + compressionOptions: { + level: 9, + }, + threshold: 1024, + }), + new CompressionPlugin({ + filename: '[path][base].br', + algorithm: 'brotliCompress', + test: compressionTest, + compressionOptions: { + params: { + [zlib.constants.BROTLI_PARAM_QUALITY]: 8, + }, + }, + threshold: 1024, + }), + ], + + optimization: { + minimize: true, + minimizer: [new TerserPlugin(terserOptions), new CssMinimizerPlugin()], + }, + }; + + return merge(baseConfig(env, argv, config), prodConfig); +}; diff --git a/build/configs/terser.js b/build/configs/terser.js new file mode 100644 index 0000000..56e0e0b --- /dev/null +++ b/build/configs/terser.js @@ -0,0 +1,22 @@ +module.exports = { + terserOptions: { + compress: { + booleans: true, + conditionals: true, + dead_code: true, + evaluate: true, + if_return: true, + sequences: true, + unused: true, + }, + mangle: { + safari10: true, + }, + format: { + comments: false, + }, + }, + extractComments: { + condition: false, + }, +}; diff --git a/build/plugins/eslint-rules/class-name-prefix.js b/build/plugins/eslint-rules/class-name-prefix.js new file mode 100644 index 0000000..b3421c3 --- /dev/null +++ b/build/plugins/eslint-rules/class-name-prefix.js @@ -0,0 +1,60 @@ +const pkg = require('../../../package.json'); + +const pkgClass = pkg.name.replace(/^kiwiirc-/, ''); +const pkgClassShort = pkgClass.replace(/^plugin-/, 'p-'); + +const allowedPrefixes = [ + 'kiwi-', + 'u-', + + `${pkgClass}-`, +]; + +const specialPrefixes = [ + // IRC colour classes + 'irc-fg-', + 'irc-bg-', + + // Special exception for google recaptcha - welcome screen. + 'g-', +]; + +if (pkgClass !== pkgClassShort) { + allowedPrefixes.push(`${pkgClassShort}-`); +} + +const prefixes = [...allowedPrefixes, ...specialPrefixes]; + +const reportMessage = `Expected class name to start with one of ['${allowedPrefixes.join('\', \'')}'] ({{ class }})`; + +module.exports = { + meta: { + docs: { + description: `html class names must start one of ['${allowedPrefixes.join('\', \'')}']`, + category: 'base', + url: null, + }, + fixable: null, + schema: [], + }, + create: (context) => context.parserServices.defineTemplateBodyVisitor({ + 'VAttribute[key.name=\'class\']': (node) => { + const classes = node.value.value.split(' '); + classes.forEach((c) => { + // Ignore empty and fontawesome classes + if (!c || c === 'fa' || c.startsWith('fa-')) { + return; + } + if (prefixes.every((p) => !c.startsWith(p))) { + context.report({ + node, + message: reportMessage, + data: { + class: c, + }, + }); + } + }); + }, + }), +}; diff --git a/build/plugins/eslint-rules/index.js b/build/plugins/eslint-rules/index.js new file mode 100644 index 0000000..e62c43b --- /dev/null +++ b/build/plugins/eslint-rules/index.js @@ -0,0 +1,7 @@ +/* eslint-disable global-require */ + +module.exports = { + rules: { + 'class-name-prefix': require('./class-name-prefix'), + }, +}; diff --git a/build/plugins/eslint-rules/package.json b/build/plugins/eslint-rules/package.json new file mode 100644 index 0000000..5970afe --- /dev/null +++ b/build/plugins/eslint-rules/package.json @@ -0,0 +1,9 @@ +{ + "name": "@kiwi/eslint-plugin", + "version": "1.0.0", + "private": true, + "main": "index.js", + "peerDependencies": { + "eslint": "^8.31.0" + } +} diff --git a/build/utils.js b/build/utils.js new file mode 100644 index 0000000..e8c4f88 --- /dev/null +++ b/build/utils.js @@ -0,0 +1,63 @@ +const os = require('os'); +const path = require('path'); +const { execSync } = require('child_process'); + +module.exports.pathResolve = (...args) => path.resolve(process.cwd(), ...args); + +module.exports.getCommitHash = () => { + let commitHash = 'unknown'; + try { + commitHash = execSync('git rev-parse --short HEAD').toString().trim(); + const modified = execSync('git diff --quiet HEAD -- || echo true').toString(); + if (modified.trim() === 'true') { + commitHash += '-modified'; + } + } catch { + console.error('Failed to get commit hash'); + } + return commitHash; +}; + +module.exports.getNetworkIPs = () => { + const interfaces = os.networkInterfaces(); + const ips = []; + /* eslint-disable no-restricted-syntax */ + for (const iface of Object.values(interfaces)) { + for (const alias of iface) { + if (alias.family === 'IPv4' && alias.address !== '127.0.0.1' && !alias.internal) { + ips.push(alias.address); + } + } + } + + return ips; +}; + +module.exports.formatSize = (_size) => { + const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB']; + let size = _size; + let pos = 0; + + while (size >= 1024 && pos < units.length) { + size /= 1024; + pos++; + } + + return `${parseFloat(size.toFixed(2))} ${units[pos]}`; +}; + +module.exports.KiB = 1024; +module.exports.MiB = 1048576; +module.exports.GiB = 1073741824; + +/* + * Re-maps a number from one range to another + * http://processing.org/reference/map_.html + */ +module.exports.mapRange = (value, vMin, vMax, dMin, dMax) => { + const vValue = parseFloat(value); + const vRange = vMax - vMin; + const dRange = dMax - dMin; + + return (vValue - vMin) * dRange / vRange + dMin; +}; diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 0000000..eac2f41 --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,5 @@ +{ + "vueCompilerOptions": { + "target": 2.7, + } +} diff --git a/package.json b/package.json index bf26523..d1d1e83 100755 --- a/package.json +++ b/package.json @@ -1,44 +1,83 @@ { "name": "kiwiirc-plugin-conference", - "version": "1.0.0", - "main": "./src/plugin.js", + "version": "1.1.0", "license": "Apache-2.0", "private": true, "scripts": { - "build": "webpack", - "watch": "webpack --watch", - "dev": "webpack-dev-server", - "lint": "npm-run-all lint:*", - "lint:eslint": "eslint --ext .js,.vue ./", - "lint:stylelint": "stylelint \"src/**/*.{vue,htm,html,css,sss,less,scss}\"" - }, - "dependencies": { - "platform": "^1.3.6", - "postcss-html": "^1.5.0" + "dev": "node build/commands/dev.js", + "build": "node build/commands/build.js", + "stats": "node build/commands/build.js --stats", + "lint": "npm-run-all \"lint:*\"", + "lint:js": "eslint --ext .js,.vue ./", + "lint:style": "stylelint --allow-empty-input \"./src/**/*.{vue,html,css,less,scss,sass}\"" }, "devDependencies": { - "@babel/core": "^7.20.12", - "@babel/eslint-parser": "^7.19.1", - "@babel/preset-env": "^7.20.2", - "@vue/eslint-config-airbnb": "^6.0.0", - "babel-loader": "9.1.2", - "css-loader": "^6.7.3", - "eslint": "^7.32.0", - "eslint-config-standard": "^14.1.1", - "eslint-plugin-import": "^2.27.5", - "eslint-plugin-node": "^11.1.0", - "eslint-plugin-promise": "^4.3.1", - "eslint-plugin-standard": "^4.1.0", - "eslint-plugin-vue": "^8.7.1", - "eslint-plugin-vuejs-accessibility": "^1.2.0", + "@babel/core": "^7.26.0", + "@babel/eslint-parser": "^7.25.9", + "@babel/plugin-transform-runtime": "^7.25.9", + "@babel/preset-env": "^7.26.0", + "@kiwiirc/eslint-plugin": "file:./build/plugins/eslint-rules/", + "@soda/friendly-errors-webpack-plugin": "^1.8.1", + "@stylistic/stylelint-plugin": "^3.1.1", + "@vue/eslint-config-airbnb": "^8.0.0", + "autoprefixer": "^10.4.20", + "babel-loader": "^9.2.1", + "case-sensitive-paths-webpack-plugin": "^2.4.0", + "chalk": "^4.1.2", + "cliui": "^8.0.1", + "compression-webpack-plugin": "^11.1.0", + "copy-webpack-plugin": "^12.0.2", + "css-loader": "^7.1.2", + "css-minimizer-webpack-plugin": "^7.0.0", + "eslint": "^8.57.1", + "eslint-config-standard": "^17.1.0", + "eslint-formatter-friendly": "^7.0.0", + "eslint-import-resolver-alias": "^1.1.2", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsdoc": "^46.10.1", + "eslint-plugin-n": "^16.6.2", + "eslint-plugin-promise": "^6.6.0", + "eslint-plugin-vue": "^9.31.0", + "eslint-plugin-vuejs-accessibility": "^2.4.1", + "eslint-webpack-plugin": "^4.2.0", + "html-loader": "^5.1.0", + "jsdoc": "^4.0.4", + "less": "^4.2.0", + "less-loader": "^12.2.0", + "mini-css-extract-plugin": "^2.9.2", + "murmurhash3js": "^3.0.1", "npm-run-all": "^4.1.5", - "style-loader": "^3.3.1", - "stylelint": "^14.16.1", - "stylelint-config-standard": "^29.0.0", - "vue-loader": "^15.10.1", - "vue-template-compiler": "^2.7.14", - "webpack": "^5.75.0", - "webpack-cli": "^5.0.1", - "webpack-dev-server": "^4.11.1" + "ora": "^5.4.1", + "portfinder": "^1.0.32", + "postcss": "^8.4.49", + "postcss-html": "^1.7.0", + "postcss-less": "^6.0.0", + "postcss-loader": "^8.1.1", + "postcss-scss": "^4.0.9", + "prettier": "3.3.3", + "prettier-eslint": "^16.3.0", + "prettier-plugin-jsdoc": "^1.3.0", + "rimraf": "^5.0.10", + "sass": "^1.81.0", + "sass-loader": "^16.0.3", + "style-loader": "^4.0.0", + "stylelint": "^16.10.0", + "stylelint-config-recess-order": "^5.1.1", + "stylelint-config-recommended": "^14.0.1", + "stylelint-config-recommended-scss": "^14.1.0", + "stylelint-config-recommended-vue": "^1.5.0", + "stylelint-config-standard": "^36.0.1", + "stylelint-config-standard-scss": "^13.1.0", + "stylelint-order": "^6.0.4", + "stylelint-webpack-plugin": "5.0.1", + "vue-eslint-parser": "^9.4.3", + "vue-loader": "^15.11.1", + "vue-style-loader": "^4.1.3", + "vue-template-compiler": "^2.7.16", + "webpack": "^5.96.1", + "webpack-bundle-analyzer": "^4.10.2", + "webpack-cli": "^5.1.4", + "webpack-dev-server": "^5.1.0", + "webpack-merge": "^6.0.1" } } diff --git a/src/components/HeaderButton.vue b/src/components/HeaderButton.vue index d228d44..fa9567e 100644 --- a/src/components/HeaderButton.vue +++ b/src/components/HeaderButton.vue @@ -1,17 +1,28 @@ - diff --git a/src/components/JitsiMediaView.vue b/src/components/JitsiMediaView.vue index d176588..1ce8cd4 100644 --- a/src/components/JitsiMediaView.vue +++ b/src/components/JitsiMediaView.vue @@ -1,16 +1,39 @@ - diff --git a/src/components/MessageTemplate.vue b/src/components/MessageTemplate.vue index cc007c4..dfe7d11 100644 --- a/src/components/MessageTemplate.vue +++ b/src/components/MessageTemplate.vue @@ -1,11 +1,11 @@ @@ -13,35 +13,37 @@