From 936ed779ed650f89d8dbdd285c61903068245e25 Mon Sep 17 00:00:00 2001 From: Jack Steam Date: Thu, 4 May 2023 19:40:36 -0500 Subject: [PATCH] Add inline sourcemap support to content scripts (#701) * return correct values in transform hook * fix transform hook * add inline sourcemap support to content scripts * Create rotten-snakes-brush.md * disable truncation in test error diff * scrub sourcemaps in test snapshots --- .changeset/rotten-snakes-brush.md | 5 + packages/vite-plugin/package.json | 2 + .../vite-plugin/src/node/fileWriter-rxjs.ts | 22 ++- .../vite-plugin/src/node/plugin-manifest.ts | 2 +- .../__snapshots__/build.test.ts.snap | 159 ++++++++++++++++++ .../__snapshots__/serve.test.ts.snap | 131 +++++++++++++++ .../tests/out/with-sourcemaps/build.test.ts | 8 + .../tests/out/with-sourcemaps/manifest.json | 18 ++ .../tests/out/with-sourcemaps/serve.test.ts | 16 ++ .../tests/out/with-sourcemaps/src/App.tsx | 12 ++ .../out/with-sourcemaps/src/background.ts | 3 + .../tests/out/with-sourcemaps/src/content.ts | 4 + .../tests/out/with-sourcemaps/src/popup.html | 12 ++ .../tests/out/with-sourcemaps/src/popup.tsx | 9 + .../tests/out/with-sourcemaps/vite.config.ts | 21 +++ packages/vite-plugin/tests/testOutput.ts | 1 + packages/vite-plugin/vitest.config.ts | 1 + pnpm-lock.yaml | 8 + 18 files changed, 431 insertions(+), 3 deletions(-) create mode 100644 .changeset/rotten-snakes-brush.md create mode 100644 packages/vite-plugin/tests/out/with-sourcemaps/__snapshots__/build.test.ts.snap create mode 100644 packages/vite-plugin/tests/out/with-sourcemaps/__snapshots__/serve.test.ts.snap create mode 100644 packages/vite-plugin/tests/out/with-sourcemaps/build.test.ts create mode 100644 packages/vite-plugin/tests/out/with-sourcemaps/manifest.json create mode 100644 packages/vite-plugin/tests/out/with-sourcemaps/serve.test.ts create mode 100644 packages/vite-plugin/tests/out/with-sourcemaps/src/App.tsx create mode 100644 packages/vite-plugin/tests/out/with-sourcemaps/src/background.ts create mode 100644 packages/vite-plugin/tests/out/with-sourcemaps/src/content.ts create mode 100644 packages/vite-plugin/tests/out/with-sourcemaps/src/popup.html create mode 100644 packages/vite-plugin/tests/out/with-sourcemaps/src/popup.tsx create mode 100644 packages/vite-plugin/tests/out/with-sourcemaps/vite.config.ts diff --git a/.changeset/rotten-snakes-brush.md b/.changeset/rotten-snakes-brush.md new file mode 100644 index 000000000..837117c50 --- /dev/null +++ b/.changeset/rotten-snakes-brush.md @@ -0,0 +1,5 @@ +--- +"@crxjs/vite-plugin": patch +--- + +Add inline sourcemap support to content scripts diff --git a/packages/vite-plugin/package.json b/packages/vite-plugin/package.json index f324dad10..7ad382fa9 100644 --- a/packages/vite-plugin/package.json +++ b/packages/vite-plugin/package.json @@ -68,6 +68,7 @@ "acorn-walk": "^8.2.0", "cheerio": "^1.0.0-rc.10", "connect-injector": "^0.4.4", + "convert-source-map": "^1.7.0", "debug": "^4.3.3", "es-module-lexer": "^0.10.0", "fast-glob": "^3.2.11", @@ -89,6 +90,7 @@ "@sveltejs/vite-plugin-svelte": "1.1.0", "@types/acorn": "4.0.6", "@types/chrome": "0.0.209", + "@types/convert-source-map": "^2.0.0", "@types/debug": "4.1.7", "@types/fs-extra": "9.0.13", "@types/jest-image-snapshot": "^5.1.0", diff --git a/packages/vite-plugin/src/node/fileWriter-rxjs.ts b/packages/vite-plugin/src/node/fileWriter-rxjs.ts index 88c567b22..3f8599df9 100644 --- a/packages/vite-plugin/src/node/fileWriter-rxjs.ts +++ b/packages/vite-plugin/src/node/fileWriter-rxjs.ts @@ -22,6 +22,7 @@ import { outputFiles } from './fileWriter-filesMap' import { getFileName, getOutputPath, getViteUrl } from './fileWriter-utilities' import { join } from './path' import { CrxDevAssetId, CrxDevScriptId, CrxPlugin } from './types' +import convertSourceMap from 'convert-source-map' /* ----------------- SERVER EVENTS ----------------- */ @@ -146,8 +147,25 @@ function prepScript( const transformResult = await server.transformRequest(viteUrl) if (!transformResult) throw new TypeError(`Unable to load "${script.id}" from server.`) - const { code, deps = [], dynamicDeps = [] } = transformResult - return { target, code, deps: [...deps, ...dynamicDeps].flat(), server } + const { deps = [], dynamicDeps = [], map } = transformResult + let { code } = transformResult + try { + if (map && server.config.build.sourcemap === 'inline') { + // remove existing source map (might be a url, which doesn't work in content scripts) + code = code.replace(/\n*\/\/# sourceMappingURL=[^\n]+/g, '') + // create a new inline source map + const sourceMap = convertSourceMap.fromObject(map).toComment() + code += `\n${sourceMap}\n` + } + } catch (error) { + console.warn('Failed to inline source map', error) + } + return { + target, + code, + deps: [...deps, ...dynamicDeps].flat(), + server, + } }), // retry in case of dependency rebundle retry({ count: 10, delay: 100 }), diff --git a/packages/vite-plugin/src/node/plugin-manifest.ts b/packages/vite-plugin/src/node/plugin-manifest.ts index c71101a32..24d90898e 100644 --- a/packages/vite-plugin/src/node/plugin-manifest.ts +++ b/packages/vite-plugin/src/node/plugin-manifest.ts @@ -250,7 +250,7 @@ export const pluginManifest: CrxPluginFn = () => { } const encoded = encodeManifest(manifest) - return encoded + return { code: encoded, map: null } }, async generateBundle(options, bundle) { const manifestName = this.getFileName(refId) diff --git a/packages/vite-plugin/tests/out/with-sourcemaps/__snapshots__/build.test.ts.snap b/packages/vite-plugin/tests/out/with-sourcemaps/__snapshots__/build.test.ts.snap new file mode 100644 index 000000000..3fe4c0437 --- /dev/null +++ b/packages/vite-plugin/tests/out/with-sourcemaps/__snapshots__/build.test.ts.snap @@ -0,0 +1,159 @@ +// Vitest Snapshot v1 + +exports[`build fs output > _00 manifest.json 1`] = ` +Object { + "action": Object { + "default_popup": "src/popup.html", + }, + "background": Object { + "service_worker": "service-worker-loader.js", + "type": "module", + }, + "content_scripts": Array [ + Object { + "js": Array [ + "assets/content.ts-loader.hash0.js", + ], + "matches": Array [ + "https://*/*", + "http://*/*", + ], + }, + ], + "description": "test extension", + "manifest_version": 3, + "name": "test extension", + "version": "0.1.0", + "web_accessible_resources": Array [ + Object { + "matches": Array [ + "http://*/*", + "https://*/*", + ], + "resources": Array [ + "assets/content.ts.hash1.js", + ], + "use_dynamic_url": true, + }, + ], +} +`; + +exports[`build fs output > _01 output files 1`] = ` +Array [ + "assets/background.ts.hash2.js", + "assets/content.ts-loader.hash0.js", + "assets/content.ts.hash1.js", + "assets/popup.html.hash3.js", + "assets/vendor.hash4.js", + "manifest.json", + "service-worker-loader.js", + "src/popup.html", +] +`; + +exports[`build fs output > assets/background.ts.hash2.js 1`] = ` +"console.log(\\"service_worker.ts\\"); +// # sourceMappingURL=data:application/json;charset=utf-8;base64, +" +`; + +exports[`build fs output > assets/content.ts.hash1.js 1`] = ` +"const message = \\"content script\\"; +console.log(message); +// # sourceMappingURL=data:application/json;charset=utf-8;base64, +" +`; + +exports[`build fs output > assets/content.ts-loader.hash0.js 1`] = ` +"(function () { + 'use strict'; + + const injectTime = performance.now(); + (async () => { + const { onExecute } = await import( + /* @vite-ignore */ + chrome.runtime.getURL(\\"assets/content.ts.hash1.js\\") + ); + onExecute?.({ perf: { injectTime, loadTime: performance.now() - injectTime } }); + })().catch(console.error); + +})(); +" +`; + +exports[`build fs output > assets/popup.html.hash3.js 1`] = ` +"import { R as React, r as reactDom } from \\"./vendor.hash4.js\\"; +(function polyfill() { + const relList = document.createElement(\\"link\\").relList; + if (relList && relList.supports && relList.supports(\\"modulepreload\\")) { + return; + } + for (const link of document.querySelectorAll('link[rel=\\"modulepreload\\"]')) { + processPreload(link); + } + new MutationObserver((mutations) => { + for (const mutation of mutations) { + if (mutation.type !== \\"childList\\") { + continue; + } + for (const node of mutation.addedNodes) { + if (node.tagName === \\"LINK\\" && node.rel === \\"modulepreload\\") + processPreload(node); + } + } + }).observe(document, { childList: true, subtree: true }); + function getFetchOpts(script) { + const fetchOpts = {}; + if (script.integrity) + fetchOpts.integrity = script.integrity; + if (script.referrerpolicy) + fetchOpts.referrerPolicy = script.referrerpolicy; + if (script.crossorigin === \\"use-credentials\\") + fetchOpts.credentials = \\"include\\"; + else if (script.crossorigin === \\"anonymous\\") + fetchOpts.credentials = \\"omit\\"; + else + fetchOpts.credentials = \\"same-origin\\"; + return fetchOpts; + } + function processPreload(link) { + if (link.ep) + return; + link.ep = true; + const fetchOpts = getFetchOpts(link); + fetch(link.href, fetchOpts); + } +})(); +const App = () => { + return /* @__PURE__ */ React.createElement(\\"div\\", null, /* @__PURE__ */ React.createElement(\\"h1\\", null, \\"Popup Page\\"), /* @__PURE__ */ React.createElement(\\"p\\", null, \\"If you are seeing this, React is working!\\")); +}; +console.log(\\"popup script\\"); +const root = document.querySelector(\\"#root\\"); +reactDom.exports.render(/* @__PURE__ */ React.createElement(App, null), root); +// # sourceMappingURL=data:application/json;charset=utf-8;base64, +" +`; + +exports[`build fs output > service-worker-loader.js 1`] = ` +"import './assets/background.ts.hash2.js'; +" +`; + +exports[`build fs output > src/popup.html 1`] = ` +" + + + + + Popup Page + + + + +
+ + + +" +`; diff --git a/packages/vite-plugin/tests/out/with-sourcemaps/__snapshots__/serve.test.ts.snap b/packages/vite-plugin/tests/out/with-sourcemaps/__snapshots__/serve.test.ts.snap new file mode 100644 index 000000000..282e48469 --- /dev/null +++ b/packages/vite-plugin/tests/out/with-sourcemaps/__snapshots__/serve.test.ts.snap @@ -0,0 +1,131 @@ +// Vitest Snapshot v1 + +exports[`serve fs output > _00 manifest.json 1`] = ` +Object { + "action": Object { + "default_popup": "src/popup.html", + }, + "background": Object { + "service_worker": "service-worker-loader.js", + "type": "module", + }, + "content_scripts": Array [ + Object { + "js": Array [ + "src/content.ts-loader.js", + ], + "matches": Array [ + "https://*/*", + "http://*/*", + ], + }, + ], + "description": "test extension", + "manifest_version": 3, + "name": "test extension", + "version": "0.1.0", + "web_accessible_resources": Array [ + Object { + "matches": Array [ + "", + ], + "resources": Array [ + "*", + "**/*", + ], + "use_dynamic_url": true, + }, + ], +} +`; + +exports[`serve fs output > _01 output files 1`] = ` +Array [ + "assets/precontroller.hash0.js", + "manifest.json", + "service-worker-loader.js", + "src/content.ts-loader.js", + "src/content.ts.js", + "src/popup.html", + "vendor/crx-client-port.js", + "vendor/vite-client.js", + "vendor/vite-dist-client-env.mjs.js", + "vendor/webcomponents-custom-elements.js", +] +`; + +exports[`serve fs output > _02 optimized deps 1`] = ` +Set { + "src/content.ts", + "src/background.ts", + "src/popup.html", +} +`; + +exports[`serve fs output > assets/precontroller.hash0.js 1`] = ` +"const id = setInterval(() => location.reload(), 100); +setTimeout(() => clearInterval(id), 5e3); +" +`; + +exports[`serve fs output > service-worker-loader.js 1`] = ` +"import 'http://localhost:3000/@vite/env'; +import 'http://localhost:3000/@crx/client-worker'; +import 'http://localhost:3000/src/background.ts'; +" +`; + +exports[`serve fs output > src/content.ts.js 1`] = ` +"const message = \\"content script\\"; +console.log(message); +export {}; + +// # sourceMappingURL=data:application/json;charset=utf-8;base64, +" +`; + +exports[`serve fs output > src/content.ts-loader.js 1`] = ` +"(function () { + 'use strict'; + + const injectTime = performance.now(); + (async () => { + if (\\"\\") + await import( + /* @vite-ignore */ + chrome.runtime.getURL(\\"\\") + ); + await import( + /* @vite-ignore */ + chrome.runtime.getURL(\\"vendor/vite-client.js\\") + ); + const { onExecute } = await import( + /* @vite-ignore */ + chrome.runtime.getURL(\\"src/content.ts.js\\") + ); + onExecute?.({ perf: { injectTime, loadTime: performance.now() - injectTime } }); + })().catch(console.error); + +})(); +" +`; + +exports[`serve fs output > src/popup.html 1`] = ` +" + + + Waiting for the extension service worker... + + + +

Waiting for service worker

+ +

+ If you see this message, it means the service worker has not loaded fully. +

+ +

This page is never added in production.

+ + +" +`; diff --git a/packages/vite-plugin/tests/out/with-sourcemaps/build.test.ts b/packages/vite-plugin/tests/out/with-sourcemaps/build.test.ts new file mode 100644 index 000000000..84bcc380a --- /dev/null +++ b/packages/vite-plugin/tests/out/with-sourcemaps/build.test.ts @@ -0,0 +1,8 @@ +import { build } from 'tests/runners' +import { testOutput } from 'tests/testOutput' +import { test } from 'vitest' + +test('build fs output', async () => { + const result = await build(__dirname) + await testOutput(result) +}) diff --git a/packages/vite-plugin/tests/out/with-sourcemaps/manifest.json b/packages/vite-plugin/tests/out/with-sourcemaps/manifest.json new file mode 100644 index 000000000..6b8040799 --- /dev/null +++ b/packages/vite-plugin/tests/out/with-sourcemaps/manifest.json @@ -0,0 +1,18 @@ +{ + "action": { + "default_popup": "src/popup.html" + }, + "background": { + "service_worker": "src/background.ts" + }, + "content_scripts": [ + { + "js": ["src/content.ts"], + "matches": ["https://*/*", "http://*/*"] + } + ], + "manifest_version": 3, + "name": "test extension", + "description": "test extension", + "version": "0.1.0" +} diff --git a/packages/vite-plugin/tests/out/with-sourcemaps/serve.test.ts b/packages/vite-plugin/tests/out/with-sourcemaps/serve.test.ts new file mode 100644 index 000000000..394772d33 --- /dev/null +++ b/packages/vite-plugin/tests/out/with-sourcemaps/serve.test.ts @@ -0,0 +1,16 @@ +import { serve } from 'tests/runners' +import { testOutput } from 'tests/testOutput' +import { afterAll, test } from 'vitest' + +let result: Awaited> | undefined + +afterAll(async () => { + try { + await result?.server.close() + } catch (error) {} +}) + +test('serve fs output', async () => { + result = await serve(__dirname) + await testOutput(result) +}) diff --git a/packages/vite-plugin/tests/out/with-sourcemaps/src/App.tsx b/packages/vite-plugin/tests/out/with-sourcemaps/src/App.tsx new file mode 100644 index 000000000..73a16b5a6 --- /dev/null +++ b/packages/vite-plugin/tests/out/with-sourcemaps/src/App.tsx @@ -0,0 +1,12 @@ +import React from 'react' + +const App: React.FC = () => { + return ( +
+

Popup Page

+

If you are seeing this, React is working!

+
+ ) +} + +export default App diff --git a/packages/vite-plugin/tests/out/with-sourcemaps/src/background.ts b/packages/vite-plugin/tests/out/with-sourcemaps/src/background.ts new file mode 100644 index 000000000..e7ab4dc9e --- /dev/null +++ b/packages/vite-plugin/tests/out/with-sourcemaps/src/background.ts @@ -0,0 +1,3 @@ +console.log('service_worker.ts') + +export {} diff --git a/packages/vite-plugin/tests/out/with-sourcemaps/src/content.ts b/packages/vite-plugin/tests/out/with-sourcemaps/src/content.ts new file mode 100644 index 000000000..ce92280b6 --- /dev/null +++ b/packages/vite-plugin/tests/out/with-sourcemaps/src/content.ts @@ -0,0 +1,4 @@ +type Log = 'content script' +const message: Log = 'content script' +console.log(message) +export {} diff --git a/packages/vite-plugin/tests/out/with-sourcemaps/src/popup.html b/packages/vite-plugin/tests/out/with-sourcemaps/src/popup.html new file mode 100644 index 000000000..4c895110c --- /dev/null +++ b/packages/vite-plugin/tests/out/with-sourcemaps/src/popup.html @@ -0,0 +1,12 @@ + + + + + + Popup Page + + +
+ + + diff --git a/packages/vite-plugin/tests/out/with-sourcemaps/src/popup.tsx b/packages/vite-plugin/tests/out/with-sourcemaps/src/popup.tsx new file mode 100644 index 000000000..f5d5e7ed1 --- /dev/null +++ b/packages/vite-plugin/tests/out/with-sourcemaps/src/popup.tsx @@ -0,0 +1,9 @@ +import React from 'react' +import { render } from 'react-dom' +import App from './App' + +console.log('popup script') + +const root = document.querySelector('#root') + +render(, root) diff --git a/packages/vite-plugin/tests/out/with-sourcemaps/vite.config.ts b/packages/vite-plugin/tests/out/with-sourcemaps/vite.config.ts new file mode 100644 index 000000000..17c4d0892 --- /dev/null +++ b/packages/vite-plugin/tests/out/with-sourcemaps/vite.config.ts @@ -0,0 +1,21 @@ +import { crx } from '../../plugin-testOptionsProvider' +import { defineConfig } from 'vite' +import manifest from './manifest.json' + +export default defineConfig({ + build: { + sourcemap: 'inline', + minify: false, + rollupOptions: { + output: { + // the hash randomly changes between environments + assetFileNames: 'assets/[name].hash[hash].[ext]', + chunkFileNames: 'assets/[name].hash[hash].js', + entryFileNames: 'assets/[name].hash[hash].js', + }, + }, + }, + clearScreen: false, + logLevel: 'error', + plugins: [crx({ manifest })], +}) diff --git a/packages/vite-plugin/tests/testOutput.ts b/packages/vite-plugin/tests/testOutput.ts index dfcdea7ff..73fb60e40 100644 --- a/packages/vite-plugin/tests/testOutput.ts +++ b/packages/vite-plugin/tests/testOutput.ts @@ -65,6 +65,7 @@ export async function testOutput( return replaced }) .replace(/(v--)([a-z0-9]+)\./g, '$1hash.') + .replace(/^\/\/#(.+?base64,)(.+)$/m, '// #$1') getTest('manifest.json', (source, name) => { const scrubbed = scrubHashes(source) diff --git a/packages/vite-plugin/vitest.config.ts b/packages/vite-plugin/vitest.config.ts index b7634586e..d5b698e1b 100644 --- a/packages/vite-plugin/vitest.config.ts +++ b/packages/vite-plugin/vitest.config.ts @@ -48,6 +48,7 @@ export default defineConfig(({ mode }) => { }, testTimeout, watchExclude: [...configDefaults.watchExclude, '**/tests/templates'], + chaiConfig: { includeStack: false, showDiff: true, truncateThreshold: 0 }, }, } }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6e67cee92..5ad156159 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -166,6 +166,7 @@ importers: '@sveltejs/vite-plugin-svelte': 1.1.0 '@types/acorn': 4.0.6 '@types/chrome': 0.0.209 + '@types/convert-source-map': ^2.0.0 '@types/debug': 4.1.7 '@types/fs-extra': 9.0.13 '@types/jest-image-snapshot': ^5.1.0 @@ -182,6 +183,7 @@ importers: cheerio: ^1.0.0-rc.10 chokidar: ^3.5.3 connect-injector: ^0.4.4 + convert-source-map: ^1.7.0 debug: ^4.3.3 es-module-lexer: ^0.10.0 esbuild: 0.17.14 @@ -216,6 +218,7 @@ importers: acorn-walk: 8.2.0 cheerio: 1.0.0-rc.10 connect-injector: 0.4.4 + convert-source-map: 1.8.0 debug: 4.3.4 es-module-lexer: 0.10.5 fast-glob: 3.2.11 @@ -236,6 +239,7 @@ importers: '@sveltejs/vite-plugin-svelte': 1.1.0_svelte@3.48.0+vite@3.1.7 '@types/acorn': 4.0.6 '@types/chrome': 0.0.209 + '@types/convert-source-map': 2.0.0 '@types/debug': 4.1.7 '@types/fs-extra': 9.0.13 '@types/jest-image-snapshot': 5.1.0 @@ -6130,6 +6134,10 @@ packages: '@types/node': 17.0.18 dev: false + /@types/convert-source-map/2.0.0: + resolution: {integrity: sha512-QUm4YOC/ENo0VjPVl2o8HGyTbHHQGDOw8PCg3rXBucYHKyZN/XjXRbPFAV1tB2FvM0/wyFoDct4cTIctzKrQFg==} + dev: true + /@types/debug/4.1.7: resolution: {integrity: sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==} dependencies: