From 9dc173b5be8f7839ef24d9fd427b18b46c957e40 Mon Sep 17 00:00:00 2001 From: Simo Kinnunen Date: Tue, 7 Jul 2026 14:06:24 +0900 Subject: [PATCH 1/3] feat(check-parser): add package.json #imports resolution [RED-688] Add hasImports()/resolveImportPath() to PackageJsonFile, resolving Node.js subpath import specifiers (#-prefixed) against the package's `imports` map. The `imports` and `exports` fields share the same conditional-target shape, so the private exports resolver is generalized to #resolveConditionalTargets and reused. Unlike exports, no subpath normalization is applied: `imports` keys and the specifier are both #-prefixed and matched verbatim. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01P3ajE9Vq8Mw8C5YNAzQwn8 --- .../__tests__/package-json-file.spec.ts | 89 +++++++++++++++++++ .../package-files/package-json-file.ts | 42 +++++++-- 2 files changed, 126 insertions(+), 5 deletions(-) diff --git a/packages/cli/src/services/check-parser/package-files/__tests__/package-json-file.spec.ts b/packages/cli/src/services/check-parser/package-files/__tests__/package-json-file.spec.ts index fd9a12ab3..cfa4b3610 100644 --- a/packages/cli/src/services/check-parser/package-files/__tests__/package-json-file.spec.ts +++ b/packages/cli/src/services/check-parser/package-files/__tests__/package-json-file.spec.ts @@ -404,4 +404,93 @@ describe('package.json file', () => { expect(paths[0].target.path).toBe('./index.js') }) }) + + describe('resolveImportPath', () => { + const importConditions = ['import', 'node', 'module-sync', 'default'] + const requireConditions = ['require', 'node', 'module-sync', 'default'] + + it('resolves a plain-string relative target', () => { + const testFile = PackageJsonFile.make('/pkg/package.json', { + name: 'foo', + version: '1.0.0', + imports: { + '#config': './config.js', + }, + }) + + const { paths } = testFile.resolveImportPath('#config', importConditions) + + expect(paths).toHaveLength(1) + expect(paths[0].target.path).toBe('./config.js') + }) + + it('resolves a conditional target differently for import and require', () => { + const testFile = PackageJsonFile.make('/pkg/package.json', { + name: 'foo', + version: '1.0.0', + imports: { + '#dep': { + import: './dep.mjs', + require: './dep.cjs', + default: './dep.js', + }, + } as any, + }) + + const importResult = testFile.resolveImportPath('#dep', importConditions) + expect(importResult.paths).toHaveLength(1) + expect(importResult.paths[0].target.path).toBe('./dep.mjs') + + const requireResult = testFile.resolveImportPath('#dep', requireConditions) + expect(requireResult.paths).toHaveLength(1) + expect(requireResult.paths[0].target.path).toBe('./dep.cjs') + }) + + it('resolves a wildcard target', () => { + const testFile = PackageJsonFile.make('/pkg/package.json', { + name: 'foo', + version: '1.0.0', + imports: { + '#internal/*': './src/internal/*.js', + }, + }) + + const { paths } = testFile.resolveImportPath('#internal/foo', importConditions) + + expect(paths).toHaveLength(1) + expect(paths[0].target.path).toBe('./src/internal/foo.js') + }) + + it('returns a bare/external target verbatim', () => { + // Import maps may point at an external package rather than a file within + // the declaring package; the target is returned unchanged so the resolver + // can treat it as an external (or workspace-neighbor) specifier. + const testFile = PackageJsonFile.make('/pkg/package.json', { + name: 'foo', + version: '1.0.0', + imports: { + '#dep': 'lodash', + }, + }) + + const { paths } = testFile.resolveImportPath('#dep', importConditions) + + expect(paths).toHaveLength(1) + expect(paths[0].target.path).toBe('lodash') + }) + + it('returns no paths for an unmatched specifier', () => { + const testFile = PackageJsonFile.make('/pkg/package.json', { + name: 'foo', + version: '1.0.0', + imports: { + '#config': './config.js', + }, + }) + + const { paths } = testFile.resolveImportPath('#missing', importConditions) + + expect(paths).toHaveLength(0) + }) + }) }) diff --git a/packages/cli/src/services/check-parser/package-files/package-json-file.ts b/packages/cli/src/services/check-parser/package-files/package-json-file.ts index da74c9b8d..eb1afb12a 100644 --- a/packages/cli/src/services/check-parser/package-files/package-json-file.ts +++ b/packages/cli/src/services/check-parser/package-files/package-json-file.ts @@ -82,10 +82,14 @@ export class PackageJsonFile { return !!this.jsonFile.data.exports } + hasImports (): boolean { + return !!this.jsonFile.data.imports + } + resolveExportPath (exportPath: string, conditions: ConditionKey[]): ResolveResult { const resolver = PathResolver.createFromPaths( this.basePath, - this.#resolveExports(this.jsonFile.data.exports ?? {}, conditions), + this.#resolveConditionalTargets(this.jsonFile.data.exports ?? {}, conditions), ) // Normalize the export path to the canonical subpath form used by @@ -107,16 +111,44 @@ export class PackageJsonFile { return resolver.resolve(exportPath) } - #resolveExports (exports: Exports, conditions: ConditionKey[]): Record { - if (typeof exports === 'string') { + /** + * Resolves a Node.js subpath import specifier (a `#`-prefixed specifier such + * as `#utils/foo`) against the package's `imports` map. + * + * Unlike `resolveExportPath`, no subpath normalization is needed: `imports` + * keys and the specifier are both `#`-prefixed and matched verbatim. Relative + * targets in the result resolve against `basePath`; bare targets are external + * package (or workspace neighbor) specifiers. + * + * See https://nodejs.org/api/packages.html#subpath-imports + */ + resolveImportPath (importPath: string, conditions: ConditionKey[]): ResolveResult { + const resolver = PathResolver.createFromPaths( + this.basePath, + this.#resolveConditionalTargets(this.jsonFile.data.imports ?? {}, conditions), + ) + + return resolver.resolve(importPath) + } + + // Resolves the conditional targets of an `exports` or `imports` map into a + // flat `subpath -> [target]` mapping. Both fields share the same target shape + // (string | null | array | nested conditions), so the same logic applies. + #resolveConditionalTargets ( + map: Exports, + conditions: ConditionKey[], + ): Record { + // A plain-string `exports` shorthand maps the root subpath to that target. + // (`imports` is never a plain string, but sharing the branch is harmless.) + if (typeof map === 'string') { return { - '.': [exports], + '.': [map], } } const resolved: Record = {} - for (const [from, target] of Object.entries(exports)) { + for (const [from, target] of Object.entries(map)) { const resolvedTarget = this.#resolveExportTarget(target, conditions) if (resolvedTarget !== undefined) { resolved[from] = [resolvedTarget] From 5b17a4eaaeb8303391979550743fb7ebe5c79a36 Mon Sep 17 00:00:00 2001 From: Simo Kinnunen Date: Tue, 7 Jul 2026 14:14:41 +0900 Subject: [PATCH 2/3] feat(check-parser): resolve and bundle subpath #imports [RED-688] Replace the resolver's TODO that silently skipped `#`-prefixed import specifiers. `#imports` now resolve against the nearest package.json's `imports` map (the importing file's package scope): relative targets are bundled as local files, and bare targets are treated as external packages or workspace neighbors. Previously the mapped files were never discovered, so subpath imports failed at runtime unless the user forced the files into the bundle with a manual `include`. This makes Playwright Check Suites resolve them automatically. The workspace-neighbor/external-fallback logic is extracted into a shared closure so both ordinary specifiers and bare `#import` targets reuse it. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01P3ajE9Vq8Mw8C5YNAzQwn8 --- .../subpath-imports/config.js | 1 + .../subpath-imports/entrypoint.js | 11 ++ .../subpath-imports/package.json | 10 ++ .../subpath-imports/src/internal/bar.js | 1 + .../__tests__/check-parser.spec.ts | 43 ++++++ .../check-parser/package-files/resolver.ts | 126 +++++++++++++----- 6 files changed, 155 insertions(+), 37 deletions(-) create mode 100644 packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/subpath-imports/config.js create mode 100644 packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/subpath-imports/entrypoint.js create mode 100644 packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/subpath-imports/package.json create mode 100644 packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/subpath-imports/src/internal/bar.js diff --git a/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/subpath-imports/config.js b/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/subpath-imports/config.js new file mode 100644 index 000000000..cef61f5de --- /dev/null +++ b/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/subpath-imports/config.js @@ -0,0 +1 @@ +module.exports = { name: 'config' } diff --git a/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/subpath-imports/entrypoint.js b/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/subpath-imports/entrypoint.js new file mode 100644 index 000000000..4c716bd45 --- /dev/null +++ b/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/subpath-imports/entrypoint.js @@ -0,0 +1,11 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable @typescript-eslint/no-var-requires */ +// Exercises Node.js subpath imports (the package.json `imports` field): +// - `#config` -> a relative file within the package +// - `#internal/bar` -> a wildcard-mapped relative file +// - `#dep` -> an external package (lodash) +const config = require('#config') +const bar = require('#internal/bar') +const dep = require('#dep') + +module.exports = { config, bar, dep } diff --git a/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/subpath-imports/package.json b/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/subpath-imports/package.json new file mode 100644 index 000000000..c0a64631e --- /dev/null +++ b/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/subpath-imports/package.json @@ -0,0 +1,10 @@ +{ + "name": "subpath-imports-fixture", + "version": "1.0.0", + "main": "entrypoint.js", + "imports": { + "#config": "./config.js", + "#internal/*": "./src/internal/*.js", + "#dep": "lodash" + } +} diff --git a/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/subpath-imports/src/internal/bar.js b/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/subpath-imports/src/internal/bar.js new file mode 100644 index 000000000..6d1307221 --- /dev/null +++ b/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/subpath-imports/src/internal/bar.js @@ -0,0 +1 @@ +module.exports = { name: 'bar' } diff --git a/packages/cli/src/services/check-parser/__tests__/check-parser.spec.ts b/packages/cli/src/services/check-parser/__tests__/check-parser.spec.ts index 6f60e05be..4e10c0bee 100644 --- a/packages/cli/src/services/check-parser/__tests__/check-parser.spec.ts +++ b/packages/cli/src/services/check-parser/__tests__/check-parser.spec.ts @@ -612,6 +612,30 @@ describe('dependency-parser - parser()', () => { // parsing must not throw a DependencyParseError. expect(dependencies.map(d => d.filePath)).not.toContain(toAbsolutePath('native.node')) }) + + it('should resolve package.json subpath #imports', async () => { + // Node.js subpath imports (the package.json `imports` field, referenced + // via `#`-prefixed specifiers) must be discovered and bundled + // automatically, without a manual `include`. + const toAbsolutePath = (...filepath: string[]) => fixt.abspath('subpath-imports', ...filepath) + const parser = new Parser({ + supportedNpmModules: defaultNpmModules, + restricted: false, + }) + + const { dependencies } = await parser.parse(toAbsolutePath('entrypoint.js')) + const filePaths = dependencies.map(d => d.filePath) + + // Both a direct relative target (#config) and a wildcard-mapped target + // (#internal/bar) are bundled. + expect(filePaths).toContain(toAbsolutePath('config.js')) + expect(filePaths).toContain(toAbsolutePath('src', 'internal', 'bar.js')) + + // The raw `#`-specifiers must never leak into the dependency set, and the + // external target (#dep -> lodash) is recorded as external, not bundled. + expect(filePaths.some(filePath => filePath.includes('#'))).toBe(false) + expect(filePaths.some(filePath => filePath.includes('lodash'))).toBe(false) + }) }) describe('restricted mode', () => { @@ -641,6 +665,25 @@ describe('dependency-parser - parser()', () => { ]) }) + it('should resolve relative subpath #imports', async () => { + // In restricted mode the nearest package.json is not bundled, so the + // `imports` map is unavailable at runtime (a documented limitation), but + // dependency resolution must still discover the mapped target files + // and must not crash. + const toAbsolutePath = (...filepath: string[]) => fixt.abspath('subpath-imports', ...filepath) + const parser = new Parser({ + supportedNpmModules: defaultNpmModules, + restricted: true, + }) + + const { dependencies } = await parser.parse(toAbsolutePath('entrypoint.js')) + const filePaths = dependencies.map(d => d.filePath) + + expect(filePaths).toContain(toAbsolutePath('config.js')) + expect(filePaths).toContain(toAbsolutePath('src', 'internal', 'bar.js')) + expect(filePaths.some(filePath => filePath.includes('#'))).toBe(false) + }) + it('should report a missing entrypoint file', async () => { const missingEntrypoint = fixt.abspath('does-not-exist.js') expect.assertions(1) diff --git a/packages/cli/src/services/check-parser/package-files/resolver.ts b/packages/cli/src/services/check-parser/package-files/resolver.ts index d6bccd055..f5c3f2520 100644 --- a/packages/cli/src/services/check-parser/package-files/resolver.ts +++ b/packages/cli/src/services/check-parser/package-files/resolver.ts @@ -288,6 +288,12 @@ type RelativePathLocalDependency = { sourceFile: SourceFile } +type ImportsPathLocalDependency = { + kind: 'imports-path' + importPath: string + sourceFile: SourceFile +} + type WorkspaceNeighborLocalDependency = { kind: 'workspace-neighbor' neighbor: Package @@ -307,6 +313,7 @@ type LocalDependency = | SupportingTSConfigResolvedPathLocalDependency | SupportingTSConfigBaseUrlRelativePathLocalDependency | RelativePathLocalDependency + | ImportsPathLocalDependency | WorkspaceNeighborLocalDependency type MissingDependency = { @@ -631,6 +638,47 @@ export class PackageFilesResolver { const context = LookupContext.forFilePath(filePath) + // Resolves a bare/external specifier: prefer a workspace-neighbor package + // when one provides the files, otherwise record it as an external + // dependency. Shared by ordinary specifiers and by bare targets produced + // by a package.json `imports` mapping. + const resolveWorkspaceOrExternal = async (specifier: string, source: RawDependencySource) => { + if (this.workspace) { + const { name, path: exportPath } = splitExternalPath(specifier) + const neighbor = this.workspace.memberByName(name) + if (neighbor) { + const sourceFile = await this.cache.sourceFile(neighbor.path, context) + if (sourceFile !== undefined) { + const resolvedFiles = await this.resolveSourceFile(sourceFile, context, { + exportPath, + source, + }) + let found = false + for (const resolvedFile of resolvedFiles) { + debug('Found workspace neighbor file %s', resolvedFile.meta.filePath) + resolved.local.push({ + kind: 'workspace-neighbor', + neighbor, + importPath: specifier, + sourceFile: resolvedFile, + }) + found = true + } + if (found) { + debug('Found workspace neighbor reference %s', neighbor.path) + usedNeighbors.add(neighbor) + return + } + } + } + } + + debug(`Found external dependency %s`, specifier) + resolved.external.push({ + importPath: specifier, + }) + } + resolve: for (const { importPath, source } of dependencies) { if (isBuiltinPath(importPath)) { @@ -765,49 +813,53 @@ export class PackageFilesResolver { } } + // Node.js subpath imports (`#`-prefixed specifiers) resolve against the + // `imports` map of the nearest package.json (the importing file's package + // scope). Every `#` specifier terminates here — an unmatched one is left + // unbundled rather than being misrecorded as an external npm dependency. if (isImportsPath(importPath)) { - debug('Found local imports path %s', - importPath, - ) - - // TODO - continue resolve - } - - if (this.workspace) { - const { name, path: exportPath } = splitExternalPath(importPath) - const neighbor = this.workspace.memberByName(name) - if (neighbor) { - const sourceFile = await this.cache.sourceFile(neighbor.path, context) - if (sourceFile !== undefined) { - const resolvedFiles = await this.resolveSourceFile(sourceFile, context, { - exportPath, - source, - }) - let found = false - for (const resolvedFile of resolvedFiles) { - debug('Found workspace neighbor file %s', resolvedFile.meta.filePath) - resolved.local.push({ - kind: 'workspace-neighbor', - neighbor, - importPath, - sourceFile: resolvedFile, - }) - found = true - } - if (found) { - debug('Found workspace neighbor reference %s', neighbor.path) - usedNeighbors.add(neighbor) - continue resolve + if (packageJson?.hasImports()) { + const { root, paths } = packageJson.resolveImportPath(importPath, [ + source, + 'node', + 'module-sync', + 'default', + ]) + + for (const { target } of paths) { + if (isLocalPath(target.path)) { + // Relative targets point at files within the declaring package, + // resolved relative to the package root (not the importing file). + const relativeDepPath = path.resolve(root, target.path) + const sourceFile = await this.cache.sourceFile(relativeDepPath, context) + if (sourceFile !== undefined) { + const resolvedFiles = await this.resolveSourceFile(sourceFile, context) + for (const resolvedFile of resolvedFiles) { + debug('Found subpath imports file %s', resolvedFile.meta.filePath) + resolved.local.push({ + kind: 'imports-path', + importPath, + sourceFile: resolvedFile, + }) + } + } else { + debug('Failed to find subpath imports file %s', relativeDepPath) + resolved.missing.push({ + importPath, + filePath: relativeDepPath, + }) + } + } else { + // Bare targets are external packages (or workspace neighbors). + await resolveWorkspaceOrExternal(target.path, source) } } } + + continue resolve } - debug(`Found external dependency %s`, importPath) - resolved.external.push({ - importPath, - }) + await resolveWorkspaceOrExternal(importPath, source) } const requiredNeighbors = new Set() From 025157aa6ba71d68a86b866d6b60887700b25bb3 Mon Sep 17 00:00:00 2001 From: Simo Kinnunen Date: Tue, 7 Jul 2026 15:55:21 +0900 Subject: [PATCH 3/3] test(check-parser): add end-to-end coverage for package.json exports Exports resolution was covered by resolveExportPath unit tests but never exercised through parser.parse(). Add an e2e fixture whose package exposes its entry point only via a conditional `exports` map (no `main`), so the target is discovered solely through exports resolution, and assert the matching `node` condition is selected over the non-matching `browser` and the `default` fallback. Covered in both unrestricted and restricted modes. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01P3ajE9Vq8Mw8C5YNAzQwn8 --- .../package-exports/entrypoint.js | 8 +++++ .../package-exports/local-pkg/lib/browser.js | 1 + .../package-exports/local-pkg/lib/fallback.js | 1 + .../package-exports/local-pkg/lib/main.js | 1 + .../package-exports/local-pkg/package.json | 11 ++++++ .../__tests__/check-parser.spec.ts | 35 +++++++++++++++++++ 6 files changed, 57 insertions(+) create mode 100644 packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/package-exports/entrypoint.js create mode 100644 packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/package-exports/local-pkg/lib/browser.js create mode 100644 packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/package-exports/local-pkg/lib/fallback.js create mode 100644 packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/package-exports/local-pkg/lib/main.js create mode 100644 packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/package-exports/local-pkg/package.json diff --git a/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/package-exports/entrypoint.js b/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/package-exports/entrypoint.js new file mode 100644 index 000000000..7d1413742 --- /dev/null +++ b/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/package-exports/entrypoint.js @@ -0,0 +1,8 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable @typescript-eslint/no-var-requires */ +// Requires a local package directory that exposes its entry point only through +// the package.json `exports` field (no `main`), so the target file is found +// solely via exports resolution. +const pkg = require('./local-pkg') + +module.exports = { pkg } diff --git a/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/package-exports/local-pkg/lib/browser.js b/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/package-exports/local-pkg/lib/browser.js new file mode 100644 index 000000000..12942b921 --- /dev/null +++ b/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/package-exports/local-pkg/lib/browser.js @@ -0,0 +1 @@ +module.exports = { name: 'browser' } diff --git a/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/package-exports/local-pkg/lib/fallback.js b/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/package-exports/local-pkg/lib/fallback.js new file mode 100644 index 000000000..20d6cf1dc --- /dev/null +++ b/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/package-exports/local-pkg/lib/fallback.js @@ -0,0 +1 @@ +module.exports = { name: 'fallback' } diff --git a/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/package-exports/local-pkg/lib/main.js b/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/package-exports/local-pkg/lib/main.js new file mode 100644 index 000000000..fc346dfed --- /dev/null +++ b/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/package-exports/local-pkg/lib/main.js @@ -0,0 +1 @@ +module.exports = { name: 'main' } diff --git a/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/package-exports/local-pkg/package.json b/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/package-exports/local-pkg/package.json new file mode 100644 index 000000000..c849e677c --- /dev/null +++ b/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/package-exports/local-pkg/package.json @@ -0,0 +1,11 @@ +{ + "name": "local-pkg", + "version": "1.0.0", + "exports": { + ".": { + "browser": "./lib/browser.js", + "node": "./lib/main.js", + "default": "./lib/fallback.js" + } + } +} diff --git a/packages/cli/src/services/check-parser/__tests__/check-parser.spec.ts b/packages/cli/src/services/check-parser/__tests__/check-parser.spec.ts index 4e10c0bee..693b0c20c 100644 --- a/packages/cli/src/services/check-parser/__tests__/check-parser.spec.ts +++ b/packages/cli/src/services/check-parser/__tests__/check-parser.spec.ts @@ -636,6 +636,26 @@ describe('dependency-parser - parser()', () => { expect(filePaths.some(filePath => filePath.includes('#'))).toBe(false) expect(filePaths.some(filePath => filePath.includes('lodash'))).toBe(false) }) + + it('should resolve a package.json exports field', async () => { + // The imported package exposes its entry point only through a conditional + // `exports` map (no `main`), so the target file is discovered solely via + // exports resolution. The `node` condition is selected: the earlier, + // non-matching `browser` condition is skipped and the `default` fallback + // is not reached. + const toAbsolutePath = (...filepath: string[]) => fixt.abspath('package-exports', ...filepath) + const parser = new Parser({ + supportedNpmModules: defaultNpmModules, + restricted: false, + }) + + const { dependencies } = await parser.parse(toAbsolutePath('entrypoint.js')) + const filePaths = dependencies.map(d => d.filePath) + + expect(filePaths).toContain(toAbsolutePath('local-pkg', 'lib', 'main.js')) + expect(filePaths).not.toContain(toAbsolutePath('local-pkg', 'lib', 'browser.js')) + expect(filePaths).not.toContain(toAbsolutePath('local-pkg', 'lib', 'fallback.js')) + }) }) describe('restricted mode', () => { @@ -684,6 +704,21 @@ describe('dependency-parser - parser()', () => { expect(filePaths.some(filePath => filePath.includes('#'))).toBe(false) }) + it('should resolve a package.json exports field', async () => { + const toAbsolutePath = (...filepath: string[]) => fixt.abspath('package-exports', ...filepath) + const parser = new Parser({ + supportedNpmModules: defaultNpmModules, + restricted: true, + }) + + const { dependencies } = await parser.parse(toAbsolutePath('entrypoint.js')) + const filePaths = dependencies.map(d => d.filePath) + + expect(filePaths).toContain(toAbsolutePath('local-pkg', 'lib', 'main.js')) + expect(filePaths).not.toContain(toAbsolutePath('local-pkg', 'lib', 'browser.js')) + expect(filePaths).not.toContain(toAbsolutePath('local-pkg', 'lib', 'fallback.js')) + }) + it('should report a missing entrypoint file', async () => { const missingEntrypoint = fixt.abspath('does-not-exist.js') expect.assertions(1)