diff --git a/Gemfile.lock b/Gemfile.lock index 4e9983228..e721455c6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - shakapacker (6.0.0.rc.13) + shakapacker (6.0.0) activesupport (>= 5.2) rack-proxy (>= 0.6.1) railties (>= 5.2) diff --git a/README.md b/README.md index ebc6fc6f9..d84ea95c2 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ Discussion forums to discuss debugging and troubleshooting tips. Please open iss - [Development](#development) - [Webpack Configuration](#webpack-configuration) - [Babel configuration](#babel-configuration) + - [SWC configuration](#swc-configuration) - [Integrations](#integrations) - [React](#react) - [Typescript](#typescript) @@ -371,6 +372,12 @@ By default, you will find the Webpacker preset in your `package.json`. Note, you Optionally, you can change your Babel configuration by removing these lines in your `package.json` and add [a Babel configuration file](https://babeljs.io/docs/en/config-files) in your project. For an example customization based on the original, see [Customizing Babel Config](./docs/customizing_babel_config.md). +### SWC configuration + +You can try out experimental integration with the SWC loader. You can read more at [SWC usage docs](./docs/using_swc_loader.md). + +Please note that if you want opt-in to use SWC, you can skip [React](#react) integration instructions as it is supported out of the box. + ### Integrations Webpacker out of the box supports JS and static assets (fonts, images etc.) compilation. To enable support for CoffeeScript or TypeScript install relevant packages: diff --git a/docs/using_swc_loader.md b/docs/using_swc_loader.md new file mode 100644 index 000000000..75211c013 --- /dev/null +++ b/docs/using_swc_loader.md @@ -0,0 +1,151 @@ +# Using SWC Loader + +:warning: This feature is currently experimental. If you face any issues, please report at https://github.com/shakacode/shakapacker/issues. + +## About SWC + +[SWC (Speedy Web compiler)](https://swc.rs/) is a Rust-based compilation and bundler tool that can be used for Javascript and Typescript files. It claims to be 20x faster than Babel! + +It supports all ECMAScript features and it's designed to be a drop-in replacement for Babel and its plugins. Out of the box, it supports TS, JSX syntax, React fast refresh, and much more. + +For comparison between SWC and Babel, see the docs at https://swc.rs/docs/migrating-from-babel. + +## Switching your Shakapacker project to SWC + +In order to use SWC as your compiler today. You need to do two things: + +1. Make sure you've installed `@swc/core` and `swc-loader` packages. + +``` +yarn add -D @swc/core swc-loader +``` + +2. Add or change `webpacker_loader` value in your default `webpacker.yml` config to `swc` +The default configuration of babel is done by using `package.json` to use the file within the `shakapacker` package. + +```json +default: &default + source_path: app/javascript + source_entry_path: / + public_root_path: public + public_output_path: packs + cache_path: tmp/webpacker + webpack_compile_output: true + + # Additional paths webpack should look up modules + # ['app/assets', 'engine/foo/app/assets'] + additional_paths: [] + + # Reload manifest.json on all requests so we reload latest compiled packs + cache_manifest: false + + # Select loader to use, available options are 'babel' (default) or 'swc' + webpack_loader: 'swc' +``` + +## Usage + +### React + +React is supported out of the box, provided you use `.jsx` or `.tsx` file extension. Shakapacker config will correctly recognize those and tell SWC to parse the JSX syntax correctly. If you wish to customize the transform options to match any existing `@babel/preset-react` settings, you can do that through customizing loader options as described below. You can see available options at https://swc.rs/docs/configuration/compilation#jsctransformreact. + +### Typescript + +Typescript is supported out of the box, but certain features like decorators need to be enabled through the custom config. You can see available customizations options at https://swc.rs/docs/configuration/compilation, which you can apply through customizing loader options as described below. + +Please note that SWC is not using the settings from `.tsconfig` file. Any non-default settings you might have there will need to be applied to the custom loader config. + +## Customizing loader options + +You can see the default loader options at [swc/index.js](../package/swc/index.js). + +If you wish to customize the loader defaults further, for example, if you want to enable support for decorators or React fast refresh, you need to create a `swc.config.js` file in your app config folder. + +This file should have a single default export which is an object with an `options` key. Your customizations will be merged with default loader options. You can use this to override or add additional configurations. + +Inside the `options` key, you can use any options available to the SWC compiler. For the options reference, please refer to [official SWC docs](https://swc.rs/docs/configuration/compilation). + +See some examples below of potential `config/swc.config.js`. + +### Example: Enabling top level await and decorators + + +```js +const customConfig = { + options: { + jsc: { + parser: { + topLevelAwait: true, + decorators: true + } + } + } +} + +module.exports = customConfig +``` + +### Example: Matching existing `@babel/present-env` config + +```js +const { env } = require('shakapacker') + +const customConfig = { + options: { + jsc: { + transform: { + react: { + development: env.isDevelopment, + useBuiltins: true + } + } + } + } +} + +module.exports = customConfig +``` + +### Example: Enabling React Fast Refresh + +:warning: Remember that you still need to add [@pmmmwh/react-refresh-webpack-plugin](https://github.com/pmmmwh/react-refresh-webpack-plugin) to your webpack config. The setting below just replaces equivalent `react-refresh/babel` Babel plugin. + + +```js +const { env } = require('shakapacker') + +const customConfig = { + options: { + jsc: { + transform: { + react: { + refresh: env.isDevelopment && env.runningWebpackDevServer + } + } + } + } +} + +module.exports = customConfig +``` + +### Example: Adding browserslist config + +```js + +const customConfig = { + options: { + env: { + targets: '> 0.25%, not dead' + } + } +} + +module.exports = customConfig +``` + + +## Known limitations + +- `browserslist` config at the moment is not being picked up automatically. [Related SWC issue](https://github.com/swc-project/swc/issues/3365). You can add your browserlist config through customizing loader options as outlined above. +- Using `.swcrc` config file is currently not supported. You might face some issues when `.swcrc` config is diverging from the SWC options we're passing in the Webpack rule. diff --git a/lib/install/config/webpacker.yml b/lib/install/config/webpacker.yml index ca3534f37..b1dc33919 100644 --- a/lib/install/config/webpacker.yml +++ b/lib/install/config/webpacker.yml @@ -15,6 +15,9 @@ default: &default # Reload manifest.json on all requests so we reload latest compiled packs cache_manifest: false + # Select loader to use, available options are 'babel' (default) or 'swc' + webpack_loader: 'babel' + development: <<: *default compile: true diff --git a/package.json b/package.json index ec3a09b83..44e042860 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "eslint-plugin-jsx-a11y": "^6.4.1", "eslint-plugin-react": "^7.26.0", "jest": "^27.2.1", + "swc-loader": "^0.1.15", "webpack": "^5.53.0", "webpack-assets-manifest": "^5.0.6", "webpack-merge": "^5.8.0" diff --git a/package/rules/babel.js b/package/rules/babel.js index 7c3bcf907..88db17708 100644 --- a/package/rules/babel.js +++ b/package/rules/babel.js @@ -1,30 +1,32 @@ const { resolve } = require('path') const { realpathSync } = require('fs') +const { loaderMatches } = require('../utils/helpers') const { source_path: sourcePath, - additional_paths: additionalPaths + additional_paths: additionalPaths, + webpack_loader: webpackLoader } = require('../config') const { isProduction } = require('../env') -module.exports = { - test: /\.(js|jsx|mjs|ts|tsx|coffee)?(\.erb)?$/, - include: [sourcePath, ...additionalPaths].map((p) => { - try { - return realpathSync(p) - } catch (e) { - return resolve(p) - } - }), - exclude: /node_modules/, - use: [ - { - loader: require.resolve('babel-loader'), - options: { - cacheDirectory: true, - cacheCompression: isProduction, - compact: isProduction +module.exports = loaderMatches(webpackLoader, 'babel', () => ({ + test: /\.(js|jsx|mjs|ts|tsx|coffee)?(\.erb)?$/, + include: [sourcePath, ...additionalPaths].map((p) => { + try { + return realpathSync(p) + } catch (e) { + return resolve(p) } - } - ] -} + }), + exclude: /node_modules/, + use: [ + { + loader: require.resolve('babel-loader'), + options: { + cacheDirectory: true, + cacheCompression: isProduction, + compact: isProduction + } + } + ] + })) diff --git a/package/rules/index.js b/package/rules/index.js index 0835ae3e1..3978af999 100644 --- a/package/rules/index.js +++ b/package/rules/index.js @@ -7,6 +7,7 @@ const rules = { css: require('./css'), sass: require('./sass'), babel: require('./babel'), + swc: require('./swc'), erb: require('./erb'), coffee: require('./coffee'), less: require('./less'), diff --git a/package/rules/swc.js b/package/rules/swc.js new file mode 100644 index 000000000..288b892c8 --- /dev/null +++ b/package/rules/swc.js @@ -0,0 +1,23 @@ +const { resolve } = require('path') +const { realpathSync } = require('fs') +const { loaderMatches } = require('../utils/helpers') +const { getSwcLoaderConfig } = require('../swc') + +const { + source_path: sourcePath, + additional_paths: additionalPaths, + webpack_loader: webpackLoader +} = require('../config') + +module.exports = loaderMatches(webpackLoader, 'swc', () => ({ + test: /\.(ts|tsx|js|jsx|mjs|coffee)?(\.erb)?$/, + include: [sourcePath, ...additionalPaths].map((p) => { + try { + return realpathSync(p) + } catch (e) { + return resolve(p) + } + }), + exclude: /node_modules/, + use: ({ resource }) => getSwcLoaderConfig(resource) +})) diff --git a/package/swc/index.js b/package/swc/index.js new file mode 100644 index 000000000..b5cee12fd --- /dev/null +++ b/package/swc/index.js @@ -0,0 +1,50 @@ +/* eslint global-require: 0 */ +/* eslint import/no-dynamic-require: 0 */ + +const { resolve } = require('path') +const { existsSync } = require('fs') +const { merge } = require('webpack-merge') + +const isJsxFile = (filename) => !!filename.match(/\.(jsx|tsx)?(\.erb)?$/) + +const isTypescriptFile = (filename) => !!filename.match(/\.(ts|tsx)?(\.erb)?$/) + +const getCustomConfig = () => { + const path = resolve('config', 'swc.config.js') + if (existsSync(path)) { + return require(path) + } + return {} +} + +const getSwcLoaderConfig = (filenameToProcess) => { + const customConfig = getCustomConfig() + const defaultConfig = { + loader: require.resolve('swc-loader'), + options: { + jsc: { + parser: { + dynamicImport: true, + syntax: isTypescriptFile(filenameToProcess) + ? 'typescript' + : 'ecmascript', + [isTypescriptFile(filenameToProcess) ? 'tsx' : 'jsx']: + isJsxFile(filenameToProcess) + } + }, + sourceMaps: true, + env: { + coreJs: '3.8', + loose: true, + exclude: ['transform-typeof-symbol'], + mode: 'entry' + } + } + } + + return merge(defaultConfig, customConfig) +} + +module.exports = { + getSwcLoaderConfig +} diff --git a/package/utils/helpers.js b/package/utils/helpers.js index 34f9ac9c9..7d3d00ca8 100644 --- a/package/utils/helpers.js +++ b/package/utils/helpers.js @@ -27,7 +27,7 @@ const resolvedPath = (packageName) => { } } -const moduleExists = (packageName) => (!!resolvedPath(packageName)) +const moduleExists = (packageName) => !!resolvedPath(packageName) const canProcess = (rule, fn) => { const modulePath = resolvedPath(rule) @@ -39,6 +39,22 @@ const canProcess = (rule, fn) => { return null } +const loaderMatches = (configLoader, loaderToCheck, fn) => { + if (configLoader !== loaderToCheck) { + return null + } + + const loaderName = `${configLoader}-loader` + + if (!moduleExists(loaderName)) { + throw new Error( + `Your webpacker config specified using ${configLoader}, but ${loaderName} package is not installed. Please install ${loaderName} first.` + ) + } + + return fn() +} + module.exports = { chdirTestApp, chdirCwd, @@ -47,5 +63,6 @@ module.exports = { ensureTrailingSlash, canProcess, moduleExists, - resetEnv + resetEnv, + loaderMatches } diff --git a/test/mounted_app/test/dummy/config/webpacker.yml b/test/mounted_app/test/dummy/config/webpacker.yml index 18939b5e4..38720832b 100644 --- a/test/mounted_app/test/dummy/config/webpacker.yml +++ b/test/mounted_app/test/dummy/config/webpacker.yml @@ -5,6 +5,7 @@ default: &default source_entry_path: entrypoints public_output_path: packs cache_path: tmp/webpacker + webpack_loader: babel # Additional paths webpack should look up modules # ['app/assets', 'engine/foo/app/assets'] diff --git a/test/test_app/config/webpacker.yml b/test/test_app/config/webpacker.yml index 257d96244..7cac6e2ff 100644 --- a/test/test_app/config/webpacker.yml +++ b/test/test_app/config/webpacker.yml @@ -7,6 +7,7 @@ default: &default public_output_path: packs cache_path: tmp/webpacker webpack_compile_output: false + webpack_loader: babel # Additional paths webpack should look up modules # ['app/assets', 'engine/foo/app/assets'] diff --git a/yarn.lock b/yarn.lock index 5680311b5..34f2edafa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2841,6 +2841,15 @@ loader-utils@^1.4.0: emojis-list "^3.0.0" json5 "^1.0.1" +loader-utils@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.2.tgz#d6e3b4fb81870721ae4e0868ab11dd638368c129" + integrity sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A== + dependencies: + big.js "^5.2.2" + emojis-list "^3.0.0" + json5 "^2.1.2" + locate-path@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" @@ -3621,6 +3630,13 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== +swc-loader@^0.1.15: + version "0.1.15" + resolved "https://registry.yarnpkg.com/swc-loader/-/swc-loader-0.1.15.tgz#cb9c630ccfbb46dabc5aebc5560cced658e32992" + integrity sha512-cn1WPIeQJvXM4bbo3OwdEIapsQ4uUGOfyFj0h2+2+brT0k76DCGnZXDE2KmcqTd2JSQ+b61z2NPMib7eEwMYYw== + dependencies: + loader-utils "^2.0.0" + symbol-tree@^3.2.4: version "3.2.4" resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"