2022-02-17 00:45:35 +01:00
|
|
|
/**
|
|
|
|
|
* Copyright (c) Microsoft Corporation.
|
|
|
|
|
*
|
|
|
|
|
* Licensed under the Apache License, Version 2.0 (the 'License');
|
|
|
|
|
* you may not use this file except in compliance with the License.
|
|
|
|
|
* You may obtain a copy of the License at
|
|
|
|
|
*
|
|
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
*
|
|
|
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
|
|
|
* distributed under the License is distributed on an 'AS IS' BASIS,
|
|
|
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
|
* See the License for the specific language governing permissions and
|
|
|
|
|
* limitations under the License.
|
|
|
|
|
*/
|
|
|
|
|
|
2022-04-22 02:30:17 +02:00
|
|
|
import path from 'path';
|
2023-09-29 02:12:57 +02:00
|
|
|
import type { T, BabelAPI, PluginObj } from 'playwright/src/transform/babelBundle';
|
2023-09-08 23:23:35 +02:00
|
|
|
import { types, declare, traverse } from 'playwright/lib/transform/babelBundle';
|
|
|
|
|
import { resolveImportSpecifierExtension } from 'playwright/lib/util';
|
2022-04-18 20:31:58 +02:00
|
|
|
const t: typeof T = types;
|
2022-02-17 00:45:35 +01:00
|
|
|
|
2022-04-22 02:30:17 +02:00
|
|
|
let componentNames: Set<string>;
|
2024-01-13 05:02:27 +01:00
|
|
|
let componentImports: Map<string, ImportInfo>;
|
2022-04-22 02:30:17 +02:00
|
|
|
|
2022-04-18 20:31:58 +02:00
|
|
|
export default declare((api: BabelAPI) => {
|
2022-02-17 00:45:35 +01:00
|
|
|
api.assertVersion(7);
|
|
|
|
|
|
2023-09-29 02:12:57 +02:00
|
|
|
const result: PluginObj = {
|
2022-02-17 00:45:35 +01:00
|
|
|
name: 'playwright-debug-transform',
|
|
|
|
|
visitor: {
|
2024-01-13 05:02:27 +01:00
|
|
|
Program: {
|
|
|
|
|
enter(path) {
|
|
|
|
|
const result = collectComponentUsages(path.node);
|
|
|
|
|
componentNames = result.names;
|
|
|
|
|
componentImports = new Map();
|
|
|
|
|
},
|
|
|
|
|
exit(path) {
|
|
|
|
|
let firstDeclaration: any;
|
|
|
|
|
let lastImportDeclaration: any;
|
|
|
|
|
path.get('body').forEach(p => {
|
|
|
|
|
if (p.isImportDeclaration())
|
|
|
|
|
lastImportDeclaration = p;
|
|
|
|
|
else if (!firstDeclaration)
|
|
|
|
|
firstDeclaration = p;
|
|
|
|
|
});
|
|
|
|
|
const insertionPath = lastImportDeclaration || firstDeclaration;
|
|
|
|
|
if (!insertionPath)
|
|
|
|
|
return;
|
|
|
|
|
for (const componentImport of [...componentImports.values()].reverse()) {
|
|
|
|
|
insertionPath.insertAfter(
|
|
|
|
|
t.variableDeclaration(
|
|
|
|
|
'const',
|
|
|
|
|
[
|
|
|
|
|
t.variableDeclarator(
|
|
|
|
|
t.identifier(componentImport.localName),
|
|
|
|
|
t.objectExpression([
|
|
|
|
|
t.objectProperty(t.identifier('__pw_type'), t.stringLiteral('importRef')),
|
|
|
|
|
t.objectProperty(t.identifier('id'), t.stringLiteral(componentImport.id)),
|
|
|
|
|
]),
|
|
|
|
|
)
|
|
|
|
|
]
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-03-11 17:00:46 +01:00
|
|
|
},
|
|
|
|
|
|
2022-04-22 02:30:17 +02:00
|
|
|
ImportDeclaration(p) {
|
|
|
|
|
const importNode = p.node;
|
|
|
|
|
if (!t.isStringLiteral(importNode.source))
|
2022-03-11 17:00:46 +01:00
|
|
|
return;
|
|
|
|
|
|
2023-09-26 02:00:52 +02:00
|
|
|
let components = 0;
|
2022-03-11 17:00:46 +01:00
|
|
|
for (const specifier of importNode.specifiers) {
|
2022-04-22 02:30:17 +02:00
|
|
|
if (t.isImportNamespaceSpecifier(specifier))
|
|
|
|
|
continue;
|
2024-01-13 05:02:27 +01:00
|
|
|
const info = importInfo(importNode, specifier, this.filename!);
|
|
|
|
|
if (!componentNames.has(info.localName))
|
|
|
|
|
continue;
|
|
|
|
|
componentImports.set(info.localName, info);
|
2023-09-26 02:00:52 +02:00
|
|
|
++components;
|
2022-04-22 02:30:17 +02:00
|
|
|
}
|
|
|
|
|
|
2023-09-26 02:00:52 +02:00
|
|
|
// All the imports were components => delete.
|
|
|
|
|
if (components && components === importNode.specifiers.length) {
|
2022-04-22 02:30:17 +02:00
|
|
|
p.skip();
|
|
|
|
|
p.remove();
|
2022-03-11 17:00:46 +01:00
|
|
|
}
|
2022-04-22 02:30:17 +02:00
|
|
|
},
|
2022-03-11 17:00:46 +01:00
|
|
|
|
2024-01-13 05:02:27 +01:00
|
|
|
MemberExpression(path) {
|
|
|
|
|
if (!t.isIdentifier(path.node.object))
|
2022-02-17 00:45:35 +01:00
|
|
|
return;
|
2024-01-13 05:02:27 +01:00
|
|
|
if (!componentImports.has(path.node.object.name))
|
|
|
|
|
return;
|
|
|
|
|
if (!t.isIdentifier(path.node.property))
|
|
|
|
|
return;
|
|
|
|
|
path.replaceWith(
|
|
|
|
|
t.objectExpression([
|
|
|
|
|
t.spreadElement(t.identifier(path.node.object.name)),
|
|
|
|
|
t.objectProperty(t.identifier('property'), t.stringLiteral(path.node.property.name)),
|
|
|
|
|
])
|
|
|
|
|
);
|
|
|
|
|
},
|
2022-02-17 00:45:35 +01:00
|
|
|
}
|
|
|
|
|
};
|
2022-04-18 20:31:58 +02:00
|
|
|
return result;
|
2022-02-17 00:45:35 +01:00
|
|
|
});
|
2022-03-11 17:00:46 +01:00
|
|
|
|
2022-04-22 02:30:17 +02:00
|
|
|
export function collectComponentUsages(node: T.Node) {
|
2022-05-25 22:59:45 +02:00
|
|
|
const importedLocalNames = new Set<string>();
|
2022-04-22 02:30:17 +02:00
|
|
|
const names = new Set<string>();
|
|
|
|
|
traverse(node, {
|
|
|
|
|
enter: p => {
|
2022-05-25 22:59:45 +02:00
|
|
|
|
|
|
|
|
// First look at all the imports.
|
|
|
|
|
if (t.isImportDeclaration(p.node)) {
|
|
|
|
|
const importNode = p.node;
|
|
|
|
|
if (!t.isStringLiteral(importNode.source))
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
for (const specifier of importNode.specifiers) {
|
|
|
|
|
if (t.isImportNamespaceSpecifier(specifier))
|
|
|
|
|
continue;
|
|
|
|
|
importedLocalNames.add(specifier.local.name);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-04-22 02:30:17 +02:00
|
|
|
// Treat JSX-everything as component usages.
|
2023-09-26 02:00:52 +02:00
|
|
|
if (t.isJSXElement(p.node)) {
|
|
|
|
|
if (t.isJSXIdentifier(p.node.openingElement.name))
|
|
|
|
|
names.add(p.node.openingElement.name.name);
|
|
|
|
|
if (t.isJSXMemberExpression(p.node.openingElement.name) && t.isJSXIdentifier(p.node.openingElement.name.object) && t.isJSXIdentifier(p.node.openingElement.name.property))
|
2024-01-13 05:02:27 +01:00
|
|
|
names.add(p.node.openingElement.name.object.name);
|
2023-09-26 02:00:52 +02:00
|
|
|
}
|
2022-04-22 02:30:17 +02:00
|
|
|
|
2022-05-25 22:59:45 +02:00
|
|
|
// Treat mount(identifier, ...) as component usage if it is in the importedLocalNames list.
|
2022-04-22 02:30:17 +02:00
|
|
|
if (t.isAwaitExpression(p.node) && t.isCallExpression(p.node.argument) && t.isIdentifier(p.node.argument.callee) && p.node.argument.callee.name === 'mount') {
|
|
|
|
|
const callExpression = p.node.argument;
|
2022-05-25 22:59:45 +02:00
|
|
|
const arg = callExpression.arguments[0];
|
|
|
|
|
if (!t.isIdentifier(arg) || !importedLocalNames.has(arg.name))
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
names.add(arg.name);
|
2022-04-22 02:30:17 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
2024-01-13 05:02:27 +01:00
|
|
|
return { names };
|
2022-04-22 02:30:17 +02:00
|
|
|
}
|
|
|
|
|
|
2024-01-13 05:02:27 +01:00
|
|
|
export type ImportInfo = {
|
|
|
|
|
id: string;
|
2023-08-25 01:19:57 +02:00
|
|
|
isModuleOrAlias: boolean;
|
2024-01-13 05:02:27 +01:00
|
|
|
importPath: string;
|
|
|
|
|
localName: string;
|
|
|
|
|
remoteName: string | undefined;
|
2022-04-22 02:30:17 +02:00
|
|
|
};
|
|
|
|
|
|
2024-01-13 05:02:27 +01:00
|
|
|
export function importInfo(importNode: T.ImportDeclaration, specifier: T.ImportSpecifier | T.ImportDefaultSpecifier, filename: string): ImportInfo {
|
|
|
|
|
const importSource = importNode.source.value;
|
2022-04-22 02:30:17 +02:00
|
|
|
const isModuleOrAlias = !importSource.startsWith('.');
|
2023-01-13 19:49:10 +01:00
|
|
|
const unresolvedImportPath = path.resolve(path.dirname(filename), importSource);
|
|
|
|
|
// Support following notations for Button.tsx:
|
2023-05-10 01:26:29 +02:00
|
|
|
// - import { Button } from './Button.js' - via resolveImportSpecifierExtension
|
2023-01-13 19:49:10 +01:00
|
|
|
// - import { Button } from './Button' - via require.resolve
|
2023-05-10 01:26:29 +02:00
|
|
|
const importPath = isModuleOrAlias ? importSource : resolveImportSpecifierExtension(unresolvedImportPath) || require.resolve(unresolvedImportPath);
|
2024-01-13 05:02:27 +01:00
|
|
|
const idPrefix = importPath.replace(/[^\w_\d]/g, '_');
|
|
|
|
|
|
|
|
|
|
const result: ImportInfo = {
|
|
|
|
|
id: idPrefix,
|
|
|
|
|
importPath,
|
|
|
|
|
isModuleOrAlias,
|
|
|
|
|
localName: specifier.local.name,
|
|
|
|
|
remoteName: undefined,
|
|
|
|
|
};
|
2023-09-26 02:00:52 +02:00
|
|
|
|
2024-01-13 05:02:27 +01:00
|
|
|
if (t.isImportDefaultSpecifier(specifier)) {
|
|
|
|
|
} else if (t.isIdentifier(specifier.imported)) {
|
|
|
|
|
result.remoteName = specifier.imported.name;
|
|
|
|
|
} else {
|
|
|
|
|
result.remoteName = specifier.imported.value;
|
|
|
|
|
}
|
2022-04-22 02:30:17 +02:00
|
|
|
|
2024-01-13 05:02:27 +01:00
|
|
|
if (result.remoteName)
|
|
|
|
|
result.id += '_' + result.remoteName;
|
|
|
|
|
return result;
|
2022-03-11 17:00:46 +01:00
|
|
|
}
|