Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use SWC loader #29

Merged
merged 18 commits into from
Jan 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can either change this package or make a new one that makes this integration super easy.

https://github.com/shakacode/react-on-webpacker/


: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']:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we move React support back into Shakapacker? Seems that we should keep it the same for both babel and swc? However, now that I'm maintaining shakapacker, I don't mind having React support being supported by default.

rails/webpacker#3224

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@justin808 there's two parts to it - understanding jsx syntax and React transform. I think we can move the latter to a config but with the former relying on the filename it might need to stay.

I'll see how it handles non-React jsx files and adjust accordingly.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@justin808 There's some weirdness with passing options to swc inline, for more details swc-project/swc#3374

This setup will work though regardless and I see no real reason not to support basic React config out of the box if we're not requiring any specific presets or plugins like @babel/preset-react. It "just" works so we might as well embrace it. We could drop the tsx/jsx setting here and move to the custom config but that will probably need to become a function that takes a filename to allow dynamic setting of jsx/tsx value (no idea why SWC just didn't settle for one setting 🤷 ).

Still, I've removed the transform setting from the default config and added section to the docs on how might one add settings to match any settings they have in preset-react if they wish to do so.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll iterate on this after the first release.

Maybe we can just be clear that SWC is experimental and the final API might change by the time 6.1.0 is released.

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.`
tomdracz marked this conversation as resolved.
Show resolved Hide resolved
)
}

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