Skip to content
Draft
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
25 changes: 25 additions & 0 deletions lib/src/ts-project.spec.ts
Original file line number Diff line number Diff line change
@@ -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([]);
});
});
41 changes: 40 additions & 1 deletion lib/src/ts-project.ts
Original file line number Diff line number Diff line change
@@ -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.
*
Expand Down Expand Up @@ -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")]
}
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"
Expand All @@ -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",
Expand Down
54 changes: 19 additions & 35 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading