2022-02-16 15:45:35 -08: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-21 16:30:17 -08:00
|
|
|
import path from 'path';
|
2023-09-08 14:23:35 -07:00
|
|
|
import type { T, BabelAPI } from 'playwright/src/transform/babelBundle';
|
|
|
|
import { types, declare, traverse } from 'playwright/lib/transform/babelBundle';
|
|
|
|
import { resolveImportSpecifierExtension } from 'playwright/lib/util';
|
2022-04-18 10:31:58 -08:00
|
|
|
const t: typeof T = types;
|
2022-02-16 15:45:35 -08:00
|
|
|
|
2022-04-21 16:30:17 -08:00
|
|
|
const fullNames = new Map<string, string | undefined>();
|
|
|
|
let componentNames: Set<string>;
|
|
|
|
let componentIdentifiers: Set<T.Identifier>;
|
|
|
|
|
2022-04-18 10:31:58 -08:00
|
|
|
export default declare((api: BabelAPI) => {
|
2022-02-16 15:45:35 -08:00
|
|
|
api.assertVersion(7);
|
|
|
|
|
2022-04-18 10:31:58 -08:00
|
|
|
const result: babel.PluginObj = {
|
2022-02-16 15:45:35 -08:00
|
|
|
name: 'playwright-debug-transform',
|
|
|
|
visitor: {
|
2022-03-11 08:00:46 -08:00
|
|
|
Program(path) {
|
2022-04-21 16:30:17 -08:00
|
|
|
fullNames.clear();
|
|
|
|
const result = collectComponentUsages(path.node);
|
|
|
|
componentNames = result.names;
|
|
|
|
componentIdentifiers = result.identifiers;
|
2022-03-11 08:00:46 -08:00
|
|
|
},
|
|
|
|
|
2022-04-21 16:30:17 -08:00
|
|
|
ImportDeclaration(p) {
|
|
|
|
const importNode = p.node;
|
|
|
|
if (!t.isStringLiteral(importNode.source))
|
2022-03-11 08:00:46 -08:00
|
|
|
return;
|
|
|
|
|
2022-04-21 16:30:17 -08:00
|
|
|
let remove = false;
|
2022-03-11 08:00:46 -08:00
|
|
|
for (const specifier of importNode.specifiers) {
|
2022-04-21 16:30:17 -08:00
|
|
|
if (!componentNames.has(specifier.local.name))
|
2022-03-11 08:00:46 -08:00
|
|
|
continue;
|
2022-04-21 16:30:17 -08:00
|
|
|
if (t.isImportNamespaceSpecifier(specifier))
|
|
|
|
continue;
|
|
|
|
const { fullName } = componentInfo(specifier, importNode.source.value, this.filename!);
|
|
|
|
fullNames.set(specifier.local.name, fullName);
|
|
|
|
remove = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// If one of the imports was a component, consider them all component imports.
|
|
|
|
if (remove) {
|
|
|
|
p.skip();
|
|
|
|
p.remove();
|
2022-03-11 08:00:46 -08:00
|
|
|
}
|
2022-04-21 16:30:17 -08:00
|
|
|
},
|
2022-03-11 08:00:46 -08:00
|
|
|
|
2022-04-21 16:30:17 -08:00
|
|
|
Identifier(p) {
|
|
|
|
if (componentIdentifiers.has(p.node)) {
|
|
|
|
const componentName = fullNames.get(p.node.name) || p.node.name;
|
|
|
|
p.replaceWith(t.stringLiteral(componentName));
|
|
|
|
}
|
2022-03-11 08:00:46 -08:00
|
|
|
},
|
|
|
|
|
2022-02-16 15:45:35 -08:00
|
|
|
JSXElement(path) {
|
|
|
|
const jsxElement = path.node;
|
|
|
|
const jsxName = jsxElement.openingElement.name;
|
|
|
|
if (!t.isJSXIdentifier(jsxName))
|
|
|
|
return;
|
|
|
|
|
2022-04-18 10:31:58 -08:00
|
|
|
const props: (T.ObjectProperty | T.SpreadElement)[] = [];
|
2022-02-16 15:45:35 -08:00
|
|
|
|
|
|
|
for (const jsxAttribute of jsxElement.openingElement.attributes) {
|
|
|
|
if (t.isJSXAttribute(jsxAttribute)) {
|
2022-04-18 10:31:58 -08:00
|
|
|
let namespace: T.JSXIdentifier | undefined;
|
|
|
|
let name: T.JSXIdentifier | undefined;
|
2022-03-11 08:00:46 -08:00
|
|
|
if (t.isJSXNamespacedName(jsxAttribute.name)) {
|
|
|
|
namespace = jsxAttribute.name.namespace;
|
|
|
|
name = jsxAttribute.name.name;
|
|
|
|
} else if (t.isJSXIdentifier(jsxAttribute.name)) {
|
|
|
|
name = jsxAttribute.name;
|
|
|
|
}
|
|
|
|
if (!name)
|
2022-02-16 15:45:35 -08:00
|
|
|
continue;
|
2022-03-11 08:00:46 -08:00
|
|
|
const attrName = (namespace ? namespace.name + ':' : '') + name.name;
|
2022-02-16 15:45:35 -08:00
|
|
|
if (t.isStringLiteral(jsxAttribute.value))
|
|
|
|
props.push(t.objectProperty(t.stringLiteral(attrName), jsxAttribute.value));
|
|
|
|
else if (t.isJSXExpressionContainer(jsxAttribute.value) && t.isExpression(jsxAttribute.value.expression))
|
|
|
|
props.push(t.objectProperty(t.stringLiteral(attrName), jsxAttribute.value.expression));
|
2022-06-10 16:34:21 -08:00
|
|
|
else if (jsxAttribute.value === null)
|
|
|
|
props.push(t.objectProperty(t.stringLiteral(attrName), t.booleanLiteral(true)));
|
2022-02-16 15:45:35 -08:00
|
|
|
else
|
|
|
|
props.push(t.objectProperty(t.stringLiteral(attrName), t.nullLiteral()));
|
|
|
|
} else if (t.isJSXSpreadAttribute(jsxAttribute)) {
|
|
|
|
props.push(t.spreadElement(jsxAttribute.argument));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-18 10:31:58 -08:00
|
|
|
const children: (T.Expression | T.SpreadElement)[] = [];
|
2022-02-16 15:45:35 -08:00
|
|
|
for (const child of jsxElement.children) {
|
|
|
|
if (t.isJSXText(child))
|
|
|
|
children.push(t.stringLiteral(child.value));
|
|
|
|
else if (t.isJSXElement(child))
|
|
|
|
children.push(child);
|
|
|
|
else if (t.isJSXExpressionContainer(child) && !t.isJSXEmptyExpression(child.expression))
|
|
|
|
children.push(child.expression);
|
|
|
|
else if (t.isJSXSpreadChild(child))
|
|
|
|
children.push(t.spreadElement(child.expression));
|
|
|
|
}
|
|
|
|
|
2022-04-21 16:30:17 -08:00
|
|
|
const componentName = fullNames.get(jsxName.name) || jsxName.name;
|
2022-02-16 15:45:35 -08:00
|
|
|
path.replaceWith(t.objectExpression([
|
2022-03-11 08:00:46 -08:00
|
|
|
t.objectProperty(t.identifier('kind'), t.stringLiteral('jsx')),
|
2022-04-21 16:30:17 -08:00
|
|
|
t.objectProperty(t.identifier('type'), t.stringLiteral(componentName)),
|
2022-02-16 15:45:35 -08:00
|
|
|
t.objectProperty(t.identifier('props'), t.objectExpression(props)),
|
|
|
|
t.objectProperty(t.identifier('children'), t.arrayExpression(children)),
|
|
|
|
]));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
2022-04-18 10:31:58 -08:00
|
|
|
return result;
|
2022-02-16 15:45:35 -08:00
|
|
|
});
|
2022-03-11 08:00:46 -08:00
|
|
|
|
2022-04-21 16:30:17 -08:00
|
|
|
export function collectComponentUsages(node: T.Node) {
|
2022-05-25 13:59:45 -07:00
|
|
|
const importedLocalNames = new Set<string>();
|
2022-04-21 16:30:17 -08:00
|
|
|
const names = new Set<string>();
|
|
|
|
const identifiers = new Set<T.Identifier>();
|
|
|
|
traverse(node, {
|
|
|
|
enter: p => {
|
2022-05-25 13:59:45 -07: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-21 16:30:17 -08:00
|
|
|
// Treat JSX-everything as component usages.
|
|
|
|
if (t.isJSXElement(p.node) && t.isJSXIdentifier(p.node.openingElement.name))
|
|
|
|
names.add(p.node.openingElement.name.name);
|
|
|
|
|
2022-05-25 13:59:45 -07:00
|
|
|
// Treat mount(identifier, ...) as component usage if it is in the importedLocalNames list.
|
2022-04-21 16:30:17 -08: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 13:59:45 -07:00
|
|
|
const arg = callExpression.arguments[0];
|
|
|
|
if (!t.isIdentifier(arg) || !importedLocalNames.has(arg.name))
|
|
|
|
return;
|
|
|
|
|
|
|
|
names.add(arg.name);
|
|
|
|
identifiers.add(arg);
|
2022-04-21 16:30:17 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
return { names, identifiers };
|
|
|
|
}
|
|
|
|
|
|
|
|
export type ComponentInfo = {
|
2023-08-24 16:19:57 -07:00
|
|
|
fullName: string;
|
|
|
|
importPath: string;
|
|
|
|
isModuleOrAlias: boolean;
|
|
|
|
importedName?: string;
|
|
|
|
deps: string[];
|
2022-04-21 16:30:17 -08:00
|
|
|
};
|
|
|
|
|
|
|
|
export function componentInfo(specifier: T.ImportSpecifier | T.ImportDefaultSpecifier, importSource: string, filename: string): ComponentInfo {
|
|
|
|
const isModuleOrAlias = !importSource.startsWith('.');
|
2023-01-13 10:49:10 -08:00
|
|
|
const unresolvedImportPath = path.resolve(path.dirname(filename), importSource);
|
|
|
|
// Support following notations for Button.tsx:
|
2023-05-09 16:26:29 -07:00
|
|
|
// - import { Button } from './Button.js' - via resolveImportSpecifierExtension
|
2023-01-13 10:49:10 -08:00
|
|
|
// - import { Button } from './Button' - via require.resolve
|
2023-05-09 16:26:29 -07:00
|
|
|
const importPath = isModuleOrAlias ? importSource : resolveImportSpecifierExtension(unresolvedImportPath) || require.resolve(unresolvedImportPath);
|
2022-04-21 16:30:17 -08:00
|
|
|
const prefix = importPath.replace(/[^\w_\d]/g, '_');
|
|
|
|
const pathInfo = { importPath, isModuleOrAlias };
|
|
|
|
|
|
|
|
if (t.isImportDefaultSpecifier(specifier))
|
2023-08-24 16:19:57 -07:00
|
|
|
return { fullName: prefix, deps: [], ...pathInfo };
|
2022-04-21 16:30:17 -08:00
|
|
|
|
|
|
|
if (t.isIdentifier(specifier.imported))
|
2023-08-24 16:19:57 -07:00
|
|
|
return { fullName: prefix + '_' + specifier.imported.name, importedName: specifier.imported.name, deps: [], ...pathInfo };
|
|
|
|
return { fullName: prefix + '_' + specifier.imported.value, importedName: specifier.imported.value, deps: [], ...pathInfo };
|
2022-03-11 08:00:46 -08:00
|
|
|
}
|