Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion docs/src/test-typescript-js.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ npx tsc -p tsconfig.json --noEmit -w

## tsconfig.json

Playwright will pick up `tsconfig.json` for each source file it loads. Note that Playwright **only supports** the following tsconfig options: `allowJs`, `baseUrl`, `paths`, `jsx`, `jsxFactory`, `jsxFragmentFactory`, `jsxImportSource`, `references` and `extends`.
Playwright will pick up `tsconfig.json` for each source file it loads. Note that Playwright **only supports** the following tsconfig options: `allowJs`, `baseUrl`, `paths`, `jsx`, `jsxFactory`, `jsxFragmentFactory`, `jsxImportSource`, `useDefineForClassFields`, `references` and `extends`. The `target` option is only used to determine `useDefineForClassFields`.

We recommend setting up a separate `tsconfig.json` in the tests directory so that you can change some preferences specifically for the tests. Here is an example directory structure.

Expand Down Expand Up @@ -86,6 +86,20 @@ Playwright compiles JSX in `.tsx` and `.jsx` files according to the `jsx`, `jsxF
}
```

### tsconfig class fields

Playwright compiles TypeScript class fields according to the `useDefineForClassFields` option in the `tsconfig.json`. Just like `tsc`, when it is not specified, it is enabled for `ES2022` and later targets, and when `target` is not specified.

With `useDefineForClassFields` enabled, class fields follow the JavaScript semantics, so field initializers run before the constructor body and can not access constructor parameter properties. Disable it to keep the legacy TypeScript semantics.

```json title="tsconfig.json"
{
"compilerOptions": {
"useDefineForClassFields": false
}
}
```

### tsconfig resolution

By default, Playwright will look up a closest tsconfig for each imported file by going up the directory structure and looking for `tsconfig.json` or `jsconfig.json`. This way, you can create a `tests/tsconfig.json` file that will be used only for your tests and Playwright will pick it up automatically.
Expand Down
36 changes: 9 additions & 27 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions packages/playwright/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,13 @@
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.65.0-next"
},
"peerDependencies": {
"esbuild": ">=0.21.5"
},
"peerDependenciesMeta": {
"esbuild": {
"optional": true
}
}
}
33 changes: 22 additions & 11 deletions packages/playwright/src/transform/babelBundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export type JsxOptions = {
jsxFragmentFactory?: string;
jsxImportSource?: string;
};
export type BabelTransformFunction = (code: string, filename: string, isModule: boolean, pluginsSuffix: BabelPlugin[], jsx?: JsxOptions) => BabelFileResult | null;
export type BabelTransformFunction = (code: string, filename: string, isModule: boolean, pluginsSuffix: BabelPlugin[], useDefineForClassFields: boolean, jsx?: JsxOptions) => BabelFileResult | null;

const nodeMajorVersion = +process.versions.node.split('.')[0];

Expand All @@ -57,39 +57,50 @@ function jsxPlugin(jsx: JsxOptions | undefined): [any, any] {
}];
}

function babelTransformOptions(isTypeScript: boolean, isModule: boolean, pluginsEpilogue: [string, any?][], jsx?: JsxOptions): TransformOptions {
function babelTransformOptions(isTypeScript: boolean, isModule: boolean, pluginsEpilogue: [string, any?][], useDefineForClassFields: boolean, jsx?: JsxOptions): TransformOptions {
const plugins = [
[require('@babel/plugin-syntax-import-attributes'), { deprecatedAssertSyntax: true }],
];

if (isTypeScript) {
plugins.push(
// Strip "declare" class fields before these plugins run:
// Strip "declare" class fields, and uninitialized ones that tsc omits without
// "useDefineForClassFields", before these plugins run:
// - plugin-proposal-decorators
// - plugin-transform-class-properties
// - plugin-transform-private-methods
// See https://github.com/microsoft/playwright/issues/38586
[
(): PluginObj => ({
name: 'strip-declare-class-fields',
name: 'strip-type-only-class-fields',
visitor: {
Class(path) {
for (const member of path.get('body.body')) {
if (member.isClassProperty() && member.node.declare)
if (!member.isClassProperty())
continue;
const { declare, value, decorators } = member.node;
if (declare || (!useDefineForClassFields && !value && !decorators?.length))
member.remove();
}
}
}
})
],
[require('@babel/plugin-proposal-decorators'), { version: '2023-05' }],
[require('@babel/plugin-transform-class-properties')],
);
// With "useDefineForClassFields", Node defines class fields natively.
if (!useDefineForClassFields) {
plugins.push(
[require('@babel/plugin-transform-class-properties')],
[require('@babel/plugin-transform-private-methods')],
);
}
plugins.push(
[require('@babel/plugin-transform-class-static-block')],
[require('@babel/plugin-transform-numeric-separator')],
[require('@babel/plugin-transform-logical-assignment-operators')],
[require('@babel/plugin-transform-nullish-coalescing-operator')],
[require('@babel/plugin-transform-optional-chaining')],
[require('@babel/plugin-transform-private-methods')],
[require('@babel/plugin-syntax-json-strings')],
[require('@babel/plugin-syntax-optional-catch-binding')],
[require('@babel/plugin-syntax-async-generators')],
Expand Down Expand Up @@ -163,7 +174,7 @@ function babelTransformOptions(isTypeScript: boolean, isModule: boolean, plugins
setPublicClassFields: true,
},
presets: isTypeScript ? [
[require('@babel/preset-typescript'), { onlyRemoveTypeImports: false }],
[require('@babel/preset-typescript'), { onlyRemoveTypeImports: false, allowDeclareFields: useDefineForClassFields }],
] : [],
plugins: [
...plugins,
Expand All @@ -180,21 +191,21 @@ function isTypeScript(filename: string) {
return filename.endsWith('.ts') || filename.endsWith('.tsx') || filename.endsWith('.mts') || filename.endsWith('.cts');
}

export function babelTransform(code: string, filename: string, isModule: boolean, pluginsEpilogue: [string, any?][], jsx?: JsxOptions): BabelFileResult | null {
export function babelTransform(code: string, filename: string, isModule: boolean, pluginsEpilogue: [string, any?][], useDefineForClassFields: boolean, jsx?: JsxOptions): BabelFileResult | null {
if (isTransforming)
return null;

// Prevent reentry while requiring plugins lazily.
isTransforming = true;
try {
const options = babelTransformOptions(isTypeScript(filename), isModule, pluginsEpilogue, jsx);
const options = babelTransformOptions(isTypeScript(filename), isModule, pluginsEpilogue, useDefineForClassFields, jsx);
return babel.transform(code, { filename, ...options });
} finally {
isTransforming = false;
}
}

export function babelParse(code: string, filename: string, isModule: boolean): babel.ParseResult {
const options = babelTransformOptions(isTypeScript(filename), isModule, []);
const options = babelTransformOptions(isTypeScript(filename), isModule, [], true);
return babel.parse(code, { filename, ...options })!;
}
Loading
Loading