Skip to content

Commit

Permalink
feat: add subpath patterns to package.json
Browse files Browse the repository at this point in the history
  • Loading branch information
coderbyheart committed Nov 14, 2024
1 parent a29a0a6 commit 2a03d7d
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 30 deletions.
24 changes: 23 additions & 1 deletion src/findDependencies.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const __dirname = new URL('.', import.meta.url).pathname

void describe('findDependencies()', () => {
void it('should honor tsconfig.json paths', () => {
const dependencies = findDependencies({
const { dependencies } = findDependencies({
sourceFilePath: path.join(
__dirname,
'test-data',
Expand Down Expand Up @@ -37,4 +37,26 @@ void describe('findDependencies()', () => {
'Should include the module file',
)
})

void it('should return an import map', () => {
const { importsSubpathPatterns } = findDependencies({
sourceFilePath: path.join(
__dirname,
'test-data',
'resolve-paths',
'lambda.ts',
),
tsConfigFilePath: path.join(
__dirname,
'test-data',
'resolve-paths',
'tsconfig.json',
),
})

assert.deepEqual(importsSubpathPatterns, {
'#foo': './foo/index.js',
'#foo/*': './foo/*',
})
})
})
100 changes: 72 additions & 28 deletions src/findDependencies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,27 @@ type TSConfigWithPaths = {
/**
* Resolve project-level dependencies for the given file using TypeScript compiler API
*/
export const findDependencies = ({
sourceFilePath,
tsConfigFilePath,
imports: importsArg,
visited: visitedArg,
}: {
export const findDependencies = (args: {
sourceFilePath: string
tsConfigFilePath?: string
imports?: string[]
visited?: string[]
}): string[] => {
const visited = visitedArg ?? []
const imports = importsArg ?? []
if (visited.includes(sourceFilePath)) return imports
tsConfigFilePath?: string
importsSubpathPatterns?: Record<string, string>
}): {
dependencies: string[]
/**
* A map of import subpath patterns to their resolved paths
* @see https://nodejs.org/api/packages.html#subpath-patterns
*/
importsSubpathPatterns: Record<string, string>
} => {
const sourceFilePath = args.sourceFilePath
const visited = args.visited ?? []
const dependencies = args.imports ?? []
let importsSubpathPatterns = args.importsSubpathPatterns ?? {}
if (visited.includes(sourceFilePath))
return { dependencies, importsSubpathPatterns }
const tsConfigFilePath = args.tsConfigFilePath
const tsConfig =

Check warning on line 36 in src/findDependencies.ts

View workflow job for this annotation

GitHub Actions / tests

Unsafe assignment of an `any` value
tsConfigFilePath !== undefined
? JSON.parse(readFileSync(tsConfigFilePath, 'utf-8').toString())
Expand All @@ -43,15 +50,20 @@ export const findDependencies = ({
const moduleSpecifier = (
(node as ImportDeclaration).moduleSpecifier as StringLiteral
).text
const file = resolve({
const {
resolvedPath: file,
importsSubpathPatterns: updatedImportsSubpathPatterns,
} = resolve({
moduleSpecifier,
sourceFilePath,
tsConfigFilePath,
tsConfig,

Check warning on line 60 in src/findDependencies.ts

View workflow job for this annotation

GitHub Actions / tests

Unsafe assignment of an `any` value
importsSubpathPatterns,
})
importsSubpathPatterns = updatedImportsSubpathPatterns
try {
const s = statSync(file)
if (!s.isDirectory()) imports.push(file)
if (!s.isDirectory()) dependencies.push(file)
} catch {
// Module or file not found
visited.push(file)
Expand All @@ -60,44 +72,54 @@ export const findDependencies = ({
ts.forEachChild(fileNode, parseChild)
visited.push(sourceFilePath)

for (const file of imports) {
for (const file of dependencies) {
findDependencies({
sourceFilePath: file,
imports,
imports: dependencies,
visited,
tsConfigFilePath,
importsSubpathPatterns,
})
}

return imports
return { dependencies, importsSubpathPatterns }
}

const resolve = ({
moduleSpecifier,
sourceFilePath,
tsConfigFilePath,
tsConfig,
importsSubpathPatterns,
}: {
moduleSpecifier: string
sourceFilePath: string
importsSubpathPatterns: Record<string, string>
} & (
| {
tsConfigFilePath: undefined
tsConfig: undefined
}
| { tsConfigFilePath: string; tsConfig: TSConfigWithPaths }
)): string => {
| {
tsConfigFilePath: string
tsConfig: TSConfigWithPaths
}
)): {
resolvedPath: string
importsSubpathPatterns: Record<string, string>
} => {
if (moduleSpecifier.startsWith('.'))
return (
path
return {
resolvedPath: path
.resolve(path.parse(sourceFilePath).dir, moduleSpecifier)
// In ECMA Script modules, all imports from local files must have an extension.
// See https://nodejs.org/api/esm.html#mandatory-file-extensions
// So we need to replace the `.js` in the import specification to find the TypeScript source for the file.
// Example: import { Network, notifyClients } from './notifyClients.js'
// The source file for that is actually in './notifyClients.ts'
.replace(/\.js$/, '.ts')
)
.replace(/\.js$/, '.ts'),
importsSubpathPatterns,
}
if (
tsConfigFilePath !== undefined &&
tsConfig?.compilerOptions?.paths !== undefined
Expand All @@ -107,28 +129,50 @@ const resolve = ({
if (resolvedPath === undefined) continue
// Exact match
if (moduleSpecifier === key) {
return path.join(
const fullResolvedPath = path.join(
path.parse(tsConfigFilePath).dir,
tsConfig.compilerOptions.baseUrl,
resolvedPath,
)
return {
resolvedPath: fullResolvedPath,
importsSubpathPatterns: {
...importsSubpathPatterns,
[key]: [
tsConfig.compilerOptions.baseUrl,
path.sep,
resolvedPath.replace(/\.ts$/, '.js'),
].join(''),
},
}
}
// Wildcard match
if (!key.includes('*')) continue
const rx = new RegExp(`^${key.replace('*', '(?<wildcard>.*)')}`)
const maybeMatch = rx.exec(moduleSpecifier)
if (maybeMatch?.groups?.wildcard === undefined) continue
return (
path
return {
resolvedPath: path
.resolve(
path.parse(tsConfigFilePath).dir,
tsConfig.compilerOptions.baseUrl,
resolvedPath.replace('*', maybeMatch.groups.wildcard),
)
// Same as above, replace `.js` with `.ts`
.replace(/\.js$/, '.ts')
)
.replace(/\.js$/, '.ts'),
importsSubpathPatterns: {
...importsSubpathPatterns,
[key]: [
tsConfig.compilerOptions.baseUrl,
path.sep,
resolvedPath.replace(/\.ts$/, '.js'),
].join(''),
},
}
}
}
return moduleSpecifier
return {
resolvedPath: moduleSpecifier,
importsSubpathPatterns,
}
}
3 changes: 2 additions & 1 deletion src/packLambda.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export const packLambda = async ({
debug?: (label: string, info: string) => void
progress?: (label: string, info: string) => void
}): Promise<{ handler: string; hash: string }> => {
const deps = findDependencies({
const { dependencies: deps, importsSubpathPatterns } = findDependencies({
sourceFilePath,
tsConfigFilePath,
})
Expand Down Expand Up @@ -103,6 +103,7 @@ export const packLambda = async ({
Buffer.from(
JSON.stringify({
type: 'module',
imports: importsSubpathPatterns,
}),
'utf-8',
),
Expand Down

0 comments on commit 2a03d7d

Please sign in to comment.