diff --git a/.changeset/quick-dingos-deliver.md b/.changeset/quick-dingos-deliver.md new file mode 100644 index 00000000..b7becb0d --- /dev/null +++ b/.changeset/quick-dingos-deliver.md @@ -0,0 +1,5 @@ +--- +'pleasantest': minor +--- + +Make module server configurable diff --git a/.eslintignore b/.eslintignore index 1521c8b7..2080fdc2 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1,2 @@ dist +.cache diff --git a/.prettierignore b/.prettierignore index 1521c8b7..2080fdc2 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1 +1,2 @@ dist +.cache diff --git a/README.md b/README.md index 389eba77..0f16e9cb 100644 --- a/README.md +++ b/README.md @@ -345,10 +345,28 @@ Call Signatures: - `withBrowser.headed(testFn: (context: PleasantestContext) => Promise)` - `withBrowser.headed(opts: WithBrowserOpts, testFn: (context: PleasantestContext) => Promise)` -`WithBrowserOpts`: +`WithBrowserOpts` (all properties are optional): - `headless`: `boolean`, default `true`: Whether to open a headless (not visible) browser. If you use the `withBrowser.headed` chain, that will override the value of `headless`. - `device`: Device Object [described here](https://pptr.dev/#?product=Puppeteer&version=v10.1.0&show=api-pageemulateoptions). +- `moduleServer`: Module Server options object (all properties are optional). They will be applied to files imported through [`utils.runJS`](#pleasantestutilsrunjscode-string-promisevoid) or [`utils.loadJS`](#pleasantestutilsloadjsjspath-string-promisevoid). + - `plugins`: Array of Rollup, Vite, or WMR plugins to add. + - `envVars`: Object with string keys and string values for environment variables to pass in as `import.meta.env.*` / `process.env.*` + - `esbuild`: [`TransformOptions`](https://esbuild.github.io/api/#transform-api) | `false`: Options to pass to esbuild. Set to false to disable esbuild. + +You can configure the default options (applied to all tests in current file) by using the `configureDefaults` method. If you want defaults to apply to all files, Create a [test setup file](https://jestjs.io/docs/configuration#setupfilesafterenv-array) and call `configureDefaults` there: + +```js +import { configureDefaults } from 'pleasantest' + +configureDefaults({ + device: /* ... */, + moduleServer: { + /* ... */ + }, + /* ... */ +}) +``` By default, `withBrowser` will launch a headless Chromium browser. You can tell it to instead launch a headed (visible) browser by chaining `.headed`: diff --git a/package-lock.json b/package-lock.json index 86974c5b..0ea47582 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,6 +38,7 @@ "@types/node": "12.20.16", "@types/polka": "0.5.3", "@types/puppeteer": "5.4.4", + "@vue/compiler-sfc": "3.1.4", "ansi-regex": "6.0.0", "aria-query": "*", "babel-plugin-un-cjs": "2.5.0", @@ -61,10 +62,12 @@ "rollup-plugin-dts": "3.0.2", "rollup-plugin-prettier": "2.1.0", "rollup-plugin-terser": "7.0.2", + "rollup-plugin-vue": "6.0.0", "sass": "1.35.2", "simple-code-frame": "1.1.1", "smoldash": "0.9.0", - "typescript": "4.3.5" + "typescript": "4.3.5", + "vue": "3.1.4" }, "engines": { "node": "^12.2 || 14 || 16" @@ -4740,6 +4743,154 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@vue/compiler-core": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.1.4.tgz", + "integrity": "sha512-TnUz+1z0y74O/A4YKAbzsdUfamyHV73MihrEfvettWpm9bQKVoZd1nEmR1cGN9LsXWlwAvVQBetBlWdOjmQO5Q==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.12.0", + "@babel/types": "^7.12.0", + "@vue/shared": "3.1.4", + "estree-walker": "^2.0.1", + "source-map": "^0.6.1" + } + }, + "node_modules/@vue/compiler-core/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, + "node_modules/@vue/compiler-core/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.1.4.tgz", + "integrity": "sha512-3tG2ScHkghhUBuFwl9KgyZhrS8CPFZsO7hUDekJgIp5b1OMkROr4AvxHu6rRMl4WkyvYkvidFNBS2VfOnwa6Kw==", + "dev": true, + "dependencies": { + "@vue/compiler-core": "3.1.4", + "@vue/shared": "3.1.4" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.1.4.tgz", + "integrity": "sha512-4KDQg60Khy3SgnF+V/TB2NZqzmM4TyGRmzsxqG1SebGdMSecCweFDSlI/F1vDYk6dKiCHgmpoT9A1sLxswkJ0A==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.13.9", + "@babel/types": "^7.13.0", + "@types/estree": "^0.0.48", + "@vue/compiler-core": "3.1.4", + "@vue/compiler-dom": "3.1.4", + "@vue/compiler-ssr": "3.1.4", + "@vue/shared": "3.1.4", + "consolidate": "^0.16.0", + "estree-walker": "^2.0.1", + "hash-sum": "^2.0.0", + "lru-cache": "^5.1.1", + "magic-string": "^0.25.7", + "merge-source-map": "^1.1.0", + "postcss": "^8.1.10", + "postcss-modules": "^4.0.0", + "postcss-selector-parser": "^6.0.4", + "source-map": "^0.6.1" + }, + "peerDependencies": { + "vue": "3.1.4" + } + }, + "node_modules/@vue/compiler-sfc/node_modules/@types/estree": { + "version": "0.0.48", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.48.tgz", + "integrity": "sha512-LfZwXoGUDo0C3me81HXgkBg5CTQYb6xzEl+fNmbO4JdRiSKQ8A0GD1OBBvKAIsbCUgoyAty7m99GqqMQe784ew==", + "dev": true + }, + "node_modules/@vue/compiler-sfc/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, + "node_modules/@vue/compiler-sfc/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@vue/compiler-sfc/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@vue/compiler-sfc/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.1.4.tgz", + "integrity": "sha512-Box8fCuCFPp0FuimIswjDkjwiSDCBkHvt/xVALyFkYCiIMWv2eR53fIjmlsnEHhcBuZ+VgRC+UanCTcKvSA1gA==", + "dev": true, + "dependencies": { + "@vue/compiler-dom": "3.1.4", + "@vue/shared": "3.1.4" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.1.4.tgz", + "integrity": "sha512-YDlgii2Cr9yAoKVZFzgY4j0mYlVT73986X3e5SPp6ifqckSEoFSUWXZK2Tb53TB/9qO29BEEbspnKD3m3wAwkA==", + "dev": true, + "dependencies": { + "@vue/shared": "3.1.4" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.1.4.tgz", + "integrity": "sha512-qmVJgJuFxfT7M4qHQ4M6KqhKC66fjuswK+aBivE8dWiZ2rtIGl9gtJGpwqwjQEcKEBTOfvvrtrwBncYArJUO8Q==", + "dev": true, + "dependencies": { + "@vue/reactivity": "3.1.4", + "@vue/shared": "3.1.4" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.1.4.tgz", + "integrity": "sha512-vbmwgTxku1BU87Kw7r29adv0OIrDXCW0PslOPQT0O/9R5SqcXgS94Yj6zsztDjvghegenwIAPNLlDR1Auh5s+w==", + "dev": true, + "dependencies": { + "@vue/runtime-core": "3.1.4", + "@vue/shared": "3.1.4", + "csstype": "^2.6.8" + } + }, + "node_modules/@vue/shared": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.1.4.tgz", + "integrity": "sha512-6O45kZAmkLvzGLToBxEz4lR2W6kXohCtebV2UxjH9GXjd8X9AhEn68FN9eNanFtWNzvgw1hqd6HkPRVQalqf7Q==", + "dev": true + }, "node_modules/abab": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", @@ -5422,6 +5573,12 @@ "readable-stream": "^3.4.0" } }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true + }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -6318,6 +6475,18 @@ "node": ">=0.10.0" } }, + "node_modules/consolidate": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/consolidate/-/consolidate-0.16.0.tgz", + "integrity": "sha512-Nhl1wzCslqXYTJVDyJCu3ODohy9OfBMB5uD2BiBTzd7w+QY0lBzafkR8y8755yMYHAaMD4NuzbAw03/xzfw+eQ==", + "dev": true, + "dependencies": { + "bluebird": "^3.7.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, "node_modules/contains-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-1.0.0.tgz", @@ -6828,6 +6997,12 @@ "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", "dev": true }, + "node_modules/csstype": { + "version": "2.6.17", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.17.tgz", + "integrity": "sha512-u1wmTI1jJGzCJzWndZo8mk4wnPTZd1eOIYTYvuEyOQGfmDl3TrabCCfKnOC86FZwW/9djqTl933UF/cS425i9A==", + "dev": true + }, "node_modules/csv": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/csv/-/csv-5.5.0.tgz", @@ -9219,6 +9394,12 @@ "node": ">=0.10.0" } }, + "node_modules/hash-sum": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-2.0.0.tgz", + "integrity": "sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==", + "dev": true + }, "node_modules/hex-color-regex": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz", @@ -14950,6 +15131,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/merge-source-map": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.1.0.tgz", + "integrity": "sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==", + "dev": true, + "dependencies": { + "source-map": "^0.6.1" + } + }, + "node_modules/merge-source-map/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -18769,6 +18968,20 @@ "rollup": "^2.0.0" } }, + "node_modules/rollup-plugin-vue": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-vue/-/rollup-plugin-vue-6.0.0.tgz", + "integrity": "sha512-oVvUd84d5u73M2HYM3XsMDLtZRIA/tw2U0dmHlXU2UWP5JARYHzh/U9vcxaN/x/9MrepY7VH3pHFeOhrWpxs/Q==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "hash-sum": "^2.0.0", + "rollup-pluginutils": "^2.8.2" + }, + "peerDependencies": { + "@vue/compiler-sfc": "*" + } + }, "node_modules/rollup-pluginutils": { "version": "2.8.2", "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", @@ -18844,6 +19057,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/sane/-/sane-4.1.0.tgz", "integrity": "sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA==", + "deprecated": "some dependency vulnerabilities fixed, support for node < 10 dropped, and newer ECMAScript syntax/features added", "dev": true, "dependencies": { "@cnakazawa/watch": "^1.0.3", @@ -21457,6 +21671,17 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/vue": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.1.4.tgz", + "integrity": "sha512-p8dcdyeCgmaAiZsbLyDkmOLcFGZb/jEVdCLW65V68LRCXTNX8jKsgah2F7OZ/v/Ai2V0Fb1MNO0vz/GFqsPVMA==", + "dev": true, + "dependencies": { + "@vue/compiler-dom": "3.1.4", + "@vue/runtime-dom": "3.1.4", + "@vue/shared": "3.1.4" + } + }, "node_modules/w3c-hr-time": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", @@ -25428,6 +25653,149 @@ "eslint-visitor-keys": "^2.0.0" } }, + "@vue/compiler-core": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.1.4.tgz", + "integrity": "sha512-TnUz+1z0y74O/A4YKAbzsdUfamyHV73MihrEfvettWpm9bQKVoZd1nEmR1cGN9LsXWlwAvVQBetBlWdOjmQO5Q==", + "dev": true, + "requires": { + "@babel/parser": "^7.12.0", + "@babel/types": "^7.12.0", + "@vue/shared": "3.1.4", + "estree-walker": "^2.0.1", + "source-map": "^0.6.1" + }, + "dependencies": { + "estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "@vue/compiler-dom": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.1.4.tgz", + "integrity": "sha512-3tG2ScHkghhUBuFwl9KgyZhrS8CPFZsO7hUDekJgIp5b1OMkROr4AvxHu6rRMl4WkyvYkvidFNBS2VfOnwa6Kw==", + "dev": true, + "requires": { + "@vue/compiler-core": "3.1.4", + "@vue/shared": "3.1.4" + } + }, + "@vue/compiler-sfc": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.1.4.tgz", + "integrity": "sha512-4KDQg60Khy3SgnF+V/TB2NZqzmM4TyGRmzsxqG1SebGdMSecCweFDSlI/F1vDYk6dKiCHgmpoT9A1sLxswkJ0A==", + "dev": true, + "requires": { + "@babel/parser": "^7.13.9", + "@babel/types": "^7.13.0", + "@types/estree": "^0.0.48", + "@vue/compiler-core": "3.1.4", + "@vue/compiler-dom": "3.1.4", + "@vue/compiler-ssr": "3.1.4", + "@vue/shared": "3.1.4", + "consolidate": "^0.16.0", + "estree-walker": "^2.0.1", + "hash-sum": "^2.0.0", + "lru-cache": "^5.1.1", + "magic-string": "^0.25.7", + "merge-source-map": "^1.1.0", + "postcss": "^8.1.10", + "postcss-modules": "^4.0.0", + "postcss-selector-parser": "^6.0.4", + "source-map": "^0.6.1" + }, + "dependencies": { + "@types/estree": { + "version": "0.0.48", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.48.tgz", + "integrity": "sha512-LfZwXoGUDo0C3me81HXgkBg5CTQYb6xzEl+fNmbO4JdRiSKQ8A0GD1OBBvKAIsbCUgoyAty7m99GqqMQe784ew==", + "dev": true + }, + "estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + } + } + }, + "@vue/compiler-ssr": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.1.4.tgz", + "integrity": "sha512-Box8fCuCFPp0FuimIswjDkjwiSDCBkHvt/xVALyFkYCiIMWv2eR53fIjmlsnEHhcBuZ+VgRC+UanCTcKvSA1gA==", + "dev": true, + "requires": { + "@vue/compiler-dom": "3.1.4", + "@vue/shared": "3.1.4" + } + }, + "@vue/reactivity": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.1.4.tgz", + "integrity": "sha512-YDlgii2Cr9yAoKVZFzgY4j0mYlVT73986X3e5SPp6ifqckSEoFSUWXZK2Tb53TB/9qO29BEEbspnKD3m3wAwkA==", + "dev": true, + "requires": { + "@vue/shared": "3.1.4" + } + }, + "@vue/runtime-core": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.1.4.tgz", + "integrity": "sha512-qmVJgJuFxfT7M4qHQ4M6KqhKC66fjuswK+aBivE8dWiZ2rtIGl9gtJGpwqwjQEcKEBTOfvvrtrwBncYArJUO8Q==", + "dev": true, + "requires": { + "@vue/reactivity": "3.1.4", + "@vue/shared": "3.1.4" + } + }, + "@vue/runtime-dom": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.1.4.tgz", + "integrity": "sha512-vbmwgTxku1BU87Kw7r29adv0OIrDXCW0PslOPQT0O/9R5SqcXgS94Yj6zsztDjvghegenwIAPNLlDR1Auh5s+w==", + "dev": true, + "requires": { + "@vue/runtime-core": "3.1.4", + "@vue/shared": "3.1.4", + "csstype": "^2.6.8" + } + }, + "@vue/shared": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.1.4.tgz", + "integrity": "sha512-6O45kZAmkLvzGLToBxEz4lR2W6kXohCtebV2UxjH9GXjd8X9AhEn68FN9eNanFtWNzvgw1hqd6HkPRVQalqf7Q==", + "dev": true + }, "abab": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", @@ -25927,6 +26295,12 @@ "readable-stream": "^3.4.0" } }, + "bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true + }, "boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -26641,6 +27015,15 @@ } } }, + "consolidate": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/consolidate/-/consolidate-0.16.0.tgz", + "integrity": "sha512-Nhl1wzCslqXYTJVDyJCu3ODohy9OfBMB5uD2BiBTzd7w+QY0lBzafkR8y8755yMYHAaMD4NuzbAw03/xzfw+eQ==", + "dev": true, + "requires": { + "bluebird": "^3.7.2" + } + }, "contains-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-1.0.0.tgz", @@ -27032,6 +27415,12 @@ } } }, + "csstype": { + "version": "2.6.17", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.17.tgz", + "integrity": "sha512-u1wmTI1jJGzCJzWndZo8mk4wnPTZd1eOIYTYvuEyOQGfmDl3TrabCCfKnOC86FZwW/9djqTl933UF/cS425i9A==", + "dev": true + }, "csv": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/csv/-/csv-5.5.0.tgz", @@ -28880,6 +29269,12 @@ } } }, + "hash-sum": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-2.0.0.tgz", + "integrity": "sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==", + "dev": true + }, "hex-color-regex": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz", @@ -33238,6 +33633,23 @@ } } }, + "merge-source-map": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.1.0.tgz", + "integrity": "sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==", + "dev": true, + "requires": { + "source-map": "^0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, "merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -36044,6 +36456,17 @@ "terser": "^5.0.0" } }, + "rollup-plugin-vue": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-vue/-/rollup-plugin-vue-6.0.0.tgz", + "integrity": "sha512-oVvUd84d5u73M2HYM3XsMDLtZRIA/tw2U0dmHlXU2UWP5JARYHzh/U9vcxaN/x/9MrepY7VH3pHFeOhrWpxs/Q==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "hash-sum": "^2.0.0", + "rollup-pluginutils": "^2.8.2" + } + }, "rollup-pluginutils": { "version": "2.8.2", "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", @@ -38196,6 +38619,17 @@ "integrity": "sha512-lXhElVO0Rq3frgPvFBwahmed3X03vjPF8OcjKMy8+F1xU/3Q3QU3tKEDp743SFtb74PdF0UWpxPvtOP0GCLheA==", "dev": true }, + "vue": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.1.4.tgz", + "integrity": "sha512-p8dcdyeCgmaAiZsbLyDkmOLcFGZb/jEVdCLW65V68LRCXTNX8jKsgah2F7OZ/v/Ai2V0Fb1MNO0vz/GFqsPVMA==", + "dev": true, + "requires": { + "@vue/compiler-dom": "3.1.4", + "@vue/runtime-dom": "3.1.4", + "@vue/shared": "3.1.4" + } + }, "w3c-hr-time": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", diff --git a/package.json b/package.json index 0cb0f6da..97adc2a5 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "@types/node": "12.20.16", "@types/polka": "0.5.3", "@types/puppeteer": "5.4.4", + "@vue/compiler-sfc": "3.1.4", "ansi-regex": "6.0.0", "aria-query": "*", "babel-plugin-un-cjs": "2.5.0", @@ -48,10 +49,12 @@ "rollup-plugin-dts": "3.0.2", "rollup-plugin-prettier": "2.1.0", "rollup-plugin-terser": "7.0.2", + "rollup-plugin-vue": "6.0.0", "sass": "1.35.2", "simple-code-frame": "1.1.1", "smoldash": "0.9.0", - "typescript": "4.3.5" + "typescript": "4.3.5", + "vue": "3.1.4" }, "dependencies": { "@rollup/plugin-commonjs": "^19.0.1", diff --git a/rollup.config.js b/rollup.config.js index 42e336b3..891e8655 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -7,6 +7,19 @@ import babel from '@rollup/plugin-babel'; import nodeResolve from '@rollup/plugin-node-resolve'; const extensions = ['.js', '.jsx', '.es6', '.es', '.mjs', '.ts', '.tsx']; +const external = [ + 'puppeteer', + 'source-map', + 'acorn', + 'es-module-lexer', + 'cjs-module-lexer', + 'rollup', + '@rollup/plugin-commonjs', + 'esbuild', + /postcss/, + /mime/, +]; + /** @type {import('rollup').RollupOptions} */ const mainConfig = { input: ['src/index.ts'], @@ -34,25 +47,14 @@ const mainConfig = { nodeResolve({ extensions }), bundlePlugin(), ], - external: [ - 'puppeteer', - 'source-map', - 'acorn', - 'es-module-lexer', - 'cjs-module-lexer', - 'rollup', - '@rollup/plugin-commonjs', - 'esbuild', - /postcss/, - /mime/, - ], + external, }; /** @type {import('rollup').RollupOptions} */ const typesConfig = { input: 'src/index.ts', output: [{ file: 'dist/index.d.ts', format: 'es' }], - external: ['puppeteer', 'pretty-format'], + external: [...external, 'polka'], plugins: [dts({ respectExternal: true })], }; diff --git a/src/index.ts b/src/index.ts index cb0e24de..3cf3b051 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ import * as puppeteer from 'puppeteer'; -import * as path from 'path'; +import { relative, join, isAbsolute, dirname, posix, sep, resolve } from 'path'; import type { BoundQueries } from './pptr-testing-library'; import { getQueriesForElement } from './pptr-testing-library'; import { connectToBrowser } from './connect-to-browser'; @@ -16,6 +16,7 @@ import { printStackLine, removeFuncFromStackTrace, } from './utils'; +import type { ModuleServerOpts } from './module-server'; import { createModuleServer } from './module-server'; import { cleanupClientRuntimeServer } from './module-server/client-runtime-server'; import { Console } from 'console'; @@ -63,6 +64,7 @@ export interface PleasantestContext { export interface WithBrowserOpts { headless?: boolean; device?: puppeteer.devices.Device; + moduleServer?: ModuleServerOpts; } interface TestFn { @@ -106,7 +108,7 @@ export const withBrowser: WithBrowser = (...args: any[]) => { return true; }); - const testPath = testFile ? path.relative(process.cwd(), testFile) : thisFile; + const testPath = testFile ? relative(process.cwd(), testFile) : thisFile; return async () => { const { state, cleanupServer, ...ctx } = await createTab({ @@ -198,7 +200,7 @@ const indent = (input: string, indentFirstLine = true) => // If there is an escape code at the beginning of the line // put the tab after the escape code // the reason for this is to prevent the indentation from getting messed up from wrapping - // you can see this if you squish the devools window + // you can see this if you squish the devtools window const match = ansiRegex.exec(line); if (!match || match.index !== 0) return ` ${line}`; const insertPoint = match[0].length; @@ -208,7 +210,11 @@ const indent = (input: string, indentFirstLine = true) => const createTab = async ({ testPath, - options: { headless = true, device }, + options: { + headless = defaultOptions.headless ?? true, + device = defaultOptions.device, + moduleServer: moduleServerOpts = {}, + }, }: { testPath: string; options: WithBrowserOpts; @@ -223,7 +229,14 @@ const createTab = async ({ const browser = await connectToBrowser('chromium', headless); const browserContext = await browser.createIncognitoBrowserContext(); const page = await browserContext.newPage(); - const { requestCache, port, close: closeServer } = await createModuleServer(); + const { + requestCache, + port, + close: closeServer, + } = await createModuleServer({ + ...defaultOptions.moduleServer, + ...moduleServerOpts, + }); if (device) { if (!headless) { @@ -327,18 +340,18 @@ const createTab = async ({ if (!fileName.startsWith(`http://localhost:${port}`)) return stackItem.raw; const url = new URL(fileName); - const id = `.${url.pathname}`; + const osPath = url.pathname.slice(1).split(posix.sep).join(sep); + // Absolute file path + const file = resolve(process.cwd(), osPath); + // Rollup-style Unix-normalized path "id": + const id = file.split(sep).join(posix.sep); const transformResult = requestCache.get(id); const map = typeof transformResult === 'object' && transformResult.map; if (!map) { let p = url.pathname; const npmPrefix = '/@npm/'; if (p.startsWith(npmPrefix)) - p = path.join( - process.cwd(), - 'node_modules', - p.slice(npmPrefix.length), - ); + p = join(process.cwd(), 'node_modules', p.slice(npmPrefix.length)); return printStackLine(p, line, column, stackItem.name); } @@ -352,8 +365,7 @@ const createTab = async ({ const mappedLine = sourceLocation.line; const mappedPath = sourceLocation.source || url.pathname; return printStackLine( - // For the code frames, Jest will only recognize absolute paths - path.join(process.cwd(), mappedPath), + mappedPath, mappedLine, mappedColumn, stackItem.name, @@ -417,9 +429,9 @@ const createTab = async ({ }; const loadCSS: PleasantestUtils['loadCSS'] = async (cssPath) => { - const fullPath = path.isAbsolute(cssPath) - ? path.relative(process.cwd(), cssPath) - : path.join(path.dirname(testPath), cssPath); + const fullPath = isAbsolute(cssPath) + ? relative(process.cwd(), cssPath) + : join(dirname(testPath), cssPath); await safeEvaluate( loadCSS, `import(${JSON.stringify( @@ -430,7 +442,7 @@ const createTab = async ({ const loadJS: PleasantestUtils['loadJS'] = async (jsPath) => { const fullPath = jsPath.startsWith('.') - ? path.join(path.dirname(testPath), jsPath) + ? join(dirname(testPath), jsPath) : jsPath; await safeEvaluate( loadJS, @@ -467,6 +479,12 @@ const createTab = async ({ }; }; +let defaultOptions: WithBrowserOpts = {}; + +export const configureDefaults = (options: WithBrowserOpts) => { + defaultOptions = options; +}; + export const devices = puppeteer.devices; afterAll(async () => { diff --git a/src/module-server/bundle-npm-module.ts b/src/module-server/bundle-npm-module.ts index efd738d4..45fc872d 100644 --- a/src/module-server/bundle-npm-module.ts +++ b/src/module-server/bundle-npm-module.ts @@ -2,7 +2,7 @@ import type { Plugin, RollupCache } from 'rollup'; import { rollup } from 'rollup'; import { promises as fs } from 'fs'; import commonjs from '@rollup/plugin-commonjs'; -import { processGlobalPlugin } from './plugins/process-global-plugin'; +import { environmentVariablesPlugin } from './plugins/environment-variables-plugin'; import * as esbuild from 'esbuild'; import { parse } from 'cjs-module-lexer'; // @ts-expect-error @types/node@12 doesn't like this import @@ -26,6 +26,7 @@ export const bundleNpmModule = async ( mod: string, id: string, optimize: boolean, + envVars: Record, ) => { let namedExports: string[] = []; if (dynamicCJSModules.has(id)) { @@ -77,7 +78,7 @@ export { default } from '${mod}'`; }, } as Plugin), pluginNodeResolve(), - processGlobalPlugin({ NODE_ENV: 'development' }), + environmentVariablesPlugin(envVars), commonjs({ extensions: ['.js', '.cjs', ''], sourceMap: false, diff --git a/src/module-server/index.ts b/src/module-server/index.ts index ddccf5d9..27bbc945 100644 --- a/src/module-server/index.ts +++ b/src/module-server/index.ts @@ -1,37 +1,66 @@ import type polka from 'polka'; -import type { Plugin, SourceDescription } from 'rollup'; +import type { SourceDescription } from 'rollup'; +import type { Plugin } from './plugin'; import { indexHTMLMiddleware } from './middleware/index-html'; import { jsMiddleware } from './middleware/js'; import { npmPlugin } from './plugins/npm-plugin'; -import { processGlobalPlugin } from './plugins/process-global-plugin'; +import { environmentVariablesPlugin } from './plugins/environment-variables-plugin'; import { resolveExtensionsPlugin } from './plugins/resolve-extensions-plugin'; import { createServer } from './server'; -import type { RollupAliasOptions } from '@rollup/plugin-alias'; -import aliasPlugin from '@rollup/plugin-alias'; import { esbuildPlugin } from './plugins/esbuild-plugin'; import { cssPlugin } from './plugins/css'; import { cssMiddleware } from './middleware/css'; import { staticMiddleware } from './middleware/static'; +import type * as esbuild from 'esbuild'; -interface ModuleServerOpts { +export interface ModuleServerOpts { root?: string; - aliases?: RollupAliasOptions['entries']; + /** List of Rollup/Vite/WMR plugins to add */ plugins?: (Plugin | false | undefined)[]; + /** + * Environment variables to pass into the bundle. + * They can be accessed via import.meta.env. (or process.env. for compatability) + */ + envVars?: Record; + /** Options to pass to esbuild. Set to false to disable esbuild */ + esbuild?: esbuild.TransformOptions | false; } export const createModuleServer = async ({ root = process.cwd(), - aliases, plugins: userPlugins = [], + envVars: _envVars = {}, + esbuild: esbuildOptions = {}, }: ModuleServerOpts = {}) => { - const plugins = [ - ...userPlugins, - aliases && aliasPlugin({ entries: aliases }), + const prePlugins: Plugin[] = []; + const normalPlugins: Plugin[] = []; + const postPlugins: Plugin[] = []; + + const envVars = { + NODE_ENV: 'development', + ..._envVars, + }; + + for (const plugin of userPlugins) { + if (!plugin) continue; + if (plugin.enforce === 'pre') prePlugins.push(plugin); + else if (plugin.enforce === 'post') postPlugins.push(plugin); + else normalPlugins.push(plugin); + } + + const plugins: (Plugin | false | undefined)[] = [ + ...prePlugins, + + ...normalPlugins, + resolveExtensionsPlugin(), - processGlobalPlugin({ NODE_ENV: 'development' }), - npmPlugin({ root }), - esbuildPlugin(), - cssPlugin(), + environmentVariablesPlugin(envVars), + npmPlugin({ root, envVars }), + + esbuildOptions && esbuildPlugin(esbuildOptions), + cssPlugin({ root }), + + ...postPlugins, ]; const filteredPlugins = plugins.filter(Boolean) as Plugin[]; const requestCache = new Map(); diff --git a/src/module-server/middleware/css.ts b/src/module-server/middleware/css.ts index aca4c114..36d5cdaa 100644 --- a/src/module-server/middleware/css.ts +++ b/src/module-server/middleware/css.ts @@ -19,7 +19,7 @@ interface CSSMiddlewareOpts { export const cssMiddleware = ({ root, }: CSSMiddlewareOpts): polka.Middleware => { - const cssPlug = cssPlugin({ returnCSS: true }); + const cssPlug = cssPlugin({ root, returnCSS: true }); return async (req, res, next) => { try { diff --git a/src/module-server/middleware/js.ts b/src/module-server/middleware/js.ts index edb06461..c8ff2ba6 100644 --- a/src/module-server/middleware/js.ts +++ b/src/module-server/middleware/js.ts @@ -1,6 +1,7 @@ -import { dirname, isAbsolute, posix, relative, resolve, sep } from 'path'; +import { dirname, posix, relative, resolve, sep } from 'path'; import type polka from 'polka'; -import type { Plugin, SourceDescription } from 'rollup'; +import type { SourceDescription } from 'rollup'; +import type { Plugin } from '../plugin'; import { createPluginContainer } from '../rollup-plugin-container'; import { promises as fs } from 'fs'; import { transformImports } from '../transform-imports'; @@ -10,6 +11,11 @@ import type { } from '@ampproject/remapping/dist/types/types'; import MagicString from 'magic-string'; import { jsExts } from '../extensions-and-detection'; +import { encode } from 'querystring'; +import * as esbuild from 'esbuild'; +import { createCodeFrame } from 'simple-code-frame'; +import * as colors from 'kolorist'; +import { Console } from 'console'; interface JSMiddlewareOpts { root: string; @@ -35,27 +41,33 @@ export const jsMiddleware = ({ let id: string; let file: string; if (path.startsWith('/@npm/')) { - id = path.slice(1); + id = path.slice(1); // Remove leading slash file = ''; // This should never be read } else { // Remove leading slash, and convert slashes to os-specific slashes const osPath = path.slice(1).split(posix.sep).join(sep); // Absolute file path file = resolve(root, osPath); - // Rollup-style CWD-relative Unix-normalized path "id": - id = `./${relative(root, file) - .replace(/^\.\//, '') - .replace(/^\0/, '') - .split(sep) - .join(posix.sep)}`; + // Rollup-style Unix-normalized path "id": + id = file.split(sep).join(posix.sep); + const qs = encode( + Object.fromEntries( + Object.entries(req.query).filter( + ([key]) => key !== 'import' && key !== 'inline-code', + ), + ) as any, + ) + // Remove trailing = + // This is necessary for rollup-plugin-vue, which ads ?lang.ts at the end of the id, + // so the file gets processed by other transformers + .replace(/=$/, ''); + if (qs) id += `?${qs}`; } res.setHeader('Content-Type', 'application/javascript;charset=utf-8'); const resolved = await rollupPlugins.resolveId(id); - const resolvedId = ( - typeof resolved === 'object' ? resolved?.id : resolved - ) as string; - let code: string | undefined; + const resolvedId = typeof resolved === 'object' ? resolved?.id : resolved; + let code: string | false | undefined; let map: DecodedSourceMap | RawSourceMap | string | undefined; if (typeof req.query['inline-code'] === 'string') { code = req.query['inline-code']; @@ -85,13 +97,12 @@ export const jsMiddleware = ({ // and none of the rollup plugins provided a load hook for it // and it doesn't have the ?import param (added for non-JS assets that can be imported into JS, like css) // Then treat it as a static asset - if (!jsExts.test(resolvedId) && req.query.import === undefined) + if ( + !jsExts.test(resolvedId || req.path) && + req.query.import === undefined + ) return next(); - // Always use the resolved id as the basis for our file - let file = resolvedId; - file = file.split(posix.sep).join(sep); - if (!isAbsolute(file)) file = resolve(root, file); code = await fs.readFile(file, 'utf-8'); } @@ -112,6 +123,7 @@ export const jsMiddleware = ({ spec = typeof resolved === 'object' ? resolved.id : resolved; if (spec.startsWith('@npm/')) return `/${spec}`; if (/^(\/|\\|[a-z]:\\)/i.test(spec)) { + // Change FS-absolute paths to relative spec = relative(dirname(file), spec).split(sep).join(posix.sep); if (!/^\.?\.?\//.test(spec)) spec = `./${spec}`; } @@ -128,7 +140,11 @@ export const jsMiddleware = ({ // If it wasn't resovled, and doesn't have a js-like extension // add the ?import query param so it is clear // that the request needs to end up as JS that can be imported - if (!jsExts.test(spec)) return `${spec}?import`; + if (!jsExts.test(spec)) { + // If there is already a query parameter, add &import + const delimiter = /\?/.test(spec) ? '&' : '?'; + return `${spec}${delimiter}import`; + } return spec; }, @@ -140,6 +156,28 @@ export const jsMiddleware = ({ 'Content-Length': Buffer.byteLength(code, 'utf-8'), }); res.end(code); + + // Start a esbuild build (just for the sake of parsing) + // That way, if there is a parsing error in the code resulting from the rollup transforms, + // we can display an error/code frame in the console + // instead of just a generic message from the browser saying it couldn't parse + // We are *not awaiting* this because we don't want to slow down sending the HTTP response + esbuild.transform(code, { loader: 'js' }).catch((error) => { + const err = error.errors[0]; + const { line, column } = err.location; + const frame = createCodeFrame(code as string, line - 1, column); + const message = `${colors.red(colors.bold(err.text))} + +${colors.red(`${id}:${line}:${(column as number) + 1}`)} + +${frame} +`; + + // Create a new console instance instead of using the global one + // Because the global one is overridden by Jest, and it adds a misleading second stack trace and code frame below it + const console = new Console(process.stdout, process.stderr); + console.error(message); + }); } catch (error) { next(error); } diff --git a/src/module-server/plugin.ts b/src/module-server/plugin.ts new file mode 100644 index 00000000..edf32d66 --- /dev/null +++ b/src/module-server/plugin.ts @@ -0,0 +1,5 @@ +import type { Plugin as RollupPlugin } from 'rollup'; + +export interface Plugin extends RollupPlugin { + enforce?: 'pre' | 'normal' | 'post'; +} diff --git a/src/module-server/plugins/css.ts b/src/module-server/plugins/css.ts index 5845eb3f..042e20b9 100644 --- a/src/module-server/plugins/css.ts +++ b/src/module-server/plugins/css.ts @@ -1,11 +1,15 @@ import postcssPlugin from 'rollup-plugin-postcss'; import { transformCssImports } from '../transform-css-imports'; -import { join } from 'path'; +import { posix, relative, resolve, sep } from 'path'; import { cssExts } from '../extensions-and-detection'; export const cssPlugin = ({ returnCSS = false, -}: { returnCSS?: boolean } = {}) => { + root, +}: { + returnCSS?: boolean; + root: string; +}) => { const transformedCSS = new Map(); const plugin = postcssPlugin({ inject: (cssVariable) => { @@ -28,9 +32,13 @@ export const cssPlugin = ({ test: cssExts, async process({ code, map }: { code: string; map?: string }) { code = await transformCssImports(code, this.id, { - resolveId(specifier, id) { - if (!specifier.startsWith('./')) return specifier; - return join(id, '..', specifier); + resolveId: (spec, id) => { + if (!spec.startsWith('./')) + return spec.split(sep).join(posix.sep); + const absolutePath = resolve(id, '..', spec); + return `./${relative(root, absolutePath) + .split(sep) + .join(posix.sep)}`; }, }); if (returnCSS) { diff --git a/src/module-server/plugins/environment-variables-plugin.ts b/src/module-server/plugins/environment-variables-plugin.ts new file mode 100644 index 00000000..80718b72 --- /dev/null +++ b/src/module-server/plugins/environment-variables-plugin.ts @@ -0,0 +1,22 @@ +import type { Plugin } from '../plugin'; + +/** + * Passes environment variables to pass into the bundle. They can be accessed via import.meta.env. (or process.env. for compatability) + */ +export const environmentVariablesPlugin = ( + env: Record, +): Plugin => { + return { + name: 'process-global', + transform(code) { + for (const [property, value] of Object.entries(env)) { + code = code.replace( + new RegExp(`(?:import\\.meta|process)\\.env\\.${property}`, 'g'), + JSON.stringify(value), + ); + } + + return code; + }, + }; +}; diff --git a/src/module-server/plugins/esbuild-plugin.ts b/src/module-server/plugins/esbuild-plugin.ts index cbb3b468..9e6d20a4 100644 --- a/src/module-server/plugins/esbuild-plugin.ts +++ b/src/module-server/plugins/esbuild-plugin.ts @@ -1,13 +1,16 @@ import * as esbuild from 'esbuild'; import { extname } from 'path'; -import type { Plugin } from 'rollup'; +import type { Plugin } from '../plugin'; +import { jsExts } from '../extensions-and-detection'; const shouldProcess = (id: string) => { if (id[0] === '\0') return false; - return /\.[jt]sx?$/.test(id); + return jsExts.test(id); }; -export const esbuildPlugin = (): Plugin => { +export const esbuildPlugin = ( + esbuildOptions: esbuild.TransformOptions, +): Plugin => { return { name: 'esbuild', async transform(code, id) { @@ -19,6 +22,7 @@ export const esbuildPlugin = (): Plugin => { sourcefile: id, loader, sourcemap: 'external', + ...esbuildOptions, }) .catch((error) => { const err = error.errors[0]; diff --git a/src/module-server/plugins/npm-plugin.ts b/src/module-server/plugins/npm-plugin.ts index ba1e1bd5..01862337 100644 --- a/src/module-server/plugins/npm-plugin.ts +++ b/src/module-server/plugins/npm-plugin.ts @@ -1,11 +1,12 @@ import { dirname, join } from 'path'; -import type { Plugin } from 'rollup'; +import type { Plugin } from '../plugin'; import { promises as fs } from 'fs'; import { fileURLToPath } from 'url'; import { jsExts, isBareImport, npmPrefix } from '../extensions-and-detection'; import { changeErrorMessage } from '../../utils'; import { bundleNpmModule } from '../bundle-npm-module'; import { resolveFromNodeModules } from '../node-resolve'; +import { createHash } from 'crypto'; // This is the folder that Pleasantest is installed in (e.g. /node_modules/pleasantest) const installFolder = dirname(dirname(dirname(fileURLToPath(import.meta.url)))); @@ -33,7 +34,13 @@ const getFromCache = async (cachePath: string) => { ); }; -export const npmPlugin = ({ root }: { root: string }): Plugin => { +export const npmPlugin = ({ + root, + envVars, +}: { + root: string; + envVars: Record; +}): Plugin => { return { name: 'npm', // Rewrite bare imports to have @npm/ prefix @@ -60,16 +67,26 @@ export const npmPlugin = ({ root }: { root: string }): Plugin => { id = id.slice(npmPrefix.length); const resolved = await resolveFromNodeModules(id, root); if (!resolved) return; - const cachePath = join(cacheDir, '@npm', `${resolved.idWithVersion}.js`); + + const cachePath = join( + cacheDir, + '@npm', + `${resolved.idWithVersion}-${hash(envVars)}.js`, + ); const cached = await getFromCache(cachePath); if (cached) return cached; - const result = await bundleNpmModule(resolved.path, id, false); + const result = await bundleNpmModule(resolved.path, id, false, envVars); // Queue up a second-pass optimized/minified build - bundleNpmModule(resolved.path, id, true).then((optimizedResult) => { - setInCache(cachePath, optimizedResult); - }); + bundleNpmModule(resolved.path, id, true, envVars).then( + (optimizedResult) => { + setInCache(cachePath, optimizedResult); + }, + ); setInCache(cachePath, result); return result; }, }; }; + +const hash = (inputs: Record) => + createHash('sha512').update(JSON.stringify(inputs)).digest('hex').slice(0, 7); diff --git a/src/module-server/plugins/process-global-plugin.ts b/src/module-server/plugins/process-global-plugin.ts deleted file mode 100644 index c4b1d6f9..00000000 --- a/src/module-server/plugins/process-global-plugin.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { Plugin } from 'rollup'; - -/** - * Set global process.env.* variables - * processGlobalPlugin({ NODE_ENV: 'development' }) - */ -export const processGlobalPlugin = (env: Record): Plugin => { - return { - name: 'process-global', - transform(code) { - for (const [property, value] of Object.entries(env)) { - code = code.replace( - new RegExp(`process\\.env\\.${property}`, 'g'), - JSON.stringify(value), - ); - } - - return code; - }, - }; -}; diff --git a/src/module-server/plugins/resolve-extensions-plugin.ts b/src/module-server/plugins/resolve-extensions-plugin.ts index 7923a4ac..586e8007 100644 --- a/src/module-server/plugins/resolve-extensions-plugin.ts +++ b/src/module-server/plugins/resolve-extensions-plugin.ts @@ -1,4 +1,4 @@ -import type { Plugin } from 'rollup'; +import type { Plugin } from '../plugin'; import { isRelativeOrAbsoluteImport } from '../extensions-and-detection'; import { resolveRelativeOrAbsolute } from '../node-resolve'; diff --git a/src/module-server/rollup-plugin-container.ts b/src/module-server/rollup-plugin-container.ts index 1cb0ef38..80671e06 100644 --- a/src/module-server/rollup-plugin-container.ts +++ b/src/module-server/rollup-plugin-container.ts @@ -42,10 +42,10 @@ import { resolve, dirname } from 'path'; import { Parser } from 'acorn'; import type { LoadResult, - Plugin, PluginContext as RollupPluginContext, ResolveIdResult, } from 'rollup'; +import type { Plugin } from './plugin'; import { combineSourceMaps } from './combine-source-maps'; import { createCodeFrame } from 'simple-code-frame'; import type { diff --git a/src/module-server/server.ts b/src/module-server/server.ts index 37b54a4b..e09a2a5e 100644 --- a/src/module-server/server.ts +++ b/src/module-server/server.ts @@ -34,7 +34,7 @@ export const createServer = ({ middleware }: ServerOpts) => // Create a new console instance instead of using the global one // Because the global one is overridden by Jest, and it adds a misleading second stack trace and code frame below it const console = new Console(process.stdout, process.stderr); - console.log(err.stack); + console.log(err.stack || err.message || err); }, }); if (middleware.length > 0) server.use(...middleware); diff --git a/src/module-server/transform-imports.ts b/src/module-server/transform-imports.ts index 8627d9fb..8f923a80 100644 --- a/src/module-server/transform-imports.ts +++ b/src/module-server/transform-imports.ts @@ -27,9 +27,14 @@ - Function style changed - Comments changed - ESLint fixes + - Parsing errors are thrown with code frame */ import { parse } from 'es-module-lexer'; +import { createCodeFrame } from 'simple-code-frame'; +import * as colors from 'kolorist'; +import { cssExts, jsExts } from './extensions-and-detection'; +import { extname } from 'path'; type MaybePromise = Promise | T; type ResolveFn = ( @@ -51,8 +56,34 @@ export const transformImports = async ( id: string, { resolveImportMeta, resolveId, resolveDynamicImport }: Options = {}, ) => { - // eslint-disable-next-line @cloudfour/typescript-eslint/await-thenable - const [imports] = await parse(code, id); + let imports; + try { + // eslint-disable-next-line @cloudfour/typescript-eslint/await-thenable + imports = (await parse(code, id))[0]; + } catch (error) { + if (!('idx' in error)) throw error; + const linesUntilError = code.slice(0, error.idx).split('\n'); + const line = linesUntilError.length; + const column = linesUntilError[linesUntilError.length - 1].length; + const frame = createCodeFrame(code, line - 1, column); + let message = `${colors.red(colors.bold(error.message))} + +${colors.red(`${id}:${line}:${column + 1}`)} + +${frame} +`; + if (!jsExts.test(id) && !cssExts.test(id)) + message += `${colors.yellow( + `You may need to add plugins to the module server to handle ${ + extname(id) ? `${extname(id)} files` : id + }`, + )}\n`; + + const modifiedError = new Error(message); + modifiedError.stack = message; + throw modifiedError; + } + let out = ''; let offset = 0; diff --git a/tests/utils/runJS.test.tsx b/tests/utils/runJS.test.tsx index 5baa2842..c7a4252a 100644 --- a/tests/utils/runJS.test.tsx +++ b/tests/utils/runJS.test.tsx @@ -1,6 +1,8 @@ import { withBrowser } from 'pleasantest'; import type { PleasantestContext, PleasantestUtils } from 'pleasantest'; import { printErrorFrames } from '../test-utils'; +import vuePlugin from 'rollup-plugin-vue'; +import aliasPlugin from '@rollup/plugin-alias'; const createHeading = async ({ utils, @@ -260,4 +262,84 @@ describe('CJS interop edge cases', () => { await expect(heading).toHaveTextContent('Hi'); }), ); + test( + 'vue component can be imported via rollup-plugin-vue', + withBrowser( + { + moduleServer: { + plugins: [ + { + name: 'replace-for-vue', + transform(code) { + return code + .replace(/__VUE_OPTIONS_API__/g, 'true') + .replace(/__VUE_PROD_DEVTOOLS__/g, 'false'); + }, + }, + vuePlugin(), + ], + }, + }, + async ({ utils, screen }) => { + await utils.injectHTML('
'); + await utils.runJS(` + import { createApp } from 'vue' + import VueComponent from './vue-component.vue' + const app = createApp(VueComponent) + app.mount('#app') + `); + const heading = await screen.getByRole('heading'); + await expect(heading).toHaveTextContent('Hiya'); + await expect(heading).toHaveStyle({ color: 'green' }); + }, + ), + ); }); + +test( + 'can use @rollup/plugin-alias', + withBrowser( + { + moduleServer: { + plugins: [ + aliasPlugin({ + entries: { asdf: 'preact', foo: './external' }, + }), + ], + }, + }, + async ({ utils }) => { + await utils.runJS(` + import * as preact from 'asdf' + if (!preact.h || !preact.Fragment || !preact.Component) + throw new Error('Alias did not load preact correctly') + import * as external from 'foo' + if (!external.render || !external.renderThrow) + throw new Error('Alias did not load ./external.tsx correctly') + `); + }, + ), +); + +test( + 'environment variables are injected into browser code', + withBrowser( + { + moduleServer: { + envVars: { asdf: '1234' }, + }, + }, + async ({ utils }) => { + await utils.runJS(` + if (process.env.NODE_ENV !== 'development') + throw new Error('process.env.NODE_ENV not set correctly') + if (import.meta.env.NODE_ENV !== 'development') + throw new Error('import.meta.env.NODE_ENV not set correctly') + if (process.env.asdf !== '1234') + throw new Error('process.env.asdf not set correctly') + if (import.meta.env.asdf !== '1234') + throw new Error('import.meta.env.asdf not set correctly') + `); + }, + ), +); diff --git a/tests/utils/vue-component.vue b/tests/utils/vue-component.vue new file mode 100644 index 00000000..f120b260 --- /dev/null +++ b/tests/utils/vue-component.vue @@ -0,0 +1,20 @@ + + + + +