Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resolve transitive dependencies based on package.json #3286

Open
wants to merge 4 commits into
base: v2
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
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
60 changes: 50 additions & 10 deletions packages/ata/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
seanoch marked this conversation as resolved.
Show resolved Hide resolved
usePackageJson?: boolean
seanoch marked this conversation as resolved.
Show resolved Hide resolved
}

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,
Expand Down Expand Up @@ -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" }))
Expand All @@ -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[]
Expand All @@ -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}`)
Expand All @@ -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
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let typesPackageJson
if (config.usePackageJson){
typesPackageJson = moduleMap.get(dts.moduleName)?.typesPackageJson
}
const typesPackageJson = config.usePackageJson && moduleMap.get(dts.moduleName)?.typesPackageJson

await resolveDeps(dtsCode, depth + 1, typesPackageJson)
}
})
)
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -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<string, ModuleMeta>, code: string) {
const refs = getReferencesForModule(config.typescript, code).map(ref => ({
export function getNewDependencies(config: ATABootstrapConfig, moduleMap: Map<string, ModuleMeta>, code: string, parentPackageJson?: PackageJson) {
const refs = getReferencesForModule(config.typescript, code, parentPackageJson).map(ref => ({
...ref,
module: mapModuleNameToModule(ref.module),
}))
Expand All @@ -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)
Expand Down
43 changes: 43 additions & 0 deletions packages/ata/tests/ata.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down