Skip to content

Commit

Permalink
fix(test runner): allow directory imports with path mapping
Browse files Browse the repository at this point in the history
This regressed in #32078.
  • Loading branch information
dgozman committed Sep 6, 2024
1 parent a52eb0c commit f2b9a33
Show file tree
Hide file tree
Showing 7 changed files with 318 additions and 70 deletions.
2 changes: 1 addition & 1 deletion packages/playwright-ct-core/src/vitePlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ function vitePlugin(registerSource: string, templateDir: string, buildInfo: Buil

async writeBundle(this: PluginContext) {
for (const importInfo of importInfos.values()) {
const importPath = resolveHook(importInfo.filename, importInfo.importSource, true);
const importPath = resolveHook(importInfo.filename, importInfo.importSource);
if (!importPath)
continue;
const deps = new Set<string>();
Expand Down
6 changes: 3 additions & 3 deletions packages/playwright-ct-core/src/viteUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,13 +147,13 @@ export async function populateComponentsFromTests(componentRegistry: ComponentRe
for (const importInfo of importList)
componentRegistry.set(importInfo.id, importInfo);
if (componentsByImportingFile)
componentsByImportingFile.set(file, importList.map(i => resolveHook(i.filename, i.importSource, true)).filter(Boolean) as string[]);
componentsByImportingFile.set(file, importList.map(i => resolveHook(i.filename, i.importSource)).filter(Boolean) as string[]);
}
}

export function hasJSComponents(components: ImportInfo[]): boolean {
for (const component of components) {
const importPath = resolveHook(component.filename, component.importSource, true);
const importPath = resolveHook(component.filename, component.importSource);
const extname = importPath ? path.extname(importPath) : '';
if (extname === '.js' || (importPath && !extname && fs.existsSync(importPath + '.js')))
return true;
Expand Down Expand Up @@ -183,7 +183,7 @@ export function transformIndexFile(id: string, content: string, templateDir: str
lines.push(registerSource);

for (const value of importInfos.values()) {
const importPath = resolveHook(value.filename, value.importSource, true) || value.importSource;
const importPath = resolveHook(value.filename, value.importSource) || value.importSource;
lines.push(`const ${value.id} = () => import('${importPath?.replaceAll(path.sep, '/')}').then((mod) => mod.${value.remoteName || 'default'});`);
}

Expand Down
2 changes: 1 addition & 1 deletion packages/playwright/src/transform/esmLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { fileIsModule } from '../util';
async function resolve(specifier: string, context: { parentURL?: string }, defaultResolve: Function) {
if (context.parentURL && context.parentURL.startsWith('file://')) {
const filename = url.fileURLToPath(context.parentURL);
const resolved = resolveHook(filename, specifier, true);
const resolved = resolveHook(filename, specifier);
if (resolved !== undefined)
specifier = url.pathToFileURL(resolved).toString();
}
Expand Down
13 changes: 6 additions & 7 deletions packages/playwright/src/transform/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,20 +129,19 @@ function loadAndValidateTsconfigsForFolder(folder: string): ParsedTsConfigData[]
const pathSeparator = process.platform === 'win32' ? ';' : ':';
const builtins = new Set(Module.builtinModules);

export function resolveHook(filename: string, specifier: string, isESM: boolean): string | undefined {
export function resolveHook(filename: string, specifier: string): string | undefined {
if (specifier.startsWith('node:') || builtins.has(specifier))
return;
if (!shouldTransform(filename))
return;

if (isRelativeSpecifier(specifier))
return resolveImportSpecifierExtension(path.resolve(path.dirname(filename), specifier), false, isESM);
return resolveImportSpecifierExtension(path.resolve(path.dirname(filename), specifier), false);

/**
* TypeScript discourages path-mapping into node_modules
* (https://www.typescriptlang.org/docs/handbook/modules/reference.html#paths-should-not-point-to-monorepo-packages-or-node_modules-packages).
* It seems like TypeScript tries path-mapping first, but does not look at the `package.json` or `index.js` files in ESM.
* If path-mapping doesn't yield a result, TypeScript falls back to the default resolution (typically node_modules).
* However, if path-mapping doesn't yield a result, TypeScript falls back to the default resolution (typically node_modules).
*/
const isTypeScript = filename.endsWith('.ts') || filename.endsWith('.tsx');
const tsconfigs = loadAndValidateTsconfigsForFile(filename);
Expand Down Expand Up @@ -185,7 +184,7 @@ export function resolveHook(filename: string, specifier: string, isESM: boolean)
if (value.includes('*'))
candidate = candidate.replace('*', matchedPartOfSpecifier);
candidate = path.resolve(tsconfig.pathsBase!, candidate);
const existing = resolveImportSpecifierExtension(candidate, true, isESM);
const existing = resolveImportSpecifierExtension(candidate, true);
if (existing) {
longestPrefixLength = keyPrefix.length;
pathMatchedByLongestPrefix = existing;
Expand All @@ -199,7 +198,7 @@ export function resolveHook(filename: string, specifier: string, isESM: boolean)
if (path.isAbsolute(specifier)) {
// Handle absolute file paths like `import '/path/to/file'`
// Do not handle module imports like `import 'fs'`
return resolveImportSpecifierExtension(specifier, false, isESM);
return resolveImportSpecifierExtension(specifier, false);
}
}

Expand Down Expand Up @@ -281,7 +280,7 @@ function installTransformIfNeeded() {
const originalResolveFilename = (Module as any)._resolveFilename;
function resolveFilename(this: any, specifier: string, parent: Module, ...rest: any[]) {
if (parent) {
const resolved = resolveHook(parent.filename, specifier, false);
const resolved = resolveHook(parent.filename, specifier);
if (resolved !== undefined)
specifier = resolved;
}
Expand Down
53 changes: 38 additions & 15 deletions packages/playwright/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,20 @@ function folderIsModule(folder: string): boolean {
return require(packageJsonPath).type === 'module';
}

const packageJsonMainFieldCache = new Map<string, string | undefined>();

function getMainFieldFromPackageJson(packageJsonPath: string) {
if (!packageJsonMainFieldCache.has(packageJsonPath)) {
let mainField: string | undefined;
try {
mainField = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')).main;
} catch {
}
packageJsonMainFieldCache.set(packageJsonPath, mainField);
}
return packageJsonMainFieldCache.get(packageJsonPath);
}

// This follows the --moduleResolution=bundler strategy from tsc.
// https://devblogs.microsoft.com/typescript/announcing-typescript-5-0-beta/#moduleresolution-bundler
const kExtLookups = new Map([
Expand All @@ -316,7 +330,7 @@ const kExtLookups = new Map([
['.mjs', ['.mts']],
['', ['.js', '.ts', '.jsx', '.tsx', '.cjs', '.mjs', '.cts', '.mts']],
]);
export function resolveImportSpecifierExtension(resolved: string, isPathMapping: boolean, isESM: boolean): string | undefined {
export function resolveImportSpecifierExtension(resolved: string, isPathMapping: boolean): string | undefined {
if (fileExists(resolved))
return resolved;

Expand All @@ -331,25 +345,34 @@ export function resolveImportSpecifierExtension(resolved: string, isPathMapping:
break; // Do not try '' when a more specific extension like '.jsx' matched.
}

// After TypeScript path mapping, here's how directories with a `package.json` are resolved:
// - `package.json#exports` is not respected
// - `package.json#main` is respected only in CJS mode
// - `index.js` default is respected only in CJS mode
//
// More info:
// - https://www.typescriptlang.org/docs/handbook/modules/reference.html#paths-should-not-point-to-monorepo-packages-or-node_modules-packages
// - https://www.typescriptlang.org/docs/handbook/modules/reference.html#directory-modules-index-file-resolution
// - https://nodejs.org/dist/latest-v20.x/docs/api/modules.html#folders-as-modules

const shouldNotResolveDirectory = isPathMapping && isESM;
if (dirExists(resolved)) {
const packageJsonPath = path.join(resolved, 'package.json');

if (isPathMapping) {
// After TypeScript path mapping, here's how directories with a `package.json` are resolved,
// assuming "moduleResolution": "bundler" that we follow:
// - `package.json#exports` is not respected
// - `package.json#main` is respected
// - `index.js` default is respected
//
// More info:
// - https://www.typescriptlang.org/docs/handbook/modules/reference.html#paths-should-not-point-to-monorepo-packages-or-node_modules-packages
// - https://www.typescriptlang.org/docs/handbook/modules/reference.html#directory-modules-index-file-resolution
// - https://nodejs.org/dist/latest-v20.x/docs/api/modules.html#folders-as-modules

const mainField = getMainFieldFromPackageJson(packageJsonPath);
const mainFieldResolved = mainField ? resolveImportSpecifierExtension(path.resolve(resolved, mainField), isPathMapping) : undefined;
return mainFieldResolved || resolveImportSpecifierExtension(path.join(resolved, 'index'), isPathMapping);
}

if (!shouldNotResolveDirectory && dirExists(resolved)) {
// If we import a package, let Node.js figure out the correct import based on package.json.
if (fileExists(path.join(resolved, 'package.json')))
if (fileExists(packageJsonPath))
return resolved;

// Fallback to "folder as module" Node.js behavior.
// https://nodejs.org/dist/latest-v20.x/docs/api/modules.html#folders-as-modules
const dirImport = path.join(resolved, 'index');
return resolveImportSpecifierExtension(dirImport, isPathMapping, isESM);
return resolveImportSpecifierExtension(dirImport, isPathMapping);
}
}

Expand Down
4 changes: 2 additions & 2 deletions tests/playwright-test/playwright-test-fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -367,8 +367,8 @@ export const test = base
const TSCONFIG = {
'compilerOptions': {
'target': 'ESNext',
'moduleResolution': 'node',
'module': 'commonjs',
'moduleResolution': 'bundler',
'module': 'esnext',
'strict': true,
'esModuleInterop': true,
'allowSyntheticDefaultImports': true,
Expand Down
Loading

0 comments on commit f2b9a33

Please sign in to comment.