diff --git a/package-lock.json b/package-lock.json index 3f0a33ed64..cc14be1a25 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2122,6 +2122,21 @@ "object.assign": "^4.1.0" } }, + "node_modules/babel-plugin-module-resolver": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-module-resolver/-/babel-plugin-module-resolver-4.1.0.tgz", + "integrity": "sha512-MlX10UDheRr3lb3P0WcaIdtCSRlxdQsB1sBqL7W0raF070bGl1HQQq5K3T2vf2XAYie+ww+5AKC/WrkjRO2knA==", + "dependencies": { + "find-babel-config": "^1.2.0", + "glob": "^7.1.6", + "pkg-up": "^3.1.0", + "reselect": "^4.0.0", + "resolve": "^1.13.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -4270,6 +4285,34 @@ "node": ">=0.10.0" } }, + "node_modules/find-babel-config": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/find-babel-config/-/find-babel-config-1.2.0.tgz", + "integrity": "sha512-jB2CHJeqy6a820ssiqwrKMeyC6nNdmrcgkKWJWmpoxpE8RKciYJXCcXRq1h2AzCo5I5BJeN2tkGEO3hLTuePRA==", + "dependencies": { + "json5": "^0.5.1", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/find-babel-config/node_modules/json5": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/find-babel-config/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "engines": { + "node": ">=4" + } + }, "node_modules/find-cache-dir": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", @@ -4983,7 +5026,6 @@ "version": "2.6.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.6.0.tgz", "integrity": "sha512-wShG8vs60jKfPWpF2KZRaAtvt3a20OAn7+IJ6hLPECpSABLcKtFKTTI4ZtH5QcBruBHlq+WsdHWyz0BCZW7svQ==", - "dev": true, "dependencies": { "has": "^1.0.3" }, @@ -6515,7 +6557,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, "engines": { "node": ">=6" } @@ -6609,8 +6650,7 @@ "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "node_modules/path-type": { "version": "4.0.0", @@ -6694,6 +6734,73 @@ "node": ">=8" } }, + "node_modules/pkg-up": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", + "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", + "dependencies": { + "find-up": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-up/node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-up/node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-up/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-up/node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-up/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "engines": { + "node": ">=4" + } + }, "node_modules/playwright": { "resolved": "packages/playwright", "link": true @@ -7199,11 +7306,15 @@ "node": ">=0.10.0" } }, + "node_modules/reselect": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.5.tgz", + "integrity": "sha512-uVdlz8J7OO+ASpBYoz1Zypgx0KasCY20H+N8JD13oUMtPvSHQuscrHop4KbXrbsBcdB9Ds7lVK7eRkBIfO43vQ==" + }, "node_modules/resolve": { "version": "1.20.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", - "dev": true, "dependencies": { "is-core-module": "^2.2.0", "path-parse": "^1.0.6" @@ -9036,12 +9147,14 @@ "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", "@babel/plugin-transform-modules-commonjs": "^7.14.5", "@babel/preset-typescript": "^7.14.5", + "babel-plugin-module-resolver": "^4.1.0", "colors": "^1.4.0", "commander": "^8.2.0", "debug": "^4.1.1", "expect": "=27.2.5", "jest-matcher-utils": "=27.2.5", "jpeg-js": "^0.4.2", + "json5": "^2.2.0", "mime": "^2.4.6", "minimatch": "^3.0.3", "ms": "^2.1.2", @@ -9849,12 +9962,14 @@ "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", "@babel/plugin-transform-modules-commonjs": "^7.14.5", "@babel/preset-typescript": "^7.14.5", + "babel-plugin-module-resolver": "^4.1.0", "colors": "^1.4.0", "commander": "^8.2.0", "debug": "^4.1.1", "expect": "=27.2.5", "jest-matcher-utils": "=27.2.5", "jpeg-js": "^0.4.2", + "json5": "^2.2.0", "mime": "^2.4.6", "minimatch": "^3.0.3", "ms": "^2.1.2", @@ -10720,6 +10835,18 @@ "object.assign": "^4.1.0" } }, + "babel-plugin-module-resolver": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-module-resolver/-/babel-plugin-module-resolver-4.1.0.tgz", + "integrity": "sha512-MlX10UDheRr3lb3P0WcaIdtCSRlxdQsB1sBqL7W0raF070bGl1HQQq5K3T2vf2XAYie+ww+5AKC/WrkjRO2knA==", + "requires": { + "find-babel-config": "^1.2.0", + "glob": "^7.1.6", + "pkg-up": "^3.1.0", + "reselect": "^4.0.0", + "resolve": "^1.13.1" + } + }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -12366,6 +12493,27 @@ "to-regex-range": "^2.1.0" } }, + "find-babel-config": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/find-babel-config/-/find-babel-config-1.2.0.tgz", + "integrity": "sha512-jB2CHJeqy6a820ssiqwrKMeyC6nNdmrcgkKWJWmpoxpE8RKciYJXCcXRq1h2AzCo5I5BJeN2tkGEO3hLTuePRA==", + "requires": { + "json5": "^0.5.1", + "path-exists": "^3.0.0" + }, + "dependencies": { + "json5": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=" + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" + } + } + }, "find-cache-dir": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", @@ -12903,7 +13051,6 @@ "version": "2.6.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.6.0.tgz", "integrity": "sha512-wShG8vs60jKfPWpF2KZRaAtvt3a20OAn7+IJ6hLPECpSABLcKtFKTTI4ZtH5QcBruBHlq+WsdHWyz0BCZW7svQ==", - "dev": true, "requires": { "has": "^1.0.3" } @@ -14070,8 +14217,7 @@ "p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" }, "param-case": { "version": "3.0.4", @@ -14141,8 +14287,7 @@ "path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "path-type": { "version": "4.0.0", @@ -14204,6 +14349,54 @@ "find-up": "^4.0.0" } }, + "pkg-up": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", + "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", + "requires": { + "find-up": "^3.0.0" + }, + "dependencies": { + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "requires": { + "locate-path": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "requires": { + "p-limit": "^2.0.0" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" + } + } + }, "playwright": { "version": "file:packages/playwright", "requires": { @@ -14622,11 +14815,15 @@ "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", "dev": true }, + "reselect": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.5.tgz", + "integrity": "sha512-uVdlz8J7OO+ASpBYoz1Zypgx0KasCY20H+N8JD13oUMtPvSHQuscrHop4KbXrbsBcdB9Ds7lVK7eRkBIfO43vQ==" + }, "resolve": { "version": "1.20.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", - "dev": true, "requires": { "is-core-module": "^2.2.0", "path-parse": "^1.0.6" diff --git a/packages/playwright-test/package.json b/packages/playwright-test/package.json index ef5132c809..6c62971d7f 100644 --- a/packages/playwright-test/package.json +++ b/packages/playwright-test/package.json @@ -44,12 +44,14 @@ "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", "@babel/plugin-transform-modules-commonjs": "^7.14.5", "@babel/preset-typescript": "^7.14.5", + "babel-plugin-module-resolver": "^4.1.0", "colors": "^1.4.0", "commander": "^8.2.0", "debug": "^4.1.1", "expect": "=27.2.5", "jest-matcher-utils": "=27.2.5", "jpeg-js": "^0.4.2", + "json5": "^2.2.0", "mime": "^2.4.6", "minimatch": "^3.0.3", "ms": "^2.1.2", diff --git a/packages/playwright-test/src/experimentalLoader.ts b/packages/playwright-test/src/experimentalLoader.ts index b61e84d273..4b996ac429 100644 --- a/packages/playwright-test/src/experimentalLoader.ts +++ b/packages/playwright-test/src/experimentalLoader.ts @@ -15,8 +15,12 @@ */ import fs from 'fs'; +import path from 'path'; +import { tsConfigLoader, TsConfigLoaderResult } from './third_party/tsconfig-loader'; import { transformHook } from './transform'; +const tsConfigCache = new Map(); + async function resolve(specifier: string, context: { parentURL: string }, defaultResolve: any) { if (specifier.endsWith('.js') || specifier.endsWith('.ts') || specifier.endsWith('.mjs')) return defaultResolve(specifier, context, defaultResolve); @@ -32,8 +36,17 @@ async function resolve(specifier: string, context: { parentURL: string }, defaul async function load(url: string, context: any, defaultLoad: any) { if (url.endsWith('.ts')) { const filename = url.substring('file://'.length); + const cwd = path.dirname(filename); + let tsconfig = tsConfigCache.get(cwd); + if (!tsconfig) { + tsconfig = tsConfigLoader({ + getEnv: (name: string) => process.env[name], + cwd + }); + tsConfigCache.set(cwd, tsconfig); + } const code = fs.readFileSync(filename, 'utf-8'); - const source = transformHook(code, filename, true); + const source = transformHook(code, filename, tsconfig, true); return { format: 'module', source }; } return defaultLoad(url, context, defaultLoad); diff --git a/packages/playwright-test/src/loader.ts b/packages/playwright-test/src/loader.ts index f5f4d5118e..b85c48763b 100644 --- a/packages/playwright-test/src/loader.ts +++ b/packages/playwright-test/src/loader.ts @@ -27,6 +27,7 @@ import { ProjectImpl } from './project'; import { Reporter } from '../types/testReporter'; import { BuiltInReporter, builtInReporters } from './runner'; import { isRegExp } from 'playwright-core/lib/utils/utils'; +import { tsConfigLoader, TsConfigLoaderResult } from './third_party/tsconfig-loader'; export class Loader { private _defaultConfig: Config; @@ -37,6 +38,7 @@ export class Loader { private _projects: ProjectImpl[] = []; private _fileSuites = new Map(); private _lastModuleInfo: { rootFolder: string, isModule: boolean } | null = null; + private _tsConfigCache = new Map(); constructor(defaultConfig: Config, configOverrides: Config) { this._defaultConfig = defaultConfig; @@ -192,7 +194,18 @@ export class Loader { private async _requireOrImport(file: string) { - const revertBabelRequire = installTransform(); + // Respect tsconfig paths. + const cwd = path.dirname(file); + let tsconfig = this._tsConfigCache.get(cwd); + if (!tsconfig) { + tsconfig = tsConfigLoader({ + getEnv: (name: string) => process.env[name], + cwd + }); + this._tsConfigCache.set(cwd, tsconfig); + } + + const revertBabelRequire = installTransform(tsconfig); // Figure out if we are importing or requiring. let isModule: boolean; diff --git a/packages/playwright-test/src/third_party/tsconfig-loader.ts b/packages/playwright-test/src/third_party/tsconfig-loader.ts new file mode 100644 index 0000000000..0d40cddbad --- /dev/null +++ b/packages/playwright-test/src/third_party/tsconfig-loader.ts @@ -0,0 +1,209 @@ +/** + * The MIT License (MIT) + * + * Copyright (c) 2016 Jonas Kello + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/* eslint-disable */ + +import * as path from 'path'; +import * as fs from 'fs'; +import JSON5 from 'json5'; + +/** + * Typing for the parts of tsconfig that we care about + */ +export interface Tsconfig { + extends?: string; + compilerOptions?: { + baseUrl?: string; + paths?: { [key: string]: Array }; + strict?: boolean; + }; +} + +export interface TsConfigLoaderResult { + tsConfigPath: string | undefined; + baseUrl: string | undefined; + paths: { [key: string]: Array } | undefined; +} + +export interface TsConfigLoaderParams { + getEnv: (key: string) => string | undefined; + cwd: string; + loadSync?( + cwd: string, + filename?: string, + baseUrl?: string + ): TsConfigLoaderResult; +} + +export function tsConfigLoader({ + getEnv, + cwd, + loadSync = loadSyncDefault, +}: TsConfigLoaderParams): TsConfigLoaderResult { + const TS_NODE_PROJECT = getEnv("TS_NODE_PROJECT"); + const TS_NODE_BASEURL = getEnv("TS_NODE_BASEURL"); + + // tsconfig.loadSync handles if TS_NODE_PROJECT is a file or directory + // and also overrides baseURL if TS_NODE_BASEURL is available. + const loadResult = loadSync(cwd, TS_NODE_PROJECT, TS_NODE_BASEURL); + return loadResult; +} + +function loadSyncDefault( + cwd: string, + filename?: string, + baseUrl?: string +): TsConfigLoaderResult { + // Tsconfig.loadSync uses path.resolve. This is why we can use an absolute path as filename + + const configPath = resolveConfigPath(cwd, filename); + + if (!configPath) { + return { + tsConfigPath: undefined, + baseUrl: undefined, + paths: undefined, + }; + } + const config = loadTsconfig(configPath); + + return { + tsConfigPath: configPath, + baseUrl: + baseUrl || + (config && config.compilerOptions && config.compilerOptions.baseUrl), + paths: config && config.compilerOptions && config.compilerOptions.paths, + }; +} + +function resolveConfigPath(cwd: string, filename?: string): string | undefined { + if (filename) { + const absolutePath = fs.lstatSync(filename).isDirectory() + ? path.resolve(filename, "./tsconfig.json") + : path.resolve(cwd, filename); + + return absolutePath; + } + + if (fs.statSync(cwd).isFile()) { + return path.resolve(cwd); + } + + const configAbsolutePath = walkForTsConfig(cwd); + return configAbsolutePath ? path.resolve(configAbsolutePath) : undefined; +} + +export function walkForTsConfig( + directory: string, + existsSync: (path: string) => boolean = fs.existsSync +): string | undefined { + const configPath = path.join(directory, "./tsconfig.json"); + if (existsSync(configPath)) { + return configPath; + } + + const parentDirectory = path.join(directory, "../"); + + // If we reached the top + if (directory === parentDirectory) { + return undefined; + } + + return walkForTsConfig(parentDirectory, existsSync); +} + +export function loadTsconfig( + configFilePath: string, + existsSync: (path: string) => boolean = fs.existsSync, + readFileSync: (filename: string) => string = (filename: string) => + fs.readFileSync(filename, "utf8") +): Tsconfig | undefined { + if (!existsSync(configFilePath)) { + return undefined; + } + + const configString = readFileSync(configFilePath); + const cleanedJson = StripBom(configString); + const config: Tsconfig = JSON5.parse(cleanedJson); + let extendedConfig = config.extends; + + if (extendedConfig) { + if ( + typeof extendedConfig === "string" && + extendedConfig.indexOf(".json") === -1 + ) { + extendedConfig += ".json"; + } + const currentDir = path.dirname(configFilePath); + let extendedConfigPath = path.join(currentDir, extendedConfig); + if ( + extendedConfig.indexOf("/") !== -1 && + extendedConfig.indexOf(".") !== -1 && + !existsSync(extendedConfigPath) + ) { + extendedConfigPath = path.join( + currentDir, + "node_modules", + extendedConfig + ); + } + + const base = + loadTsconfig(extendedConfigPath, existsSync, readFileSync) || {}; + + // baseUrl should be interpreted as relative to the base tsconfig, + // but we need to update it so it is relative to the original tsconfig being loaded + if (base.compilerOptions && base.compilerOptions.baseUrl) { + const extendsDir = path.dirname(extendedConfig); + base.compilerOptions.baseUrl = path.join( + extendsDir, + base.compilerOptions.baseUrl + ); + } + + return { + ...base, + ...config, + compilerOptions: { + ...base.compilerOptions, + ...config.compilerOptions, + }, + }; + } + return config; +} + +function StripBom(string: string) { + if (typeof string !== 'string') { + throw new TypeError(`Expected a string, got ${typeof string}`); + } + + // Catches EFBBBF (UTF-8 BOM) because the buffer-to-string + // conversion translates it to FEFF (UTF-16 BOM). + if (string.charCodeAt(0) === 0xFEFF) { + return string.slice(1); + } + + return string; +} diff --git a/packages/playwright-test/src/transform.ts b/packages/playwright-test/src/transform.ts index adde50a689..aa20f6e629 100644 --- a/packages/playwright-test/src/transform.ts +++ b/packages/playwright-test/src/transform.ts @@ -22,6 +22,7 @@ import * as pirates from 'pirates'; import * as sourceMapSupport from 'source-map-support'; import * as url from 'url'; import type { Location } from './types'; +import { TsConfigLoaderResult } from './third_party/tsconfig-loader'; const version = 4; const cacheDir = process.env.PWTEST_CACHE_DIR || path.join(os.tmpdir(), 'playwright-transform-cache'); @@ -52,7 +53,7 @@ function calculateCachePath(content: string, filePath: string): string { return path.join(cacheDir, hash[0] + hash[1], fileName); } -export function transformHook(code: string, filename: string, isModule = false): string { +export function transformHook(code: string, filename: string, tsconfig: TsConfigLoaderResult, isModule = false): string { const cachePath = calculateCachePath(code, filename); const codePath = cachePath + '.js'; const sourceMapPath = cachePath + '.map'; @@ -64,6 +65,21 @@ export function transformHook(code: string, filename: string, isModule = false): process.env.BROWSERSLIST_IGNORE_OLD_DATA = 'true'; const babel: typeof import('@babel/core') = require('@babel/core'); + const alias: { [key: string]: string | ((s: string[]) => string) } = {}; + for (const [key, values] of Object.entries(tsconfig.paths || {})) { + const regexKey = '^' + key.replace('*', '.*'); + alias[regexKey] = ([name]) => { + for (const value of values) { + const relative = (key.endsWith('/*') ? value.substring(0, value.length - 1) + name.substring(key.length - 1) : value) + .replace(/\//g, path.sep); + const result = path.resolve(tsconfig.baseUrl || '', relative); + if (fs.existsSync(result) || fs.existsSync(result + '.js') || fs.existsSync(result + '.ts')) + return result; + } + return name; + }; + } + const plugins = [ [require.resolve('@babel/plugin-proposal-class-properties')], [require.resolve('@babel/plugin-proposal-numeric-separator')], @@ -75,6 +91,10 @@ export function transformHook(code: string, filename: string, isModule = false): [require.resolve('@babel/plugin-syntax-async-generators')], [require.resolve('@babel/plugin-syntax-object-rest-spread')], [require.resolve('@babel/plugin-proposal-export-namespace-from')], + [require.resolve('babel-plugin-module-resolver'), { + root: ['./'], + alias + }], ]; if (!isModule) { plugins.push([require.resolve('@babel/plugin-transform-modules-commonjs')]); @@ -104,8 +124,8 @@ export function transformHook(code: string, filename: string, isModule = false): return result.code || ''; } -export function installTransform(): () => void { - return pirates.addHook(transformHook, { exts: ['.ts'] }); +export function installTransform(tsconfig: TsConfigLoaderResult): () => void { + return pirates.addHook((code: string, filename: string) => transformHook(code, filename, tsconfig), { exts: ['.ts'] }); } export function wrapFunctionWithLocation(func: (location: Location, ...args: A) => R): (...args: A) => R { diff --git a/tests/playwright-test/resolver.spec.ts b/tests/playwright-test/resolver.spec.ts new file mode 100644 index 0000000000..d3162ed5ba --- /dev/null +++ b/tests/playwright-test/resolver.spec.ts @@ -0,0 +1,147 @@ +/** + * 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. + */ + +import { test, expect } from './playwright-test-fixtures'; + +test('should respect path resolver', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright.config.ts': ` + export default { + projects: [{name: 'foo'}], + }; + `, + 'tsconfig.json': `{ + "compilerOptions": { + "target": "ES2019", + "module": "commonjs", + "lib": ["esnext", "dom", "DOM.Iterable"], + "baseUrl": ".", + "paths": { + "util/*": ["./foo/bar/util/*"], + "util2/*": ["./non-existent/*", "./foo/bar/util/*"], + "util3": ["./foo/bar/util/b"], + }, + }, + }`, + 'a.test.ts': ` + import { foo } from 'util/b'; + const { test } = pwt; + test('test', ({}, testInfo) => { + expect(testInfo.project.name).toBe(foo); + }); + `, + 'b.test.ts': ` + import { foo } from 'util2/b'; + const { test } = pwt; + test('test', ({}, testInfo) => { + expect(testInfo.project.name).toBe(foo); + }); + `, + 'c.test.ts': ` + import { foo } from 'util3'; + const { test } = pwt; + test('test', ({}, testInfo) => { + expect(testInfo.project.name).toBe(foo); + }); + `, + 'foo/bar/util/b.ts': ` + export const foo: string = 'foo'; + `, + }); + + expect(result.passed).toBe(3); + expect(result.exitCode).toBe(0); +}); + +test('should respect baseurl', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright.config.ts': ` + export default { + projects: [{name: 'foo'}], + }; + `, + 'tsconfig.json': `{ + "compilerOptions": { + "target": "ES2019", + "module": "commonjs", + "lib": ["esnext", "dom", "DOM.Iterable"], + "baseUrl": "./foo", + "paths": { + "util/*": ["./bar/util/*"], + "util2": ["./bar/util/b"], + }, + }, + }`, + 'a.test.ts': ` + import { foo } from 'util/b'; + const { test } = pwt; + test('test', ({}, testInfo) => { + expect(testInfo.project.name).toBe(foo); + }); + `, + 'b.test.ts': ` + import { foo } from 'util2'; + const { test } = pwt; + test('test', ({}, testInfo) => { + expect(testInfo.project.name).toBe(foo); + }); + `, + 'foo/bar/util/b.ts': ` + export const foo: string = 'foo'; + `, + }); + + expect(result.passed).toBe(2); + expect(result.exitCode).toBe(0); +}); + +test('should respect path resolver in experimental mode', async ({ runInlineTest }) => { + // We only support experimental esm mode on Node 16+ + test.skip(parseInt(process.version.slice(1), 10) < 16); + const result = await runInlineTest({ + 'package.json': JSON.stringify({ type: 'module' }), + 'playwright.config.ts': ` + export default { + projects: [{name: 'foo'}], + }; + `, + 'tsconfig.json': `{ + "compilerOptions": { + "target": "ES2019", + "module": "commonjs", + "lib": ["esnext", "dom", "DOM.Iterable"], + "baseUrl": ".", + "paths": { + "util/*": ["./foo/bar/util/*"], + }, + }, + }`, + 'a.test.ts': ` + import { foo } from 'util/b.ts'; + const { test } = pwt; + test('check project name', ({}, testInfo) => { + expect(testInfo.project.name).toBe(foo); + }); + `, + 'foo/bar/util/b.ts': ` + export const foo: string = 'foo'; + `, + }, {}, { + PW_EXPERIMENTAL_TS_ESM: true + }); + + expect(result.exitCode).toBe(0); +}); diff --git a/tests/playwright-test/stable-test-runner/package-lock.json b/tests/playwright-test/stable-test-runner/package-lock.json index a32f5de875..100dca21fc 100644 --- a/tests/playwright-test/stable-test-runner/package-lock.json +++ b/tests/playwright-test/stable-test-runner/package-lock.json @@ -5,7 +5,7 @@ "packages": { "": { "dependencies": { - "@playwright/test": "^1.17.0-next-1635886706000" + "@playwright/test": "=1.18.0-alpha-dec-9-2021" } }, "node_modules/@babel/code-frame": { @@ -677,9 +677,9 @@ } }, "node_modules/@playwright/test": { - "version": "1.17.0-next-1635886706000", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.17.0-next-1635886706000.tgz", - "integrity": "sha512-shbXRTr1af9YuekWz/OAJl/3h2j8uERqzWGjMCTGzA16juBfhhAuzfc+CgRPHbEdKDaKykFmjmeeror02OvjdQ==", + "version": "1.18.0-alpha-dec-9-2021", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.18.0-alpha-dec-9-2021.tgz", + "integrity": "sha512-aIMeVxEtzqJT8mJWnImI0vjc0hPQP3eIV5yQL6WguaA/i+TfH0Fwvsm9xDVCaC/Nt4dUMt3lSM7xhQ44RJ+DXQ==", "dependencies": { "@babel/code-frame": "^7.14.5", "@babel/core": "^7.14.8", @@ -704,16 +704,18 @@ "expect": "=27.2.5", "jest-matcher-utils": "=27.2.5", "jpeg-js": "^0.4.2", + "mime": "^2.4.6", "minimatch": "^3.0.3", "ms": "^2.1.2", "open": "^8.3.0", "pirates": "^4.0.1", "pixelmatch": "^5.2.1", - "playwright-core": "=1.17.0-next-1635886706000", + "playwright-core": "=1.18.0-alpha-dec-9-2021", "pngjs": "^5.0.0", "rimraf": "^3.0.2", "source-map-support": "^0.4.18", - "stack-utils": "^2.0.3" + "stack-utils": "^2.0.3", + "yazl": "^2.5.1" }, "bin": { "playwright": "cli.js" @@ -1688,9 +1690,9 @@ } }, "node_modules/playwright-core": { - "version": "1.17.0-next-1635886706000", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.17.0-next-1635886706000.tgz", - "integrity": "sha512-QenETXX7I7J+s5RYRtZIyW6pKKyfBYNf9XPq/exmv8jBPhhRD+0hy/NB1WwmWUt/nze8GV/ZS+2G5IN2PUXFVw==", + "version": "1.18.0-alpha-dec-9-2021", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.18.0-alpha-dec-9-2021.tgz", + "integrity": "sha512-ySI1nRgwvXzlh6alhU/ha3/NA1atBPFShSF/D/Vt6yaa7xJwFGye+/eXX+XKqhbp/zXjSImgXbgGNoYKJwuXtQ==", "dependencies": { "commander": "^8.2.0", "debug": "^4.1.1", @@ -1822,9 +1824,9 @@ } }, "node_modules/signal-exit": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.5.tgz", - "integrity": "sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ==" + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.6.tgz", + "integrity": "sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ==" }, "node_modules/slash": { "version": "3.0.0", @@ -1857,9 +1859,9 @@ } }, "node_modules/socks-proxy-agent": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.1.0.tgz", - "integrity": "sha512-57e7lwCN4Tzt3mXz25VxOErJKXlPfXmkMLnk310v/jwW20jWRVcgsOit+xNkN3eIEdB47GwnfAEBLacZ/wVIKg==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.1.1.tgz", + "integrity": "sha512-t8J0kG3csjA4g6FTbsMOWws+7R7vuRC8aQ/wy3/1OWmsgwA68zs/+cExQ0koSitUDXqhufF/YJr9wtNMZHw5Ew==", "dependencies": { "agent-base": "^6.0.2", "debug": "^4.3.1", @@ -1940,9 +1942,9 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "node_modules/ws": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz", - "integrity": "sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==", + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.6.tgz", + "integrity": "sha512-6GLgCqo2cy2A2rjCNFlxQS6ZljG/coZfZXclldI8FB/1G3CCI36Zd8xy2HrFVACi8tfk5XrgLQEk+P0Tnz9UcA==", "engines": { "node": ">=8.3.0" }, @@ -2499,9 +2501,9 @@ } }, "@playwright/test": { - "version": "1.17.0-next-1635886706000", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.17.0-next-1635886706000.tgz", - "integrity": "sha512-shbXRTr1af9YuekWz/OAJl/3h2j8uERqzWGjMCTGzA16juBfhhAuzfc+CgRPHbEdKDaKykFmjmeeror02OvjdQ==", + "version": "1.18.0-alpha-dec-9-2021", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.18.0-alpha-dec-9-2021.tgz", + "integrity": "sha512-aIMeVxEtzqJT8mJWnImI0vjc0hPQP3eIV5yQL6WguaA/i+TfH0Fwvsm9xDVCaC/Nt4dUMt3lSM7xhQ44RJ+DXQ==", "requires": { "@babel/code-frame": "^7.14.5", "@babel/core": "^7.14.8", @@ -2526,16 +2528,18 @@ "expect": "=27.2.5", "jest-matcher-utils": "=27.2.5", "jpeg-js": "^0.4.2", + "mime": "^2.4.6", "minimatch": "^3.0.3", "ms": "^2.1.2", "open": "^8.3.0", "pirates": "^4.0.1", "pixelmatch": "^5.2.1", - "playwright-core": "=1.17.0-next-1635886706000", + "playwright-core": "=1.18.0-alpha-dec-9-2021", "pngjs": "^5.0.0", "rimraf": "^3.0.2", "source-map-support": "^0.4.18", - "stack-utils": "^2.0.3" + "stack-utils": "^2.0.3", + "yazl": "^2.5.1" } }, "@types/istanbul-lib-coverage": { @@ -3268,9 +3272,9 @@ } }, "playwright-core": { - "version": "1.17.0-next-1635886706000", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.17.0-next-1635886706000.tgz", - "integrity": "sha512-QenETXX7I7J+s5RYRtZIyW6pKKyfBYNf9XPq/exmv8jBPhhRD+0hy/NB1WwmWUt/nze8GV/ZS+2G5IN2PUXFVw==", + "version": "1.18.0-alpha-dec-9-2021", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.18.0-alpha-dec-9-2021.tgz", + "integrity": "sha512-ySI1nRgwvXzlh6alhU/ha3/NA1atBPFShSF/D/Vt6yaa7xJwFGye+/eXX+XKqhbp/zXjSImgXbgGNoYKJwuXtQ==", "requires": { "commander": "^8.2.0", "debug": "^4.1.1", @@ -3371,9 +3375,9 @@ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" }, "signal-exit": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.5.tgz", - "integrity": "sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ==" + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.6.tgz", + "integrity": "sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ==" }, "slash": { "version": "3.0.0", @@ -3395,9 +3399,9 @@ } }, "socks-proxy-agent": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.1.0.tgz", - "integrity": "sha512-57e7lwCN4Tzt3mXz25VxOErJKXlPfXmkMLnk310v/jwW20jWRVcgsOit+xNkN3eIEdB47GwnfAEBLacZ/wVIKg==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.1.1.tgz", + "integrity": "sha512-t8J0kG3csjA4g6FTbsMOWws+7R7vuRC8aQ/wy3/1OWmsgwA68zs/+cExQ0koSitUDXqhufF/YJr9wtNMZHw5Ew==", "requires": { "agent-base": "^6.0.2", "debug": "^4.3.1", @@ -3459,9 +3463,9 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "ws": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz", - "integrity": "sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==", + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.6.tgz", + "integrity": "sha512-6GLgCqo2cy2A2rjCNFlxQS6ZljG/coZfZXclldI8FB/1G3CCI36Zd8xy2HrFVACi8tfk5XrgLQEk+P0Tnz9UcA==", "requires": {} }, "yauzl": { diff --git a/tests/playwright-test/stable-test-runner/package.json b/tests/playwright-test/stable-test-runner/package.json index f47ae4f6d6..f280a8fa64 100644 --- a/tests/playwright-test/stable-test-runner/package.json +++ b/tests/playwright-test/stable-test-runner/package.json @@ -1,6 +1,6 @@ { "private": true, "dependencies": { - "@playwright/test": "^1.17.0-next-1635886706000" + "@playwright/test": "=1.18.0-alpha-dec-9-2021" } }