Skip to content

Commit

Permalink
feat(expotools): use swc to compile source files (expo#20790)
Browse files Browse the repository at this point in the history
# Why

- We use swc to compile Expo CLI, it's really fast.

## Before

`time yarn tsc` -- `14.22s`

## After

`time yarn build` -- `0.67s`

> Over 21x faster.

# How

- Wrote a Taskr file that bundled the code similar to before. One very
notable difference is that type-checking is skipped.
- Not sure if the `patch`, `sh`, `jar`, `txt` files should be copied
into the build folder. Looks like they weren't being copied over with
tsc.

# Test Plan

- `yarn clean && yarn et -h`
  • Loading branch information
EvanBacon authored Jan 13, 2023
1 parent dd207f8 commit cd9c29b
Show file tree
Hide file tree
Showing 6 changed files with 504 additions and 16 deletions.
5 changes: 4 additions & 1 deletion .github/workflows/expotools.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ jobs:
run: yarn install --ignore-scripts --frozen-lockfile
working-directory: tools
- name: 🛠 Compile TypeScript sources
run: yarn tsc
run: yarn build
working-directory: tools
- name: 🛠 Typecheck sources
run: yarn tsc --noEmit
working-directory: tools
- name: 🚨 Lint TypeScript sources
run: yarn lint --max-warnings 0
Expand Down
19 changes: 15 additions & 4 deletions tools/bin/expotools.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,11 @@ async function maybeRebuildAndRun() {

try {
// Compile TypeScript files into build folder.
await spawnAsync('yarn', ['run', 'tsc']);
await spawnAsync('yarn', ['run', 'build']);
state.schema = await getCommandsSchemaAsync();
} catch (error) {
console.error(LogModifiers.error(` 💥 Rebuilding failed: ${error.stack}`));
process.exit(1);
return;
}
console.log(` ✨ Successfully built ${LogModifiers.name('expotools')}\n`);
}
Expand Down Expand Up @@ -110,7 +109,18 @@ async function calculateSourceChecksumAsync() {
exclude: ['build', 'cache', 'node_modules'],
},
files: {
include: ['*.ts', 'expotools.js', 'tsconfig.json'],
include: [
// source files
'src/**/*.ts',
// src/versioning files
'src/**/*.json',
'expotools.js',
// swc build files
'taskfile.js',
'taskfile-swc.js',
// type checking
'tsconfig.json',
],
},
});
}
Expand Down Expand Up @@ -177,6 +187,7 @@ function readState() {
}

function saveState(state) {
fs.mkdirSync(path.dirname(STATE_PATH), { recursive: true });
fs.writeFileSync(STATE_PATH, JSON.stringify(state, null, 2));
}

Expand Down Expand Up @@ -207,7 +218,7 @@ function canRequire(packageName) {
try {
require.resolve(packageName);
return true;
} catch (error) {
} catch {
return false;
}
}
Expand Down
18 changes: 14 additions & 4 deletions tools/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,16 @@
"templates"
],
"scripts": {
"build": "tsc",
"watch": "tsc --watch",
"build": "taskr release",
"watch": "taskr",
"clean": "rm -rf build cache",
"et": "node bin/expotools.js",
"lint": "eslint ./src"
"lint": "eslint ./src taskfile.js taskfile-swc.js"
},
"taskr": {
"requires": [
"./taskfile-swc.js"
]
},
"author": "[email protected]",
"license": "MIT",
Expand Down Expand Up @@ -66,6 +71,10 @@
"xcode": "^3.0.1"
},
"devDependencies": {
"@swc/core": "^1.2.126",
"@taskr/clear": "1.1.0",
"@taskr/esnext": "1.1.0",
"@taskr/watch": "1.1.0",
"@babel/core": "^7.16.0",
"@types/diff": "^5.0.2",
"@types/folder-hash": "^4.0.2",
Expand All @@ -81,6 +90,7 @@
"eslint-config-universe": "^11.1.1",
"eslint-plugin-lodash": "^7.4.0",
"prettier": "^2.8.1",
"typescript": "^4.9.4"
"typescript": "^4.9.4",
"taskr": "1.1.0"
}
}
69 changes: 69 additions & 0 deletions tools/taskfile-swc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Based on Next.js swc taskr file.
// https://github.com/vercel/next.js/blob/5378db8f807dbb9ff0993662f0a39d0f6cba2452/packages/next/taskfile-swc.js

const { transform } = require('@swc/core');
const path = require('path');

module.exports = function (task) {
task.plugin('swc', {}, function* (file, environment, { stripExtension } = {}) {
// Don't compile .d.ts
if (file.base.endsWith('.d.ts')) return;

const setting = {
output: 'build',
options: {
module: {
type: 'commonjs',
},
env: {
targets: {
node: '14.17.6',
},
},
jsc: {
loose: true,
parser: {
syntax: 'typescript',
dynamicImport: true,
},
},
},
};

const filePath = path.join(file.dir, file.base);
const inputFilePath = path.join(__dirname, filePath);
const outputFilePath = path.dirname(path.join(__dirname, setting.output, filePath));

const options = {
filename: path.join(file.dir, file.base),
sourceMaps: true,
sourceFileName: path.relative(outputFilePath, inputFilePath),
...setting.options,
};

const output = yield transform(file.data.toString('utf-8'), options);
const ext = path.extname(file.base);

// Replace `.ts|.tsx` with `.js` in files with an extension
if (ext) {
const extRegex = new RegExp(ext.replace('.', '\\.') + '$', 'i');
// Remove the extension if stripExtension is enabled or replace it with `.js`
file.base = file.base.replace(extRegex, stripExtension ? '' : '.js');
}

if (output.map) {
const map = `${file.base}.map`;

output.code += Buffer.from(`\n//# sourceMappingURL=${map}`);

// add sourcemap to `files` array
this._.files.push({
base: map,
dir: file.dir,
data: Buffer.from(output.map),
});
}

file.data = Buffer.from(output.code);
});
};
38 changes: 38 additions & 0 deletions tools/taskfile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
const { boolish } = require('getenv');
const process = require('process');

export async function build(task, opts) {
// Process JS/TS files with SWC
await task
.source('src/**/*.+(js|ts)', {
ignore: ['**/__tests__/**', '**/__mocks__/**'],
})
.swc('cli', { dev: opts.dev })
.target('build');

// Copy over JSON files
await task
.source('src/**/*.+(json)', {
ignore: ['**/__tests__/**', '**/__mocks__/**'],
})
.target('build');
}

export default async function (task) {
await task.clear('build');
await task.start('build', { dev: true });
}

export async function watch(task) {
const opts = { dev: true };
await task.clear('build');
await task.start('build', opts);
if (process.stdout.isTTY && !boolish('CI', false) && !boolish('EXPO_NONINTERACTIVE', false)) {
// Watch source folder
await task.watch('src/**/*.+(js|ts|json)', 'build', opts);
}
}

export async function release(task) {
await task.clear('build').start('build');
}
Loading

0 comments on commit cd9c29b

Please sign in to comment.