diff --git a/apps/rr7/README.md b/apps/rr7/README.md index aeecf8161ba..3d70ef878a2 100644 --- a/apps/rr7/README.md +++ b/apps/rr7/README.md @@ -27,4 +27,4 @@ make backend-docker-start ## About this app -- [Remix Docs](https://remix.run/docs/en/main) +- [Remix Docs](https://reactrouter.com/start/framework/installation) diff --git a/docs/source/development/add-ons/index.md b/docs/source/development/add-ons/index.md index 106ad0464ad..87de3d60b64 100644 --- a/docs/source/development/add-ons/index.md +++ b/docs/source/development/add-ons/index.md @@ -19,6 +19,7 @@ load-add-on-configuration create-an-add-on-18 create-an-add-on-17 theme-add-on +test-add-ons-19 test-add-ons-18 test-add-ons-17 extend-webpack-add-on diff --git a/docs/source/development/add-ons/test-add-ons-18.md b/docs/source/development/add-ons/test-add-ons-18.md index 871a7f95a5a..854e124aaed 100644 --- a/docs/source/development/add-ons/test-add-ons-18.md +++ b/docs/source/development/add-ons/test-add-ons-18.md @@ -99,13 +99,6 @@ The following code is boilerplate setup for Vitest. ```javascript import '@testing-library/jest-dom'; -import { expect, describe, it, vi } from 'vitest'; - -// Make Vitest globals available throughout the test suite -global.describe = describe; -global.it = it; -global.expect = expect; -global.vi = vi; // Stub the global fetch API to prevent actual network requests in tests vi.stubGlobal('fetch', vi.fn(() => diff --git a/docs/source/development/add-ons/test-add-ons-19.md b/docs/source/development/add-ons/test-add-ons-19.md new file mode 100644 index 00000000000..6f6c857a389 --- /dev/null +++ b/docs/source/development/add-ons/test-add-ons-19.md @@ -0,0 +1,15 @@ +--- +myst: + html_meta: + "description": "Test add-ons in Volto 19" + "property=og:description": "Test add-ons in Volto 19" + "property=og:title": "Test add-ons in Volto 19" + "keywords": "Volto, Plone, testing, test, CI, add-ons, Vitest, Jest" +--- + +# Test add-ons in Volto 19 + +In Volto 19, Jest has been completely removed, and add-ons that rely on Jest-based test suites are no longer supported. +You must migrate your add-on tests to Vitest. + +See the guide {doc}`test-add-ons-18` for how to migrate your add-on tests from Jest to Vitest. diff --git a/docs/source/upgrade-guide/index.md b/docs/source/upgrade-guide/index.md index 451906a2794..5c7d51b804e 100644 --- a/docs/source/upgrade-guide/index.md +++ b/docs/source/upgrade-guide/index.md @@ -48,6 +48,38 @@ You should take the following actions for your Volto 19 projects. If you can't upgrade immediately, you may continue to run Volto 19 on Node.js 20 at your own risk, but be aware that issues specific to Node.js 20 will not be fixed in the Volto core CI or releases. +(replace-razzle-with-volto-razzle)= + +### Replace `razzle` with `@volto/razzle` (fork) +```{versionchanged} Volto 19.0.0-alpha.14 +``` + +`@volto/razzle` is a fork of the upstream `razzle` package that contains Volto-specific fixes and patches. +Use `@volto/razzle` in your Volto 19 projects when either you need the Volto-compatible build behavior, or the Volto team provides temporary patches that are not yet merged upstream in Razzle. + +For most projects, no action is required. +The fork maintains full compatibility with the original `razzle` package, preserving all CLI entry points such as `razzle start`, `razzle build`, and `razzle test`. + +However, if you have customized Volto's internals in your project—for example, by importing internal modules directly from the `razzle` package such as `require('razzle/some/path')`—then you need to update those imports to reference `@volto/razzle` instead. + +To verify whether your project requires updates, search for any direct references to internal `razzle` modules: + +```shell +grep -R "require.*razzle/" -n --exclude-dir=node_modules || true +grep -R "from.*razzle/" -n --exclude-dir=node_modules || true +``` + +If you find any matches, check in particular: + +- build and Babel configurations, including {file}`babel.config.js`, {file}`.babelrc`, {file}`webpack.config.js`, and {file}`razzle.config.js` +- any presets or plugins sections that import internal `razzle` modules +- custom build scripts that reference `razzle` internals + +```{note} +The fork exists so we can ship fixes and compatibility patches required by Volto, since the upstream is no longer maintained. +Our goal is to keep `@volto/razzle` compatible with the `razzle` public API. +``` + ### `pnpm` has been upgraded to version 10 ```{versionchanged} Volto 19.0.0-alpha.7 ``` diff --git a/package.json b/package.json index bd4941d6e49..4b091824818 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "start:project": "pnpm --filter plone run start", "lint": "make build-all-deps && eslint --max-warnings=0 '{apps,packages}/**/*.{js,jsx,ts,tsx}'", "lint:volto": "pnpm --filter @plone/volto run lint", - "test": "pnpm --filter @plone/volto run vitest", + "test": "pnpm --filter @plone/volto run test", "test:ci": "pnpm --filter @plone/volto run test:ci", "i18n": "pnpm --filter @plone/volto run i18n", "i18n:ci": "pnpm --filter @plone/volto i18n:ci", @@ -45,12 +45,7 @@ "packageManager": "pnpm@10.20.0", "pnpm": { "overrides": { - "@pmmmwh/react-refresh-webpack-plugin": "0.5.11", - "immer@8": "10.1.3", - "react-refresh": "0.14.0" - }, - "patchedDependencies": { - "jest-resolve@26.6.2": "patches/jest-resolve@26.6.2.patch" + "immer@8": "10.1.3" }, "ignoredBuiltDependencies": [ "@parcel/watcher" diff --git a/packages/scripts/news/7619.breaking b/packages/scripts/news/7619.breaking new file mode 100644 index 00000000000..043946e0bac --- /dev/null +++ b/packages/scripts/news/7619.breaking @@ -0,0 +1 @@ +Fork `babel-razzle-preset` from Razzle. Update dependencies. @sneridagh diff --git a/packages/scripts/package.json b/packages/scripts/package.json index 5bbc2d04732..7f625c12d17 100644 --- a/packages/scripts/package.json +++ b/packages/scripts/package.json @@ -42,7 +42,7 @@ "dependencies": { "@babel/core": "^7.0.0", "babel-plugin-react-intl": "5.1.17", - "babel-preset-razzle": "4.2.17", + "@plone/babel-preset-razzle": "workspace:^", "chalk": "4", "commander": "8.2.0", "comment-json": "^4.2.3", diff --git a/packages/volto-babel-preset-razzle/.release-it.json b/packages/volto-babel-preset-razzle/.release-it.json new file mode 100644 index 00000000000..9643c3f5a2e --- /dev/null +++ b/packages/volto-babel-preset-razzle/.release-it.json @@ -0,0 +1,29 @@ +{ + "plugins": { + "../scripts/prepublish.js": {} + }, + "hooks": { + "after:bump": [ + "pipx run towncrier build --draft --yes --version ${version} > .changelog.draft", + "pipx run towncrier build --yes --version ${version}" + ], + "after:release": "rm .changelog.draft" + }, + "npm": { + "publish": false + }, + "git": { + "commitArgs": ["--no-verify"], + "changelog": "pipx run towncrier build --draft --yes --version 0.0.0", + "requireUpstream": false, + "requireCleanWorkingDir": false, + "commitMessage": "Release @plone/babel-preset-razzle ${version}", + "tagName": "plone-slate-${version}", + "tagAnnotation": "Release @plone/babel-preset-razzle ${version}" + }, + "github": { + "release": true, + "releaseName": "@plone/babel-preset-razzle ${version}", + "releaseNotes": "cat .changelog.draft" + } +} diff --git a/packages/volto-babel-preset-razzle/CHANGELOG.md b/packages/volto-babel-preset-razzle/CHANGELOG.md new file mode 100644 index 00000000000..f8dc93cb9d8 --- /dev/null +++ b/packages/volto-babel-preset-razzle/CHANGELOG.md @@ -0,0 +1,9 @@ +# @plone/babel-preset-razzle Release Notes + + + + diff --git a/packages/volto-babel-preset-razzle/LICENSE b/packages/volto-babel-preset-razzle/LICENSE new file mode 100644 index 00000000000..2ced0608a83 --- /dev/null +++ b/packages/volto-babel-preset-razzle/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Jared Palmer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/volto-babel-preset-razzle/README.md b/packages/volto-babel-preset-razzle/README.md new file mode 100644 index 00000000000..fce8ba5c69a --- /dev/null +++ b/packages/volto-babel-preset-razzle/README.md @@ -0,0 +1,28 @@ +> [!IMPORTANT] +> This package is a maintained fork of the original [`babel-preset-razzle`](https://github.com/jaredpalmer/razzle/tree/master/packages/babel-preset-razzle). +> The upstream project is currently unmaintained, so we forked it into the Volto monorepo to keep its dependencies updated and address security issues. +> All upstream attributions are preserved below. + +# @plone/babel-preset-razzle + +This package includes the [Babel](https://babeljs.io) preset used by [Razzle](https://github.com/jaredpalmer/razzle). + +## Usage in Razzle projects + +The easiest way to use this configuration is with Razzle, which includes it by default. + +## Usage outside of Razzle + +If you want to use this Babel preset in a project not built with Razzle, you can install it through the following steps. + +First, [install Babel](https://babeljs.io/docs/setup/). + +Then create a file named `.babelrc` with the following contents in the root folder of your project: + +```js +{ + "presets": ["@volto/razzle"] +} +``` + +This preset uses the `useBuiltIns` option with [`transform-object-rest-spread`](http://babeljs.io/docs/plugins/transform-object-rest-spread/), which assumes that `Object.assign` is available or polyfilled. diff --git a/packages/volto-babel-preset-razzle/babel-plugins/commonjs.js b/packages/volto-babel-preset-razzle/babel-plugins/commonjs.js new file mode 100644 index 00000000000..352b708e293 --- /dev/null +++ b/packages/volto-babel-preset-razzle/babel-plugins/commonjs.js @@ -0,0 +1,27 @@ +const commonjsPlugin = require('@babel/plugin-transform-modules-commonjs'); + +module.exports = function (api, options, dirname) { + const commonjs = commonjsPlugin.default(api, options, dirname); + return { + visitor: { + Program: { + exit: function (path, state) { + let foundModuleExports = false; + path.traverse({ + MemberExpression: function (expressionPath) { + if (expressionPath.node.object.name !== 'module') return; + if (expressionPath.node.property.name !== 'exports') return; + foundModuleExports = true; + }, + }); + + if (!foundModuleExports) { + return; + } + + commonjs.visitor.Program.exit.call(this, path, state); + }, + }, + }, + }; +}; diff --git a/packages/volto-babel-preset-razzle/babel-plugins/jsx-pragma.js b/packages/volto-babel-preset-razzle/babel-plugins/jsx-pragma.js new file mode 100644 index 00000000000..ef98c057d09 --- /dev/null +++ b/packages/volto-babel-preset-razzle/babel-plugins/jsx-pragma.js @@ -0,0 +1,97 @@ +'use strict'; + +module.exports = function (opts) { + const t = opts.types; + return { + inherits: require('babel-plugin-syntax-jsx'), + visitor: { + JSXElement: (_path, state) => { + state.set('jsx', true); + }, + + // Fragment syntax is still JSX since it compiles to createElement(), + // but JSXFragment is not a JSXElement + JSXFragment: (_path, state) => { + state.set('jsx', true); + }, + + Program: { + exit: (path, state) => { + if (state.get('jsx')) { + const pragma = t.identifier(state.opts.pragma); + let importAs = pragma; + + // if there's already a React in scope, use that instead of adding an import + const existingBinding = + state.opts.reuseImport !== false && + state.opts.importAs && + path.scope.getBinding(state.opts.importAs); + + // var _jsx = _pragma.createElement; + if (state.opts.property) { + if (state.opts.importAs) { + importAs = t.identifier(state.opts.importAs); + } else { + importAs = path.scope.generateUidIdentifier('pragma'); + } + + const mapping = t.variableDeclaration('var', [ + t.variableDeclarator( + pragma, + t.memberExpression( + importAs, + t.identifier(state.opts.property), + ), + ), + ]); + + // if the React binding came from a require('react'), + // make sure that our usage comes after it. + let newPath; + if ( + existingBinding && + t.isVariableDeclarator(existingBinding.path.node) && + t.isCallExpression(existingBinding.path.node.init) && + t.isIdentifier(existingBinding.path.node.init.callee) && + existingBinding.path.node.init.callee.name === 'require' + ) { + [newPath] = + existingBinding.path.parentPath.insertAfter(mapping); + } else { + // @ts-ignore + [newPath] = path.unshiftContainer('body', mapping); + } + + for (const declar of newPath.get('declarations')) { + path.scope.registerBinding(newPath.node.kind, declar); + } + } + + if (!existingBinding) { + const importSpecifier = t.importDeclaration( + [ + state.opts.import + ? // import { $import as _pragma } from '$module' + t.importSpecifier( + importAs, + t.identifier(state.opts.import), + ) + : state.opts.importNamespace + ? t.importNamespaceSpecifier(importAs) + : // import _pragma from '$module' + t.importDefaultSpecifier(importAs), + ], + t.stringLiteral(state.opts.module || 'react'), + ); + + const [newPath] = path.unshiftContainer('body', importSpecifier); + for (const specifier of newPath.get('specifiers')) { + path.scope.registerBinding('module', specifier); + } + } + } + }, + }, + }, + }; +}; diff --git a/packages/volto-babel-preset-razzle/babel-plugins/no-anonymous-default-export.js b/packages/volto-babel-preset-razzle/babel-plugins/no-anonymous-default-export.js new file mode 100644 index 00000000000..57195026250 --- /dev/null +++ b/packages/volto-babel-preset-razzle/babel-plugins/no-anonymous-default-export.js @@ -0,0 +1,77 @@ +const chalk = require('chalk'); + +module.exports = function (opts) { + const t = opts.types; + let onWarning = null; + opts.caller((caller) => { + onWarning = caller.onWarning; + return ''; // Intentionally empty to not invalidate cache + }); + + if (typeof onWarning !== 'function') { + return { visitor: {} }; + } + + const warn = onWarning; + return { + visitor: { + ExportDefaultDeclaration: function (path) { + const def = path.node.declaration; + + if ( + !( + def.type === 'ArrowFunctionExpression' || + def.type === 'FunctionDeclaration' + ) + ) { + return; + } + + switch (def.type) { + case 'ArrowFunctionExpression': { + warn( + [ + chalk.yellow.bold( + 'Anonymous arrow functions cause Fast Refresh to not preserve local component state.', + ), + 'Please add a name to your function, for example:', + '', + chalk.bold('Before'), + chalk.cyan('export default () =>
;'), + '', + chalk.bold('After'), + chalk.cyan('const Named = () => ;'), + chalk.cyan('export default Named;'), + ].join('\n'), + ); + break; + } + case 'FunctionDeclaration': { + const isAnonymous = !Boolean(def.id); + if (isAnonymous) { + warn( + [ + chalk.yellow.bold( + 'Anonymous function declarations cause Fast Refresh to not preserve local component state.', + ), + 'Please add a name to your function, for example:', + '', + chalk.bold('Before'), + chalk.cyan('export default function () { /* ... */ }'), + '', + chalk.bold('After'), + chalk.cyan('export default function Named() { /* ... */ }'), + ].join('\n'), + ); + } + break; + } + default: { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const never = def; + } + } + }, + }, + }; +}; diff --git a/packages/volto-babel-preset-razzle/babel-plugins/optimize-hook-destructuring.js b/packages/volto-babel-preset-razzle/babel-plugins/optimize-hook-destructuring.js new file mode 100644 index 00000000000..6ab9407db97 --- /dev/null +++ b/packages/volto-babel-preset-razzle/babel-plugins/optimize-hook-destructuring.js @@ -0,0 +1,68 @@ +'use strict'; + +// matches any hook-like (the default) +const isHook = /^use[A-Z]/; + +// matches only built-in hooks provided by React et al +const isBuiltInHook = + /^use(Callback|Context|DebugValue|Effect|ImperativeHandle|LayoutEffect|Memo|Reducer|Ref|State)$/; + +module.exports = function (opts) { + const t = opts.types; + const visitor = { + CallExpression: function (path, state) { + const onlyBuiltIns = state.opts.onlyBuiltIns; + + // if specified, options.lib is a list of libraries that provide hook functions + const libs = + state.opts.lib && + (state.opts.lib === true + ? ['react', 'preact/hooks'] + : [].concat(state.opts.lib)); + + // skip function calls that are not the init of a variable declaration: + if (!t.isVariableDeclarator(path.parent)) return; + + // skip function calls where the return value is not Array-destructured: + if (!t.isArrayPattern(path.parent.id)) return; + + // name of the (hook) function being called: + const hookName = path.node.callee.name; + + if (libs) { + const binding = path.scope.getBinding(hookName); + // not an import + if (!binding || binding.kind !== 'module') return; + + const specifier = binding.path.parent.source.value; + // not a match + if (!libs.some((lib) => lib === specifier)) return; + } + + // only match function calls with names that look like a hook + if (!(onlyBuiltIns ? isBuiltInHook : isHook).test(hookName)) return; + + path.parent.id = t.objectPattern( + path.parent.id.elements.reduce((patterns, element, i) => { + if (element === null) { + return patterns; + } + + return patterns.concat( + t.objectProperty(t.numericLiteral(i), element), + ); + }, []), + ); + }, + }; + + return { + name: 'optimize-hook-destructuring', + visitor: { + // this is a workaround to run before preset-env destroys destructured assignments + Program: function (path, state) { + path.traverse(visitor, state); + }, + }, + }; +}; diff --git a/packages/volto-babel-preset-razzle/index.js b/packages/volto-babel-preset-razzle/index.js new file mode 100644 index 00000000000..b88e5d95626 --- /dev/null +++ b/packages/volto-babel-preset-razzle/index.js @@ -0,0 +1,138 @@ +'use strict'; + +const path = require('path'); +const PluginItem = require('@babel/core').PluginItem; +const env = process.env.NODE_ENV; +const isProduction = env === 'production'; +const isDevelopment = env === 'development'; +const isTest = env === 'test'; + +// Taken from https://github.com/babel/babel/commit/d60c5e1736543a6eac4b549553e107a9ba967051#diff-b4beead8ad9195361b4537601cc22532R158 +const supportsStaticESM = function (caller) { + return !!caller && caller.supportsStaticESM; +}; + +module.exports = function (api, options) { + options = options || {}; + + const supportsESM = api.caller(supportsStaticESM); + const isServer = api.caller(function (caller) { + return !!caller && caller.isServer; + }); + const isModern = api.caller(function (caller) { + return !!caller && caller.isModern; + }); + + const isLaxModern = + isModern || + ((options['preset-env'] || {}).targets && + options['preset-env'].targets.esmodules === true); + + const presetEnvConfig = Object.assign( + { + // In the test environment `modules` is often needed to be set to true, babel figures that out by itself using the `'auto'` option + // In production/development this option is set to `false` so that webpack can handle import/export with tree-shaking + modules: 'auto', + exclude: ['transform-typeof-symbol'], + }, + options['preset-env'] || {}, + ); + + // When transpiling for the server or tests, target the current Node version + // if not explicitly specified: + if ( + (isServer || isTest) && + (!presetEnvConfig.targets || + !( + typeof presetEnvConfig.targets === 'object' && + 'node' in presetEnvConfig.targets + )) + ) { + presetEnvConfig.targets = { + // Targets the current process' version of Node. This requires apps be + // built and deployed on the same version of Node. + node: 'current', + }; + } + + // specify a preset to use instead of @babel/preset-env + const customModernPreset = + isLaxModern && options['experimental-modern-preset']; + + const result = { + sourceType: 'unambiguous', + presets: [ + customModernPreset || [ + require('@babel/preset-env').default, + presetEnvConfig, + ], + [ + require('@babel/preset-react'), + Object.assign( + { + // This adds @babel/plugin-transform-react-jsx-source and + // @babel/plugin-transform-react-jsx-self automatically in development + development: isDevelopment || isTest, + }, + (options['preset-react'] || {}).runtime !== 'automatic' + ? { pragma: '__jsx' } + : {}, + options['preset-react'] || {}, + ), + ], + options['preset-typescript'] !== false && [ + require('@babel/preset-typescript'), + Object.assign( + { allowNamespaces: true, allExtensions: true, isTSX: true }, + options['preset-typescript'] || {}, + ), + ], + ].filter(Boolean), + plugins: [ + (options['preset-react'] || {}).runtime !== 'automatic' && [ + require('./babel-plugins/jsx-pragma'), + { + // This produces the following injected import for modules containing JSX: + // import React from 'react'; + // var __jsx = React.createElement; + module: 'react', + importAs: 'React', + pragma: '__jsx', + property: 'createElement', + }, + ], + [ + require('./babel-plugins/optimize-hook-destructuring'), + { + // only optimize hook functions imported from React/Preact + lib: true, + }, + ], + !isServer && [ + require('@babel/plugin-transform-runtime'), + Object.assign( + { + corejs: false, + helpers: true, + regenerator: true, + useESModules: supportsESM && presetEnvConfig.modules !== 'commonjs', + absoluteRuntime: path.dirname( + require.resolve('@babel/runtime/package.json'), + ), + version: require('@babel/runtime/package.json').version, + }, + options['transform-runtime'] || {}, + ), + ], + isProduction && [ + require('babel-plugin-transform-react-remove-prop-types'), + { + removeImport: true, + }, + ], + isServer && require('@babel/plugin-syntax-bigint'), + ].filter(Boolean), + }; + + return result; +}; diff --git a/packages/volto-babel-preset-razzle/package.json b/packages/volto-babel-preset-razzle/package.json new file mode 100644 index 00000000000..379c0d7e22c --- /dev/null +++ b/packages/volto-babel-preset-razzle/package.json @@ -0,0 +1,43 @@ +{ + "name": "@plone/babel-preset-razzle", + "version": "1.0.0-alpha.0", + "description": "Babel presets for Razzle", + "main": "index.js", + "author": "jaredpalmer", + "repository": { + "type": "git", + "url": "https://github.com:jaredpalmer/razzle.git", + "directory": "packages/babel-preset-razzle" + }, + "license": "MIT", + "files": [ + "index.js", + "babel-plugins/amp-attributes.js", + "babel-plugins/commonjs.js", + "babel-plugins/jsx-pragma.js", + "babel-plugins/no-anonymous-default-export.js", + "babel-plugins/optimize-hook-destructuring.js" + ], + "scripts": { + "dry-release": "release-it --dry-run", + "release": "release-it", + "release-major-alpha": "release-it major --preRelease=alpha", + "release-alpha": "release-it --preRelease=alpha" + }, + "dependencies": { + "@babel/core": "^7.28.5", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-transform-modules-commonjs": "^7.10.4", + "@babel/plugin-transform-runtime": "^7.9.0", + "@babel/preset-env": "^7.28.5", + "@babel/preset-react": "^7.28.5", + "@babel/preset-typescript": "^7.28.5", + "@babel/runtime": "^7.28.4", + "babel-plugin-syntax-jsx": "^6.18.0", + "babel-plugin-transform-react-remove-prop-types": "^0.4.24", + "chalk": "^5.6.2" + }, + "devDependencies": { + "release-it": "^19.0.5" + } +} diff --git a/packages/volto-babel-preset-razzle/towncrier.toml b/packages/volto-babel-preset-razzle/towncrier.toml new file mode 100644 index 00000000000..3ef721f3785 --- /dev/null +++ b/packages/volto-babel-preset-razzle/towncrier.toml @@ -0,0 +1,33 @@ +[tool.towncrier] +filename = "CHANGELOG.md" +directory = "news/" +title_format = "## {version} ({project_date})" +underlines = ["", "", ""] +template = "../scripts/templates/towncrier_template.jinja" +start_string = "\n" +issue_format = "[#{issue}](https://github.com/plone/volto/issues/{issue})" + +[[tool.towncrier.type]] +directory = "breaking" +name = "Breaking" +showcontent = true + +[[tool.towncrier.type]] +directory = "feature" +name = "Feature" +showcontent = true + +[[tool.towncrier.type]] +directory = "bugfix" +name = "Bugfix" +showcontent = true + +[[tool.towncrier.type]] +directory = "internal" +name = "Internal" +showcontent = true + +[[tool.towncrier.type]] +directory = "documentation" +name = "Documentation" +showcontent = true diff --git a/packages/volto-razzle/.release-it.json b/packages/volto-razzle/.release-it.json new file mode 100644 index 00000000000..1a3e6a32c17 --- /dev/null +++ b/packages/volto-razzle/.release-it.json @@ -0,0 +1,29 @@ +{ + "plugins": { + "../scripts/prepublish.js": {} + }, + "hooks": { + "after:bump": [ + "pipx run towncrier build --draft --yes --version ${version} > .changelog.draft", + "pipx run towncrier build --yes --version ${version}" + ], + "after:release": "rm .changelog.draft" + }, + "npm": { + "publish": false + }, + "git": { + "commitArgs": ["--no-verify"], + "changelog": "pipx run towncrier build --draft --yes --version 0.0.0", + "requireUpstream": false, + "requireCleanWorkingDir": false, + "commitMessage": "Release @plone/razzle ${version}", + "tagName": "plone-slate-${version}", + "tagAnnotation": "Release @plone/razzle ${version}" + }, + "github": { + "release": true, + "releaseName": "@plone/razzle ${version}", + "releaseNotes": "cat .changelog.draft" + } +} diff --git a/packages/volto-razzle/CHANGELOG.md b/packages/volto-razzle/CHANGELOG.md new file mode 100644 index 00000000000..f59a65fce56 --- /dev/null +++ b/packages/volto-razzle/CHANGELOG.md @@ -0,0 +1,9 @@ +# @plone/razzle Release Notes + + + + diff --git a/packages/volto-razzle/README.md b/packages/volto-razzle/README.md new file mode 100644 index 00000000000..fed64f52b97 --- /dev/null +++ b/packages/volto-razzle/README.md @@ -0,0 +1,74 @@ +> [!IMPORTANT] +> This package is a maintained fork of the original [Razzle](https://github.com/jaredpalmer/razzle). +> The upstream project is currently unmaintained, so we forked it into the Volto monorepo to keep its dependencies updated and address security issues. +> All upstream attributions are preserved below. + + + +[](https://www.npmjs.com/package/@plone/razzle) [](https://www.npmjs.com/package/@plone/razzle) + +Universal JavaScript applications are tough to setup. Either you buy into a framework like Next.js or Nuxt, fork a boilerplate, or set things up yourself. Aiming to fill this void, Razzle is a tool that abstracts all the complex configuration needed for building SPA's and SSR applications into a single dependency--giving you the awesome developer experience of [create-react-app](https://github.com/facebook/create-react-app), but then leaving the rest of your app's architectural decisions about frameworks, routing, and data fetching up to you. With this approach, Razzle not only works with React, but also Preact, Vue, Svelte, and Angular, and most importantly......whatever comes next. + +## Getting Started + +Visit https://razzlejs.org/getting-started to get started with Razzle. + +## Examples + +Razzle has many examples, we might have one that fits your needs + +See: [The examples](https://github.com/jaredpalmer/razzle/tree/master/examples) + +## Documentation + +Visit https://razzlejs.org/ to view the documentation. + +## Contributing + +Please see our [CONTRIBUTING.md](../../CONTRIBUTING.md). + +## Inspiration + +- [jaredpalmer/backpack](https://github.com/jaredpalmer/backpack) +- [nytimes/kyt](https://github.com/nytimes/kyt) +- [facebookincubator/create-react-app](https://github.com/facebook/create-react-app) +- [ndreckshage/sambell](https://github.com/ndreckshage/sambell) +- [vercel/next.js](https://github.com/vercel/next.js) + +### Author + +- [Jared Palmer](https://twitter.com/jaredpalmer) + +## Contributors + +Thanks goes to these wonderful people ([emoji key](https://github.com/all-contributors/all-contributors.github.io#emoji-key)): + + + +- **Jared Palmer** - [@jaredpalmer](https://jaredpalmer.com) + - **Contributions:** question, code, design, doc, example, ideas, review, test, tool +- **Nima Arefi** - [@Nimaa77](https://github.com/Nimaa77) + - **Contributions:** question, code, doc, example, ideas, review, test, tool +- **Øyvind Saltvik** - [@fivethreeo](https://github.com/fivethreeo/) + - **Contributions:** question, code, example, ideas, review, test, tool +- **Jari Zwarts** - [@jariz](https://jari.io) + - **Contributions:** question, code, ideas, plugin, review +- **Dan Abramov** - [@gaearon](http://twitter.com/dan_abramov) + - **Contributions:** code, ideas +- **Eric Clemmons** + - **Contributions:** code, ideas +- **Zino Hofmann** - [@HofmannZ](https://www.linkedin.com/in/zinohofmann/) + - **Contributions:** example +- **Lucas Terra** - [@lucasterra](https://www.linkedin.com/in/lucasterra7/) + - **Contributions:** code, example, plugin +- **Ray Andrew** + - **Contributions:** code, example, plugin +- **Heithem Moumni** - [@heithemmoumni](https://www.linkedin.com/in/heithemmoumni/) + - **Contributions:** code, example, plugin + + +This project follows the [all-contributors](https://github.com/all-contributors/all-contributors.github.io) specification. Contributions of any kind welcome! + +--- + +MIT License diff --git a/packages/volto-razzle/babel.js b/packages/volto-razzle/babel.js new file mode 100644 index 00000000000..9caa22c39bf --- /dev/null +++ b/packages/volto-razzle/babel.js @@ -0,0 +1 @@ +module.exports = require('@plone/babel-preset-razzle'); diff --git a/packages/volto-razzle/bin/razzle.js b/packages/volto-razzle/bin/razzle.js new file mode 100755 index 00000000000..7aaf999f694 --- /dev/null +++ b/packages/volto-razzle/bin/razzle.js @@ -0,0 +1,81 @@ +#!/usr/bin/env node +'use strict'; + +const sade = require('sade'); +const spawn = require('react-dev-utils/crossSpawn'); +const pkg = require('../package.json'); +const prog = sade('razzle'); +prog.version(pkg.version); + +const argv = process.argv.slice(3); + +prog + .command('build') + .describe('Build the application') + .option( + '-t, --type', + 'Change the application build type. Must be either `iso` or `spa`.', + 'iso', + ) + .action(() => { + runCommand('build', [], argv); + }); + +prog + .command('start') + .describe('Start the application in development mode.') + .option( + '-t, --type', + 'Change the application build type. Must be either `iso` or `spa`.', + 'iso', + ) + .action(() => { + runCommand('start', [], argv); + }); + +prog + .command('export') + .describe('Export a static version of the application in production mode.') + .action(() => { + runCommand('export', [], argv); + }); + +prog + .command('test') + .describe('Runs the test watcher in an interactive mode.') + .action(() => { + runCommand( + 'test', + argv.filter((x) => x.includes('--inspect')), + argv.filter((x) => !x.includes('--inspect')), + ); + }); + +function runCommand(script, node_args, script_args) { + const result = spawn.sync( + 'node', + node_args + .concat([require.resolve('../scripts/' + script)]) + .concat(script_args), + { stdio: 'inherit' }, + ); + if (result.signal) { + if (result.signal === 'SIGKILL') { + console.log( + 'The build failed because the process exited too early. ' + + 'This probably means the system ran out of memory or someone called ' + + '`kill -9` on the process.', + ); + } else if (result.signal === 'SIGTERM') { + console.log( + 'The build failed because the process exited too early. ' + + 'Someone might have called `kill` or `killall`, or the system could ' + + 'be shutting down.', + ); + } + process.exit(1); + } + process.exit(result.status); +} + +prog.parse(process.argv); diff --git a/packages/volto-razzle/config/babel-loader/razzle-babel-loader.js b/packages/volto-razzle/config/babel-loader/razzle-babel-loader.js new file mode 100644 index 00000000000..e97f9620acd --- /dev/null +++ b/packages/volto-razzle/config/babel-loader/razzle-babel-loader.js @@ -0,0 +1,229 @@ +'use strict'; + +const babelLoader = require('babel-loader'); +const hash = require('string-hash'); +const path = require('path'); +const merge = require('deepmerge'); +const basename = path.basename; +const join = path.join; + +// increment 'm' to invalidate cache +// eslint-disable-razzle-line no-useless-concat +const cacheKey = 'babel-cache-' + 'm' + '-'; +const razzleBabelPreset = require('@plone/babel-preset-razzle'); + +const getModernOptions = function (babelOptions) { + babelOptions = babelOptions || {}; + const presetEnvOptions = Object.assign({}, babelOptions['preset-env']); + const transformRuntimeOptions = Object.assign( + {}, + babelOptions['transform-runtime'], + { regenerator: false }, + ); + + presetEnvOptions.targets = { + esmodules: true, + }; + presetEnvOptions.exclude = (presetEnvOptions.exclude || []).concat([ + // Block accidental inclusions + 'transform-regenerator', + 'transform-async-to-generator', + ]); + + return Object.assign({}, babelOptions, { + 'preset-env': presetEnvOptions, + 'transform-runtime': transformRuntimeOptions, + }); +}; + +const razzleBabelPresetModern = function (presetOptions) { + return function (context) { + return razzleBabelPreset(context, getModernOptions(presetOptions)); + }; +}; + +module.exports = babelLoader.custom(function (babel) { + const presetItem = babel.createConfigItem(razzleBabelPreset, { + type: 'preset', + }); + const applyCommonJs = babel.createConfigItem( + require('@plone/babel-preset-razzle/babel-plugins/commonjs'), + { type: 'plugin' }, + ); + const commonJsItem = babel.createConfigItem( + require('@babel/plugin-transform-modules-commonjs'), + { type: 'plugin' }, + ); + + const configs = new Set(); + + return { + customOptions: function (opts) { + const custom = { + verbose: opts.verbose, + isServer: opts.isServer, + isModern: opts.isModern, + hasModern: opts.hasModern, + development: opts.development, + shouldUseReactRefresh: opts.shouldUseReactRefresh, + }; + const filename = join(opts.cwd, 'noop.js'); + const loader = Object.assign( + opts.cache + ? { + cacheCompression: false, + cacheDirectory: join(opts.cwd, 'cache', 'razzle-babel-loader'), + cacheIdentifier: + cacheKey + + (opts.isServer ? '-server' : '') + + (opts.isModern ? '-modern' : '') + + (opts.hasModern ? '-has-modern' : '') + + '-new-polyfills' + + (opts.development ? '-development' : '-production') + + (opts.hasReactRefresh ? '-react-refresh' : '') + + JSON.stringify( + babel.loadPartialConfig({ + filename, + cwd: opts.cwd, + sourceFileName: filename, + }).options, + ), + } + : { + cacheDirectory: false, + }, + opts, + ); + + delete loader.verbose; + delete loader.isServer; + delete loader.cache; + delete loader.distDir; + delete loader.isModern; + delete loader.hasModern; + delete loader.development; + delete loader.shouldUseReactRefresh; + return { loader, custom }; + }, + config: function (cfg, cfgOpts) { + const source = cfgOpts.source; + const customOptions = cfgOpts.customOptions; + const verbose = customOptions.verbose; + const isServer = customOptions.isServer; + const isModern = customOptions.isModern; + const hasModern = customOptions.hasModern; + const development = customOptions.development; + const shouldUseReactRefresh = customOptions.shouldUseReactRefresh; + + const filename = this.resourcePath; + const presetOptions = Object.assign({}, cfg.options); + + if (cfg.hasFilesystemConfig()) { + for (const file of [cfg.babelrc, cfg.config]) { + // We only log for first compilation otherwise there will be double output + if ( + file && + verbose && + !configs.has(`${file}.${isServer ? 'node' : 'web'}`) + ) { + configs.add(`${file}.${isServer ? 'node' : 'web'}`); + console.info( + `Using external babel configuration from ${file} for "${isServer ? 'node' : 'web'}" build`, + ); + } + } + } else { + // Add our default preset if the no "babelrc" found. + presetOptions.presets = (presetOptions.presets || []).concat([ + presetItem, + ]); + } + + presetOptions.caller.isServer = isServer; + presetOptions.caller.isModern = isModern; + presetOptions.caller.isDev = development; + + const emitWarning = this.emitWarning.bind(this); + Object.defineProperty(presetOptions.caller, 'onWarning', { + enumerable: false, + writable: false, + value: (presetOptions.caller.onWarning = function (reason) { + if (!(reason instanceof Error)) { + reason = new Error(reason); + } + emitWarning(reason); + }), + }); + + presetOptions.plugins = presetOptions.plugins || []; + + if (shouldUseReactRefresh) { + const reactRefreshPlugin = babel.createConfigItem( + [require('react-refresh/babel'), { skipEnvCheck: true }], + { type: 'plugin' }, + ); + presetOptions.plugins.unshift(reactRefreshPlugin); + if (!isServer) { + const noAnonymousDefaultExportPlugin = babel.createConfigItem( + [ + require('@plone/babel-preset-razzle/babel-plugins/no-anonymous-default-export'), + {}, + ], + { type: 'plugin' }, + ); + presetOptions.plugins.unshift(noAnonymousDefaultExportPlugin); + } + } + + if (isModern) { + const razzlePreset = presetOptions.presets.find( + (preset) => preset && preset.value === razzleBabelPreset, + ) || { options: {} }; + + const additionalPresets = presetOptions.presets.filter( + (preset) => preset !== razzlePreset, + ); + + const presetItemModern = babel.createConfigItem( + razzleBabelPresetModern(razzlePreset.options), + { + type: 'preset', + }, + ); + + presetOptions.presets = (additionalPresets || []).concat([ + presetItemModern, + ]); + } + + // If the file has `module.exports` we have to transpile commonjs because Babel adds `import` statements + // That break webpack, since webpack doesn't support combining commonjs and esmodules + if (!hasModern && source.indexOf('module.exports') !== -1) { + presetOptions.plugins.push(applyCommonJs); + } + + presetOptions.plugins.push([ + require.resolve('babel-plugin-transform-define'), + { + 'process.env.NODE_ENV': development ? 'development' : 'production', + 'typeof window': isServer ? 'undefined' : 'object', + 'process.browser': isServer ? false : true, + }, + 'razzle-js-transform-define-instance', + ]); + + // As lib has stateful modules we have to transpile commonjs + presetOptions.overrides = presetOptions.overrides || []; + // .concat([ + // { + // test: [ + // /razzle[\\/]dist[\\/]lib/, + // ], + // plugins: [commonJsItem], + // }, + // ]) + + return presetOptions; + }, + }; +}); diff --git a/packages/volto-razzle/config/createConfigAsync.js b/packages/volto-razzle/config/createConfigAsync.js new file mode 100644 index 00000000000..b7d18f3b6ab --- /dev/null +++ b/packages/volto-razzle/config/createConfigAsync.js @@ -0,0 +1,1140 @@ +'use strict'; + +const fs = require('fs-extra'); +const path = require('path'); +const webpack = require('webpack'); +const crypto = require('crypto'); +const util = require('util'); +const TerserPlugin = require('terser-webpack-plugin'); +const HtmlWebpackPlugin = require('html-webpack-plugin'); +const StartServerPlugin = require('razzle-start-server-webpack-plugin'); +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); +const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); +const getClientEnv = require('./env').getClientEnv; +const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin'); +const errorOverlayMiddleware = require('react-dev-utils/errorOverlayMiddleware'); +const WebpackBar = require('webpackbar'); +const ManifestPlugin = require('webpack-manifest-plugin').WebpackManifestPlugin; +const CopyPlugin = require('copy-webpack-plugin'); +const PnpWebpackPlugin = require('pnp-webpack-plugin'); +const modules = require('./modules'); +const postcssLoadConfig = require('postcss-load-config'); +const resolveRequest = require('razzle-dev-utils/resolveRequest'); +const logger = require('razzle-dev-utils/logger'); +const razzlePaths = require('./paths'); +const getCacheIdentifier = require('react-dev-utils/getCacheIdentifier'); +const webpackMajor = require('razzle-dev-utils/webpackMajor'); +const devServerMajorVersion = require('razzle-dev-utils/devServerMajor'); + +const hasPostCssConfigTest = () => { + try { + return !!postcssLoadConfig.sync(); + } catch (_error) { + return false; + } +}; + +const hasPostCssConfig = hasPostCssConfigTest(); + +let webpackDevClientEntry; +if (devServerMajorVersion > 3) { + webpackDevClientEntry = require.resolve( + 'razzle-dev-utils/webpackHotDevClientV4', + ); +} else { + webpackDevClientEntry = require.resolve( + 'razzle-dev-utils/webpackHotDevClient', + ); +} + +const isModuleCSS = (module) => { + return ( + // mini-css-extract-plugin + module.type === `css/mini-extract` || + // extract-css-chunks-webpack-plugin (old) + module.type === `css/extract-chunks` || + // extract-css-chunks-webpack-plugin (new) + module.type === `css/extract-css-chunks` + ); +}; + +// This is the Webpack configuration factory. It's the juice! +module.exports = ( + target = 'web', + env = 'dev', + { + clearConsole = true, + host = 'localhost', + port = 3000, + modify = null, + modifyWebpackOptions = null, + modifyWebpackConfig = null, + modifyBabelPreset = null, + experimental = {}, + disableStartServer = false, + }, + webpackObject, + clientOnly = false, + paths = razzlePaths, + plugins = [], + razzleOptions = {}, +) => { + return new Promise(async (resolve) => { + // Define some useful shorthands. + const IS_NODE = target === 'node'; + const IS_WEB = target === 'web'; + const IS_SERVERLESS = /serverless/.test(razzleOptions.buildType); + const IS_PROD = env === 'prod'; + const IS_DEV = env === 'dev'; + const IS_DEV_ENV = process.env.NODE_ENV === 'development'; + + // Contains various versions of the Webpack SplitChunksPlugin used in different build types + const splitChunksConfigs = { + dev: { + cacheGroups: { + default: false, + vendors: false, + // In webpack 5 vendors was renamed to defaultVendors + defaultVendors: false, + }, + }, + prod: { + cacheGroups: { + default: false, + vendors: false, + // In webpack 5 vendors was renamed to defaultVendors + defaultVendors: false, + }, + }, + }; + + const shouldUseReactRefresh = + IS_WEB && IS_DEV && razzleOptions.enableReactRefresh ? true : false; + + const shouldDisableWebpackbar = + razzleOptions.disableWebpackbar === true || + razzleOptions.disableWebpackbar === target; + + let webpackOptions = {}; + + const hasPublicDir = fs.existsSync(paths.appPublic); + + const hasStaticExportJs = + fs.existsSync(paths.appStaticExportJs + '.js') || + fs.existsSync(paths.appStaticExportJs + '.jsx') || + fs.existsSync(paths.appStaticExportJs + '.ts') || + fs.existsSync(paths.appStaticExportJs + '.tsx'); + + const dotenv = getClientEnv( + target, + IS_DEV, + { + clearConsole, + host, + port, + shouldUseReactRefresh, + forceRuntimeEnvVars: razzleOptions.forceRuntimeEnvVars, + webpackObject, + }, + paths, + ); + + const portOffset = clientOnly ? 0 : 1; + + const devServerPort = + (process.env.PORT_DEV && parseInt(process.env.PORT_DEV, 10)) || + (process.env.PORT && parseInt(process.env.PORT, 10) + portOffset) || + 3000 + portOffset; + + // VMs, Docker containers might not be available at localhost:3001. CLIENT_PUBLIC_PATH can override. + const clientPublicPath = + dotenv.raw.CLIENT_PUBLIC_PATH || + (IS_DEV ? `http://${dotenv.raw.HOST}:${devServerPort}/` : '/'); + + const modulesConfig = modules(paths); + const additionalModulePaths = modulesConfig.additionalModulePaths || []; + const additionalAliases = modulesConfig.additionalAliases || {}; + const additionalIncludes = modulesConfig.additionalIncludes || []; + + webpackOptions.fileLoaderExclude = [ + /\.html$/, + /\.(js|jsx|mjs)$/, + /\.(ts|tsx)$/, + /\.(vue)$/, + /\.(less)$/, + /\.(re)$/, + /\.(s?css|sass)$/, + /\.json$/, + /\.bmp$/, + /\.gif$/, + /\.jpe?g$/, + /\.png$/, + ]; + + webpackOptions.urlLoaderTest = [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/]; + + webpackOptions.fileLoaderOutputName = `${razzleOptions.mediaPrefix}/[name].[contenthash:8].[ext]`; + + webpackOptions.urlLoaderOutputName = `${razzleOptions.mediaPrefix}/[name].[contenthash:8].[ext]`; + + webpackOptions.cssTest = [/\.css(\.map)?$/]; + + webpackOptions.cssOutputFilename = `${razzleOptions.cssPrefix}/[name].[contenthash:8].css`; + + webpackOptions.cssOutputChunkFilename = `${razzleOptions.cssPrefix}/[name].[contenthash:8].chunk.css`; + + webpackOptions.jsTest = [/\.js(\.map)?$/]; + + webpackOptions.definePluginOptions = dotenv.stringified; + + webpackOptions.appAssetsManifestPath = paths.appAssetsManifest; + + if (IS_NODE) { + webpackOptions.jsOutputFilename = `[name].js`; + webpackOptions.jsOutputChunkFilename = `[name].chunk.js`; + + if (IS_DEV) { + const nodeArgs = ['-r', require.resolve('source-map-support/register')]; + + // Passthrough --inspect and --inspect-brk flags (with optional [host:port] value) to node + if (process.env.INSPECT_BRK) { + nodeArgs.push(process.env.INSPECT_BRK); + } else if (process.env.INSPECT) { + nodeArgs.push(process.env.INSPECT); + } + + webpackOptions.startServerOptions = { + verbose: razzleOptions.verbose, + name: 'server.js', + entryName: 'server', + killOnExit: false, + killOnError: false, + nodeArgs, + }; + } else { + webpackOptions.terserPluginOptions = {}; + } + } + + if (IS_WEB) { + if (IS_DEV) { + webpackOptions.jsOutputFilename = `${razzleOptions.jsPrefix}/[name].js`; + webpackOptions.jsOutputChunkFilename = `${razzleOptions.jsPrefix}/[name].chunk.js`; + + webpackOptions.splitChunksConfig = splitChunksConfigs.dev; + } else { + webpackOptions.jsOutputFilename = `${razzleOptions.jsPrefix}/[name].[contenthash:8].js`; + webpackOptions.jsOutputChunkFilename = `${razzleOptions.jsPrefix}/[name].[contenthash:8].chunk.js`; + + webpackOptions.splitChunksConfig = splitChunksConfigs.prod; + webpackOptions.terserPluginOptions = { + terserOptions: { + parse: { + // we want uglify-js to parse ecma 8 code. However, we don't want it + // to apply any minfication steps that turns valid ecma 5 code + // into invalid ecma 5 code. This is why the 'compress' and 'output' + // sections only apply transformations that are ecma 5 safe + // https://github.com/facebook/create-react-app/pull/4234 + ecma: 8, + }, + compress: { + ecma: 5, + warnings: false, + // Disabled because of an issue with Uglify breaking seemingly valid code: + // https://github.com/facebook/create-react-app/issues/2376 + // Pending further investigation: + // https://github.com/mishoo/UglifyJS2/issues/2011 + comparisons: false, + // Disabled because of an issue with Terser breaking valid code: + // https://github.com/facebook/create-react-app/issues/5250 + // Pending futher investigation: + // https://github.com/terser-js/terser/issues/120 + inline: 2, + }, + mangle: { + safari10: true, + }, + output: { + ecma: 5, + comments: false, + // Turned on because emoji and regex is not minified properly using default + // https://github.com/facebook/create-react-app/issues/2488 + ascii_only: true, + }, + }, + }; + } + } + + webpackOptions.enableHtmlWebpackPlugin = clientOnly; + + webpackOptions.htmlWebpackPluginOptions = Object.assign( + {}, + { + inject: false, + template: paths.appHtml, + }, + IS_PROD + ? { + minify: { + removeComments: true, + collapseWhitespace: true, + removeRedundantAttributes: true, + useShortDoctype: true, + removeEmptyAttributes: true, + removeStyleLinkTypeAttributes: true, + keepClosingSlash: true, + minifyJS: true, + minifyCSS: true, + minifyURLs: true, + }, + } + : {}, + ); + + webpackOptions.browserslist = razzleOptions.browserslist; + + webpackOptions.babelRule = { + test: /\.(js|jsx|mjs|ts|tsx)$/, + include: [paths.appSrc].concat(additionalIncludes), + use: [ + { + loader: require.resolve('./babel-loader/razzle-babel-loader'), + options: { + verbose: razzleOptions.verbose, + sourceMaps: razzleOptions.enableSourceMaps, + isServer: IS_NODE, + cwd: paths.appPath, + cache: razzleOptions.enableBabelCache, + configFile: razzleOptions.enableTargetBabelrc + ? path.resolve(paths.appPath, `.babelrc.${target}`) + : undefined, + hasModern: false, + development: IS_DEV, + shouldUseReactRefresh: shouldUseReactRefresh, + }, + ident: 'razzle-babel-loader', + }, + ], + }; + + webpackOptions.watchIgnorePaths = [paths.appAssetsManifest]; + + webpackOptions.notNodeExternalResMatch = null; + + webpackOptions.nodeExternals = []; + webpackOptions.clientExternals = []; + webpackOptions.clientExternals = []; + + webpackOptions.postCssOptions = { + ident: 'postcss', + sourceMap: razzleOptions.enableSourceMaps, + plugins: [ + [ + require('autoprefixer'), + { + overrideBrowserslist: razzleOptions.browserslist || [ + '>1%', + 'last 4 versions', + 'Firefox ESR', + 'not ie < 9', + ], + flexbox: 'no-2009', + }, + ], + ], + }; + + webpackOptions.nullNodeCss = false; + + for (const [plugin, pluginOptions] of plugins) { + // Check if .modifyWebpackConfig is a function. + // If it is, call it on the configs we created. + if (plugin.modifyWebpackOptions) { + webpackOptions = await plugin.modifyWebpackOptions({ + env: { target, dev: IS_DEV, serverless: IS_SERVERLESS }, + webpackObject: webpackObject, + options: { + pluginOptions, + razzleOptions, + webpackOptions, + }, + paths, + }); + } + } + // Check if razzle.config.js has a modifyWebpackOptions function. + // If it does, call it on the configs we created. + if (modifyWebpackOptions) { + webpackOptions = await modifyWebpackOptions({ + env: { target, dev: IS_DEV, serverless: IS_SERVERLESS }, + webpackObject: webpackObject, + options: { + razzleOptions, + webpackOptions, + }, + paths, + }); + } + + if (razzleOptions.debug.options) { + console.log(`Printing webpack options for ${target} target`); + console.log(util.inspect(webpackOptions, { depth: null })); + } + + const debugNodeExternals = razzleOptions.debug.nodeExternals; + + const nodeExternalsFunc = (context, request, callback) => { + if ( + webpackOptions.notNodeExternalResMatch && + webpackOptions.notNodeExternalResMatch(request, context) + ) { + if (debugNodeExternals) { + console.log( + `Not externalizing ${request} (using notNodeExternalResMatch)`, + ); + } + return callback(); + } + + const isLocal = + request.startsWith('.') || + // Always check for unix-style path, as webpack sometimes + // normalizes as posix. + path.posix.isAbsolute(request) || + // When on Windows, we also want to check for Windows-specific + // absolute paths. + (process.platform === 'win32' && path.win32.isAbsolute(request)); + + // Relative requires don't need custom resolution, because they + // are relative to requests we've already resolved here. + // Absolute requires (require('/foo')) are extremely uncommon, but + // also have no need for customization as they're already resolved. + if (isLocal) { + if (debugNodeExternals) { + console.log(`Not externalizing ${request} (relative require)`); + } + return callback(); + } + + let res; + try { + res = resolveRequest(request, `${context}/`); + } catch (err) { + // If the request cannot be resolved, we need to tell webpack to + // "bundle" it so that webpack shows an error (that it cannot be + // resolved). + if (debugNodeExternals) { + console.log(`Not externalizing ${request} (cannot resolve)`); + } + return callback(); + } + // Same as above, if the request cannot be resolved we need to have + // webpack "bundle" it so it surfaces the not found error. + if (!res) { + if (debugNodeExternals) { + console.log(`Not externalizing ${request} (cannot resolve)`); + } + return callback(); + } + // This means we need to make sure its request resolves to the same + // package that'll be available at runtime. If it's not identical, + // we need to bundle the code (even if it _should_ be external). + let baseRes = null; + try { + baseRes = resolveRequest(request, `${paths.appPath}/`); + } catch (err) { + baseRes = null; + } + + // Same as above: if the package, when required from the root, + // would be different from what the real resolution would use, we + // cannot externalize it. + if (baseRes !== res) { + if (debugNodeExternals) { + console.log(`Not externalizing ${request} (real resolution differs)`); + } + return callback(); + } + + // This is the @babel/plugin-transform-runtime "helpers: true" option + if (res.match(/node_modules[/\\]@babel[/\\]runtime[/\\]/)) { + if (debugNodeExternals) { + console.log(`Not externalizing @babel/plugin-transform-runtime`); + } + return callback(); + } + + // Anything else that is standard JavaScript within `node_modules` + // can be externalized. + if (res.match(/node_modules[/\\].*\.c?js$/)) { + if (debugNodeExternals) { + console.log(`Externalizing ${request} (node_modules)`); + } + return callback(undefined, `commonjs ${request}`); + } + + if (debugNodeExternals) { + console.log(`Not externalizing ${request} (default)`); + } + // Default behavior: bundle the code! + return callback(); + }; + + const postCssOptions = hasPostCssConfig + ? undefined + : { postcssOptions: webpackOptions.postCssOptions }; + + // This is our base webpack config. + let config = { + // Set webpack mode: + mode: IS_DEV || IS_DEV_ENV ? 'development' : 'production', + // Set webpack context to the current apps directory + context: paths.appPath, + // Specify target (either 'node' or 'web') + target: target, + // Controversially, decide on sourcemaps. + devtool: + IS_DEV || IS_DEV_ENV + ? 'cheap-module-source-map' + : razzleOptions.enableSourceMaps + ? 'source-map' + : false, + // We need to tell webpack how to resolve both Razzle's node_modules and + // the users', so we use resolve and resolveLoader. + resolve: { + mainFields: IS_NODE + ? ['main', 'module'] + : ['browser', 'module', 'main'], + modules: ['node_modules', paths.appNodeModules].concat( + additionalModulePaths, + ), + extensions: ['.mjs', '.js', '.jsx', '.json', '.ts', '.tsx'], + alias: Object.assign( + { + // This is required so symlinks work during development. + 'webpack/hot/poll': require.resolve('webpack/hot/poll'), + // Support React Native Web + // https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/ + 'react-native': 'react-native-web', + }, + additionalAliases, + ), + plugins: [webpackMajor !== 5 && PnpWebpackPlugin].filter((x) => x), + }, + resolveLoader: { + modules: [paths.appNodeModules, paths.ownNodeModules], + plugins: [ + webpackMajor !== 5 && PnpWebpackPlugin.moduleLoader(module), + ].filter((x) => x), + }, + module: { + strictExportPresence: true, + rules: [ + webpackOptions.babelRule, + { + exclude: webpackOptions.fileLoaderExclude, + use: + clientOnly || webpackOptions.enableHtmlWebpackPlugin + ? (info) => { + return info.compiler !== 'HtmlWebpackCompiler' + ? [ + { + loader: require.resolve('file-loader'), + options: { + name: webpackOptions.fileLoaderOutputName, + emitFile: IS_WEB, + }, + ident: 'razzle-file-loader', + }, + ] + : []; + } + : [ + { + // fix for vue-loader plugin + loader: require.resolve('file-loader'), + options: { + name: webpackOptions.fileLoaderOutputName, + emitFile: IS_WEB, + }, + ident: 'razzle-file-loader', + }, + ], + }, + // "url" loader works like "file" loader except that it embeds assets + // smaller than specified limit in bytes as data URLs to avoid requests. + // A missing `test` is equivalent to a match. + { + test: webpackOptions.urlLoaderTest, + use: [ + { + loader: require.resolve('url-loader'), + options: { + limit: 10000, + name: webpackOptions.urlLoaderOutputName, + emitFile: IS_WEB, + }, + ident: 'razzle-url-loader', + }, + ], + }, + + // "postcss" loader applies autoprefixer to our CSS. + // "css" loader resolves paths in CSS and adds assets as dependencies. + // "style" loader turns CSS into JS modules that inject