diff --git a/.yarn/cache/glob-hasher-darwin-arm64-npm-1.4.2-9ddb83f4d7-10.zip b/.yarn/cache/glob-hasher-darwin-arm64-npm-1.4.2-9ddb83f4d7-10.zip new file mode 100644 index 000000000..e24f62fd9 Binary files /dev/null and b/.yarn/cache/glob-hasher-darwin-arm64-npm-1.4.2-9ddb83f4d7-10.zip differ diff --git a/.yarn/cache/glob-hasher-darwin-x64-npm-1.4.2-280fed77df-10.zip b/.yarn/cache/glob-hasher-darwin-x64-npm-1.4.2-280fed77df-10.zip new file mode 100644 index 000000000..a20bbaa40 Binary files /dev/null and b/.yarn/cache/glob-hasher-darwin-x64-npm-1.4.2-280fed77df-10.zip differ diff --git a/.yarn/cache/glob-hasher-linux-x64-gnu-npm-1.4.2-b396ded5b0-10.zip b/.yarn/cache/glob-hasher-linux-x64-gnu-npm-1.4.2-b396ded5b0-10.zip new file mode 100644 index 000000000..00a4013ce Binary files /dev/null and b/.yarn/cache/glob-hasher-linux-x64-gnu-npm-1.4.2-b396ded5b0-10.zip differ diff --git a/.yarn/cache/glob-hasher-npm-1.4.2-e479950379-7d21697e63.zip b/.yarn/cache/glob-hasher-npm-1.4.2-e479950379-7d21697e63.zip new file mode 100644 index 000000000..80b209a9a Binary files /dev/null and b/.yarn/cache/glob-hasher-npm-1.4.2-e479950379-7d21697e63.zip differ diff --git a/.yarn/cache/glob-hasher-win32-arm64-msvc-npm-1.4.2-ca7c6b652c-10.zip b/.yarn/cache/glob-hasher-win32-arm64-msvc-npm-1.4.2-ca7c6b652c-10.zip new file mode 100644 index 000000000..45abea64e Binary files /dev/null and b/.yarn/cache/glob-hasher-win32-arm64-msvc-npm-1.4.2-ca7c6b652c-10.zip differ diff --git a/.yarn/cache/glob-hasher-win32-x64-msvc-npm-1.4.2-5073d583bc-10.zip b/.yarn/cache/glob-hasher-win32-x64-msvc-npm-1.4.2-5073d583bc-10.zip new file mode 100644 index 000000000..c054215f2 Binary files /dev/null and b/.yarn/cache/glob-hasher-win32-x64-msvc-npm-1.4.2-5073d583bc-10.zip differ diff --git a/.yarn/cache/lage-npm-2.12.6-f4b72dc1f4-1b22aaab31.zip b/.yarn/cache/lage-npm-2.12.6-f4b72dc1f4-1b22aaab31.zip new file mode 100644 index 000000000..722395e16 Binary files /dev/null and b/.yarn/cache/lage-npm-2.12.6-f4b72dc1f4-1b22aaab31.zip differ diff --git a/internals/lage-workers/eslint-worker.js b/internals/lage-workers/eslint-worker.js new file mode 100644 index 000000000..6f45c0870 --- /dev/null +++ b/internals/lage-workers/eslint-worker.js @@ -0,0 +1,41 @@ +import { ESLint } from 'eslint'; + +/** @type {ESLint} */ +let eslintInstance = null; + +/** caches an ESLint instance for the worker */ +function getEslintInstance(target) { + if (!eslintInstance) { + eslintInstance = new ESLint({ + fix: false, + cache: false, + cwd: target.cwd, + }); + } + return eslintInstance; +} + +/** Workers should have a run function that gets called per package task */ +async function run(data) { + const { target } = data; + const eslint = getEslintInstance(target); + + // You can also use "options" to pass different files pattern to lint + // e.g. data.options.files; you'll need to then configure this inside + // lage.config.js's pipeline + const files = 'src/**/*.ts'; + const results = await eslint.lintFiles(files); + const formatter = await eslint.loadFormatter('stylish'); + const resultText = formatter.format(results); + + // Output results to stdout + process.stdout.write(resultText + '\n'); + if (results.some((r) => r.errorCount > 0)) { + // throw an error to indicate that this task has failed + throw new Error(`Linting failed with errors`); + } +} + +// The module export is picked up by `lage` to run inside a worker, and the +// module's state is preserved from target run to target run. +export default run; diff --git a/internals/lage-workers/package.json b/internals/lage-workers/package.json new file mode 100644 index 000000000..0ed5a21a7 --- /dev/null +++ b/internals/lage-workers/package.json @@ -0,0 +1,17 @@ +{ + "name": "@repo/lage-workers", + "version": "0.0.0", + "private": true, + "type": "module", + "engines": { + "node": ">=18" + }, + "exports": { + "./eslint-worker": "./eslint-worker.js", + "./tsc-worker": "./tsc-worker.js" + }, + "dependencies": { + "eslint": "^9.18.0", + "typescript": "^5.7.3" + } +} diff --git a/internals/lage-workers/tsc-worker.js b/internals/lage-workers/tsc-worker.js new file mode 100644 index 000000000..2a48eda5c --- /dev/null +++ b/internals/lage-workers/tsc-worker.js @@ -0,0 +1,128 @@ +import ts from 'typescript'; +import path from 'node:path'; +import { existsSync } from 'node:fs'; + +// Save the previously run ts.program to be fed inside the next call +let oldProgram; + +let compilerHost; + +/** this is the patch to ts.compilerHost that retains sourceFiles in a Map **/ +function createCompilerHost(compilerOptions) { + const host = ts.createCompilerHost(compilerOptions, true); + const sourceFiles = new Map(); + const originalGetSourceFile = host.getSourceFile; + + // monkey patch host to cache source files + host.getSourceFile = ( + fileName, + languageVersion, + onError, + shouldCreateNewSourceFile, + ) => { + if (sourceFiles.has(fileName)) { + return sourceFiles.get(fileName); + } + + const sourceFile = originalGetSourceFile( + fileName, + languageVersion, + onError, + shouldCreateNewSourceFile, + ); + + sourceFiles.set(fileName, sourceFile); + + return sourceFile; + }; + + return host; +} + +async function tsc(data) { + const { target } = data; // Lage target data + + const tsconfigJsonFile = path.join(target.cwd, 'tsconfig.json'); + + if (!existsSync(tsconfigJsonFile)) { + console.log(`this package (${target.cwd}) has no tsconfig.json, skipping work!`); + return; + } + + // Parse tsconfig + const configParserHost = parseConfigHostFromCompilerHostLike(compilerHost ?? ts.sys); + const parsedCommandLine = ts.getParsedCommandLineOfConfigFile( + tsconfigJsonFile, + {}, + configParserHost, + ); + if (!parsedCommandLine) { + throw new Error('Could not parse tsconfig.json'); + } + const compilerOptions = parsedCommandLine.options; + + // Creating compilation host program + compilerHost = compilerHost ?? createCompilerHost(compilerOptions); + + // The re-use of oldProgram is a trick we all learned from gulp-typescript, credit to ivogabe + // @see https://github.com/ivogabe/gulp-typescript + const program = ts.createProgram( + parsedCommandLine.fileNames, + compilerOptions, + compilerHost, + oldProgram, + ); + + oldProgram = program; + + const errors = { + semantics: program.getSemanticDiagnostics(), + declaration: program.getDeclarationDiagnostics(), + syntactic: program.getSyntacticDiagnostics(), + global: program.getGlobalDiagnostics(), + }; + + const allErrors = []; + + try { + program.emit(); + } catch (error) { + console.log(error.messageText); + throw new Error('Encountered errors while emitting'); + } + + let hasErrors = false; + + for (const kind of Object.keys(errors)) { + for (const diagnostics of errors[kind]) { + hasErrors = true; + allErrors.push(diagnostics); + } + } + + if (hasErrors) { + console.log(ts.formatDiagnosticsWithColorAndContext(allErrors, compilerHost)); + throw new Error('Failed to compile'); + } else { + console.log('Compiled successfully\n'); + return; + } +} + +function parseConfigHostFromCompilerHostLike(host) { + return { + fileExists: (f) => host.fileExists(f), + readDirectory(root, extensions, excludes, includes, depth) { + return host.readDirectory(root, extensions, excludes, includes, depth); + }, + readFile: (f) => host.readFile(f), + useCaseSensitiveFileNames: host.useCaseSensitiveFileNames, + getCurrentDirectory: host.getCurrentDirectory, + onUnRecoverableConfigFileDiagnostic: (d) => { + throw new Error(ts.flattenDiagnosticMessageText(d.messageText, '\n')); + }, + trace: host.trace, + }; +} + +export default tsc; diff --git a/internals/tsconfig/tsconfig.json b/internals/tsconfig/tsconfig.json index 5cb5b2f5a..273ed9c80 100644 --- a/internals/tsconfig/tsconfig.json +++ b/internals/tsconfig/tsconfig.json @@ -4,7 +4,7 @@ "esm": true }, "compilerOptions": { - "lib": ["es2023"], + "lib": ["es2023", "WebWorker"], "skipLibCheck": false, "esModuleInterop": true } diff --git a/lage.config.cjs b/lage.config.cjs new file mode 100644 index 000000000..d2e71c3b4 --- /dev/null +++ b/lage.config.cjs @@ -0,0 +1,23 @@ +module.exports = { + pipeline: { + attw: { + dependsOn: ['tsc'], + outputs: [], + }, + tsc: { + type: 'worker', + options: { + worker: require.resolve('@repo/lage-workers/tsc-worker'), + }, + dependsOn: ['^tsc'], + outputs: ['dist/**'], + }, + lint: { + type: 'worker', + options: { + worker: require.resolve('@repo/lage-workers/eslint-worker'), + }, + }, + }, + npmClient: 'yarn', +}; diff --git a/package.json b/package.json index 05cb2a072..3c89ee020 100644 --- a/package.json +++ b/package.json @@ -74,6 +74,7 @@ "devDependencies": { "@eslint/js": "^9.18.0", "@repo/hoist-peer-dependencies": "workspace:*", + "@repo/lage-workers": "workspace:*", "@repo/tsconfig": "workspace:*", "@types/node": "^22.10.5", "@vitest/coverage-v8": "^3.0.2", @@ -86,6 +87,7 @@ "globals": "^15.14.0", "globby": "^14.0.2", "husky": "^9.1.7", + "lage": "^2.12.6", "lerna": "^8.1.9", "nano-staged": "^0.8.0", "prettier": "^3.4.2", diff --git a/yarn.lock b/yarn.lock index 2e46e08fb..e396b22f8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -733,6 +733,7 @@ __metadata: dependencies: "@eslint/js": "npm:^9.18.0" "@repo/hoist-peer-dependencies": "workspace:*" + "@repo/lage-workers": "workspace:*" "@repo/tsconfig": "workspace:*" "@types/node": "npm:^22.10.5" "@vitest/coverage-v8": "npm:^3.0.2" @@ -745,6 +746,7 @@ __metadata: globals: "npm:^15.14.0" globby: "npm:^14.0.2" husky: "npm:^9.1.7" + lage: "npm:^2.12.6" lerna: "npm:^8.1.9" nano-staged: "npm:^0.8.0" prettier: "npm:^3.4.2" @@ -1509,6 +1511,15 @@ __metadata: languageName: unknown linkType: soft +"@repo/lage-workers@workspace:*, @repo/lage-workers@workspace:internals/lage-workers": + version: 0.0.0-use.local + resolution: "@repo/lage-workers@workspace:internals/lage-workers" + dependencies: + eslint: "npm:^9.18.0" + typescript: "npm:^5.7.3" + languageName: unknown + linkType: soft + "@repo/tsconfig@workspace:*, @repo/tsconfig@workspace:internals/tsconfig": version: 0.0.0-use.local resolution: "@repo/tsconfig@workspace:internals/tsconfig" @@ -4360,6 +4371,65 @@ __metadata: languageName: node linkType: hard +"glob-hasher-darwin-arm64@npm:1.4.2": + version: 1.4.2 + resolution: "glob-hasher-darwin-arm64@npm:1.4.2" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"glob-hasher-darwin-x64@npm:1.4.2": + version: 1.4.2 + resolution: "glob-hasher-darwin-x64@npm:1.4.2" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"glob-hasher-linux-x64-gnu@npm:1.4.2": + version: 1.4.2 + resolution: "glob-hasher-linux-x64-gnu@npm:1.4.2" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"glob-hasher-win32-arm64-msvc@npm:1.4.2": + version: 1.4.2 + resolution: "glob-hasher-win32-arm64-msvc@npm:1.4.2" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"glob-hasher-win32-x64-msvc@npm:1.4.2": + version: 1.4.2 + resolution: "glob-hasher-win32-x64-msvc@npm:1.4.2" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"glob-hasher@npm:^1.4.2": + version: 1.4.2 + resolution: "glob-hasher@npm:1.4.2" + dependencies: + glob-hasher-darwin-arm64: "npm:1.4.2" + glob-hasher-darwin-x64: "npm:1.4.2" + glob-hasher-linux-x64-gnu: "npm:1.4.2" + glob-hasher-win32-arm64-msvc: "npm:1.4.2" + glob-hasher-win32-x64-msvc: "npm:1.4.2" + dependenciesMeta: + glob-hasher-darwin-arm64: + optional: true + glob-hasher-darwin-x64: + optional: true + glob-hasher-linux-x64-gnu: + optional: true + glob-hasher-win32-arm64-msvc: + optional: true + glob-hasher-win32-x64-msvc: + optional: true + checksum: 10/7d21697e63cc43f6edcec52b88ed0cc877011edaca1446c0fb1fb2efb3b7bd26f9900dfde7a7ab62ee425c6bd8da359c0a3d34997c9916de6b887029ca2995d8 + languageName: node + linkType: hard + "glob-parent@npm:6.0.2, glob-parent@npm:^6.0.2": version: 6.0.2 resolution: "glob-parent@npm:6.0.2" @@ -5351,6 +5421,22 @@ __metadata: languageName: node linkType: hard +"lage@npm:^2.12.6": + version: 2.12.6 + resolution: "lage@npm:2.12.6" + dependencies: + fsevents: "npm:~2.3.2" + glob-hasher: "npm:^1.4.2" + dependenciesMeta: + fsevents: + optional: true + bin: + lage: dist/lage.js + lage-server: dist/lage-server.js + checksum: 10/1b22aaab3170a1a6c83ddddf5ab6569e73e24c66e349abffcc12f812071517d32fd4860f7eb530c6db9e9aac56348ebe29415367dfb45377f8b93c3c514c91b5 + languageName: node + linkType: hard + "lerna@npm:^8.1.9": version: 8.1.9 resolution: "lerna@npm:8.1.9"