Skip to content

Commit

Permalink
Use SWC loader (#29)
Browse files Browse the repository at this point in the history
* Use webpack config to determine loader to use
* Try and match babel preset env configurations
* Add development check for refresh and add dynamic imports
* Use correct object shape for the SWC rules
* Only run refresh if running webpack dev server
* Add SWC loader usage doc
* Don't install swc libs by default from peer dependencies
* Allow customisation of SWC config
* Document SWC loader customisation
* Add another example showcasing browserslist config
* Don't define React transform
* Clarify fast refresh usage
  • Loading branch information
Tom Dracz authored Jan 28, 2022
1 parent 4d41b5a commit 40218f2
Show file tree
Hide file tree
Showing 13 changed files with 297 additions and 24 deletions.
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down
151 changes: 151 additions & 0 deletions docs/using_swc_loader.md
Original file line number Diff line number Diff line change
@@ -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.
3 changes: 3 additions & 0 deletions lib/install/config/webpacker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
44 changes: 23 additions & 21 deletions package/rules/babel.js
Original file line number Diff line number Diff line change
@@ -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
}
}
]
}))
1 change: 1 addition & 0 deletions package/rules/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
23 changes: 23 additions & 0 deletions package/rules/swc.js
Original file line number Diff line number Diff line change
@@ -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)
}))
50 changes: 50 additions & 0 deletions package/swc/index.js
Original file line number Diff line number Diff line change
@@ -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
}
21 changes: 19 additions & 2 deletions package/utils/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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,
Expand All @@ -47,5 +63,6 @@ module.exports = {
ensureTrailingSlash,
canProcess,
moduleExists,
resetEnv
resetEnv,
loaderMatches
}
1 change: 1 addition & 0 deletions test/mounted_app/test/dummy/config/webpacker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand Down
1 change: 1 addition & 0 deletions test/test_app/config/webpacker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand Down
Loading

0 comments on commit 40218f2

Please sign in to comment.