diff --git a/.travis.yml b/.travis.yml index 026dd861b..8a480e51b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,4 @@ language: node_js node_js: - 6.0 -script: cd packages/kyt-core && npm install && cd ../kyt-cli && npm install && cd ../kyt-utils && npm install && cd ../.. && npm run lint && npm install && npm test && npm run e2e +script: npm install && cd packages/kyt-core && npm install && cd ../kyt-cli && npm install && cd ../kyt-utils && npm install && cd ../.. && npm run lint && npm test && npm run e2e diff --git a/e2e_tests/tests/cli.test.js b/e2e_tests/tests/cli.test.js index 71942aa5e..997b1c9b5 100644 --- a/e2e_tests/tests/cli.test.js +++ b/e2e_tests/tests/cli.test.js @@ -54,6 +54,7 @@ describe('KYT CLI', () => { expect(setupArr.includes('👍 Created .stylelintrc.json file')).toBe(true); expect(setupArr.includes('👍 Created kyt.config.js file')).toBe(true); expect(setupArr.includes('👍 Created .editorconfig file')).toBe(true); + expect(setupArr.includes('👍 Created .babelrc')).toBe(true); expect(setupArr.includes('👍 Created .gitignore file')).toBe(true); expect(setupArr.includes('👍 Created src directory')).toBe(true); }); @@ -61,6 +62,7 @@ describe('KYT CLI', () => { expect(shell.test('-d', 'src')).toBe(true); expect(shell.test('-f', 'kyt.config.js')).toBe(true); expect(shell.test('-f', '.editorconfig')).toBe(true); + expect(shell.test('-f', '.babelrc')).toBe(true); expect(shell.test('-f', '.eslintrc.json')).toBe(true); expect(shell.test('-f', '.stylelintrc.json')).toBe(true); expect(shell.test('-f', 'prototype.js')).toBe(true); diff --git a/package.json b/package.json index 1391e3583..fd7c1f83f 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "test-watch": "jest --watch", "test-coverage": "jest --coverage", "e2e": "jest --config ./e2e_tests/jest.config.json --verbose --no-cache", - "lint": "packages/kyt-core/node_modules/.bin/eslint --config packages/kyt-core/.eslintrc.json --ignore-pattern **/node_modules --ignore-pattern packages/starter-kyts ./" + "lint": "packages/kyt-core/node_modules/.bin/eslint --config packages/kyt-core/.eslintrc.json --ignore-pattern **/node_modules --ignore-pattern packages/babel-presets --ignore-pattern packages/starter-kyts ./" }, "repository": { "type": "git", diff --git a/packages/babel-presets/babel-preset-kyt-core/.eslintrc.json b/packages/babel-presets/babel-preset-kyt-core/.eslintrc.json new file mode 100644 index 000000000..92629674b --- /dev/null +++ b/packages/babel-presets/babel-preset-kyt-core/.eslintrc.json @@ -0,0 +1,5 @@ +{ + "rules": { + "no-var": 0 + } +} diff --git a/packages/babel-presets/babel-preset-kyt-core/lib/index.js b/packages/babel-presets/babel-preset-kyt-core/lib/index.js new file mode 100644 index 000000000..8113a1080 --- /dev/null +++ b/packages/babel-presets/babel-preset-kyt-core/lib/index.js @@ -0,0 +1,22 @@ +var babelPresetLatest = require('babel-preset-latest'); +var babelTransformRuntime = require('babel-plugin-transform-runtime'); +var babelTransformModules = require('babel-plugin-transform-es2015-modules-commonjs'); + +module.exports = function(context, opts) { + opts = opts || {}; + return { + // modules are handled by webpack, don't transform them + presets: [[babelPresetLatest, { modules: false }]], + + // provide the ability to opt into babel-plugin-transform-runtime inclusion + plugins: [opts.includeRuntime === true && babelTransformRuntime].filter(Boolean), + + env: { + test: { + plugins: [ + [babelTransformModules, { loose: true }], + ], + }, + }, + }; +}; diff --git a/packages/babel-presets/babel-preset-kyt-core/package.json b/packages/babel-presets/babel-preset-kyt-core/package.json new file mode 100644 index 000000000..d5190e289 --- /dev/null +++ b/packages/babel-presets/babel-preset-kyt-core/package.json @@ -0,0 +1,15 @@ +{ + "name": "babel-preset-kyt-core", + "version": "0.1.0-alpha.1", + "description": "an opinionated babel preset, best used with kyt", + "main": "lib/index.js", + "author": "NYTimes", + "license": "Apache-2.0", + "dependencies": { + "babel-plugin-transform-es2015-modules-commonjs": "6.16.0", + "babel-plugin-transform-runtime": "6.15.0", + "babel-preset-latest": "6.16.0" + }, + "keywords": ["babel", "babel-preset", "kyt"], + "files": ["lib"] +} diff --git a/packages/babel-presets/babel-preset-kyt-react/.eslintrc.json b/packages/babel-presets/babel-preset-kyt-react/.eslintrc.json new file mode 100644 index 000000000..92629674b --- /dev/null +++ b/packages/babel-presets/babel-preset-kyt-react/.eslintrc.json @@ -0,0 +1,5 @@ +{ + "rules": { + "no-var": 0 + } +} diff --git a/packages/babel-presets/babel-preset-kyt-react/lib/index.js b/packages/babel-presets/babel-preset-kyt-react/lib/index.js new file mode 100644 index 000000000..389fa7e74 --- /dev/null +++ b/packages/babel-presets/babel-preset-kyt-react/lib/index.js @@ -0,0 +1,31 @@ +var babelPresetReact = require('babel-preset-react'); +var reactRemovePropTypes = require('babel-plugin-transform-react-remove-prop-types'); +var reactTransformConstant = require('babel-plugin-transform-react-constant-elements'); +var reactTransformInline = require('babel-plugin-transform-react-inline-elements'); +var reactTransformJsxSource = require('babel-plugin-transform-react-jsx-source'); +var babelPresetKytCore = require('babel-preset-kyt-core'); + +module.exports = function(context, opts) { + opts = opts || {}; + return { + presets: [ + babelPresetReact, + // pass options through to core preset + [babelPresetKytCore, opts.coreOptions || {}], + ], + env: { + development: { + plugins: [ + reactTransformJsxSource, + ], + }, + production: { + plugins: [ + reactRemovePropTypes, + reactTransformConstant, + reactTransformInline, + ], + }, + }, + }; +}; diff --git a/packages/babel-presets/babel-preset-kyt-react/package.json b/packages/babel-presets/babel-preset-kyt-react/package.json new file mode 100644 index 000000000..aac1e9f60 --- /dev/null +++ b/packages/babel-presets/babel-preset-kyt-react/package.json @@ -0,0 +1,25 @@ +{ + "name": "babel-preset-kyt-react", + "version": "0.1.0-alpha.1", + "description": "an opinionated babel preset for react apps, best used with kyt", + "main": "lib/index.js", + "author": "NYTimes", + "license": "Apache-2.0", + "dependencies": { + "babel-plugin-transform-react-constant-elements": "6.9.1", + "babel-plugin-transform-react-inline-elements": "^6.8.0", + "babel-plugin-transform-react-jsx-source": "6.9.0", + "babel-plugin-transform-react-remove-prop-types": "0.2.10", + "babel-preset-kyt-core": "0.1.0-alpha.1", + "babel-preset-react": "6.16.0" + }, + "keywords": [ + "babel", + "babel-preset", + "kyt", + "react" + ], + "files": [ + "lib" + ] +} diff --git a/packages/kyt-cli/cli/actions/setup.js b/packages/kyt-cli/cli/actions/setup.js index c33778932..61e43bea9 100644 --- a/packages/kyt-cli/cli/actions/setup.js +++ b/packages/kyt-cli/cli/actions/setup.js @@ -34,6 +34,7 @@ module.exports = (flags, args) => { userKytConfigPath, userNodeModulesPath, userPackageJSONPath, + userBabelrcPath, } = require('kyt-utils/paths')(); // eslint-disable-line const date = Date.now(); @@ -282,6 +283,17 @@ module.exports = (flags, args) => { logger.task('Created .editorconfig file'); }; + const createBabelrc = () => { + // back up existing .babelrc, if it exists + if (shell.test('-f', userBabelrcPath)) { + const mvTo = path.join(userRootPath, `.babelrc-${date}.bak`); + shell.mv(userBabelrcPath, mvTo); + logger.info(`Backed up current .babelrc to ${mvTo}`); + } + shell.cp(`${tmpDir}/.babelrc`, userBabelrcPath); + logger.task('Created .babelrc'); + }; + // Copies the starter kyt kyt.config.js // to the user's base directory. const createKytConfig = () => { @@ -389,11 +401,16 @@ module.exports = (flags, args) => { logger.log(error); bailProcess(); } + if (!args.repository) { + // temporary - get the right version of the starter-kyts + shell.exec('cd .kyt-tmp && git checkout babelrc'); + } // eslint-disable-next-line global-require,import/no-dynamic-require tempPackageJSON = require(`${tmpDir}/package.json`); updateUserPackageJSON(false); installUserDependencies(); createESLintFile(); + createBabelrc(); createStylelintFile(); createEditorconfigLink(); createKytConfig(); diff --git a/packages/kyt-core/.babelrc b/packages/kyt-core/.babelrc deleted file mode 100644 index c36776bb9..000000000 --- a/packages/kyt-core/.babelrc +++ /dev/null @@ -1,19 +0,0 @@ -{ - "presets": ["react", ["latest", { "modules": false }]], - "env": { - "development": { - "plugins": ["transform-react-jsx-source"] - }, - "production": { - "plugins": [ - "transform-react-remove-prop-types", - "transform-react-constant-elements" - ] - }, - "test": { - "plugins": [ - ["transform-es2015-modules-commonjs", { "loose": true }] - ] - } - } -} diff --git a/packages/kyt-core/.eslintignore b/packages/kyt-core/.eslintignore index 4ebc8aea5..55e476f05 100644 --- a/packages/kyt-core/.eslintignore +++ b/packages/kyt-core/.eslintignore @@ -1 +1,2 @@ coverage +**/node_modules diff --git a/packages/kyt-core/cli/actions/__tests__/proto.test.js b/packages/kyt-core/cli/actions/__tests__/proto.test.js index 7a951f428..4959485dd 100644 --- a/packages/kyt-core/cli/actions/__tests__/proto.test.js +++ b/packages/kyt-core/cli/actions/__tests__/proto.test.js @@ -6,7 +6,7 @@ jest.mock('kyt-utils/logger'); jest.mock('shelljs'); const listen = jest.fn(); -jest.setMock('webpack-dev-server', () => ({ listen })); +jest.setMock('webpack-dev-server', jest.fn(() => ({ listen }))); const config = { modifyWebpackConfig: jest.fn(c => c), diff --git a/packages/kyt-core/config/__tests__/babel.test.js b/packages/kyt-core/config/__tests__/babel.test.js deleted file mode 100644 index 8cbe62e45..000000000 --- a/packages/kyt-core/config/__tests__/babel.test.js +++ /dev/null @@ -1,65 +0,0 @@ -const pkgJson = require('../../package.json'); - -const thisResolved = require.resolve(__filename); -const BABEL_PREFIX = 'babel-plugin-'; - -const getBabelDep = () => { - let dep; - Object.keys(pkgJson.dependencies).some((key) => { - if (key.indexOf(BABEL_PREFIX) === 0) { - dep = key; - return true; - } - return false; - }); - return dep; -}; - -const babelDep = getBabelDep(); -const babelDepResolved = require.resolve(babelDep); -const babelDepNoPrefix = babelDep.replace(BABEL_PREFIX, ''); - -const presets = [[__filename]]; -const plugins = [__filename, babelDep, babelDepNoPrefix]; - -const presetsResolved = [[thisResolved]]; -const pluginsResolved = [thisResolved, babelDepResolved, babelDepResolved]; - -const stubBabelrc = { - stub: true, - env: { - development: { - plugins, - presets, - }, - }, -}; - -jest.setMock('fs', { - readFileSync: () => JSON.stringify(stubBabelrc), -}); -jest.setMock('path', { - resolve: a => a, -}); - -const babel = require('../babel'); - -describe('babel', () => { - it('sets flags', () => { - const babelrc = babel(); - expect(babelrc.babelrc).toBe(false); - expect(babelrc.cacheDirectory).toBe(false); - }); - - it('adds RHL when given the option', () => { - const babelrc = babel({ reactHotLoader: true }); - const devPlugins = babelrc.env.development.plugins; - expect(devPlugins[devPlugins.length - 1]).toMatch(/react-hot-loader\/babel\.js$/); - }); - - it('resolves plugins and presets', () => { - const babelrc = babel(); - expect(babelrc.env.development.plugins).toEqual(pluginsResolved); - expect(babelrc.env.development.presets).toEqual(presetsResolved); - }); -}); diff --git a/packages/kyt-core/config/__tests__/jest.test.js b/packages/kyt-core/config/__tests__/jest.test.js index 653eab226..e4cb3bcee 100644 --- a/packages/kyt-core/config/__tests__/jest.test.js +++ b/packages/kyt-core/config/__tests__/jest.test.js @@ -1,15 +1,16 @@ -const jest = require('../jest'); +// can't call this `jest` because that's a global in tests +const jestConfig = require('../jest'); -it('jest() returns a jest config', () => { +it('jestConfig() returns a jest config', () => { const rootDir = 'rootDir'; - const jestConfig = jest(rootDir); + const config = jestConfig(rootDir); - expect(typeof jestConfig).toBe('object'); - expect(jestConfig.moduleNameMapper).toBeDefined(); - expect(jestConfig.scriptPreprocessor).toBeDefined(); - expect(jestConfig.testPathIgnorePatterns).toBeDefined(); - expect(jestConfig.testEnvironment).toBeDefined(); - expect(jestConfig.testRegex).toBeDefined(); - expect(jestConfig.collectCoverageFrom).toBeDefined(); - expect(jestConfig.rootDir).toBe(rootDir); + expect(typeof config).toBe('object'); + expect(config.moduleNameMapper).toBeDefined(); + expect(config.scriptPreprocessor).toBeDefined(); + expect(config.testPathIgnorePatterns).toBeDefined(); + expect(config.testEnvironment).toBeDefined(); + expect(config.testRegex).toBeDefined(); + expect(config.collectCoverageFrom).toBeDefined(); + expect(config.rootDir).toBe(rootDir); }); diff --git a/packages/kyt-core/config/__tests__/webpack.test.js b/packages/kyt-core/config/__tests__/webpack.test.js index f073ec053..ce008f11b 100644 --- a/packages/kyt-core/config/__tests__/webpack.test.js +++ b/packages/kyt-core/config/__tests__/webpack.test.js @@ -1,7 +1,19 @@ +const shell = { + test: jest.fn(), +}; + +const logger = { + warn: jest.fn(), +}; + +jest.setMock('shelljs', shell); +jest.setMock('kyt-utils/logger', logger); + const devClientConfig = require('../webpack.dev.client'); const devServerConfig = require('../webpack.dev.server'); const prodClientConfig = require('../webpack.prod.client'); const prodServerConfig = require('../webpack.prod.server'); +const baseConfig = require('../webpack.base'); describe('webpack.dev.client', () => { it('has babel-polyfill as first entry in entry.main array', () => { @@ -30,3 +42,24 @@ describe('webpack.prod.server', () => { expect(config.entry.main[0]).toBe('babel-polyfill'); }); }); + +describe('webpack.base', () => { + beforeEach(() => { + logger.warn.mockClear(); + }); + it('doesn\'t set up a babel preset if a .babelrc exists', () => { + shell.test.mockImplementationOnce(() => true); + const config = baseConfig({ clientURL: {}, publicPath: '/' }); + const babelLoader = config.module.rules.find(({ loader }) => loader === 'babel-loader'); + expect(babelLoader.options.presets).toBeUndefined(); + expect(logger.warn).not.toHaveBeenCalled(); + }); + it('sets up kyt-core babel preset if a .babelrc exists', () => { + shell.test.mockImplementationOnce(() => false); + const config = baseConfig({ clientURL: {}, publicPath: '/' }); + const babelLoader = config.module.rules.find(({ loader }) => loader === 'babel-loader'); + expect(babelLoader.options.presets.length).toBe(1); + expect(babelLoader.options.presets[0]).toMatch(/babel-preset-kyt-core/); + expect(logger.warn).toHaveBeenCalledWith('No user .babelrc found. Using kyt default babel preset...'); + }); +}); diff --git a/packages/kyt-core/config/babel.js b/packages/kyt-core/config/babel.js deleted file mode 100644 index 131ee8041..000000000 --- a/packages/kyt-core/config/babel.js +++ /dev/null @@ -1,51 +0,0 @@ -const path = require('path'); -const fs = require('fs'); - -/** - * Support preset and plugin definitions in .babelrc - * without the babel-* prefixes. - * - * E.g. {presets: ['babel-preset-react']} --> {presets: ['react']} - * - * @param {String} prefix One of either 'babel-plugin' or 'babel-preset' - * @param {String} dep The dependency - * @return {String} The normalized dependency - */ -const normalizeDep = (prefix, dep) => { - let resolved; - if (dep.indexOf('babel') !== 0) { - try { - resolved = require.resolve(`${prefix}-${dep}`); - } catch (e) { /* eslint-disable no-empty */ } - } - - return resolved || require.resolve(dep); -}; - -// Uses require.resolve to add the full paths to all of the plugins -// and presets, making sure that we handle the new array syntax. -const resolvePluginsPresets = (babelGroup) => { - const resolve = (prefix, dep) => { - if (typeof dep === 'object') { - dep[0] = normalizeDep(prefix, dep[0]); - return dep; - } - return normalizeDep(prefix, dep); - }; - babelGroup.plugins = (babelGroup.plugins || []).map(resolve.bind(null, 'babel-plugin')); - babelGroup.presets = (babelGroup.presets || []).map(resolve.bind(null, 'babel-preset')); -}; - -module.exports = (options) => { - // Create the babelrc query for the babel loader. - const babelrc = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../.babelrc'), 'utf8')); - babelrc.babelrc = false; - babelrc.cacheDirectory = false; - resolvePluginsPresets(babelrc); - if (options && options.reactHotLoader) { - babelrc.env.development.plugins.push('react-hot-loader/babel'); - } - Object.keys(babelrc.env || {}).forEach(env => resolvePluginsPresets(babelrc.env[env])); - - return babelrc; -}; diff --git a/packages/kyt-core/config/jest.js b/packages/kyt-core/config/jest.js index faafd4875..d3a1e20c0 100644 --- a/packages/kyt-core/config/jest.js +++ b/packages/kyt-core/config/jest.js @@ -11,6 +11,9 @@ module.exports = (rootDir, aliases = {}) => ({ resolveFromUtils('file.stub'), '^[./a-zA-Z0-9$_-]+\\.(css|scss)$': resolveFromUtils('style.stub'), + // when this is removed from the base webpack config, we can likely + // remove the runtime and include the polyfill in the test environment + 'babel-runtime': require.resolve('babel-plugin-transform-runtime'), }, aliases ), diff --git a/packages/kyt-core/config/webpack.base.js b/packages/kyt-core/config/webpack.base.js index f82520802..f56d32cf2 100644 --- a/packages/kyt-core/config/webpack.base.js +++ b/packages/kyt-core/config/webpack.base.js @@ -5,75 +5,104 @@ const path = require('path'); const webpack = require('webpack'); +const shell = require('shelljs'); const autoprefixer = require('autoprefixer'); -const babel = require('./babel'); -const { buildPath, userNodeModulesPath } = require('kyt-utils/paths')(); +const { buildPath, userNodeModulesPath, userBabelrcPath } = require('kyt-utils/paths')(); +const logger = require('kyt-utils/logger'); -module.exports = options => ({ - node: { - __dirname: true, - __filename: true, - }, +module.exports = (options) => { + const hasBabelrc = shell.test('-f', userBabelrcPath); + if (!hasBabelrc) { + logger.warn('No user .babelrc found. Using kyt default babel preset...'); + } - devtool: 'source-map', + return { + node: { + __dirname: true, + __filename: true, + }, - resolve: { - extensions: ['.js', '.json'], - modules: [userNodeModulesPath, path.resolve(__dirname, '../node_modules')], - }, + devtool: 'source-map', - resolveLoader: { - modules: [userNodeModulesPath, path.resolve(__dirname, '../node_modules')], - }, + resolve: { + extensions: ['.js', '.json'], + modules: [userNodeModulesPath, path.resolve(__dirname, '../node_modules')], + }, - plugins: [ - new webpack.DefinePlugin({ - // Hardcode NODE_ENV at build time so libraries like React get optimized - 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || options.environment), - KYT: { - SERVER_PORT: JSON.stringify((options.serverURL && options.serverURL.port) || ''), - CLIENT_PORT: JSON.stringify((options.clientURL && options.clientURL.port) || ''), - PUBLIC_PATH: JSON.stringify(options.publicPath || ''), - PUBLIC_DIR: JSON.stringify(options.publicDir || ''), - ASSETS_MANIFEST: - JSON.stringify(path.join(buildPath || '', options.clientAssetsFile || '')), - }, - }), + resolveLoader: { + modules: [userNodeModulesPath, path.resolve(__dirname, '../node_modules')], + }, - new webpack.LoaderOptionsPlugin({ - options: { - postcss: [autoprefixer({ browsers: ['last 2 versions'] })], - context: '/', - }, - }), - ], + plugins: [ + new webpack.DefinePlugin({ + // Hardcode NODE_ENV at build time so libraries like React get optimized + 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || options.environment), + KYT: { + SERVER_PORT: JSON.stringify((options.serverURL && options.serverURL.port) || ''), + CLIENT_PORT: JSON.stringify((options.clientURL && options.clientURL.port) || ''), + PUBLIC_PATH: JSON.stringify(options.publicPath || ''), + PUBLIC_DIR: JSON.stringify(options.publicDir || ''), + ASSETS_MANIFEST: + JSON.stringify(path.join(buildPath || '', options.clientAssetsFile || '')), + }, + }), - module: { - rules: [ - { - test: /\.html$/, - loader: 'file?name=[name].[ext]', - }, - { - test: /\.(jpg|jpeg|png|gif|eot|svg|ttf|woff|woff2)$/, - loader: 'url-loader', + new webpack.LoaderOptionsPlugin({ options: { - limit: 20000, + postcss: [autoprefixer({ browsers: ['last 2 versions'] })], + context: '/', }, - }, - { - test: /\.json$/, - loader: 'json-loader', - }, - { - test: /\.(js|jsx)$/, - loader: 'babel-loader', - exclude: [ - /node_modules/, - buildPath, - ], - options: babel(options), - }, + }), ], - }, -}); + + module: { + rules: [ + { + test: /\.html$/, + loader: 'file?name=[name].[ext]', + }, + { + test: /\.(jpg|jpeg|png|gif|eot|svg|ttf|woff|woff2)$/, + loader: 'url-loader', + options: { + limit: 20000, + }, + }, + { + test: /\.json$/, + loader: 'json-loader', + }, + { + test: /\.(js|jsx)$/, + loader: 'babel-loader', + exclude: [ + /node_modules/, + buildPath, + ], + // babel configuration should come from presets defined in the user's + // .babelrc, unless there's a specific reason why it has to be put in + // the webpack loader query + options: Object.assign({ + // this is a loader-specific option and can't be put in a babel preset + cacheDirectory: false, + }, + // add react hot loader babel plugin for development here--users + // should only need to specify the reactHotLoader option in one place + // (kyt.config.js), instead of two (kyt.config.js and .babelrc). + // additionally, .babelrc has no notion of client vs server + (options.type === 'client' && options.reactHotLoader) ? { + env: { + development: { + plugins: [require.resolve('react-hot-loader/babel')], + }, + }, + } : {}, + // if the user hasn't defined a .babelrc, use the kyt default + !hasBabelrc ? { + presets: [require.resolve('babel-preset-kyt-core')], + } : {}), + }, + ], + }, + }; +}; diff --git a/packages/kyt-core/package.json b/packages/kyt-core/package.json index 5a5eb740e..4d5c234d0 100644 --- a/packages/kyt-core/package.json +++ b/packages/kyt-core/package.json @@ -27,12 +27,8 @@ "babel-core": "6.18.0", "babel-jest": "16.0.0", "babel-loader": "6.2.7", - "babel-plugin-transform-react-constant-elements": "6.9.1", - "babel-plugin-transform-react-jsx-source": "6.9.0", - "babel-plugin-transform-react-remove-prop-types": "0.2.10", "babel-polyfill": "6.16.0", - "babel-preset-latest": "6.16.0", - "babel-preset-react": "6.16.0", + "babel-preset-kyt-core": "0.1.0-alpha.1", "chokidar": "1.6.0", "commander": "2.9.0", "css-loader": "0.25.0", diff --git a/packages/kyt-core/utils/__tests__/buildConfigs.test.js b/packages/kyt-core/utils/__tests__/buildConfigs.test.js index 0a6c64191..45f040d50 100644 --- a/packages/kyt-core/utils/__tests__/buildConfigs.test.js +++ b/packages/kyt-core/utils/__tests__/buildConfigs.test.js @@ -1,3 +1,26 @@ +const prodClientConfig = jest.fn(() => ({ + target: 'web', +})); +const prodServerConfig = jest.fn(() => ({ + target: 'node', + entry: { main: 'something' }, +})); +const devClientConfig = jest.fn(() => ({ + target: 'web', +})); +const devServerConfig = jest.fn(() => ({ + target: 'node', + entry: { main: 'something' }, +})); +const baseConfig = jest.fn(() => ({})); + +// for now just mock these +jest.setMock('../../config/webpack.dev.client', devClientConfig); +jest.setMock('../../config/webpack.dev.server', devServerConfig); +jest.setMock('../../config/webpack.prod.client', prodClientConfig); +jest.setMock('../../config/webpack.prod.server', prodServerConfig); +jest.setMock('../../config/webpack.base', baseConfig); + const stubConfig = { modifyWebpackConfig: jest.fn(c => c), clientPort: 1000, @@ -8,9 +31,12 @@ const stubConfig = { }; const buildConfigs = require('../buildConfigs'); +global.process.exit = jest.fn(); + describe('buildConfigs', () => { beforeEach(() => { stubConfig.modifyWebpackConfig.mockClear(); + global.process.exit.mockClear(); }); it('should call the userland modifyWebpackConfig', () => { @@ -31,6 +57,8 @@ describe('buildConfigs', () => { expect(built.clientConfig.target).toBe('web'); expect(built.serverConfig).toBeDefined(); expect(built.serverConfig.target).toBe('node'); + + expect(global.process.exit).not.toHaveBeenCalled(); }); it('for production', () => { @@ -51,5 +79,7 @@ describe('buildConfigs', () => { expect(built.clientConfig.target).toBe('web'); expect(built.serverConfig).toBeDefined(); expect(built.serverConfig.target).toBe('node'); + + expect(global.process.exit).not.toHaveBeenCalled(); }); }); diff --git a/packages/kyt-core/utils/__tests__/kytConfig.test.js b/packages/kyt-core/utils/__tests__/kytConfig.test.js index 4cfa1a896..5986e76b9 100644 --- a/packages/kyt-core/utils/__tests__/kytConfig.test.js +++ b/packages/kyt-core/utils/__tests__/kytConfig.test.js @@ -32,8 +32,9 @@ describe('kytConfig', () => { expect(logger.info).toBeCalled(); expect(typeof config.modifyWebpackConfig).toBe('function'); expect(typeof config.modifyJestConfig).toBe('function'); - expect(() => { - config.productionPublicPath = 'frozen!'; - }).toThrow(); + + expect(Object.isFrozen(config)).toBe(true); + config.productionPublicPath = 'frozen!'; + expect(config.productionPublicPath).not.toBe('frozen!'); }); }); diff --git a/packages/kyt-core/utils/jest/__tests__/preprocessor.test.js b/packages/kyt-core/utils/jest/__tests__/preprocessor.test.js new file mode 100644 index 000000000..cefe5f068 --- /dev/null +++ b/packages/kyt-core/utils/jest/__tests__/preprocessor.test.js @@ -0,0 +1,95 @@ +const babelJest = { + createTransformer: jest.fn(), +}; + +const shell = { + test: jest.fn(), +}; + +const paths = jest.fn(() => ({ + userRootPath: '', + userBabelrcPath: '', +})); + +const fs = { + readFileSync: jest.fn(), +}; + +const resolve = { + sync: jest.fn(p => `/path/to/${p}`), +}; + +const logger = { + warn: jest.fn(), + error: jest.fn(), +}; + +global.process.exit = jest.fn(); + +jest.setMock('babel-jest', babelJest); +jest.setMock('shelljs', shell); +jest.setMock('kyt-utils/paths', paths); +jest.setMock('fs', fs); +jest.setMock('resolve', resolve); +jest.setMock('kyt-utils/logger', logger); + +describe('jest preprocessor', () => { + beforeEach(() => { + jest.resetModules(); + babelJest.createTransformer.mockClear(); + logger.warn.mockClear(); + logger.error.mockClear(); + fs.readFileSync.mockClear(); + global.process.exit.mockClear(); + }); + + it('uses user-defined .babelrc if it exists', () => { + shell.test.mockImplementationOnce(() => true); + const fakeBabelrc = { + presets: ['my-whatever-preset'], + plugins: ['babel-plugin-my-whatever-plugin'], + }; + fs.readFileSync.mockImplementationOnce(() => JSON.stringify(fakeBabelrc)); + // eslint-disable-next-line global-require, import/newline-after-import + require('../preprocessor'); + expect(babelJest.createTransformer.mock.calls.length).toBe(1); + expect(logger.warn).not.toHaveBeenCalled(); + expect(logger.error).not.toHaveBeenCalled(); + expect(global.process.exit).not.toHaveBeenCalled(); + expect(babelJest.createTransformer.mock.calls[0][0]).toEqual({ + presets: ['/path/to/babel-preset-my-whatever-preset'], + plugins: ['/path/to/babel-plugin-my-whatever-plugin'], + }); + }); + + it('otherwise uses kyt default babel preset', () => { + shell.test.mockImplementationOnce(() => false); + // eslint-disable-next-line global-require, import/newline-after-import + require('../preprocessor'); + expect(fs.readFileSync).not.toHaveBeenCalled(); + expect(babelJest.createTransformer.mock.calls.length).toBe(1); + expect(babelJest.createTransformer.mock.calls[0][0].presets.length).toBe(1); + expect(logger.warn).toHaveBeenCalledWith('No user .babelrc found. Using kyt default babel preset...'); + expect(logger.error).not.toHaveBeenCalled(); + expect(global.process.exit).not.toHaveBeenCalled(); + expect(babelJest.createTransformer.mock.calls[0][0].presets[0]) + .toMatch(/babel-preset-kyt-core/); + }); + + it('errors and exits if a plugin or preset is not resolvable', () => { + shell.test.mockImplementationOnce(() => true); + const fakeBabelrc = { + presets: ['my-whatever-preset'], + plugins: ['babel-plugin-my-whatever-plugin'], + }; + fs.readFileSync.mockImplementationOnce(() => JSON.stringify(fakeBabelrc)); + const err = new Error('fake error'); + resolve.sync.mockImplementationOnce(() => { throw err; }); + // eslint-disable-next-line global-require, import/newline-after-import + require('../preprocessor'); + expect(logger.error).toHaveBeenCalledWith('Could not resolve dependency', 'babel-plugin-my-whatever-plugin'); + expect(logger.error).toHaveBeenCalledWith('Error output', err); + expect(logger.error).toHaveBeenCalledWith('Exiting...'); + expect(process.exit).toHaveBeenCalledWith(1); + }); +}); diff --git a/packages/kyt-core/utils/jest/preprocessor.js b/packages/kyt-core/utils/jest/preprocessor.js index 43c24dcd1..ad26c59fa 100644 --- a/packages/kyt-core/utils/jest/preprocessor.js +++ b/packages/kyt-core/utils/jest/preprocessor.js @@ -1,17 +1,61 @@ -const mergeAll = require('ramda').mergeAll; -const babel = require('../../config/babel')(); const babelJest = require('babel-jest'); -const buildConfigs = require('../buildConfigs'); -const config = require('../kytConfig')(); +const fs = require('fs'); +const shell = require('shelljs'); +const { userBabelrcPath, userRootPath } = require('kyt-utils/paths')(); +const logger = require('kyt-utils/logger'); +const resolve = require('resolve'); -const { clientConfig } = buildConfigs(config); +/** + * Support preset and plugin definitions in .babelrc + * without the babel-* prefixes. + * + * E.g. {presets: ['babel-preset-react']} --> {presets: ['react']} + * + * @param {String} prefix One of either 'babel-plugin' or 'babel-preset' + * @param {String} dep The dependency + * @return {String} The normalized dependency + */ +const normalizeDep = (prefix, dep) => { + let resolved; + try { + if (dep.indexOf('babel') !== 0) { + resolved = resolve.sync(`${prefix}-${dep}`, { basedir: userRootPath }); + } + return resolved || resolve.sync(dep, { basedir: userRootPath }); + } catch (e) { + logger.error('Could not resolve dependency', dep); + logger.error('Error output', e); + logger.error('Exiting...'); + return process.exit(1); + } +}; -// Merge userland babel config with our babel config -// This should go away after https://github.com/NYTimes/kyt/issues/134 -const clientBabelConfig = clientConfig.module.rules - .find(loader => loader.loader === 'babel-loader') - .options; +// Uses require.resolve to add the full paths to all of the plugins +// and presets, making sure that we handle the new array syntax. +const resolvePluginsPresets = (babelGroup) => { + const resolver = (prefix, dep) => { + if (typeof dep === 'object') { + dep[0] = normalizeDep(prefix, dep[0]); + return dep; + } + return normalizeDep(prefix, dep); + }; + babelGroup.plugins = (babelGroup.plugins || []).map(resolver.bind(null, 'babel-plugin')); + babelGroup.presets = (babelGroup.presets || []).map(resolver.bind(null, 'babel-preset')); +}; -const babelConfigForJest = mergeAll([{}, babel, clientBabelConfig]); +let babelrc; -module.exports = babelJest.createTransformer(babelConfigForJest); +if (shell.test('-f', userBabelrcPath)) { + babelrc = JSON.parse(fs.readFileSync(userBabelrcPath)); + resolvePluginsPresets(babelrc); + Object.keys(babelrc.env || {}).forEach(env => resolvePluginsPresets(babelrc.env[env])); +} else { + // if the user hasn't defined a .babelrc, use the kyt default preset + logger.warn('No user .babelrc found. Using kyt default babel preset...'); + babelrc = { + presets: [require.resolve('babel-preset-kyt-core')], + }; +} + +module.exports = babelJest.createTransformer(babelrc); diff --git a/packages/kyt-utils/paths.js b/packages/kyt-utils/paths.js index 0a5c313f5..4acce1702 100644 --- a/packages/kyt-utils/paths.js +++ b/packages/kyt-utils/paths.js @@ -24,5 +24,6 @@ module.exports = () => { userKytConfigPath: path.join(userRootPath, 'kyt.config.js'), userNodeModulesPath: path.join(userRootPath, 'node_modules'), userPackageJSONPath: path.join(userRootPath, 'package.json'), + userBabelrcPath: path.join(userRootPath, '.babelrc'), }; }; diff --git a/packages/starter-kyts/kyt-starter-static/.babelrc b/packages/starter-kyts/kyt-starter-static/.babelrc new file mode 100644 index 000000000..5c4a8544a --- /dev/null +++ b/packages/starter-kyts/kyt-starter-static/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["kyt-react"] +} diff --git a/packages/starter-kyts/kyt-starter-static/package.json b/packages/starter-kyts/kyt-starter-static/package.json index a0faffce6..b9ba58459 100644 --- a/packages/starter-kyts/kyt-starter-static/package.json +++ b/packages/starter-kyts/kyt-starter-static/package.json @@ -15,6 +15,7 @@ }, "homepage": "https://github.com/nytimes/kyt-starter-static#readme", "dependencies": { + "babel-preset-kyt-react": "0.1.0-alpha.1", "html-webpack-plugin": "^2.22.0", "react": "^15.3.0", "react-dom": "^15.3.0", diff --git a/packages/starter-kyts/kyt-starter-universal/.babelrc b/packages/starter-kyts/kyt-starter-universal/.babelrc new file mode 100644 index 000000000..5c4a8544a --- /dev/null +++ b/packages/starter-kyts/kyt-starter-universal/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["kyt-react"] +} diff --git a/packages/starter-kyts/kyt-starter-universal/package.json b/packages/starter-kyts/kyt-starter-universal/package.json index 98537ec69..403e27bea 100644 --- a/packages/starter-kyts/kyt-starter-universal/package.json +++ b/packages/starter-kyts/kyt-starter-universal/package.json @@ -3,8 +3,7 @@ "version": "1.0.0", "description": "", "main": "index.js", - "scripts": { - }, + "scripts": {}, "repository": { "type": "git", "url": "git+https://github.com/nytm/wf-kyt-starter-universal.git" @@ -16,6 +15,7 @@ }, "homepage": "https://github.com/nytm/wf-kyt-starter-universal#readme", "dependencies": { + "babel-preset-kyt-react": "0.1.0-alpha.1", "compression": "^1.6.2", "express": "^4.14.0", "react": "^15.3.0",