From 2a03d7d074f7900dcda08f11f9283c6b09542492 Mon Sep 17 00:00:00 2001 From: Markus Tacker Date: Thu, 14 Nov 2024 23:23:42 +0100 Subject: [PATCH] feat: add subpath patterns to package.json --- src/findDependencies.spec.ts | 24 ++++++++- src/findDependencies.ts | 100 +++++++++++++++++++++++++---------- src/packLambda.ts | 3 +- 3 files changed, 97 insertions(+), 30 deletions(-) diff --git a/src/findDependencies.spec.ts b/src/findDependencies.spec.ts index b66c799..a94c0ce 100644 --- a/src/findDependencies.spec.ts +++ b/src/findDependencies.spec.ts @@ -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', @@ -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/*', + }) + }) }) diff --git a/src/findDependencies.ts b/src/findDependencies.ts index 8cb23f8..637e51b 100644 --- a/src/findDependencies.ts +++ b/src/findDependencies.ts @@ -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 +}): { + dependencies: string[] + /** + * A map of import subpath patterns to their resolved paths + * @see https://nodejs.org/api/packages.html#subpath-patterns + */ + importsSubpathPatterns: Record +} => { + 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 = tsConfigFilePath !== undefined ? JSON.parse(readFileSync(tsConfigFilePath, 'utf-8').toString()) @@ -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, + 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) @@ -60,16 +72,17 @@ 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 = ({ @@ -77,27 +90,36 @@ const resolve = ({ sourceFilePath, tsConfigFilePath, tsConfig, + importsSubpathPatterns, }: { moduleSpecifier: string sourceFilePath: string + importsSubpathPatterns: Record } & ( | { tsConfigFilePath: undefined tsConfig: undefined } - | { tsConfigFilePath: string; tsConfig: TSConfigWithPaths } -)): string => { + | { + tsConfigFilePath: string + tsConfig: TSConfigWithPaths + } +)): { + resolvedPath: string + importsSubpathPatterns: Record +} => { 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 @@ -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('*', '(?.*)')}`) 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, + } } diff --git a/src/packLambda.ts b/src/packLambda.ts index a59c383..e7d15a1 100644 --- a/src/packLambda.ts +++ b/src/packLambda.ts @@ -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, }) @@ -103,6 +103,7 @@ export const packLambda = async ({ Buffer.from( JSON.stringify({ type: 'module', + imports: importsSubpathPatterns, }), 'utf-8', ),