diff --git a/cli.js b/cli.js index cffd0de03..4ee2ca021 100755 --- a/cli.js +++ b/cli.js @@ -17,6 +17,7 @@ updateNotifier({ }).notify() const cli = require('yargs') +const { config } = require('./src/config/user') cli .scriptName('aegir') .env('AEGIR') @@ -26,13 +27,18 @@ cli .example('$0 test -t webworker -- --browsers Firefox', 'If the command supports `--` can be used to forward options to the underlying tool.') .example('npm test -- -- --browsers Firefox', 'If `npm test` translates to `aegir test -t browser` and you want to forward options you need to use `-- --` instead.') .epilog('Use `$0 --help` to learn more about each command.') + .middleware((yargs) => { + yargs.config = config() + }) .commandDir('cmds') - .demandCommand(1, 'You need at least one command.') - .option('D', { + .help() + .alias('help', 'h') + .alias('version', 'v') + .option('debug', { desc: 'Show debug output.', type: 'boolean', default: false, - alias: 'debug' + alias: 'd' }) // TODO remove after webpack 5 upgrade .options('node', { @@ -40,10 +46,13 @@ cli describe: 'Flag to control if bundler should inject node globals or built-ins.', default: false }) - .help() - .alias('h', 'help') - .alias('v', 'version') - .group(['help', 'version', 'debug'], 'Global Options:') + .options('ts', { + type: 'boolean', + describe: 'Enable support for Typescript', + default: false + }) + .group(['help', 'version', 'debug', 'node', 'ts'], 'Global Options:') + .demandCommand(1, 'You need at least one command.') .wrap(cli.terminalWidth()) .parserConfiguration({ 'populate--': true }) .recommendCommands() diff --git a/package.json b/package.json index ed048892c..41dfaefa5 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,8 @@ "@babel/plugin-transform-regenerator": "^7.10.1", "@babel/plugin-transform-runtime": "^7.10.1", "@babel/preset-env": "^7.10.2", + "@babel/preset-typescript": "^7.10.1", + "@babel/register": "^7.10.1", "@babel/runtime": "^7.10.2", "@commitlint/cli": "^8.3.5", "@commitlint/config-conventional": "^8.3.4", @@ -55,6 +57,8 @@ "@commitlint/travis-cli": "^8.3.5", "@electron/get": "^1.10.0", "@polka/send-type": "^0.5.2", + "@typescript-eslint/eslint-plugin": "^3.3.0", + "@typescript-eslint/parser": "^3.3.0", "babel-loader": "^8.0.5", "buffer": "^5.6.0", "bytes": "^3.1.0", @@ -67,6 +71,7 @@ "conventional-changelog": "^3.1.18", "conventional-github-releaser": "^3.1.3", "cors": "^2.8.5", + "cosmiconfig": "^6.0.0", "dependency-check": "^4.1.0", "dirty-chai": "^2.0.1", "documentation": "^13.0.1", @@ -114,6 +119,7 @@ "semver": "^7.3.2", "simple-git": "^2.7.0", "terser-webpack-plugin": "^3.0.5", + "typescript": "^3.9.5", "update-notifier": "^4.0.0", "webpack": "^4.43.0", "webpack-bundle-analyzer": "^3.7.0", diff --git a/src/build/index.js b/src/build/index.js index cd34a875c..2608295ca 100644 --- a/src/build/index.js +++ b/src/build/index.js @@ -33,13 +33,25 @@ module.exports = async (argv) => { env: { NODE_ENV: process.env.NODE_ENV || 'production', AEGIR_BUILD_ANALYZE: argv.bundlesize, - AEGIR_NODE: argv.node + AEGIR_NODE: argv.node, + AEGIR_TS: argv.ts }, localDir: path.join(__dirname, '../..'), preferLocal: true, stdio: 'inherit' }) + if (argv.ts) { + await execa('tsc', [ + '--outDir', './dist/src', + '--declaration' + ], { + localDir: path.join(__dirname, '../..'), + preferLocal: true, + stdio: 'inherit' + }) + } + if (argv.bundlesize) { const stats = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'dist/stats.json'))) const gzip = await gzipSize(path.join(stats.outputPath, stats.assets[0].name)) diff --git a/src/config/babelrc.js b/src/config/babelrc.js index fceb8a3cc..65a73d48b 100644 --- a/src/config/babelrc.js +++ b/src/config/babelrc.js @@ -6,8 +6,8 @@ module.exports = function (opts = {}) { const isEnvDevelopment = env === 'development' const isEnvProduction = env === 'production' const isEnvTest = env === 'test' + const isTSEnable = process.env.AEGIR_TS === 'true' const targets = { browsers: pkg.browserslist || browserslist } - if (!isEnvDevelopment && !isEnvProduction && !isEnvTest) { throw new Error( 'Using `babel-preset-env` requires that you specify `NODE_ENV` or ' + @@ -29,9 +29,16 @@ module.exports = function (opts = {}) { bugfixes: true, targets } + ], + isTSEnable && [ + '@babel/preset-typescript', + { + allowNamespaces: true + } ] ].filter(Boolean), plugins: [ + '@babel/plugin-proposal-class-properties', [ require('@babel/plugin-transform-runtime').default, { diff --git a/src/config/eslintrc-ts.js b/src/config/eslintrc-ts.js new file mode 100644 index 000000000..f068824da --- /dev/null +++ b/src/config/eslintrc-ts.js @@ -0,0 +1,13 @@ +'use strict' +const { fromAegir } = require('../utils') +module.exports = { + parser: '@typescript-eslint/parser', + parserOptions: { + sourceType: 'module' + }, + plugins: ['@typescript-eslint'], + extends: [ + fromAegir('src/config/eslintrc.js'), + 'plugin:@typescript-eslint/recommended' + ] +} diff --git a/src/config/eslintrc.js b/src/config/eslintrc.js index 33c6d4909..4d514c8d6 100644 --- a/src/config/eslintrc.js +++ b/src/config/eslintrc.js @@ -6,7 +6,8 @@ module.exports = { sourceType: 'script' }, globals: { - self: true + self: true, + mocha: true }, plugins: [ 'no-only-tests' diff --git a/src/config/karma-entry.js b/src/config/karma-entry.js index 700382f16..5525b5ae2 100644 --- a/src/config/karma-entry.js +++ b/src/config/karma-entry.js @@ -1,6 +1,11 @@ 'use strict' /* eslint-disable */ -const testsContext = require.context(TEST_DIR, true, /\.spec\.js$/) +let testsContext +if (TS_ENABLED) { + testsContext = require.context(TEST_DIR, true, /\.spec\.ts$/) +} else { + testsContext = require.context(TEST_DIR, true, /\.spec\.js$/) +} if (TEST_BROWSER_JS) { require(TEST_BROWSER_JS) diff --git a/src/config/karma.conf.js b/src/config/karma.conf.js index debe539a7..8b3348bf3 100644 --- a/src/config/karma.conf.js +++ b/src/config/karma.conf.js @@ -5,15 +5,16 @@ const webpack = require('webpack') const webpackConfig = require('./webpack.config') const { fromRoot, hasFile } = require('../utils') const userConfig = require('./user')() - +const isTSEnable = process.env.AEGIR_TS === 'true' const isWebworker = process.env.AEGIR_RUNNER === 'webworker' // Env to pass in the bundle with DefinePlugin const env = { + TS_ENABLED: process.env.AEGIR_TS, 'process.env': JSON.stringify(process.env), TEST_DIR: JSON.stringify(fromRoot('test')), - TEST_BROWSER_JS: hasFile('test', 'browser.js') - ? JSON.stringify(fromRoot('test', 'browser.js')) + TEST_BROWSER_JS: hasFile('test', isTSEnable ? 'browser.ts' : 'browser.js') + ? JSON.stringify(fromRoot('test', isTSEnable ? 'browser.ts' : 'browser.js')) : JSON.stringify('') } @@ -25,7 +26,27 @@ const karmaWebpackConfig = merge.strategy({ plugins: 'replace' })(webpackConfig( }, plugins: [ new webpack.DefinePlugin(env) - ] + ], + module: { + rules: [ + { + oneOf: [ + { + test: /\.(js|ts)$/, + include: fromRoot('test'), + use: { + loader: require.resolve('babel-loader'), + options: { + presets: [require('./babelrc')()], + babelrc: false, + cacheDirectory: true + } + } + } + ] + } + ] + } }) const karmaConfig = (config, argv) => { diff --git a/src/config/register.js b/src/config/register.js new file mode 100644 index 000000000..9ca2a01fb --- /dev/null +++ b/src/config/register.js @@ -0,0 +1,7 @@ +'use strict' +const { fromAegir } = require('./../utils') + +require('@babel/register')({ + extensions: ['.ts'], + presets: [fromAegir('src/config/babelrc.js')] +}) diff --git a/src/config/user.js b/src/config/user.js index 9abddc80c..92164382b 100644 --- a/src/config/user.js +++ b/src/config/user.js @@ -1,5 +1,6 @@ 'use strict' +const { cosmiconfigSync } = require('cosmiconfig') const merge = require('merge-options') const utils = require('../utils') @@ -49,4 +50,31 @@ function userConfig () { return user } +const config = () => { + let cosmiconfig + try { + const configExplorer = cosmiconfigSync('aegir', { + searchPlaces: ['package.json', '.aegir.js'] + }) + const { config } = configExplorer.search() + cosmiconfig = config || {} + } catch (err) { + cosmiconfig = {} + } + const conf = merge({ + webpack: {}, + karma: {}, + hooks: {}, + entry: utils.fromRoot('src', 'index.js'), + bundlesize: { + path: './dist/index.min.js', + maxSize: '100kB' + } + }, + cosmiconfig) + + return conf +} + module.exports = userConfig +userConfig.config = config diff --git a/src/config/webpack.config.js b/src/config/webpack.config.js index 58e110c1b..e24002b78 100644 --- a/src/config/webpack.config.js +++ b/src/config/webpack.config.js @@ -8,6 +8,7 @@ const TerserPlugin = require('terser-webpack-plugin') const { fromRoot, pkg, paths, getLibraryName } = require('../utils') const userConfig = require('./user')() const isProduction = process.env.NODE_ENV === 'production' +const isTSEnable = process.env.AEGIR_TS === 'true' const base = (env, argv) => { const filename = [ @@ -21,7 +22,7 @@ const base = (env, argv) => { return { bail: Boolean(isProduction), mode: isProduction ? 'production' : 'development', - entry: [userConfig.entry], + entry: [isTSEnable ? fromRoot('src', 'index.ts') : fromRoot('src', 'index.js')], output: { path: fromRoot(paths.dist), filename: filename, @@ -35,7 +36,7 @@ const base = (env, argv) => { { oneOf: [ { - test: /\.js$/, + test: /\.(js|ts)$/, include: fromRoot(paths.src), use: { loader: require.resolve('babel-loader'), @@ -64,6 +65,7 @@ const base = (env, argv) => { ] }, resolve: { + extensions: ['.wasm', '.mjs', '.js', '.json', '.ts', '.d.ts'], alias: { '@babel/runtime': path.dirname( require.resolve('@babel/runtime/package.json') diff --git a/src/lint.js b/src/lint.js index d1c84ca55..d1906d693 100644 --- a/src/lint.js +++ b/src/lint.js @@ -7,15 +7,16 @@ const userConfig = require('./config/user') const formatter = CLIEngine.getFormatter() const FILES = [ - '*.js', + '*.{js,ts}', 'bin/**', - 'config/**/*.js', - 'test/**/*.js', - 'src/**/*.js', - 'tasks/**/*.js', - 'benchmarks/**/*.js', - 'utils/**/*.js', - '!**/node_modules/**' + 'config/**/*.{js,ts}', + 'test/**/*.{js,ts}', + 'src/**/*.{js,ts}', + 'tasks/**/*.{js,ts}', + 'benchmarks/**/*.{js,ts}', + 'utils/**/*.{js,ts}', + '!**/node_modules/**', + '!**/*.d.ts' ] function checkDependencyVersions () { @@ -82,7 +83,7 @@ function checkDependencyVersions () { function runLinter (opts = {}) { const cli = new CLIEngine({ useEslintrc: true, - baseConfig: require('./config/eslintrc.js'), + baseConfig: opts.ts ? require('./config/eslintrc-ts.js') : require('./config/eslintrc.js'), fix: opts.fix }) diff --git a/src/test/browser.js b/src/test/browser.js index 0b9557d5c..ae28f642c 100644 --- a/src/test/browser.js +++ b/src/test/browser.js @@ -39,6 +39,7 @@ module.exports = (argv, execaOptions) => { NODE_ENV: process.env.NODE_ENV || 'test', AEGIR_RUNNER: argv.webworker ? 'webworker' : 'browser', AEGIR_NODE: argv.node, + AEGIR_TS: argv.ts, IS_WEBPACK_BUILD: true, ...hook.env }, diff --git a/src/test/electron.js b/src/test/electron.js index 4a069afb3..ad59406a9 100644 --- a/src/test/electron.js +++ b/src/test/electron.js @@ -1,19 +1,20 @@ 'use strict' const path = require('path') const execa = require('execa') -const { hook, getElectron } = require('../utils') +const { hook, getElectron, fromAegir } = require('../utils') module.exports = (argv) => { const input = argv._.slice(1) const forwardOptions = argv['--'] ? argv['--'] : [] const watch = argv.watch ? ['--watch'] : [] - const files = argv.files.length ? [...argv.files] : ['test/**/*.spec.js'] + const files = argv.files.length ? [...argv.files] : ['test/**/*.spec.{js,ts}'] const verbose = argv.verbose ? ['--log-level', 'debug'] : ['--log-level', 'error'] const grep = argv.grep ? ['--grep', argv.grep] : [] const progress = argv.progress ? ['--reporter=progress'] : [] const bail = argv.bail ? ['--bail', argv.bail] : [] const timeout = argv.timeout ? ['--timeout', argv.bail] : [] const renderer = argv.renderer ? ['--renderer'] : [] + const ts = argv.ts ? ['--require', fromAegir('src/config/register.js')] : [] return hook('browser', 'pre')(argv.userConfig) .then((hook = {}) => Promise.all([hook, getElectron()])) @@ -27,6 +28,7 @@ module.exports = (argv) => { ...progress, ...bail, ...timeout, + ...ts, ['--colors'], ['--full-trace'], ...renderer, @@ -39,6 +41,7 @@ module.exports = (argv) => { NODE_ENV: process.env.NODE_ENV || 'test', AEGIR_RUNNER: argv.renderer ? 'electron-renderer' : 'electron-main', ELECTRON_PATH: electronPath, + AEGIR_TS: argv.ts, ...hook.env } }) diff --git a/src/test/node.js b/src/test/node.js index 187c349fb..9be577e29 100644 --- a/src/test/node.js +++ b/src/test/node.js @@ -2,7 +2,7 @@ const execa = require('execa') const path = require('path') -const { hook } = require('../utils') +const { hook, fromAegir } = require('../utils') const merge = require('merge-options') const DEFAULT_TIMEOUT = global.DEFAULT_TIMEOUT || 5 * 1000 @@ -13,7 +13,8 @@ function testNode (ctx, execaOptions) { let exec = 'mocha' const env = { NODE_ENV: 'test', - AEGIR_RUNNER: 'node' + AEGIR_RUNNER: 'node', + AEGIR_TS: ctx.ts } const timeout = ctx.timeout || DEFAULT_TIMEOUT @@ -25,8 +26,8 @@ function testNode (ctx, execaOptions) { ].filter(Boolean) let files = [ - 'test/node.js', - 'test/**/*.spec.js' + 'test/node.{js,ts}', + 'test/**/*.spec.{js,ts}' ] if (ctx.colors) { @@ -59,6 +60,10 @@ function testNode (ctx, execaOptions) { args.push('--bail') } + if (ctx.ts) { + args.push(...['--require', fromAegir('src/config/register.js')]) + } + const postHook = hook('node', 'post') const preHook = hook('node', 'pre') @@ -80,7 +85,10 @@ function testNode (ctx, execaOptions) { args.concat(files.map((p) => path.normalize(p))), merge( { - env: { ...env, ...hook.env }, + env: { + ...env, + ...hook.env + }, preferLocal: true, localDir: path.join(__dirname, '../..'), stdio: 'inherit'