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 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
58 changes: 49 additions & 9 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
/** Whether to use package.json as the source of truth for transitive dependencies' versions */
resolveDependenciesFromPackageJson?: 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,
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 Down Expand Up @@ -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.resolveDependenciesFromPackageJson){
typesPackageJson = 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
2 changes: 2 additions & 0 deletions packages/ata/src/userFacingTypes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
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