From ca3dab80538b741c465840b843d80c11bb5cf5af Mon Sep 17 00:00:00 2001 From: sean Date: Mon, 16 Dec 2024 12:04:04 +0200 Subject: [PATCH 1/6] Support transitive dependencies Resolve transitive dependencies' versions based on package.json --- packages/ata/src/index.ts | 60 ++++++++++++++++++++++++++++------ packages/ata/tests/ata.spec.ts | 43 ++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 10 deletions(-) diff --git a/packages/ata/src/index.ts b/packages/ata/src/index.ts index f52d3dcb04f8..76dc30a0ac49 100644 --- a/packages/ata/src/index.ts +++ b/packages/ata/src/index.ts @@ -29,9 +29,22 @@ export interface ATABootstrapConfig { fetcher?: typeof fetch /** If you need a custom logger instead of the console global */ logger?: Logger + /** Wheather to use package.json as the source of truth for transitive dependencies' versions */ + usePackageJson?: boolean } -type ModuleMeta = { state: "loading" } +type PackageJsonDependencies = { + [packageName: string]: string; +}; + +type PackageJson = { + dependencies?: PackageJsonDependencies; + devDependencies?: PackageJsonDependencies; + peerDependencies?: PackageJsonDependencies; + optionalDependencies? :PackageJsonDependencies; +} + +type ModuleMeta = { state: "loading", typesPackageJson?: PackageJson } /** * The function which starts up type acquisition, @@ -60,8 +73,8 @@ export const setupTypeAcquisition = (config: ATABootstrapConfig) => { }) } - async function resolveDeps(initialSourceFile: string, depth: number) { - const depsToGet = getNewDependencies(config, moduleMap, initialSourceFile) + async function resolveDeps(initialSourceFile: string, depth: number, parentPackageJson?: PackageJson) { + const depsToGet = getNewDependencies(config, moduleMap, initialSourceFile, parentPackageJson) // Make it so it won't get re-downloaded depsToGet.forEach(dep => moduleMap.set(dep.module, { state: "loading" })) @@ -78,7 +91,7 @@ export const setupTypeAcquisition = (config: ATABootstrapConfig) => { const mightBeOnDT = treesOnly.filter(t => !hasDTS.includes(t)) const dtTrees = await Promise.all( // TODO: Switch from 'latest' to the version from the original tree which is user-controlled - mightBeOnDT.map(f => getFileTreeForModuleWithTag(config, `@types/${getDTName(f.moduleName)}`, "latest")) + mightBeOnDT.map(f => getFileTreeForModuleWithTag(config, `@types/${getDTName(f.moduleName)}`, f.version)) ) const dtTreesOnly = dtTrees.filter(t => !("error" in t)) as NPMTreeMeta[] @@ -100,6 +113,12 @@ export const setupTypeAcquisition = (config: ATABootstrapConfig) => { if (typeof pkgJSON == "string") { fsMap.set(path, pkgJSON) + + const moduleMeta = moduleMap.get(tree.moduleName); + if (moduleMeta) { + moduleMeta.typesPackageJson = JSON.parse(pkgJSON) as PackageJson + } + config.delegate.receivedFile?.(pkgJSON, path) } else { config.logger?.error(`Could not download package.json for ${tree.moduleName}`) @@ -124,7 +143,11 @@ export const setupTypeAcquisition = (config: ATABootstrapConfig) => { } // Recurse through deps - await resolveDeps(dtsCode, depth + 1) + let typesPackageJson + if (config.usePackageJson){ + typesPackageJson = moduleMap.get(dts.moduleName)?.typesPackageJson + } + await resolveDeps(dtsCode, depth + 1, typesPackageJson) } }) ) @@ -154,11 +177,23 @@ function treeToDTSFiles(tree: NPMTreeMeta, vfsPrefix: string) { return dtsRefs } +function hasSemverOperator(version: string) { + return /^[\^~>=<]/.test(version); +} + +function findModuleVersionInPackageJson(packageJson: PackageJson, moduleName: string): string | undefined { + const depTypes = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'] as const; + + return depTypes + .map(type => packageJson[type]?.[moduleName]) + .find(version => version !== undefined); +} + /** * Pull out any potential references to other modules (including relatives) with their * npm versioning strat too if someone opts into a different version via an inline end of line comment */ -export const getReferencesForModule = (ts: typeof import("typescript"), code: string) => { +export const getReferencesForModule = (ts: typeof import("typescript"), code: string, parentPackageJson?: PackageJson) => { const meta = ts.preProcessFile(code) // Ensure we don't try download TypeScript lib references @@ -178,7 +213,12 @@ export const getReferencesForModule = (ts: typeof import("typescript"), code: st if (!r.fileName.startsWith(".")) { version = "latest" const line = code.slice(r.end).split("\n")[0]! - if (line.includes("// types:")) version = line.split("// types: ")[1]!.trim() + if (line.includes("// types:")) { + version = line.split("// types: ")[1]!.trim() + } else if (parentPackageJson) { + const moduleName = mapModuleNameToModule(r.fileName) + version = findModuleVersionInPackageJson(parentPackageJson, moduleName) ?? version + } } return { @@ -189,8 +229,8 @@ export const getReferencesForModule = (ts: typeof import("typescript"), code: st } /** A list of modules from the current sourcefile which we don't have existing files for */ -export function getNewDependencies(config: ATABootstrapConfig, moduleMap: Map, code: string) { - const refs = getReferencesForModule(config.typescript, code).map(ref => ({ +export function getNewDependencies(config: ATABootstrapConfig, moduleMap: Map, code: string, parentPackageJson?: PackageJson) { + const refs = getReferencesForModule(config.typescript, code, parentPackageJson).map(ref => ({ ...ref, module: mapModuleNameToModule(ref.module), })) @@ -210,7 +250,7 @@ export const getFileTreeForModuleWithTag = async ( // I think having at least 2 dots is a reasonable approx for being a semver and not a tag, // we can skip an API request, TBH this is probably rare - if (toDownload.split(".").length < 2) { + if (toDownload.split(".").length < 2 || hasSemverOperator(toDownload)) { // The jsdelivr API needs a _version_ not a tag. So, we need to switch out // the tag to the version via an API request. const response = await getNPMVersionForModuleReference(config, moduleName, toDownload) diff --git a/packages/ata/tests/ata.spec.ts b/packages/ata/tests/ata.spec.ts index 1b779c97eb23..eb68121daf81 100644 --- a/packages/ata/tests/ata.spec.ts +++ b/packages/ata/tests/ata.spec.ts @@ -16,6 +16,49 @@ describe(getReferencesForModule, () => { const code = "import {asda} from '123' // types: 1.2.3" expect(getReferencesForModule(ts, code)[0]).toEqual({ module: "123", version: "1.2.3" }) }) + + describe("given a package.json", () => { + const moduleName = 'some-module' + const version = '^1.0.0' + const packageJsons = [ + { + key: "dependencies", + pkgJsonContent: { + dependencies: { + [moduleName]: version + } + } + }, + { + key: "devDependencies", + pkgJsonContent: { + devDependencies: { + [moduleName]: version + } + } + }, + { + key: "peerDependencies", + pkgJsonContent: { + peerDependencies: { + [moduleName]: version + } + } + }, + { + key: "optionalDependencies", + pkgJsonContent: { + optionalDependencies: { + [moduleName]: version + } + } + }, + ] + it.each(packageJsons)("uses $key as source of truth for the module's version", (pkgJson) => { + const code = `import {asda} from '${moduleName}'` + expect(getReferencesForModule(ts, code, pkgJson.pkgJsonContent)[0]).toEqual({ module: moduleName, version: version }) + }) + }) }) describe("ignores lib references", () => { From c341f092e4781cdeb13d629dea2836a2ca93abc0 Mon Sep 17 00:00:00 2001 From: sean Date: Mon, 16 Dec 2024 14:26:03 +0200 Subject: [PATCH 2/6] renaming --- packages/ata/src/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ata/src/index.ts b/packages/ata/src/index.ts index 76dc30a0ac49..1dd734e5e8ff 100644 --- a/packages/ata/src/index.ts +++ b/packages/ata/src/index.ts @@ -29,8 +29,8 @@ export interface ATABootstrapConfig { fetcher?: typeof fetch /** If you need a custom logger instead of the console global */ logger?: Logger - /** Wheather to use package.json as the source of truth for transitive dependencies' versions */ - usePackageJson?: boolean + /** Whether to use package.json as the source of truth for transitive dependencies' versions */ + resolveDependenciesFromPackageJson?: boolean } type PackageJsonDependencies = { @@ -144,7 +144,7 @@ export const setupTypeAcquisition = (config: ATABootstrapConfig) => { // Recurse through deps let typesPackageJson - if (config.usePackageJson){ + if (config.resolveDependenciesFromPackageJson){ typesPackageJson = moduleMap.get(dts.moduleName)?.typesPackageJson } await resolveDeps(dtsCode, depth + 1, typesPackageJson) From 6eab50c382fd3bfde7244419677eda10481435b0 Mon Sep 17 00:00:00 2001 From: sean Date: Mon, 16 Dec 2024 18:02:24 +0200 Subject: [PATCH 3/6] revert @types version change --- packages/ata/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ata/src/index.ts b/packages/ata/src/index.ts index 1dd734e5e8ff..f102f0923d55 100644 --- a/packages/ata/src/index.ts +++ b/packages/ata/src/index.ts @@ -91,7 +91,7 @@ export const setupTypeAcquisition = (config: ATABootstrapConfig) => { const mightBeOnDT = treesOnly.filter(t => !hasDTS.includes(t)) const dtTrees = await Promise.all( // TODO: Switch from 'latest' to the version from the original tree which is user-controlled - mightBeOnDT.map(f => getFileTreeForModuleWithTag(config, `@types/${getDTName(f.moduleName)}`, f.version)) + mightBeOnDT.map(f => getFileTreeForModuleWithTag(config, `@types/${getDTName(f.moduleName)}`, "latest")) ) const dtTreesOnly = dtTrees.filter(t => !("error" in t)) as NPMTreeMeta[] From e37ae0430126f06c9808a8e3f0346b9f175125d2 Mon Sep 17 00:00:00 2001 From: sean Date: Wed, 18 Dec 2024 16:17:10 +0200 Subject: [PATCH 4/6] added new flag to userFacingTypes --- packages/ata/src/userFacingTypes.d.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/ata/src/userFacingTypes.d.ts b/packages/ata/src/userFacingTypes.d.ts index 4580d7a27e9e..a8a46f162f39 100644 --- a/packages/ata/src/userFacingTypes.d.ts +++ b/packages/ata/src/userFacingTypes.d.ts @@ -20,6 +20,8 @@ export interface ATABootstrapConfig { fetcher?: typeof fetch /** If you need a custom logger instead of the console global */ logger?: Logger + /** Whether to use package.json as the source of truth for transitive dependencies' versions */ + resolveDependenciesFromPackageJson?: boolean; } type ModuleMeta = { state: "loading" } From c6f24f2d2a7f3ecc11caaa6d666c089d8025be9d Mon Sep 17 00:00:00 2001 From: sean Date: Sun, 26 Jan 2025 10:40:53 +0200 Subject: [PATCH 5/6] added resolveDependenciesFromPackageJson to the playground --- packages/sandbox/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/sandbox/src/index.ts b/packages/sandbox/src/index.ts index b60c9ad5114f..1c7d075abec8 100644 --- a/packages/sandbox/src/index.ts +++ b/packages/sandbox/src/index.ts @@ -209,6 +209,7 @@ export const createTypeScriptSandbox = ( projectName: "TypeScript Playground", typescript: ts, logger: console, + resolveDependenciesFromPackageJson: true, delegate: { receivedFile: addLibraryToRuntime, progress: (downloaded: number, total: number) => { From 58bd9fc1cf9581d98eaa4eb91d4ff3c389d59540 Mon Sep 17 00:00:00 2001 From: sean Date: Sun, 26 Jan 2025 13:00:44 +0200 Subject: [PATCH 6/6] added changeset --- .changeset/clever-bulldogs-retire.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/clever-bulldogs-retire.md diff --git a/.changeset/clever-bulldogs-retire.md b/.changeset/clever-bulldogs-retire.md new file mode 100644 index 000000000000..5a470dd85d9f --- /dev/null +++ b/.changeset/clever-bulldogs-retire.md @@ -0,0 +1,6 @@ +--- +"@typescript/sandbox": patch +"@typescript/ata": patch +--- + +Added new ATA flag (resolveDependenciesFromPackageJson) to resolve transitive dependencies