|
1 | 1 | import type { Config, Options } from "@swc/core";
|
2 |
| -import { transformFile } from "@swc/core"; |
| 2 | +import { transform } from "@swc/core"; |
| 3 | +import { createRequire } from "node:module"; |
| 4 | +import type { XXHashAPI } from "xxhash-wasm"; |
| 5 | +import xxhash from "xxhash-wasm"; |
| 6 | + |
3 | 7 | import findRoot from "find-root";
|
4 | 8 | import * as fs from "fs/promises";
|
5 | 9 | import globby from "globby";
|
| 10 | +import _ from "lodash"; |
| 11 | +import { hasher } from "node-object-hash"; |
6 | 12 | import path from "path";
|
| 13 | +import { fileURLToPath } from "url"; |
7 | 14 | import writeFileAtomic from "write-file-atomic";
|
8 | 15 | import type { Compiler } from "./Compiler.js";
|
9 | 16 | import type { ProjectConfig } from "./Options.js";
|
10 | 17 | import { log, projectConfig } from "./utils.js";
|
11 | 18 |
|
| 19 | +const __filename = fileURLToPath(import.meta.url); |
| 20 | +const require = createRequire(import.meta.url); |
| 21 | + |
| 22 | +const getPackageVersion = async (packageDir: string) => { |
| 23 | + const packageJson = JSON.parse(await fs.readFile(path.join(packageDir, "package.json"), "utf-8")); |
| 24 | + return packageJson.version; |
| 25 | +}; |
| 26 | + |
12 | 27 | export class MissingDestinationError extends Error {
|
13 | 28 | ignoredFile: boolean;
|
14 | 29 |
|
@@ -83,11 +98,43 @@ class CompiledFiles {
|
83 | 98 | export class SwcCompiler implements Compiler {
|
84 | 99 | private compiledFiles: CompiledFiles;
|
85 | 100 | private invalidatedFiles: Set<string>;
|
| 101 | + private knownCacheEntries = new Set<string>(); |
| 102 | + |
| 103 | + static async create(workspaceRoot: string, outDir: string) { |
| 104 | + const compiler = new SwcCompiler(workspaceRoot, outDir); |
| 105 | + await compiler.initialize(); |
| 106 | + return compiler; |
| 107 | + } |
| 108 | + |
| 109 | + /** @private */ |
86 | 110 | constructor(readonly workspaceRoot: string, readonly outDir: string) {
|
87 | 111 | this.compiledFiles = new CompiledFiles();
|
88 | 112 | this.invalidatedFiles = new Set();
|
89 | 113 | }
|
90 | 114 |
|
| 115 | + private xxhash!: XXHashAPI; |
| 116 | + private cacheEpoch!: string; |
| 117 | + |
| 118 | + async initialize() { |
| 119 | + this.xxhash = await xxhash(); |
| 120 | + try { |
| 121 | + const files = await globby(path.join(this.outDir, "*", "*"), { onlyFiles: true }); |
| 122 | + for (const file of files) { |
| 123 | + this.knownCacheEntries.add(path.basename(file)); |
| 124 | + } |
| 125 | + } catch (error) { |
| 126 | + // no complaints if the cache dir doesn't exist yet |
| 127 | + } |
| 128 | + |
| 129 | + // Get package versions for cache keys |
| 130 | + const [thisPackageVersion, swcCoreVersion] = await Promise.all([ |
| 131 | + getPackageVersion(findRoot(__filename)), |
| 132 | + getPackageVersion(findRoot(require.resolve("@swc/core"))), |
| 133 | + ]); |
| 134 | + |
| 135 | + this.cacheEpoch = `${thisPackageVersion}-${swcCoreVersion}`; |
| 136 | + } |
| 137 | + |
91 | 138 | async invalidateBuildSet() {
|
92 | 139 | this.invalidatedFiles = new Set();
|
93 | 140 | this.compiledFiles = new CompiledFiles();
|
@@ -151,20 +198,33 @@ export class SwcCompiler implements Compiler {
|
151 | 198 | }
|
152 | 199 |
|
153 | 200 | private async buildFile(filename: string, root: string, config: Config): Promise<CompiledFile> {
|
154 |
| - const output = await transformFile(filename, { |
155 |
| - cwd: root, |
156 |
| - filename: filename, |
157 |
| - root: this.workspaceRoot, |
158 |
| - rootMode: "root", |
159 |
| - sourceMaps: "inline", |
160 |
| - swcrc: false, |
161 |
| - inlineSourcesContent: true, |
162 |
| - ...config, |
163 |
| - }); |
| 201 | + const content = await fs.readFile(filename, "utf8"); |
| 202 | + |
| 203 | + const contentHash = this.xxhash.h32ToString(this.cacheEpoch + "///" + filename + "///" + content); |
| 204 | + const cacheKey = `${path.basename(filename).replace(/[^a-zA-Z0-9]/g, "")}-${contentHash.slice(2)}-${hashConfig(config)}`; |
| 205 | + const destination = path.join(this.outDir, contentHash.slice(0, 2), cacheKey); |
| 206 | + |
| 207 | + if (!this.knownCacheEntries.has(cacheKey)) { |
| 208 | + const options: Options = { |
| 209 | + cwd: root, |
| 210 | + filename: filename, |
| 211 | + root: this.workspaceRoot, |
| 212 | + rootMode: "root", |
| 213 | + sourceMaps: "inline", |
| 214 | + swcrc: false, |
| 215 | + inlineSourcesContent: true, |
| 216 | + ...config, |
| 217 | + }; |
| 218 | + |
| 219 | + const [transformResult, _] = await Promise.all([ |
| 220 | + transform(content, options), |
| 221 | + fs.mkdir(path.dirname(destination), { recursive: true }), |
| 222 | + ]); |
| 223 | + |
| 224 | + await writeFileAtomic(destination, transformResult.code); |
| 225 | + this.knownCacheEntries.add(cacheKey); |
| 226 | + } |
164 | 227 |
|
165 |
| - const destination = path.join(this.outDir, filename).replace(this.workspaceRoot, ""); |
166 |
| - await fs.mkdir(path.dirname(destination), { recursive: true }); |
167 |
| - await writeFileAtomic(destination, output.code); |
168 | 228 | const file = { filename, root, destination, config };
|
169 | 229 |
|
170 | 230 | this.compiledFiles.addFile(file);
|
@@ -267,3 +327,6 @@ export class SwcCompiler implements Compiler {
|
267 | 327 | return;
|
268 | 328 | }
|
269 | 329 | }
|
| 330 | + |
| 331 | +const hashObject = hasher({ sort: true }); |
| 332 | +const hashConfig = _.memoize((config: Config) => hashObject.hash(config)); |
0 commit comments