diff --git a/.eslintrc.js b/.eslintrc.js index 959bd8e316ea..75543a3c0dae 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,7 +1,24 @@ const path = require('path') +const findUp = require('findup-sync') + +// Framework Babel config is monorepo root ./babel.config.js +// `yarn lint` runs for each workspace, which needs findup for path to root +const findBabelConfig = (cwd = process.cwd()) => { + const configPath = findUp('babel.config.js', { cwd }) + if (!configPath) { + throw new Error(`Eslint-parser could not find a "babel.config.js" file`) + } + return configPath +} + module.exports = { extends: path.join(__dirname, 'packages/eslint-config/shared.js'), + parserOptions: { + babelOptions: { + configFile: findBabelConfig(), + }, + }, ignorePatterns: [ 'dist', 'fixtures', diff --git a/__fixtures__/example-todo-main/api/src/lib/transform.js b/__fixtures__/example-todo-main/api/src/lib/transform.js new file mode 100644 index 000000000000..b94e029e5787 --- /dev/null +++ b/__fixtures__/example-todo-main/api/src/lib/transform.js @@ -0,0 +1,19 @@ +var sym = Symbol() + +var promise = Promise.resolve() + +var check = arr.includes('yeah!') + +console.log(arr[Symbol.iterator]()) + +Promise.allSettled() + +console.log([].includes('bazinga')) + +Promise.any() + + +Object.hasOwn({ x: 2 }, "x") + +var arr = [1, 2, 3]; +arr.at(0) === 1 diff --git a/__fixtures__/example-todo-main/babel.config.js b/__fixtures__/example-todo-main/babel.config.js index 0bb758f7df49..af57d102cfe0 100644 --- a/__fixtures__/example-todo-main/babel.config.js +++ b/__fixtures__/example-todo-main/babel.config.js @@ -1,3 +1,18 @@ +// This is added as a test for prebuild; +// check: packages/internal/**/build_api.test.ts module.exports = { - presets: ['@redwoodjs/core/config/babel-preset'], + plugins: [ + [ + 'babel-plugin-auto-import', + { + declarations: [ + { + // import kitty from 'kitty-purr' + default: 'kitty', + path: 'kitty-purr', + }, + ], + }, + ], + ] } diff --git a/__fixtures__/example-todo-main/web/.babelrc.js b/__fixtures__/example-todo-main/web/.babelrc.js deleted file mode 100644 index 562431e7d380..000000000000 --- a/__fixtures__/example-todo-main/web/.babelrc.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = { extends: "../babel.config.js" } diff --git a/babel.config.js b/babel.config.js index a1d6732131e5..15b84c76ee44 100644 --- a/babel.config.js +++ b/babel.config.js @@ -55,6 +55,18 @@ module.exports = { ['@babel/plugin-proposal-class-properties', { loose: true }], ['@babel/plugin-proposal-private-methods', { loose: true }], ['@babel/plugin-proposal-private-property-in-object', { loose: true }], + [ + '@babel/plugin-transform-runtime', + { + // https://babeljs.io/docs/en/babel-plugin-transform-runtime/#core-js-aliasing + // Setting the version here also requires `@babel/runtime-corejs3` + corejs: { version: 3, proposals: true }, + // https://babeljs.io/docs/en/babel-plugin-transform-runtime/#version + // Transform-runtime assumes that @babel/runtime@7.0.0 is installed. + // Specifying the version can result in a smaller bundle size. + version: packageJSON.devDependencies['@babel/runtime-corejs3'], + }, + ], ], overrides: [ // ** WEB PACKAGES ** @@ -93,18 +105,6 @@ module.exports = { ], }, ], - [ - '@babel/plugin-transform-runtime', - { - // https://babeljs.io/docs/en/babel-plugin-transform-runtime/#core-js-aliasing - // Setting the version here also requires `@babel/runtime-corejs3` - corejs: { version: 3, proposals: true }, - // https://babeljs.io/docs/en/babel-plugin-transform-runtime/#version - // Transform-runtime assumes that @babel/runtime@7.0.0 is installed. - // Specifying the version can result in a smaller bundle size. - version: packageJSON.devDependencies['@babel/runtime-corejs3'], - }, - ], // normally provided through preset-env detecting TARGET_BROWSER // but webpack 4 has an issue with this // see https://github.com/PaulLeCam/react-leaflet/issues/883 diff --git a/packages/api/package.json b/packages/api/package.json index 33457e34e834..05a0ec7afe7b 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -10,8 +10,8 @@ "types": "./dist/index.d.ts", "license": "MIT", "dependencies": { + "@babel/runtime-corejs3": "7.15.4", "@prisma/client": "3.3.0", - "core-js": "3.18.3", "crypto-js": "4.1.1", "jsonwebtoken": "8.5.1", "jwks-rsa": "2.0.5", diff --git a/packages/cli/package.json b/packages/cli/package.json index 6cb33f51fe87..01fb84184fae 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -35,6 +35,7 @@ "dotenv-defaults": "3.0.0", "envinfo": "7.8.1", "execa": "5.1.1", + "fast-glob": "3.2.7", "fs-extra": "10.0.0", "humanize-string": "2.1.0", "latest-version": "5.1.0", diff --git a/packages/cli/src/commands/build.js b/packages/cli/src/commands/build.js index 5866b35f4b96..3a9916d9cd26 100644 --- a/packages/cli/src/commands/build.js +++ b/packages/cli/src/commands/build.js @@ -12,6 +12,7 @@ import { detectPrerenderRoutes } from '@redwoodjs/prerender/detection' import { getPaths } from '../lib' import c from '../lib/colors' import { generatePrismaCommand } from '../lib/generatePrismaClient' +import checkForBabelConfig from '../middleware/checkForBabelConfig' import { getTasks as getPrerenderTasks } from './prerender' @@ -71,6 +72,7 @@ export const builder = (yargs) => { default: false, description: 'Measure build performance', }) + .middleware(checkForBabelConfig) .epilogue( `Also see the ${terminalLink( 'Redwood CLI Reference', diff --git a/packages/cli/src/commands/dev.js b/packages/cli/src/commands/dev.js index f320261b0aba..be59ab84f78c 100644 --- a/packages/cli/src/commands/dev.js +++ b/packages/cli/src/commands/dev.js @@ -8,6 +8,7 @@ import { getConfig, getConfigPath, shutdownPort } from '@redwoodjs/internal' import { getPaths } from '../lib' import c from '../lib/colors' import { generatePrismaClient } from '../lib/generatePrismaClient' +import checkForBabelConfig from '../middleware/checkForBabelConfig' export const command = 'dev [side..]' export const description = 'Start development servers for api, and web' @@ -34,6 +35,7 @@ export const builder = (yargs) => { type: 'boolean', description: 'Reload on changes to node_modules', }) + .middleware(checkForBabelConfig) .epilogue( `Also see the ${terminalLink( 'Redwood CLI Reference', diff --git a/packages/cli/src/commands/test.js b/packages/cli/src/commands/test.js index 7427940d0777..8f11fbc7e068 100644 --- a/packages/cli/src/commands/test.js +++ b/packages/cli/src/commands/test.js @@ -103,7 +103,7 @@ export const handler = async ({ collectCoverage ? '--collectCoverage' : null, '--passWithNoTests', ...jestFilterArgs, - '--runInBand', + '--runInBand', // @TODO always run in band, even for web as we get babel errors https://github.com/redwoodjs/redwood/issues/3646 ].filter((flagOrValue) => flagOrValue !== null) // Filter out nulls, not booleans because user may have passed a --something false flag // If the user wants to watch, set the proper watch flag based on what kind of repo this is diff --git a/packages/cli/src/middleware/checkForBabelConfig.js b/packages/cli/src/middleware/checkForBabelConfig.js new file mode 100644 index 000000000000..a17f8a13f249 --- /dev/null +++ b/packages/cli/src/middleware/checkForBabelConfig.js @@ -0,0 +1,43 @@ +import boxen from 'boxen' +import fg from 'fast-glob' + +import { getPaths } from '@redwoodjs/internal' + +import c from '../lib/colors' + +const isUsingBabelRc = () => { + return ( + fg.sync('**/*/.babelrc(.*)?', { + cwd: getPaths().base, + ignore: 'node_modules', + }).length > 0 + ) +} +const BABEL_SETTINGS_LINK = c.warning('https://redwoodjs.com/docs/builds') + +const checkForBabelConfig = () => { + if (isUsingBabelRc()) { + const messages = [ + "Looks like you're trying to configure one of your sides with a .babelrc file.", + 'These settings will be ignored, unless you use a babel.config.js file', + '', + 'Your plugins and settings will be automatically merged with', + `the Redwood built-in config, more details here: ${BABEL_SETTINGS_LINK}`, + ] + + const errTitle = 'Incorrect project configuration' + + console.log( + boxen(messages.join('\n'), { + title: errTitle, + padding: { top: 0, bottom: 0, right: 1, left: 1 }, + margin: 1, + borderColor: 'red', + }) + ) + + throw new Error(errTitle) + } +} + +export default checkForBabelConfig diff --git a/packages/core/config/babel-preset.js b/packages/core/config/babel-preset.js deleted file mode 100644 index 93b254665ea5..000000000000 --- a/packages/core/config/babel-preset.js +++ /dev/null @@ -1,230 +0,0 @@ -/** - * This is the babel preset used in `create-redwood-app` - */ -const { getPaths } = require('@redwoodjs/internal') - -const packageJSON = require('../package.json') - -const TARGETS_NODE = '12.16' -// Warning! Use the minor core-js version: "corejs: '3.6'", instead of "corejs: 3", -// because we want to include the features added in the minor version. -// https://github.com/zloirock/core-js/blob/master/README.md#babelpreset-env - -const CORE_JS_VERSION = packageJSON.dependencies['core-js'] - .split('.') - .slice(0, 2) - .join('.') // Produces: 3.12, instead of 3.12.1 -if (!CORE_JS_VERSION) { - throw new Error( - 'RedwoodJS Project Babel: Could not determine core-js version.' - ) -} - -const RUNTIME_CORE_JS_VERSION = - packageJSON.dependencies['@babel/runtime-corejs3'] -if (!RUNTIME_CORE_JS_VERSION) { - throw new Error( - 'RedwoodJS Project Babel: Could not determine core-js runtime version' - ) -} - -/** @type {import('@babel/core').TransformOptions} */ -module.exports = () => { - const rwjsPaths = getPaths() - - return { - presets: ['@babel/preset-react', '@babel/preset-typescript'], - plugins: [ - ['@babel/plugin-proposal-class-properties', { loose: true }], - // Note: The private method loose mode configuration setting must be the - // same as @babel/plugin-proposal class-properties. - // (https://babeljs.io/docs/en/babel-plugin-proposal-private-methods#loose) - ['@babel/plugin-proposal-private-methods', { loose: true }], - ['@babel/plugin-proposal-private-property-in-object', { loose: true }], - [ - '@babel/plugin-transform-runtime', - { - // https://babeljs.io/docs/en/babel-plugin-transform-runtime/#core-js-aliasing - // Setting the version here also requires `@babel/runtime-corejs3` - corejs: { version: 3, proposals: true }, - // https://babeljs.io/docs/en/babel-plugin-transform-runtime/#version - // Transform-runtime assumes that @babel/runtime@7.0.0 is installed. - // Specifying the version can result in a smaller bundle size. - version: RUNTIME_CORE_JS_VERSION, - }, - ], - [ - require('@redwoodjs/internal/dist/build/babelPlugins/babel-plugin-redwood-directory-named-import'), - ], - ], - overrides: [ - // ** API (also applies to Jest API config) ** - // ** SCRIPTS ** - { - test: ['./api/', './scripts/'], - presets: [ - [ - '@babel/preset-env', - { - targets: { - node: TARGETS_NODE, - }, - useBuiltIns: 'usage', - corejs: { - version: CORE_JS_VERSION, - // List of supported proposals: https://github.com/zloirock/core-js/blob/master/docs/2019-03-19-core-js-3-babel-and-a-look-into-the-future.md#ecmascript-proposals - proposals: true, - }, - exclude: [ - // Remove class-properties from preset-env, and include separately with loose - // https://github.com/webpack/webpack/issues/9708 - '@babel/plugin-proposal-class-properties', - '@babel/plugin-proposal-private-methods', - ], - }, - ], - ], - plugins: [ - [ - 'babel-plugin-module-resolver', - { - alias: { - src: - // Jest monorepo and multi project runner is not correctly determining - // the `cwd`: https://github.com/facebook/jest/issues/7359 - process.env.NODE_ENV !== 'test' ? './src' : rwjsPaths.api.src, - }, - }, - ], - [ - 'babel-plugin-auto-import', - { - declarations: [ - { - // import { context } from '@redwoodjs/graphql-server' - members: ['context'], - path: '@redwoodjs/graphql-server', - }, - { - default: 'gql', - path: 'graphql-tag', - }, - ], - }, - ], - ['babel-plugin-graphql-tag'], - [ - require('@redwoodjs/internal/dist/build/babelPlugins/babel-plugin-redwood-import-dir'), - ], - ], - }, - // ** WEB ** - { - test: './web', - presets: [ - [ - '@babel/preset-env', - { - // the targets are set in /web/package.json - useBuiltIns: 'usage', - corejs: { - version: CORE_JS_VERSION, - proposals: true, - }, - exclude: [ - // Remove class-properties from preset-env, and include separately - // https://github.com/webpack/webpack/issues/9708 - '@babel/plugin-proposal-class-properties', - '@babel/plugin-proposal-private-methods', - ], - }, - ], - ], - plugins: [ - [ - 'babel-plugin-module-resolver', - { - alias: { - src: - // Jest monorepo and multi project runner is not correctly determining - // the `cwd`: https://github.com/facebook/jest/issues/7359 - process.env.NODE_ENV !== 'test' ? './src' : rwjsPaths.web.src, - }, - }, - ], - [ - 'babel-plugin-auto-import', - { - declarations: [ - { - // import { React } from 'react' - default: 'React', - path: 'react', - }, - { - // import PropTypes from 'prop-types' - default: 'PropTypes', - path: 'prop-types', - }, - { - // import gql from 'graphql-tag' - default: 'gql', - path: 'graphql-tag', - }, - ], - }, - ], - ['babel-plugin-graphql-tag'], - [ - 'inline-react-svg', - { - svgo: { - plugins: [ - { - name: 'removeAttrs', - params: { attrs: '(data-name)' }, - }, - // Otherwise having style="xxx" breaks - 'convertStyleToAttrs', - ], - }, - }, - ], - // @MARK needed to enable ?? operator - // normally provided through preset-env detecting TARGET_BROWSER - // but webpack 4 has an issue with this - // see https://github.com/PaulLeCam/react-leaflet/issues/883 - ['@babel/plugin-proposal-nullish-coalescing-operator'], - ], - }, - // ** Files ending in `Cell.[js,ts]` ** - { - test: /.+Cell.(js|tsx)$/, - plugins: [ - require('@redwoodjs/internal/dist/build/babelPlugins/babel-plugin-redwood-cell'), - ], - }, - // Automatically import files in `./web/src/pages/*` in to - // the `./web/src/Routes.[ts|jsx]` file. - { - test: ['./web/src/Routes.js', './web/src/Routes.tsx'], - plugins: [ - [ - require('@redwoodjs/internal/dist/build/babelPlugins/babel-plugin-redwood-routes-auto-loader'), - { - useStaticImports: process.env.__REDWOOD__PRERENDERING === '1', - }, - ], - ], - }, - // ** Files ending in `Cell.mock.[js,ts]` ** - // Automatically determine keys for saving and retrieving mock data. - { - test: /.+Cell.mock.(js|ts)$/, - plugins: [ - require('@redwoodjs/internal/dist/build/babelPlugins/babel-plugin-redwood-mock-cell-data'), - ], - }, - ], - } -} diff --git a/packages/core/config/webpack.common.js b/packages/core/config/webpack.common.js index aea9a35e3fc5..bc734995569b 100644 --- a/packages/core/config/webpack.common.js +++ b/packages/core/config/webpack.common.js @@ -12,7 +12,11 @@ const { WebpackManifestPlugin } = require('webpack-manifest-plugin') const { merge } = require('webpack-merge') const { RetryChunkLoadPlugin } = require('webpack-retry-chunk-load-plugin') -const { getConfig, getPaths } = require('@redwoodjs/internal') +const { + getConfig, + getPaths, + getWebSideDefaultBabelConfig, +} = require('@redwoodjs/internal') const redwoodConfig = getConfig() const redwoodPaths = getPaths() @@ -125,6 +129,15 @@ const getStyleLoaders = (isEnvProduction) => { const getSharedPlugins = (isEnvProduction) => { const shouldIncludeFastRefresh = redwoodConfig.web.fastRefresh !== false && !isEnvProduction + + const devTimeAutoImports = isEnvProduction + ? {} + : { + mockGraphQLQuery: ['@redwoodjs/testing/web', 'mockGraphQLQuery'], + mockGraphQLMutation: ['@redwoodjs/testing/web', 'mockGraphQLMutation'], + mockCurrentUser: ['@redwoodjs/testing/web', 'mockCurrentUser'], + } + return [ isEnvProduction && new MiniCssExtractPlugin({ @@ -139,10 +152,7 @@ const getSharedPlugins = (isEnvProduction) => { React: 'react', PropTypes: 'prop-types', gql: 'graphql-tag', - // Possibly used by storybook? - mockGraphQLQuery: ['@redwoodjs/testing/web', 'mockGraphQLQuery'], - mockGraphQLMutation: ['@redwoodjs/testing/web', 'mockGraphQLMutation'], - mockCurrentUser: ['@redwoodjs/testing/web', 'mockCurrentUser'], + ...devTimeAutoImports, }), // The define plugin will replace these keys with their values during build // time. Note that they're used in packages/web/src/config.ts, and made available in globalThis @@ -174,6 +184,8 @@ module.exports = (webpackEnv) => { const shouldIncludeFastRefresh = redwoodConfig.web.experimentalFastRefresh && !isEnvProduction + const webBabelOptions = getWebSideDefaultBabelConfig() + return { mode: isEnvProduction ? 'production' : 'development', devtool: isEnvProduction ? 'source-map' : 'cheap-module-source-map', @@ -264,46 +276,32 @@ module.exports = (webpackEnv) => { }, // (1) { - test: /\.(js|mjs|jsx)$/, + test: /\.(js|mjs|jsx|ts|tsx)$/, exclude: /(node_modules)/, use: [ { loader: 'babel-loader', options: { + ...webBabelOptions, cwd: redwoodPaths.base, plugins: [ shouldIncludeFastRefresh && require.resolve('react-refresh/babel'), + ...webBabelOptions.plugins, ].filter(Boolean), }, }, ].filter(Boolean), }, // (2) - { - test: /\.(ts|tsx)$/, - exclude: /(node_modules)/, - use: [ - { - loader: 'babel-loader', - options: { - cwd: redwoodPaths.base, - plugins: [ - shouldIncludeFastRefresh && - require.resolve('react-refresh/babel'), - ].filter(Boolean), - }, - }, - ].filter(Boolean), - }, - // .module.css (3), .css (4), .module.scss (5), .scss (6) + // .module.css (2), .css (3), .module.scss (4), .scss (5) ...getStyleLoaders(isEnvProduction), - // (7) + // (6) isEnvProduction && { test: require.resolve('@redwoodjs/router/dist/splash-page'), use: 'null-loader', }, - // (8) + // (7) { test: /\.(svg|ico|jpg|jpeg|png|gif|eot|otf|webp|ttf|woff|woff2|cur|ani|pdf)(\?.*)?$/, type: 'asset/resource', diff --git a/packages/create-redwood-app/template/babel.config.js b/packages/create-redwood-app/template/babel.config.js deleted file mode 100644 index faf653ff5441..000000000000 --- a/packages/create-redwood-app/template/babel.config.js +++ /dev/null @@ -1,4 +0,0 @@ -/** @type {import('@babel/core').TransformOptions} */ -module.exports = { - presets: ['@redwoodjs/core/config/babel-preset'], -} diff --git a/packages/create-redwood-app/template/web/.babelrc.js b/packages/create-redwood-app/template/web/.babelrc.js deleted file mode 100644 index 8382a67fcaf1..000000000000 --- a/packages/create-redwood-app/template/web/.babelrc.js +++ /dev/null @@ -1,2 +0,0 @@ -/** @type {import('@babel/core').TransformOptions} */ -module.exports = { extends: "../babel.config.js" } diff --git a/packages/eslint-config/index.js b/packages/eslint-config/index.js index 4380253e3fdd..9dd727c65f46 100644 --- a/packages/eslint-config/index.js +++ b/packages/eslint-config/index.js @@ -1,13 +1,47 @@ // This is the ESLint configuation used by Redwood projects. -const { getConfig } = require('@redwoodjs/internal') +// Shared eslint config (projects and framework) is located in ./shared.js +// Framework main config is in monorepo root ./.eslintrc.js + +const { + getConfig, + getCommonPlugins, + getWebSideDefaultBabelConfig, + getApiSideDefaultBabelConfig, +} = require('@redwoodjs/internal') const config = getConfig() +const getProjectBabelOptions = () => { + // We cant nest the web overrides inside the overrides block + // So we just take it out and put it as a separate item + // Ignoring ovverrides, as I don't think it has any impact on linting + const { overrides: _overrides, ...otherWebConfig } = + getWebSideDefaultBabelConfig() + + return { + plugins: getCommonPlugins(), + overrides: [ + { + test: ['./api/', './scripts/'], + ...getApiSideDefaultBabelConfig(), + }, + { + test: ['./web/'], + ...otherWebConfig, + }, + ], + } +} + module.exports = { extends: [ './shared.js', config.web.a11y && 'plugin:jsx-a11y/recommended', ].filter(Boolean), + parserOptions: { + requireConfigFile: false, + babelOptions: getProjectBabelOptions(), + }, overrides: [ { files: ['web/src/Routes.js', 'web/src/Routes.tsx'], diff --git a/packages/eslint-config/package.json b/packages/eslint-config/package.json index 101917a8bb5e..f7edc56ab40c 100644 --- a/packages/eslint-config/package.json +++ b/packages/eslint-config/package.json @@ -7,6 +7,7 @@ "@babel/core": "7.15.8", "@babel/eslint-parser": "7.15.8", "@babel/eslint-plugin": "7.14.5", + "@redwoodjs/internal": "0.38.3", "@typescript-eslint/eslint-plugin": "5.1.0", "@typescript-eslint/parser": "5.1.0", "eslint": "7.32.0", diff --git a/packages/eslint-config/shared.js b/packages/eslint-config/shared.js index a557a9c8bdd2..ca9afbef05d9 100644 --- a/packages/eslint-config/shared.js +++ b/packages/eslint-config/shared.js @@ -13,16 +13,6 @@ // [^1] https://eslint.org/docs/rules/ // [^2] https://www.npmjs.com/package/eslint-plugin-react#list-of-supported-rules -const findUp = require('findup-sync') - -const babelConfigPath = (cwd = process.env.RWJS_CWD ?? process.cwd()) => { - const configPath = findUp('babel.config.js', { cwd }) - if (!configPath) { - throw new Error(`Eslint-parser could not find a "babel.config.js" file`) - } - return configPath -} - module.exports = { extends: [ 'eslint:recommended', @@ -30,12 +20,8 @@ module.exports = { 'plugin:prettier/recommended', 'plugin:jest-dom/recommended', ], + // @NOTE parserOptions defined separately for project and framework parser: '@babel/eslint-parser', - parserOptions: { - babelOptions: { - configFile: babelConfigPath(), - }, - }, plugins: [ 'prettier', '@babel', diff --git a/packages/internal/src/__tests__/build_api.test.ts b/packages/internal/src/__tests__/build_api.test.ts index 398b6633d6c1..fadb1361ef1f 100644 --- a/packages/internal/src/__tests__/build_api.test.ts +++ b/packages/internal/src/__tests__/build_api.test.ts @@ -124,25 +124,66 @@ test('api prebuild finds babel.config.js', () => { expect(p).toEqual('api/babel.config.js') }) -test('api prebuild uses babel config', () => { +test('api prebuild uses babel config only from the api side root', () => { const p = prebuiltFiles.filter((p) => p.endsWith('dog.js')).pop() const code = fs.readFileSync(p, 'utf-8') - const firstLine = stripInlineSourceMap(code).split('\n')[0] - expect(firstLine).toMatchInlineSnapshot(`"import dog from \\"dog-bless\\";"`) + expect(code).toContain(`import dog from "dog-bless";`) + + // Should ignore root babel config + expect(code).not.toContain(`import kitty from "kitty-purr"`) +}) + +// Still a bit of a mystery why this plugin isn't transforming gql tags +test.skip('api prebuild transforms gql with `babel-plugin-graphql-tag`', () => { + // babel-plugin-graphql-tag should transpile the "gql" parts of our files, + // achieving the following: + // 1. removing the `graphql-tag` import + // 2. convert the gql syntax into graphql's ast. + // + // https://www.npmjs.com/package/babel-plugin-graphql-tag + const builtFiles = prebuildApiFiles(findApiFiles()) + const p = builtFiles + .filter((x) => typeof x !== 'undefined') + .filter((p) => p.endsWith('todos.sdl.js')) + .pop() + + const code = fs.readFileSync(p, 'utf-8') + expect(code.includes('import gql from "graphql-tag";')).toEqual(false) + expect(code.includes('gql`')).toEqual(false) }) test('Pretranspile polyfills unsupported functionality', () => { const p = prebuiltFiles.filter((p) => p.endsWith('polyfill.js')).pop() const code = fs.readFileSync(p, 'utf-8') - const firstLine = stripInlineSourceMap(code).split('\n')[0] - expect(firstLine).toMatchInlineSnapshot( - `"import \\"core-js/modules/esnext.string.replace-all.js\\";"` + expect(code).toContain( + 'import _replaceAllInstanceProperty from "@babel/runtime-corejs3/core-js/instance/replace-all"' ) }) -function stripInlineSourceMap(src: string): string { - return src - .split('\n') - .filter((line) => !line.startsWith('//# sourceMappingURL=')) - .join('\n') -} +test('Pretranspile uses corejs3 aliasing', () => { + // See https://babeljs.io/docs/en/babel-plugin-transform-runtime#core-js-aliasing + // This is because we configure the transform runtime plugin corejs + + const p = prebuiltFiles.filter((p) => p.endsWith('transform.js')).pop() + const code = fs.readFileSync(p, 'utf-8') + + // Polyfill for Symbol + expect(code).toContain( + `import _Symbol from "@babel/runtime-corejs3/core-js/symbol"` + ) + + // Polyfill for Promise + expect(code).toContain( + `import _Promise from "@babel/runtime-corejs3/core-js/promise"` + ) + + // Polyfill for .includes + expect(code).toContain( + 'import _includesInstanceProperty from "@babel/runtime-corejs3/core-js/instance/includes"' + ) + + // Polyfill for .iterator + expect(code).toContain( + `import _getIterator from "@babel/runtime-corejs3/core-js/get-iterator"` + ) +}) diff --git a/packages/internal/src/build/babel/api.ts b/packages/internal/src/build/babel/api.ts index 4c38dc590a88..f8adc8411be0 100644 --- a/packages/internal/src/build/babel/api.ts +++ b/packages/internal/src/build/babel/api.ts @@ -4,12 +4,51 @@ import path from 'path' import type { TransformOptions } from '@babel/core' import * as babel from '@babel/core' -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore Not inside tsconfig rootdir -import pkgJson from '../../../package.json' import { getPaths } from '../../paths' -import { registerBabel, RegisterHookOptions } from './common' +import { + registerBabel, + RegisterHookOptions, + CORE_JS_VERSION, + RUNTIME_CORE_JS_VERSION, + getCommonPlugins, +} from './common' + +const TARGETS_NODE = '12.16' +// Warning! Use the minor core-js version: "corejs: '3.6'", instead of "corejs: 3", +// because we want to include the features added in the minor version. +// https://github.com/zloirock/core-js/blob/master/README.md#babelpreset-env + +// Use preset env in all cases other than actual build +// e.g. jest, console and exec - because they rely on having transpiled code +export const getApiSideBabelPresets = ( + { presetEnv } = { presetEnv: false } +) => { + return [ + '@babel/preset-typescript', + // Preset-env is required when we are not doing the transpilation with esbuild + presetEnv && [ + '@babel/preset-env', + { + targets: { + node: TARGETS_NODE, + }, + useBuiltIns: 'usage', + corejs: { + version: CORE_JS_VERSION, + // List of supported proposals: https://github.com/zloirock/core-js/blob/master/docs/2019-03-19-core-js-3-babel-and-a-look-into-the-future.md#ecmascript-proposals + proposals: true, + }, + exclude: [ + // Remove class-properties from preset-env, and include separately with loose + // https://github.com/webpack/webpack/issues/9708 + '@babel/plugin-proposal-class-properties', + '@babel/plugin-proposal-private-methods', + ], + }, + ], + ].filter(Boolean) as TransformOptions['presets'] +} export const getApiSideBabelPlugins = () => { const rwjsPaths = getPaths() @@ -17,22 +56,49 @@ export const getApiSideBabelPlugins = () => { // a custom "name" is supplied so that user's do not accidently overwrite // Redwood's own plugins when they specify their own. - const corejsMajorMinorVersion = pkgJson.dependencies['core-js'] - .split('.') - .splice(0, 2) - .join('.') // Gives '3.16' instead of '3.16.12' + // const corejsMajorMinorVersion = pkgJson.dependencies['core-js'] + // .split('.') + // .splice(0, 2) + // .join('.') // Gives '3.16' instead of '3.16.12' const plugins: TransformOptions['plugins'] = [ + ...getCommonPlugins(), ['@babel/plugin-transform-typescript', undefined, 'rwjs-babel-typescript'], + // [ + // 'babel-plugin-polyfill-corejs3', + // { + // method: 'usage-global', + // corejs: corejsMajorMinorVersion, + // proposals: true, // Bug: https://github.com/zloirock/core-js/issues/978#issuecomment-904839852 + // targets: { node: TARGETS_NODE }, // Netlify defaults NodeJS 12: https://answers.netlify.com/t/aws-lambda-now-supports-node-js-14/31789/3 + // }, + // 'rwjs-babel-polyfill', + // ], + + /** + * Uses modular polyfills from @babel/runtime-corejs3 but means + * @babel/runtime-corejs3 MUST be included as a dependency (esp on the api side) + * + * Before: import "core-js/modules/esnext.string.replace-all.js" + * which pollutes the global scope + * After: import _replaceAllInstanceProperty from "@babel/runtime-corejs3/core-js/instance/replace-all" + * See packages/internal/src/__tests__/build_api.test.ts for examples + * + * its important that we have @babel/runtime-corejs3 as a RUNTIME dependency on rwjs/api + * See table on https://babeljs.io/docs/en/babel-plugin-transform-runtime#corejs + * + */ [ - 'babel-plugin-polyfill-corejs3', + '@babel/plugin-transform-runtime', { - method: 'usage-global', - corejs: corejsMajorMinorVersion, - proposals: true, // Bug: https://github.com/zloirock/core-js/issues/978#issuecomment-904839852 - targets: { node: 12 }, // Netlify defaults NodeJS 12: https://answers.netlify.com/t/aws-lambda-now-supports-node-js-14/31789/3 + // https://babeljs.io/docs/en/babel-plugin-transform-runtime/#core-js-aliasing + // Setting the version here also requires `@babel/runtime-corejs3` + corejs: { version: 3, proposals: true }, + // https://babeljs.io/docs/en/babel-plugin-transform-runtime/#version + // Transform-runtime assumes that @babel/runtime@7.0.0 is installed. + // Specifying the version can result in a smaller bundle size. + version: RUNTIME_CORE_JS_VERSION, }, - 'rwjs-babel-polyfill', ], [ require('../babelPlugins/babel-plugin-redwood-src-alias').default, @@ -82,7 +148,17 @@ export const getApiSideBabelConfigPath = () => { if (fs.existsSync(p)) { return p } else { - return undefined + return false + } +} + +export const getApiSideDefaultBabelConfig = () => { + return { + presets: getApiSideBabelPresets(), + plugins: getApiSideBabelPlugins(), + configFile: getApiSideBabelConfigPath(), + babelrc: false, + ignore: ['node_modules'], } } @@ -91,12 +167,15 @@ export const registerApiSideBabelHook = ({ plugins = [], ...rest }: RegisterHookOptions = {}) => { + const defaultOptions = getApiSideDefaultBabelConfig() + registerBabel({ - configFile: getApiSideBabelConfigPath(), // incase user has a custom babel.config.js in api - babelrc: false, // Disables `.babelrc` config + ...defaultOptions, + presets: getApiSideBabelPresets({ + presetEnv: true, + }), extensions: ['.js', '.ts'], - plugins: [...getApiSideBabelPlugins(), ...plugins], - ignore: ['node_modules'], + plugins: [...defaultOptions.plugins, ...plugins], cache: false, ...rest, }) @@ -110,11 +189,11 @@ export const prebuildFile = ( plugins: TransformOptions['plugins'] ) => { const code = fs.readFileSync(srcPath, 'utf-8') + const defaultOptions = getApiSideDefaultBabelConfig() const result = babel.transform(code, { + ...defaultOptions, cwd: getPaths().api.base, - babelrc: false, // Disables `.babelrc` config - configFile: getApiSideBabelConfigPath(), filename: srcPath, // we set the sourceFile (for the sourcemap) as a correct, relative path // this is why this function (prebuildFile) must know about the dstPath diff --git a/packages/internal/src/build/babel/common.ts b/packages/internal/src/build/babel/common.ts index 1635842936f3..429f3ba2ad27 100644 --- a/packages/internal/src/build/babel/common.ts +++ b/packages/internal/src/build/babel/common.ts @@ -1,5 +1,7 @@ import type { TransformOptions, PluginItem } from '@babel/core' +const pkgJson = require('../../../package.json') + export interface RegisterHookOptions { /** * Be careful: plugins are a nested array e.g. [[plug1, x, x], [plug2, y, y]]. @@ -30,3 +32,33 @@ interface BabelRegisterOptions extends TransformOptions { export const registerBabel = (options: BabelRegisterOptions) => { require('@babel/register')(options) } + +export const CORE_JS_VERSION = pkgJson.dependencies['core-js'] + .split('.') + .slice(0, 2) + .join('.') // Produces: 3.12, instead of 3.12.1 + +if (!CORE_JS_VERSION) { + throw new Error( + 'RedwoodJS Project Babel: Could not determine core-js version.' + ) +} + +export const RUNTIME_CORE_JS_VERSION = + pkgJson.dependencies['@babel/runtime-corejs3'] +if (!RUNTIME_CORE_JS_VERSION) { + throw new Error( + 'RedwoodJS Project Babel: Could not determine core-js runtime version' + ) +} + +export const getCommonPlugins = () => { + return [ + ['@babel/plugin-proposal-class-properties', { loose: true }], + // Note: The private method loose mode configuration setting must be the + // same as @babel/plugin-proposal class-properties. + // (https://babeljs.io/docs/en/babel-plugin-proposal-private-methods#loose) + ['@babel/plugin-proposal-private-methods', { loose: true }], + ['@babel/plugin-proposal-private-property-in-object', { loose: true }], + ] +} diff --git a/packages/internal/src/build/babel/web.ts b/packages/internal/src/build/babel/web.ts index d7cfb1e57b5b..52290fdb29be 100644 --- a/packages/internal/src/build/babel/web.ts +++ b/packages/internal/src/build/babel/web.ts @@ -1,40 +1,186 @@ import fs from 'fs' import path from 'path' +import type { TransformOptions } from '@babel/core' + import { getPaths } from '../../paths' -import { registerBabel, RegisterHookOptions } from './common' +import { + CORE_JS_VERSION, + getCommonPlugins, + registerBabel, + RegisterHookOptions, +} from './common' + +export const getWebSideBabelPlugins = () => { + const rwjsPaths = getPaths() + + const plugins: TransformOptions['plugins'] = [ + ...getCommonPlugins(), + + // === Import path handling + [ + require('../babelPlugins/babel-plugin-redwood-src-alias').default, + { + srcAbsPath: rwjsPaths.web.src, + }, + 'rwjs-babel-src-alias', + ], + [ + require('../babelPlugins/babel-plugin-redwood-directory-named-import') + .default, + undefined, + 'rwjs-directory-named-modules', + ], -// TODO: move web side babel plugins here too when we pretranspile web side -// and export getWebSideBabelPlugins + // === Auto imports, and transforms + [ + 'babel-plugin-auto-import', + { + declarations: [ + { + // import { React } from 'react' + default: 'React', + path: 'react', + }, + { + // import PropTypes from 'prop-types' + default: 'PropTypes', + path: 'prop-types', + }, + { + // import gql from 'graphql-tag' + default: 'gql', + path: 'graphql-tag', + }, + ], + }, + 'rwjs-web-auto-import', + ], + ['babel-plugin-graphql-tag', undefined, 'rwjs-babel-graphql-tag'], + [ + 'inline-react-svg', + { + svgo: { + plugins: [ + { + name: 'removeAttrs', + params: { attrs: '(data-name)' }, + }, + // Otherwise having style="xxx" breaks + 'convertStyleToAttrs', + ], + }, + }, + 'rwjs-inline-svg', + ], + + // === Handling redwood "magic" + ].filter(Boolean) + + return plugins +} + +export const getWebSideOverrides = ( + { staticImports } = { + staticImports: false, + } +) => { + const overrides = [ + { + test: /.+Cell.(js|tsx)$/, + plugins: [require('../babelPlugins/babel-plugin-redwood-cell').default], + }, + // Automatically import files in `./web/src/pages/*` in to + // the `./web/src/Routes.[ts|jsx]` file. + { + test: /web\/src\/Routes.(js|tsx)$/, + plugins: [ + [ + require('../babelPlugins/babel-plugin-redwood-routes-auto-loader') + .default, + { + useStaticImports: staticImports, + }, + ], + ], + }, + // ** Files ending in `Cell.mock.[js,ts]` ** + // Automatically determine keys for saving and retrieving mock data. + // Only required for storybook and jest + process.env.NODE_ENV !== 'production' && { + test: /.+Cell.mock.(js|ts)$/, + plugins: [ + require('../babelPlugins/babel-plugin-redwood-mock-cell-data').default, + ], + }, + ].filter(Boolean) + + return overrides as TransformOptions[] +} + +export const getWebSideBabelPresets = () => { + return [ + '@babel/preset-react', + '@babel/preset-typescript', + [ + '@babel/preset-env', + { + // the targets are set in /web/package.json + useBuiltIns: 'usage', + corejs: { + version: CORE_JS_VERSION, + proposals: true, + }, + exclude: [ + // Remove class-properties from preset-env, and include separately + // https://github.com/webpack/webpack/issues/9708 + '@babel/plugin-proposal-class-properties', + '@babel/plugin-proposal-private-methods', + ], + }, + ], + ] +} export const getWebSideBabelConfigPath = () => { - // NOTE: web side has .babel.rc still, not babel.config.js - // This should be handled in the prebuild for web PR - const babelRcPath = path.join(getPaths().web.base, '.babelrc.js') - if (fs.existsSync(babelRcPath)) { - return babelRcPath + const customBabelConfig = path.join(getPaths().web.base, 'babel.config.js') + if (fs.existsSync(customBabelConfig)) { + return customBabelConfig } else { return undefined } } +export const getWebSideDefaultBabelConfig = () => { + // NOTE: + // Even though we specify the config file, babel will still search for .babelrc + // and merge them because we have specified the filename property, unless babelrc = false + + return { + presets: getWebSideBabelPresets(), + plugins: getWebSideBabelPlugins(), + overrides: getWebSideOverrides(), + extends: getWebSideBabelConfigPath(), + babelrc: false, + ignore: ['node_modules'], + } +} + // Used in prerender only currently export const registerWebSideBabelHook = ({ plugins = [], - overrides, + overrides = [], }: RegisterHookOptions = {}) => { - // NOTE: - // Even though we specify the config file, babel will still search for .babelrc - // and merge them because we have specified the filename property, unless babelrc = false + const defaultOptions = getWebSideDefaultBabelConfig() registerBabel({ + ...defaultOptions, root: getPaths().base, - configFile: getWebSideBabelConfigPath(), // incase user has a custom babel.config.js in api - babelrc: false, extensions: ['.js', '.ts', '.tsx', '.jsx'], - plugins: [...plugins], - ignore: [/node_modules/], + plugins: [...defaultOptions.plugins, ...plugins], cache: false, - overrides, + // We only register for prerender currently + // Static importing pages makes sense + overrides: [...getWebSideOverrides({ staticImports: true }), ...overrides], }) } diff --git a/packages/internal/src/index.ts b/packages/internal/src/index.ts index 677f29f3d3d9..ed3561c705c2 100644 --- a/packages/internal/src/index.ts +++ b/packages/internal/src/index.ts @@ -12,3 +12,4 @@ export * from './validateSchema' // Babel helpers export * from './build/babel/api' export * from './build/babel/web' +export * from './build/babel/common' diff --git a/packages/internal/tsconfig.json b/packages/internal/tsconfig.json index abab26201af5..04de4467b027 100644 --- a/packages/internal/tsconfig.json +++ b/packages/internal/tsconfig.json @@ -6,7 +6,7 @@ "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo", "outDir": "dist", }, - "include": ["src/**/*", "./ambient.d.ts", "src/../package.json"], + "include": ["src/**/*", "./ambient.d.ts"], "references": [ { "path": "../graphql-server" }, // ODD, but we do this so we dont have to have internal as a runtime dependency ] diff --git a/packages/testing/config/jest/api/index.js b/packages/testing/config/jest/api/index.js index 760bde242fbd..49163b3b27cc 100644 --- a/packages/testing/config/jest/api/index.js +++ b/packages/testing/config/jest/api/index.js @@ -1,6 +1,10 @@ const path = require('path') -const { getPaths } = require('@redwoodjs/internal') +const { + getPaths, + getApiSideDefaultBabelConfig, + getApiSideBabelPresets, +} = require('@redwoodjs/internal') const rwjsPaths = getPaths() const NODE_MODULES_PATH = path.join(rwjsPaths.base, 'node_modules') @@ -21,4 +25,15 @@ module.exports = { '@redwoodjs/testing/api' ), }, + transform: { + '\\.[jt]sx?$': [ + 'babel-jest', + { + ...getApiSideDefaultBabelConfig(), + presets: getApiSideBabelPresets({ + presetEnv: true, // jest needs code transpiled + }), + }, + ], + }, } diff --git a/packages/testing/config/jest/web/index.js b/packages/testing/config/jest/web/index.js index 70fc6e15a014..85f57fa2ae76 100644 --- a/packages/testing/config/jest/web/index.js +++ b/packages/testing/config/jest/web/index.js @@ -1,13 +1,17 @@ const path = require('path') const { TextDecoder } = require('util') -const { getPaths } = require('@redwoodjs/internal') +const { + getPaths, + getWebSideDefaultBabelConfig, +} = require('@redwoodjs/internal') const rwjsPaths = getPaths() const NODE_MODULES_PATH = path.join(rwjsPaths.base, 'node_modules') module.exports = { roots: ['/src/'], + testEnvironment: 'jest-environment-jsdom', displayName: { color: 'blueBright', name: 'web', @@ -50,5 +54,7 @@ module.exports = { '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga|css)$': '@redwoodjs/testing/dist/web/fileMock.js', }, - testEnvironment: 'jest-environment-jsdom', + transform: { + '\\.[jt]sx?$': ['babel-jest', getWebSideDefaultBabelConfig()], + }, } diff --git a/packages/testing/config/storybook/.babelrc b/packages/testing/config/storybook/.babelrc deleted file mode 100644 index 2122bbfd9135..000000000000 --- a/packages/testing/config/storybook/.babelrc +++ /dev/null @@ -1,5 +0,0 @@ -const path = require('path') - -const { getPaths } = require('@redwoodjs/internal') - -module.export = require(path.join(getPaths().web.base, '.babelrc.js')) diff --git a/yarn.lock b/yarn.lock index 9069649e71ec..4e75c77c2397 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4071,13 +4071,13 @@ __metadata: resolution: "@redwoodjs/api@workspace:packages/api" dependencies: "@babel/cli": 7.15.7 + "@babel/runtime-corejs3": 7.15.4 "@prisma/client": 3.3.0 "@redwoodjs/auth": 0.38.3 "@types/crypto-js": 4.0.2 "@types/jsonwebtoken": 8.5.5 "@types/md5": 2.3.1 aws-lambda: 1.0.6 - core-js: 3.18.3 crypto-js: 4.1.1 jest: 27.3.1 jsonwebtoken: 8.5.1 @@ -4140,6 +4140,7 @@ __metadata: dotenv-defaults: 3.0.0 envinfo: 7.8.1 execa: 5.1.1 + fast-glob: 3.2.7 fs-extra: 10.0.0 humanize-string: 2.1.0 jest: 27.3.1 @@ -4263,6 +4264,7 @@ __metadata: "@babel/core": 7.15.8 "@babel/eslint-parser": 7.15.8 "@babel/eslint-plugin": 7.14.5 + "@redwoodjs/internal": 0.38.3 "@typescript-eslint/eslint-plugin": 5.1.0 "@typescript-eslint/parser": 5.1.0 eslint: 7.32.0