diff --git a/lib/src/ts-project.spec.ts b/lib/src/ts-project.spec.ts new file mode 100644 index 000000000..6a4ba7e5e --- /dev/null +++ b/lib/src/ts-project.spec.ts @@ -0,0 +1,25 @@ +import { createContractProject } from "./ts-project"; + +describe("createContractProject", () => { + it("type-checks a contract that imports a node builtin", () => { + // TypeScript 6 stopped pulling in `node_modules/@types` on its own, so the + // types a contract can rely on are only the ones `contractCompilerOptions` + // names. Without `types: ["node"]` this reports "Cannot find name 'path'" — + // it still parses, it stops type-checking. `typeRoots` is not pinned here: + // this passes under the default roots too, because the repository root has + // a `node_modules`. The parity job's self-containment case is what covers + // that half. + const project = createContractProject(); + project.createSourceFile( + "contract.ts", + 'import * as path from "path";\n' + + 'export const prefix: string = path.join("/", "v1");\n' + ); + + expect( + project + .getPreEmitDiagnostics() + .map(diagnostic => diagnostic.getMessageText()) + ).toEqual([]); + }); +}); diff --git a/lib/src/ts-project.ts b/lib/src/ts-project.ts index b088c11a2..99810c118 100644 --- a/lib/src/ts-project.ts +++ b/lib/src/ts-project.ts @@ -1,6 +1,25 @@ import * as path from "path"; import { CompilerOptions, Project, ts } from "ts-morph"; +/** + * The type root `@types/node` resolves from. + * + * Derived from where the package actually resolved rather than from a fixed + * number of `..` steps, so it survives both the compiled layout and pnpm's + * symlinked store. + * + * A root for `@types/node` and nothing else, under pnpm: `require.resolve` + * realpaths into the store, and that directory holds the one package. npm and + * yarn hoist, so the same expression there returns a directory holding every + * `@types` package installed. A second entry in `types` would resolve under + * those and fail under pnpm. + */ +function nodeTypesRoot(): string { + return path.dirname( + path.dirname(require.resolve("@types/node/package.json")) + ); +} + /** * The compiler options every Spot contract is parsed and type-checked under. * @@ -30,7 +49,27 @@ export const contractCompilerOptions: CompilerOptions = { skipLibCheck: true, moduleResolution: ts.ModuleResolutionKind.NodeJs, experimentalDecorators: true, - baseUrl: "./", + // TypeScript 6 reports `moduleResolution: node10` as deprecated and refuses + // to run without this. Keeping node10 keeps contract resolution exactly as it + // is; moving off it changes how a contract's own imports resolve, which is a + // decision about published behaviour rather than part of a dependency bump. + // It stops working altogether in TypeScript 7, which is + // https://airtasker.atlassian.net/browse/COMPASS-31 + ignoreDeprecations: "6.0", + // TypeScript 6 no longer picks up `node_modules/@types` on its own, so a + // contract that imports a node builtin stops type-checking unless the types + // are named. `typeRoots` points at Spot's own copy for the same reason + // `paths` does below: the default walks up from the working directory, which + // for the image is a mounted workspace that has no `node_modules` at all. + // `@types/node` is a runtime dependency because of this — it has to reach the + // image, which installs production dependencies only. + // + // These two are the whole of what a contract can rely on ambiently. Under + // TypeScript 5 it reached every `@types` package resolvable from where it + // sat, and there is no option that restores that. Whether to give the reach + // back is https://airtasker.atlassian.net/browse/COMPASS-32 + types: ["node"], + typeRoots: [nodeTypesRoot()], paths: { "@airtasker/spot": [path.join(__dirname, "lib")] } diff --git a/package.json b/package.json index 2e1a88537..050511833 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "@oclif/errors": "^1.3.6", "@oclif/help": "^1.0.15", "@oclif/plugin-help": "^3.2.3", + "@types/node": "^22.20.1", "ajv": "^8.18.0", "ajv-formats": "^3.0.1", "assert-never": "^1.4.0", @@ -26,7 +27,7 @@ "prettier": "^3.9.6", "qs": "^6.15.3", "randomstring": "^1.3.1", - "ts-morph": "18.0.0", + "ts-morph": "^28.0.0", "typescript": "^5.9.3", "typescript-eslint": "^8.67.0", "validator": "^13.12.0" @@ -40,7 +41,6 @@ "@types/express": "^5.0.6", "@types/fs-extra": "^11.0.4", "@types/jest": "^30.0.0", - "@types/node": "^22.20.1", "@types/qs": "^6.15.1", "@types/randomstring": "^1.3.0", "@types/supertest": "^7.2.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 40823d174..c0392b62b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -30,6 +30,9 @@ importers: '@oclif/plugin-help': specifier: ^3.2.3 version: 3.3.1 + '@types/node': + specifier: ^22.20.1 + version: 22.20.1 ajv: specifier: ^8.18.0 version: 8.18.0 @@ -70,8 +73,8 @@ importers: specifier: ^1.3.1 version: 1.3.1 ts-morph: - specifier: 18.0.0 - version: 18.0.0 + specifier: ^28.0.0 + version: 28.0.0 typescript: specifier: ^5.9.3 version: 5.9.3 @@ -106,9 +109,6 @@ importers: '@types/jest': specifier: ^30.0.0 version: 30.0.0 - '@types/node': - specifier: ^22.20.1 - version: 22.20.1 '@types/qs': specifier: ^6.15.1 version: 6.15.1 @@ -938,8 +938,8 @@ packages: resolution: {integrity: sha512-JZlVFE6/dYpP9tQmV0/ADfn32L9uFarHWxfcRhReKUnljz1ZiUM5zpX+PH8h5CJs6lao3TuFqnPm9IJJCEkE2w==} engines: {node: '>=10.8'} - '@ts-morph/common@0.19.0': - resolution: {integrity: sha512-Unz/WHmd4pGax91rdIKWi51wnVUW11QttMEPpBiBgIewnc9UQIX7UDLxr5vRlqeByXCwhkF6VabSsI0raWcyAQ==} + '@ts-morph/common@0.29.0': + resolution: {integrity: sha512-35oUmphHbJvQ/+UTwFNme/t2p3FoKiGJ5auTjjpNTop2dyREspirjMy82PLSC1pnDJ8ah1GU98hwpVt64YXQsg==} '@tybys/wasm-util@0.10.3': resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} @@ -1704,8 +1704,8 @@ packages: resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} - code-block-writer@12.0.0: - resolution: {integrity: sha512-q4dMFMlXtKR3XNBHyMHt/3pwYNA69EDk00lloMOaaUMKPUXBw6lpXtbu3MMVG6/uOihGnRDOlkyqsONEUj60+w==} + code-block-writer@13.0.3: + resolution: {integrity: sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==} collect-v8-coverage@1.0.2: resolution: {integrity: sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==} @@ -3124,10 +3124,6 @@ packages: resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} engines: {node: '>=10'} - minimatch@7.4.6: - resolution: {integrity: sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==} - engines: {node: '>=10'} - minimatch@9.0.9: resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} engines: {node: '>=16 || 14 >=14.17'} @@ -3147,11 +3143,6 @@ packages: engines: {node: '>=10'} hasBin: true - mkdirp@2.1.6: - resolution: {integrity: sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==} - engines: {node: '>=10'} - hasBin: true - mobx-react-lite@4.1.1: resolution: {integrity: sha512-iUxiMpsvNraCKXU+yPotsOncNNmyeS2B5DKL+TL6Tar/xm+wwNJAubJmtRSeAoYawdZqwv8Z/+5nPRHeQxTiXg==} peerDependencies: @@ -4077,8 +4068,8 @@ packages: typescript: '*' webpack: ^5.0.0 - ts-morph@18.0.0: - resolution: {integrity: sha512-Kg5u0mk19PIIe4islUI/HWRvm9bC1lHejK4S0oh1zaZ77TMZAEmQC0sHQYiu2RgCQFZKXz1fMVi/7nOOeirznA==} + ts-morph@28.0.0: + resolution: {integrity: sha512-Wp3tnZ2bzwxyTZMtgWVzXDfm7lB1Drz+y9DmmYH/L702PQhPyVrp3pkou3yIz4qjS14GY9kcpmLiOOMvl8oG1g==} tslib@1.14.1: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} @@ -5511,12 +5502,11 @@ snapshots: '@stoplight/yaml-ast-parser': 0.0.50 tslib: 2.8.1 - '@ts-morph/common@0.19.0': + '@ts-morph/common@0.29.0': dependencies: - fast-glob: 3.3.1 - minimatch: 7.4.6 - mkdirp: 2.1.6 + minimatch: 10.2.6 path-browserify: 1.0.1 + tinyglobby: 0.2.17 '@tybys/wasm-util@0.10.3': dependencies: @@ -6347,7 +6337,7 @@ snapshots: co@4.6.0: {} - code-block-writer@12.0.0: {} + code-block-writer@13.0.3: {} collect-v8-coverage@1.0.2: {} @@ -8035,10 +8025,6 @@ snapshots: dependencies: brace-expansion: 2.0.1 - minimatch@7.4.6: - dependencies: - brace-expansion: 2.0.1 - minimatch@9.0.9: dependencies: brace-expansion: 2.1.4 @@ -8051,8 +8037,6 @@ snapshots: mkdirp@1.0.4: {} - mkdirp@2.1.6: {} - mobx-react-lite@4.1.1(mobx@6.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: mobx: 6.15.0 @@ -9020,7 +9004,7 @@ snapshots: dependencies: '@istanbuljs/schema': 0.1.3 glob: 7.2.3 - minimatch: 3.1.2 + minimatch: 3.1.5 tinyglobby@0.2.17: dependencies: @@ -9081,10 +9065,10 @@ snapshots: typescript: 5.9.3 webpack: 5.104.1(webpack-cli@5.1.4) - ts-morph@18.0.0: + ts-morph@28.0.0: dependencies: - '@ts-morph/common': 0.19.0 - code-block-writer: 12.0.0 + '@ts-morph/common': 0.29.0 + code-block-writer: 13.0.3 tslib@1.14.1: {}