diff --git a/.gitignore b/.gitignore index 5fa1d822a..ddd558e54 100644 --- a/.gitignore +++ b/.gitignore @@ -67,3 +67,6 @@ __TEST__ /playwright/.cache/ /e2e-report/ _examples + +# specs +specs/ diff --git a/examples/content-preact/content/scripts.tsx b/examples/content-preact/content/scripts.tsx index 4d4998c07..2ee91fa1c 100644 --- a/examples/content-preact/content/scripts.tsx +++ b/examples/content-preact/content/scripts.tsx @@ -1,12 +1,15 @@ import {render} from 'preact' import ContentApp from './ContentApp' -import './styles.css?inline_style' + +let unmount: () => void +import.meta.webpackHot?.accept() +import.meta.webpackHot?.dispose(() => unmount?.()) if (document.readyState === 'complete') { - initial() + unmount = initial() || (() => {}) } else { document.addEventListener('readystatechange', () => { - if (document.readyState === 'complete') initial() + if (document.readyState === 'complete') unmount = initial() || (() => {}) }) } @@ -21,13 +24,36 @@ function initial() { // This way, styles from the extension won't leak into the host page. const shadowRoot = rootDiv.attachShadow({mode: 'open'}) - // Inform Extension.js that the shadow root is available. - window.__EXTENSION_SHADOW_ROOT__ = shadowRoot + const style = document.createElement('style') + shadowRoot.appendChild(style) + + fetchCSS().then((response) => { + style.textContent = response + }) + console.log('Running....???') + + import.meta.webpackHot?.accept('./styles.css', () => { + fetchCSS().then((response) => { + style.textContent = response + }) + }) + // Preact specific rendering render(
, shadowRoot ) + + return () => { + // Preact's render returns undefined, so we just remove the root + rootDiv.remove() + } +} + +async function fetchCSS() { + const response = await fetch(new URL('./styles.css', import.meta.url)) + const text = await response.text() + return response.ok ? text : Promise.reject(text) } diff --git a/examples/content-react/content/scripts.tsx b/examples/content-react/content/scripts.tsx index 9191e1893..3cde20755 100644 --- a/examples/content-react/content/scripts.tsx +++ b/examples/content-react/content/scripts.tsx @@ -1,15 +1,22 @@ import ReactDOM from 'react-dom/client' import ContentApp from './ContentApp' -import './styles.css?inline_style' + +let unmount: () => void +// @ts-expect-error - global reference. +import.meta.webpackHot?.accept() +// @ts-expect-error - global reference. +import.meta.webpackHot?.dispose(() => unmount?.()) if (document.readyState === 'complete') { - initial() + unmount = initial() || (() => {}) } else { document.addEventListener('readystatechange', () => { - if (document.readyState === 'complete') initial() + if (document.readyState === 'complete') unmount = initial() || (() => {}) }) } +console.log('Extension running...') + function initial() { // Create a new div element and append it to the document's body const rootDiv = document.createElement('div') @@ -21,13 +28,36 @@ function initial() { // This way, styles from the extension won't leak into the host page. const shadowRoot = rootDiv.attachShadow({mode: 'open'}) - // Use the shadow root as the root element to inject styles into. - window.__EXTENSION_SHADOW_ROOT__ = shadowRoot + const style = document.createElement('style') + shadowRoot.appendChild(style) + + fetchCSS().then((response) => { + style.textContent = response + }) + + // This needs to be inside initial() since it references the style element + // that is created and used within this function's scope + // @ts-expect-error - global reference. + import.meta.webpackHot?.accept('./styles.css', () => { + fetchCSS().then((response) => { + style.textContent = response + }) + }) - const root = ReactDOM.createRoot(shadowRoot) - root.render( + const mountingPoint = ReactDOM.createRoot(shadowRoot) + mountingPoint.render(
) + return () => { + mountingPoint.unmount() + rootDiv.remove() + } +} + +async function fetchCSS() { + const response = await fetch(new URL('./styles.css', import.meta.url)) + const text = await response.text() + return response.ok ? text : Promise.reject(text) } diff --git a/examples/content-sass/content/scripts.js b/examples/content-sass/content/scripts.js index ff923e7af..86fabcd05 100644 --- a/examples/content-sass/content/scripts.js +++ b/examples/content-sass/content/scripts.js @@ -1,13 +1,16 @@ -import './styles.scss?inline_style' import logo from '../images/logo.svg' console.log('hello from content_scripts') +let unmount +import.meta.webpackHot.accept() +import.meta.webpackHot.dispose(() => unmount()) + if (document.readyState === 'complete') { - initial() + unmount = initial() || (() => {}) } else { document.addEventListener('readystatechange', () => { - if (document.readyState === 'complete') initial() + if (document.readyState === 'complete') unmount = initial() || (() => {}) }) } @@ -21,24 +24,51 @@ function initial() { // This way, styles from the extension won't leak into the host page. const shadowRoot = rootDiv.attachShadow({mode: 'open'}) - // Inform Extension.js that the shadow root is available. - window.__EXTENSION_SHADOW_ROOT__ = shadowRoot - - shadowRoot.innerHTML = ` -
- -

- Welcome to your Sass Extension -

-

- Learn more about creating cross-browser extensions at - https://extension.js.org - -

-
- ` + const style = document.createElement('style') + shadowRoot.appendChild(style) + + // Create content container div + const contentDiv = document.createElement('div') + contentDiv.className = 'content_script' + shadowRoot.appendChild(contentDiv) + + // Create logo image + const logoImg = document.createElement('img') + logoImg.className = 'content_logo' + logoImg.src = logo + contentDiv.appendChild(logoImg) + + // Create title + const title = document.createElement('h1') + title.className = 'content_title' + title.textContent = 'Welcome to your Sass Extension' + contentDiv.appendChild(title) + + // Create description + const description = document.createElement('p') + description.className = 'content_description' + description.innerHTML = + 'Learn more about creating cross-browser extensions at https://extension.js.org' + contentDiv.appendChild(description) + + // Handle CSS injection + fetchCSS().then((response) => { + style.textContent = response + }) + + import.meta.webpackHot?.accept('./styles.scss', () => { + fetchCSS().then((response) => { + style.textContent = response + }) + }) + + return () => { + rootDiv.remove() + } +} + +async function fetchCSS() { + const response = await fetch(new URL('./styles.scss', import.meta.url)) + const text = await response.text() + return response.ok ? text : Promise.reject(text) } diff --git a/examples/content-sass/content/styles.scss b/examples/content-sass/content/styles.scss index 35586168b..3627000b1 100644 --- a/examples/content-sass/content/styles.scss +++ b/examples/content-sass/content/styles.scss @@ -19,22 +19,24 @@ } .content_title { - font-size: 1.85em; + font: { + size: 1.85em; + family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, + 'Helvetica Neue', Arial, 'Noto Sans', sans-serif; + weight: 700; + } line-height: 1.1; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, - 'Helvetica Neue', Arial, 'Noto Sans', sans-serif; - font-weight: 700; margin: 0; } .content_description { font-size: small; margin: 0; -} -.content_description a { - text-decoration: none; - border-bottom: 2px solid #c9c9c9; - color: #e5e7eb; - margin: 0; + a { + text-decoration: none; + border-bottom: 2px solid #c9c9c9; + color: #e5e7eb; + margin: 0; + } } \ No newline at end of file diff --git a/examples/content-svelte/.gitignore b/examples/content-svelte/.gitignore new file mode 100644 index 000000000..5e8c65b73 --- /dev/null +++ b/examples/content-svelte/.gitignore @@ -0,0 +1,31 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +node_modules + +# testing +coverage + +# production +dist + +# misc +.DS_Store + +# local env files +.env.local +.env.development.local +.env.test.local +.env.production.local + +# lock files +yarn.lock +package-lock.json + +# debug files +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# extension.js +extension-env.d.ts diff --git a/examples/content-svelte/background.ts b/examples/content-svelte/background.ts new file mode 100644 index 000000000..798d5018d --- /dev/null +++ b/examples/content-svelte/background.ts @@ -0,0 +1 @@ +console.log('Hello from the background script!') diff --git a/examples/content-svelte/content/ContentApp.svelte b/examples/content-svelte/content/ContentApp.svelte new file mode 100644 index 000000000..62ea4eecd --- /dev/null +++ b/examples/content-svelte/content/ContentApp.svelte @@ -0,0 +1,84 @@ + + +{#if !isDialogOpen} +
+ +
+{:else} +
+
+
+
+ + + +
+
+
+
+ React logo +
+
+ TypeScript logo +
+
+ Tailwind logo +
+

+ This is a content script running Svelte, TypeScript, and Tailwind.css +

+

+ Learn more about creating cross-browser extensions by + + . +

+
+
+ Chrome window screenshot +
+
+
+{/if} diff --git a/examples/content-svelte/content/scripts.ts b/examples/content-svelte/content/scripts.ts new file mode 100644 index 000000000..c69d39825 --- /dev/null +++ b/examples/content-svelte/content/scripts.ts @@ -0,0 +1,55 @@ +import {mount} from 'svelte' +import ContentApp from './ContentApp.svelte' + +let unmount: () => void +import.meta.webpackHot?.accept() +import.meta.webpackHot?.dispose(() => unmount?.()) + +if (document.readyState === 'complete') { + unmount = initial() || (() => {}) +} else { + document.addEventListener('readystatechange', () => { + if (document.readyState === 'complete') unmount = initial() || (() => {}) + }) +} + +export default function initial() { + const rootDiv = document.createElement('div') + rootDiv.id = 'extension-root' + document.body.appendChild(rootDiv) + + const shadowRoot = rootDiv.attachShadow({mode: 'open'}) + + const style = document.createElement('style') + shadowRoot.appendChild(style) + + fetchCSS().then((response) => { + style.textContent = response + }) + + import.meta.webpackHot?.accept('./styles.css', () => { + fetchCSS().then((response) => { + style.textContent = response + }) + }) + + // Create container for Svelte app + const contentDiv = document.createElement('div') + contentDiv.className = 'content_script' + shadowRoot.appendChild(contentDiv) + + // Mount Svelte app using Svelte 5's mount function + mount(ContentApp, { + target: contentDiv + }) + + return () => { + rootDiv.remove() + } +} + +async function fetchCSS() { + const response = await fetch(new URL('./styles.css', import.meta.url)) + const text = await response.text() + return response.ok ? text : Promise.reject(text) +} diff --git a/examples/content-svelte/content/styles.css b/examples/content-svelte/content/styles.css new file mode 100644 index 000000000..c0fc3552e --- /dev/null +++ b/examples/content-svelte/content/styles.css @@ -0,0 +1,10 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +.content_script { + position: fixed; + bottom: 0; + right: 0; + z-index: 99999; +} diff --git a/examples/content-svelte/content/svelte.d.ts b/examples/content-svelte/content/svelte.d.ts new file mode 100644 index 000000000..e54bc1b62 --- /dev/null +++ b/examples/content-svelte/content/svelte.d.ts @@ -0,0 +1,5 @@ +declare module '*.svelte' { + import type {ComponentType} from 'svelte' + const component: ComponentType + export default component +} diff --git a/examples/content-svelte/images/chromeWindow.png b/examples/content-svelte/images/chromeWindow.png new file mode 100644 index 000000000..da525dd8e Binary files /dev/null and b/examples/content-svelte/images/chromeWindow.png differ diff --git a/examples/content-svelte/images/extension_48.png b/examples/content-svelte/images/extension_48.png new file mode 100644 index 000000000..f60575b39 Binary files /dev/null and b/examples/content-svelte/images/extension_48.png differ diff --git a/examples/content-svelte/images/svelte.png b/examples/content-svelte/images/svelte.png new file mode 100644 index 000000000..a520d188d Binary files /dev/null and b/examples/content-svelte/images/svelte.png differ diff --git a/examples/content-svelte/images/tailwind.png b/examples/content-svelte/images/tailwind.png new file mode 100644 index 000000000..83ed5e126 Binary files /dev/null and b/examples/content-svelte/images/tailwind.png differ diff --git a/examples/content-svelte/images/tailwind_bg.png b/examples/content-svelte/images/tailwind_bg.png new file mode 100644 index 000000000..edc40be8d Binary files /dev/null and b/examples/content-svelte/images/tailwind_bg.png differ diff --git a/examples/content-svelte/images/typescript.png b/examples/content-svelte/images/typescript.png new file mode 100644 index 000000000..936146940 Binary files /dev/null and b/examples/content-svelte/images/typescript.png differ diff --git a/examples/content-svelte/manifest.json b/examples/content-svelte/manifest.json new file mode 100644 index 000000000..d277c75fe --- /dev/null +++ b/examples/content-svelte/manifest.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json.schemastore.org/chrome-manifest.json", + "manifest_version": 3, + "version": "0.0.1", + "name": "Content Scripts Svelte", + "description": "An Extension.js example.", + "icons": { + "48": "images/extension_48.png" + }, + "background": { + "chromium:service_worker": "background.ts", + "firefox:scripts": ["background.ts"] + }, + "content_scripts": [ + { + "matches": [""], + "js": ["./content/scripts.ts"] + } + ] +} diff --git a/examples/content-svelte/package.json b/examples/content-svelte/package.json new file mode 100644 index 000000000..23a4f2b42 --- /dev/null +++ b/examples/content-svelte/package.json @@ -0,0 +1,20 @@ +{ + "private": true, + "name": "content-svelte", + "description": "An Extension.js example.", + "version": "0.0.1", + "author": { + "name": "Cezar Augusto", + "email": "boss@cezaraugusto.net", + "url": "https://cezaraugusto.com" + }, + "license": "MIT", + "dependencies": { + "svelte": "5.15.0", + "tailwindcss": "^3.4.1" + }, + "devDependencies": { + "@tsconfig/svelte": "5.0.4", + "typescript": "5.3.3" + } +} diff --git a/examples/content-svelte/postcss.config.js b/examples/content-svelte/postcss.config.js new file mode 100644 index 000000000..85f717cc0 --- /dev/null +++ b/examples/content-svelte/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {} + } +} diff --git a/examples/content-svelte/public/extension.svg b/examples/content-svelte/public/extension.svg new file mode 100644 index 000000000..ebe0773a6 --- /dev/null +++ b/examples/content-svelte/public/extension.svg @@ -0,0 +1,3 @@ + + + diff --git a/examples/content-svelte/public/logo.png b/examples/content-svelte/public/logo.png new file mode 100644 index 000000000..a520d188d Binary files /dev/null and b/examples/content-svelte/public/logo.png differ diff --git a/examples/content-svelte/tailwind.config.js b/examples/content-svelte/tailwind.config.js new file mode 100644 index 000000000..b8a301e04 --- /dev/null +++ b/examples/content-svelte/tailwind.config.js @@ -0,0 +1,8 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ['./content/**/*.svelte', './content/**/*.ts'], + theme: { + extend: {} + }, + plugins: [] +} diff --git a/examples/content-svelte/template.spec.ts b/examples/content-svelte/template.spec.ts new file mode 100644 index 000000000..0a77244f2 --- /dev/null +++ b/examples/content-svelte/template.spec.ts @@ -0,0 +1,80 @@ +import path from 'path' +import {execSync} from 'child_process' +import {extensionFixtures, getShadowRootElement} from '../extension-fixtures' + +const exampleDir = 'examples/content-svelte' +const pathToExtension = path.join(__dirname, `dist/chrome`) +const test = extensionFixtures(pathToExtension, true) + +test.beforeAll(async () => { + execSync(`pnpm extension build ${exampleDir}`, { + cwd: path.join(__dirname, '..') + }) +}) + +test('should exist an element with the id extension-root', async ({page}) => { + await page.goto('https://extension.js.org/') + const shadowRootHandle = await page + .locator('#extension-root') + .evaluateHandle((host: HTMLElement) => host.shadowRoot) + + // Check that the Shadow DOM exists + test.expect(shadowRootHandle).not.toBeNull() + + // Verify if the Shadow DOM contains children + const shadowChildrenCount = await shadowRootHandle.evaluate( + (shadowRoot: ShadowRoot) => shadowRoot.children.length + ) + test.expect(shadowChildrenCount).toBeGreaterThan(0) +}) + +test('should exist an h2 element with specified content', async ({page}) => { + await page.goto('https://extension.js.org/') + const h2 = await getShadowRootElement(page, '#extension-root', 'h2') + if (!h2) { + throw new Error('h2 element not found in Shadow DOM') + } + + const textContent = await h2.evaluate((node) => node.textContent) + test.expect(textContent).toContain('This is a content script') +}) + +test('should exist a default color value', async ({page}) => { + await page.goto('https://extension.js.org/') + const h2 = await getShadowRootElement(page, '#extension-root', 'h2') + if (!h2) { + throw new Error('h2 element not found in Shadow DOM') + } + + const color = await h2.evaluate((node) => + window.getComputedStyle(node as HTMLElement).getPropertyValue('color') + ) + test.expect(color).toEqual('rgb(255, 255, 255)') +}) + +test('should load all images successfully', async ({page}) => { + await page.goto('https://extension.js.org/') + const shadowRoot = await page + .locator('#extension-root') + .evaluateHandle((host: HTMLElement) => host.shadowRoot) + + const imagesHandle = await shadowRoot.evaluateHandle((shadow: ShadowRoot) => + Array.from(shadow.querySelectorAll('img')) + ) + + const imageHandles = await imagesHandle.getProperties() + const results: boolean[] = [] + + for (const [, imageHandle] of imageHandles) { + const naturalWidth = await imageHandle.evaluate( + (img) => (img as HTMLImageElement).naturalWidth + ) + const naturalHeight = await imageHandle.evaluate( + (img) => (img as HTMLImageElement).naturalHeight + ) + const loadedSuccessfully = naturalWidth > 0 && naturalHeight > 0 + results.push(loadedSuccessfully) + } + + test.expect(results.every((result) => result)).toBeTruthy() +}) diff --git a/examples/content-svelte/tsconfig.json b/examples/content-svelte/tsconfig.json new file mode 100644 index 000000000..415f65637 --- /dev/null +++ b/examples/content-svelte/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "@tsconfig/svelte/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "jsx": "react-jsx", + "lib": ["dom", "dom.iterable", "esnext"], + "moduleResolution": "node", + "module": "esnext", + "noEmit": true, + "resolveJsonModule": true, + "strict": true, + "target": "esnext", + "verbatimModuleSyntax": true, + "useDefineForClassFields": true, + "skipLibCheck": true + }, + "include": ["./", "extension-env.d.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/examples/content/content/scripts.js b/examples/content/content/scripts.js index 46150272d..f24dd9a7c 100644 --- a/examples/content/content/scripts.js +++ b/examples/content/content/scripts.js @@ -3,11 +3,15 @@ import logo from '../images/logo.svg' console.log('hello from content_scripts') +let unmount +import.meta.webpackHot.accept() +import.meta.webpackHot.dispose(() => unmount()) + if (document.readyState === 'complete') { - initial() + unmount = initial() || (() => {}) } else { document.addEventListener('readystatechange', () => { - if (document.readyState === 'complete') initial() + if (document.readyState === 'complete') unmount = initial() || (() => {}) }) } @@ -22,23 +26,61 @@ function initial() { const shadowRoot = rootDiv.attachShadow({mode: 'open'}) // Inform Extension.js that the shadow root is available. - window.__EXTENSION_SHADOW_ROOT__ = shadowRoot - - shadowRoot.innerHTML = ` -
- -

- Welcome to your Content Script Extension -

-

- Learn more about creating cross-browser extensions at - https://extension.js.org - -

-
- ` + // window.__EXTENSION_SHADOW_ROOT__ = shadowRoot + + const style = document.createElement('style') + shadowRoot.appendChild(style) + + const contentDiv = document.createElement('div') + contentDiv.className = 'content_script' + + const logoImg = document.createElement('img') + logoImg.className = 'content_logo' + logoImg.src = logo + contentDiv.appendChild(logoImg) + + const title = document.createElement('h1') + title.className = 'content_title' + title.textContent = 'Welcome to your Content Script Extension' + contentDiv.appendChild(title) + + const description = document.createElement('p') + description.className = 'content_description' + + const text = document.createTextNode( + 'Learn more about creating cross-browser extensions at ' + ) + description.appendChild(text) + + const link = document.createElement('a') + link.className = 'underline hover:no-underline' + link.href = 'https://extension.js.org' + link.target = '_blank' + link.textContent = 'https://extension.js.org' + + description.appendChild(link) + contentDiv.appendChild(description) + + shadowRoot.appendChild(contentDiv) + + // Handle CSS injection + fetchCSS().then((response) => { + style.textContent = response + }) + + import.meta.webpackHot?.accept('./styles.css', () => { + fetchCSS().then((response) => { + style.textContent = response + }) + }) + + return () => { + rootDiv.remove() + } +} + +async function fetchCSS() { + const response = await fetch(new URL('./styles.css', import.meta.url)) + const text = await response.text() + return response.ok ? text : Promise.reject(text) } diff --git a/examples/data.ts b/examples/data.ts index 57f8ff0b2..37dd89df5 100644 --- a/examples/data.ts +++ b/examples/data.ts @@ -365,6 +365,15 @@ const FRAMEWORK_TEMPLATES: Template[] = [ hasEnv: false, configFiles: ['postcss.config.js', 'tailwind.config.js', 'tsconfig.json'] }, + { + name: 'content-svelte', + uiContext: ['content'], + uiFramework: 'svelte', + css: 'css', + hasBackground: false, + hasEnv: false, + configFiles: ['postcss.config.js', 'tailwind.config.js', 'tsconfig.json'] + }, { name: 'new-svelte', uiContext: ['newTab'], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c03bacedb..5f9759f5f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -227,6 +227,22 @@ importers: specifier: ^1.77.8 version: 1.79.4 + examples/content-svelte: + dependencies: + svelte: + specifier: 5.15.0 + version: 5.15.0 + tailwindcss: + specifier: ^3.4.1 + version: 3.4.13(ts-node@10.9.2(@swc/core@1.10.1)(@types/node@22.10.1)(typescript@5.3.3)) + devDependencies: + '@tsconfig/svelte': + specifier: 5.0.4 + version: 5.0.4 + typescript: + specifier: 5.3.3 + version: 5.3.3 + examples/content-tailwind: dependencies: tailwindcss: @@ -707,8 +723,8 @@ importers: specifier: ^7.0.6 version: 7.0.6 css-loader: - specifier: ^6.10.0 - version: 6.11.0(webpack@5.97.1(@swc/core@1.10.1)(esbuild@0.24.0)) + specifier: ^7.1.2 + version: 7.1.2(webpack@5.97.1(@swc/core@1.10.1)(esbuild@0.24.0)) csv-loader: specifier: ^3.0.5 version: 3.0.5 @@ -3716,12 +3732,12 @@ packages: peerDependencies: postcss: ^8.4 - css-loader@6.11.0: - resolution: {integrity: sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g==} - engines: {node: '>= 12.13.0'} + css-loader@7.1.2: + resolution: {integrity: sha512-6WvYYn7l/XEGN8Xu2vWFt9nVzrCn39vKyTEFf/ExEyoksJjjSZV/0/35XPlMbpnr6VGhZIUg5yJrL8tGfes/FA==} + engines: {node: '>= 18.12.0'} peerDependencies: '@rspack/core': 0.x || 1.x - webpack: ^5.0.0 + webpack: ^5.27.0 peerDependenciesMeta: '@rspack/core': optional: true @@ -5726,14 +5742,14 @@ packages: peerDependencies: postcss: ^8.1.0 - postcss-modules-local-by-default@4.0.5: - resolution: {integrity: sha512-6MieY7sIfTK0hYfafw1OMEG+2bg8Q1ocHCpoWLqOKj3JXlKu4G7btkmM/B7lFubYkYWmRSPLZi5chid63ZaZYw==} + postcss-modules-local-by-default@4.2.0: + resolution: {integrity: sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==} engines: {node: ^10 || ^12 || >= 14} peerDependencies: postcss: ^8.1.0 - postcss-modules-scope@3.2.0: - resolution: {integrity: sha512-oq+g1ssrsZOsx9M96c5w8laRmvEu9C3adDSjI8oTcbfkrTE8hx/zfyobUoWIxaKPO8bt6S62kxpw5GqypEw1QQ==} + postcss-modules-scope@3.2.1: + resolution: {integrity: sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==} engines: {node: ^10 || ^12 || >= 14} peerDependencies: postcss: ^8.1.0 @@ -10495,13 +10511,13 @@ snapshots: postcss-value-parser: 4.2.0 optional: true - css-loader@6.11.0(webpack@5.97.1(@swc/core@1.10.1)(esbuild@0.24.0)): + css-loader@7.1.2(webpack@5.97.1(@swc/core@1.10.1)(esbuild@0.24.0)): dependencies: icss-utils: 5.1.0(postcss@8.4.49) postcss: 8.4.49 postcss-modules-extract-imports: 3.1.0(postcss@8.4.49) - postcss-modules-local-by-default: 4.0.5(postcss@8.4.49) - postcss-modules-scope: 3.2.0(postcss@8.4.49) + postcss-modules-local-by-default: 4.2.0(postcss@8.4.49) + postcss-modules-scope: 3.2.1(postcss@8.4.49) postcss-modules-values: 4.0.0(postcss@8.4.49) postcss-value-parser: 4.2.0 semver: 7.6.3 @@ -12695,17 +12711,17 @@ snapshots: dependencies: postcss: 8.4.49 - postcss-modules-local-by-default@4.0.5(postcss@8.4.49): + postcss-modules-local-by-default@4.2.0(postcss@8.4.49): dependencies: icss-utils: 5.1.0(postcss@8.4.49) postcss: 8.4.49 - postcss-selector-parser: 6.1.2 + postcss-selector-parser: 7.0.0 postcss-value-parser: 4.2.0 - postcss-modules-scope@3.2.0(postcss@8.4.49): + postcss-modules-scope@3.2.1(postcss@8.4.49): dependencies: postcss: 8.4.49 - postcss-selector-parser: 6.1.2 + postcss-selector-parser: 7.0.0 postcss-modules-values@4.0.0(postcss@8.4.49): dependencies: @@ -12865,7 +12881,6 @@ snapshots: dependencies: cssesc: 3.0.0 util-deprecate: 1.0.2 - optional: true postcss-value-parser@4.2.0: {} diff --git a/programs/cli/types/index.d.ts b/programs/cli/types/index.d.ts index 7cb12422b..a364e71c4 100644 --- a/programs/cli/types/index.d.ts +++ b/programs/cli/types/index.d.ts @@ -22,8 +22,14 @@ interface ImportMetaEnv { readonly EXTENSION_MODE: NodeJS.ProcessEnv['EXTENSION_MODE'] } +interface WebpackHot { + accept: (...args: string[]) => void + dispose: (...args: string[]) => void +} + interface ImportMeta { readonly env: ImportMetaEnv + readonly webpackHot: WebpackHot } interface Window { diff --git a/programs/develop/install_scripts.sh b/programs/develop/install_scripts.sh index 21b9b0bd4..4031d61a6 100644 --- a/programs/develop/install_scripts.sh +++ b/programs/develop/install_scripts.sh @@ -14,7 +14,6 @@ resolve_plugin_files=( ) scripts_plugin_files=( - "$(dirname "$0")/webpack/plugin-extension/feature-scripts/steps/inject-content-css-during-dev.ts" "$(dirname "$0")/webpack/plugin-extension/feature-scripts/steps/add-hmr-accept-code.ts" ) @@ -89,8 +88,6 @@ done echo '►►► Setting up client helper files' static_files=( - "$(dirname "$0")/tailwind.config.js" - "$(dirname "$0")/stylelint.config.json" "$(dirname "$0")/webpack/plugin-reload/extensions" ) diff --git a/programs/develop/package.json b/programs/develop/package.json index 07bcfc8ab..26e3d9ad0 100644 --- a/programs/develop/package.json +++ b/programs/develop/package.json @@ -44,7 +44,7 @@ "chrome-location": "^1.2.1", "content-security-policy-parser": "^0.6.0", "cross-spawn": "^7.0.6", - "css-loader": "^6.10.0", + "css-loader": "^7.1.2", "csv-loader": "^3.0.5", "dotenv": "^16.4.7", "dotenv-webpack": "^8.1.0", diff --git a/programs/develop/webpack/lib/messages.ts b/programs/develop/webpack/lib/messages.ts index 975cf96ab..7bc8d0e93 100644 --- a/programs/develop/webpack/lib/messages.ts +++ b/programs/develop/webpack/lib/messages.ts @@ -620,3 +620,14 @@ export function noExtensionIdError() { `of your ${brightYellow('extension.config.js')} is defined as your extension URL.` ) } + +export function deprecatedShadowRoot() { + return ( + `${getLoggingPrefix('DEPRECATION', 'warn')} Using ` + + `${brightYellow('window.__EXTENSION_SHADOW_ROOT__')} in content_scripts is deprecated\n` + + 'and will be removed in a future version of Extension.js. To use content_scripts with\nthe shadow DOM, ' + + 'see one of the many examples at:\nhttps://github.com/extension-js/extension.js/tree/main/examples\n\n' + + 'If you really need to use the shadow DOM as-is, the latest version of Extension.js\n' + + `to support it is ${'extension@2.0.0-beta.9'}.\n` + ) +} diff --git a/programs/develop/webpack/plugin-css/common-style-loaders.ts b/programs/develop/webpack/plugin-css/common-style-loaders.ts index d5bf1346b..b32474917 100644 --- a/programs/develop/webpack/plugin-css/common-style-loaders.ts +++ b/programs/develop/webpack/plugin-css/common-style-loaders.ts @@ -1,5 +1,4 @@ import {type RuleSetRule} from 'webpack' -import MiniCssExtractPlugin from 'mini-css-extract-plugin' import {DevOptions} from '../../commands/commands-lib/config-types' import {isUsingTailwind} from './css-tools/tailwind' import {isUsingSass} from './css-tools/sass' @@ -13,64 +12,11 @@ export interface StyleLoaderOptions { useShadowDom: boolean } -function whereToInsertStyleTag(element: HTMLElement) { - // Function to insert the style tag - const insertElement = () => { - // @ts-expect-error - global reference. - const shadowRoot = window.__EXTENSION_SHADOW_ROOT__ - - if (shadowRoot) { - shadowRoot.appendChild(element) - console.log('Element inserted into shadowRoot') - } else { - document.head.appendChild(element) - console.log('Element inserted into document.head') - } - } - - // If Shadow DOM exists, insert immediately - // @ts-expect-error - global reference. - if (window.__EXTENSION_SHADOW_ROOT__) { - insertElement() - return - } - - // Use a MutationObserver to wait for the Shadow DOM to be created - const observer = new MutationObserver(() => { - // @ts-expect-error - global reference. - if (window.__EXTENSION_SHADOW_ROOT__) { - insertElement() - observer.disconnect() // Disconnect once the Shadow DOM is found - } - }) - - observer.observe(document.body, {childList: true, subtree: true}) - console.log('Observer waiting for Shadow DOM...') -} - export async function commonStyleLoaders( projectPath: string, opts: StyleLoaderOptions ): Promise { - const miniCssLoader = MiniCssExtractPlugin.loader - const styleLoaders: RuleSetRule['use'] = [ - opts.useMiniCssExtractPlugin - ? miniCssLoader - : { - loader: require.resolve('style-loader'), - options: - (opts.useShadowDom && { - insert: whereToInsertStyleTag - }) || - undefined - }, - { - loader: require.resolve('css-loader'), - options: { - sourceMap: opts.mode === 'development' - } - } - ].filter(Boolean) + const styleLoaders: RuleSetRule['use'] = [] if ( isUsingTailwind(projectPath) || diff --git a/programs/develop/webpack/plugin-css/index.ts b/programs/develop/webpack/plugin-css/index.ts index 93ce8ea74..68a58bc0a 100644 --- a/programs/develop/webpack/plugin-css/index.ts +++ b/programs/develop/webpack/plugin-css/index.ts @@ -4,8 +4,8 @@ import { type Compiler, type RuleSetRule } from 'webpack' -import MiniCssExtractPlugin from 'mini-css-extract-plugin' import {commonStyleLoaders} from './common-style-loaders' +import {legacyStyleLoaders} from './legacy-style-loaders' import {PluginInterface} from '../webpack-types' import {maybeUseSass} from './css-tools/sass' import {maybeUseLess} from './css-tools/less' @@ -23,8 +23,7 @@ export class CssPlugin { private async configureOptions(compiler: Compiler) { const mode = compiler.options.mode || 'development' const projectPath = path.dirname(this.manifestPath) - - const plugins: WebpackPluginInstance[] = [new MiniCssExtractPlugin()] + const plugins: WebpackPluginInstance[] = [] plugins.forEach((plugin) => plugin.apply(compiler)) @@ -36,9 +35,11 @@ export class CssPlugin { test: /\.css$/, exclude: /\.module\.css$/, oneOf: [ + // Legacy inline style loader. This will + // be deprecated in v2.0.0 { resourceQuery: /inline_style/, - use: await commonStyleLoaders(projectPath, { + use: await legacyStyleLoaders(projectPath, { mode: mode as 'development' | 'production', useMiniCssExtractPlugin: false, useShadowDom: true @@ -47,7 +48,7 @@ export class CssPlugin { { use: await commonStyleLoaders(projectPath, { mode: mode as 'development' | 'production', - useMiniCssExtractPlugin: mode === 'production', + useMiniCssExtractPlugin: false, useShadowDom: false }) } diff --git a/programs/develop/webpack/plugin-css/legacy-style-loaders.ts b/programs/develop/webpack/plugin-css/legacy-style-loaders.ts new file mode 100644 index 000000000..11968920e --- /dev/null +++ b/programs/develop/webpack/plugin-css/legacy-style-loaders.ts @@ -0,0 +1,109 @@ +import {type RuleSetRule} from 'webpack' +import MiniCssExtractPlugin from 'mini-css-extract-plugin' +import {DevOptions} from '../../commands/commands-lib/config-types' +import {isUsingTailwind} from './css-tools/tailwind' +import {isUsingSass} from './css-tools/sass' +import {isUsingLess} from './css-tools/less' +import {maybeUsePostCss} from './css-tools/postcss' + +export interface StyleLoaderOptions { + mode: DevOptions['mode'] + useMiniCssExtractPlugin: boolean + loader?: string + useShadowDom: boolean +} + +function whereToInsertStyleTag(element: HTMLElement) { + // Function to insert the style tag + const insertElement = () => { + // @ts-expect-error - global reference. + const shadowRoot = window.__EXTENSION_SHADOW_ROOT__ + + if (shadowRoot) { + shadowRoot.appendChild(element) + console.log('Element inserted into shadowRoot') + } else { + document.head.appendChild(element) + console.log('Element inserted into document.head') + } + } + + // If Shadow DOM exists, insert immediately + // @ts-expect-error - global reference. + if (window.__EXTENSION_SHADOW_ROOT__) { + insertElement() + return + } + + // Use a MutationObserver to wait for the Shadow DOM to be created + const observer = new MutationObserver(() => { + // @ts-expect-error - global reference. + if (window.__EXTENSION_SHADOW_ROOT__) { + insertElement() + observer.disconnect() // Disconnect once the Shadow DOM is found + } + }) + + observer.observe(document.body, {childList: true, subtree: true}) + console.log('Observer waiting for Shadow DOM...') +} + +export async function legacyStyleLoaders( + projectPath: string, + opts: StyleLoaderOptions +): Promise { + const miniCssLoader = MiniCssExtractPlugin.loader + const styleLoaders: RuleSetRule['use'] = [ + opts.useMiniCssExtractPlugin + ? { + loader: miniCssLoader + } + : { + loader: require.resolve('style-loader'), + options: { + ...(opts.useShadowDom && { + insert: whereToInsertStyleTag + }) + } + }, + { + loader: require.resolve('css-loader'), + options: { + sourceMap: opts.mode === 'development' + } + } + ].filter(Boolean) + + if ( + isUsingTailwind(projectPath) || + isUsingSass(projectPath) || + isUsingLess(projectPath) + ) { + const maybeInstallPostCss = await maybeUsePostCss(projectPath, opts) + if (maybeInstallPostCss.loader) { + styleLoaders.push(maybeInstallPostCss) + } + } + + if (opts.loader) { + styleLoaders.push( + ...[ + { + loader: require.resolve('resolve-url-loader'), + options: { + sourceMap: opts.mode === 'development', + root: projectPath + } + }, + { + loader: require.resolve(opts.loader), + options: { + sourceMap: opts.mode === 'development' + } + } + ] + ) + } + + return styleLoaders.filter(Boolean) +} diff --git a/programs/develop/webpack/plugin-extension/feature-html/html-lib/patch-html.ts b/programs/develop/webpack/plugin-extension/feature-html/html-lib/patch-html.ts index eb6885bcb..3b7aeaa43 100644 --- a/programs/develop/webpack/plugin-extension/feature-html/html-lib/patch-html.ts +++ b/programs/develop/webpack/plugin-extension/feature-html/html-lib/patch-html.ts @@ -144,20 +144,14 @@ export function patchHtml( // since we use style-loader to enable HMR for CSS files // and it inlines the styles into the page. if (hasCssEntry) { - // In development mode we use style-loader to enable HMR for CSS files - // and it inlines the styles into the page. - // In production mode we use MiniCssExtractPlugin to extract the CSS - // into a separate file. - if (compilation.options.mode === 'production') { - const linkTag: {attrs: {name: string; value: string}[]} = - parse5utils.createNode('link') - linkTag.attrs = [ - {name: 'rel', value: 'stylesheet'}, - {name: 'href', value: getFilePath(feature, '.css', true)} - ] + const linkTag: {attrs: {name: string; value: string}[]} = + parse5utils.createNode('link') + linkTag.attrs = [ + {name: 'rel', value: 'stylesheet'}, + {name: 'href', value: getFilePath(feature, '.css', true)} + ] - parse5utils.append(htmlChildNode, linkTag) - } + parse5utils.append(htmlChildNode, linkTag) } } diff --git a/programs/develop/webpack/plugin-extension/feature-manifest/steps/emit-manifest.ts b/programs/develop/webpack/plugin-extension/feature-manifest/steps/emit-manifest.ts index 2fe286342..cfb8e6100 100644 --- a/programs/develop/webpack/plugin-extension/feature-manifest/steps/emit-manifest.ts +++ b/programs/develop/webpack/plugin-extension/feature-manifest/steps/emit-manifest.ts @@ -12,13 +12,13 @@ export class EmitManifest { } apply(compiler: Compiler): void { - compiler.hooks.compilation.tap( + compiler.hooks.thisCompilation.tap( 'manifest:emit-manifest', (compilation: Compilation) => { compilation.hooks.processAssets.tap( { name: 'manifest:emit-manifest', - stage: Compilation.PROCESS_ASSETS_STAGE_ADDITIONS + stage: Compilation.PROCESS_ASSETS_STAGE_PRE_PROCESS }, () => { const manifestPath = this.manifestPath diff --git a/programs/develop/webpack/plugin-extension/feature-scripts/index.ts b/programs/develop/webpack/plugin-extension/feature-scripts/index.ts index a95f51b4c..141da50f7 100644 --- a/programs/develop/webpack/plugin-extension/feature-scripts/index.ts +++ b/programs/develop/webpack/plugin-extension/feature-scripts/index.ts @@ -4,6 +4,7 @@ import {type FilepathList, type PluginInterface} from '../../webpack-types' import {AddScripts} from './steps/add-scripts' import {AddPublicPathRuntimeModule} from './steps/add-public-path-runtime-module' import {AddPublicPathForMainWorld} from './steps/add-public-path-for-main-world' +import {DeprecatedShadowRoot} from './steps/deprecated-shadow-root' import {DevOptions} from '../../../module' /** @@ -45,31 +46,6 @@ export class ScriptsPlugin { excludeList: this.excludeList || {} }).apply(compiler) - // In development: Extracts the content_scripts css files - // from content_scripts and injects them as dynamic imports - // so we can benefit from HMR. In production we adds the CSS - // files to the entry points along with other content_script files, - // so this is not necessary. - if (compiler.options.mode === 'development') { - compiler.options.module.rules.push({ - test: /\.(js|mjs|jsx|mjsx|ts|mts|tsx|mtsx)$/, - include: [path.dirname(this.manifestPath)], - exclude: /node_modules/, - use: [ - { - loader: require.resolve( - path.join(__dirname, 'inject-content-css-during-dev.js') - ), - options: { - manifestPath: this.manifestPath, - includeList: this.includeList || {}, - excludeList: this.excludeList || {} - } - } - ] - }) - } - // 2 - Ensure scripts are HMR enabled by adding the HMR accept code. if (compiler.options.mode === 'development') { compiler.options.module.rules.push({ @@ -107,5 +83,8 @@ export class ScriptsPlugin { includeList: this.includeList || {}, excludeList: this.excludeList || {} }).apply(compiler) + + // 5 - Deprecate the use of window.__EXTENSION_SHADOW_ROOT__ + new DeprecatedShadowRoot().apply(compiler) } } diff --git a/programs/develop/webpack/plugin-extension/feature-scripts/steps/add-hmr-accept-code.ts b/programs/develop/webpack/plugin-extension/feature-scripts/steps/add-hmr-accept-code.ts index ca763fe1c..14454bda6 100644 --- a/programs/develop/webpack/plugin-extension/feature-scripts/steps/add-hmr-accept-code.ts +++ b/programs/develop/webpack/plugin-extension/feature-scripts/steps/add-hmr-accept-code.ts @@ -4,6 +4,31 @@ import {urlToRequest} from 'loader-utils' import {validate} from 'schema-utils' import {type Schema} from 'schema-utils/declarations/validate' import {type LoaderContext} from '../../../webpack-types' +import * as messages from '../../../lib/messages' + +function whichFramework(projectPath: string): string | null { + const packageJsonPath = path.join(projectPath, 'package.json') + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')) + const frameworks = [ + 'react', + 'vue', + '@angular/core', + 'svelte', + 'solid-js', + 'preact' + ] + + for (const framework of frameworks) { + if ( + packageJson.dependencies[framework] || + packageJson.devDependencies[framework] + ) { + return framework + } + } + + return null +} function isUsingJSFramework(projectPath: string): boolean { const packageJsonPath = path.join(projectPath, 'package.json') @@ -62,7 +87,7 @@ export default function (this: LoaderContext, source: string) { if (this._compilation?.options.mode === 'production') return source const url = urlToRequest(this.resourcePath) - const reloadCode = ` + const standardReloadCode = ` if (import.meta.webpackHot) { import.meta.webpackHot.accept() }; ` @@ -74,7 +99,7 @@ if (import.meta.webpackHot) { import.meta.webpackHot.accept() }; for (const bgScript of manifest.background.scripts) { const absoluteUrl = path.resolve(projectPath, bgScript as string) if (url.includes(absoluteUrl)) { - return `${reloadCode}${source}` + return `${standardReloadCode}${source}` } } } @@ -82,14 +107,33 @@ if (import.meta.webpackHot) { import.meta.webpackHot.accept() }; // 2 - Handle content_scripts. if (manifest.content_scripts) { - if (!isUsingJSFramework(projectPath)) { - for (const contentScript of manifest.content_scripts) { - if (!contentScript.js) continue - for (const js of contentScript.js) { - const absoluteUrl = path.resolve(projectPath, js as string) - if (url.includes(absoluteUrl)) { - return `${reloadCode}${source}` + for (const contentScript of manifest.content_scripts) { + if (!contentScript.js) continue + + for (const js of contentScript.js) { + const absoluteUrl = path.resolve(projectPath, js as string) + + if (url.includes(absoluteUrl)) { + if (source.includes('__EXTENSION_SHADOW_ROOT__')) { + console.warn(messages.deprecatedShadowRoot()) + } + + if (isUsingJSFramework(projectPath)) { + const framework = whichFramework(projectPath) + + if (source.includes('use shadow-dom')) { + switch (framework) { + // case 'react': + // return `${standardReloadCode}${source}` + default: + return `${standardReloadCode}${source}` + } + } + + return `${source}` } + + return `${standardReloadCode}${source}` } } } @@ -100,7 +144,7 @@ if (import.meta.webpackHot) { import.meta.webpackHot.accept() }; for (const userScript of manifest.user_scripts) { const absoluteUrl = path.resolve(projectPath, userScript as string) if (url.includes(absoluteUrl)) { - return `${reloadCode}${source}` + return `${standardReloadCode}${source}` } } } diff --git a/programs/develop/webpack/plugin-extension/feature-scripts/steps/add-scripts.ts b/programs/develop/webpack/plugin-extension/feature-scripts/steps/add-scripts.ts index db0fb7f7e..33a9fbe87 100644 --- a/programs/develop/webpack/plugin-extension/feature-scripts/steps/add-scripts.ts +++ b/programs/develop/webpack/plugin-extension/feature-scripts/steps/add-scripts.ts @@ -22,16 +22,7 @@ export class AddScripts { for (const [feature, scriptPath] of Object.entries(scriptFields)) { const scriptImports = getScriptEntries(scriptPath, this.excludeList) const cssImports = getCssEntries(scriptPath, this.excludeList) - const entryImports = [...scriptImports] - - // During development, we extract the content_scripts css files from - // content_scripts and inject them as dynamic imports - // so we can benefit from HMR. - // In production we don't need that, so we add the files to the entry points - // along with other content_script files. - if (compiler.options.mode === 'production') { - entryImports.push(...cssImports) - } + const entryImports = [...scriptImports, ...cssImports] if (cssImports.length || scriptImports.length) { newEntries[feature] = {import: entryImports} diff --git a/programs/develop/webpack/plugin-extension/feature-scripts/steps/deprecated-shadow-root.ts b/programs/develop/webpack/plugin-extension/feature-scripts/steps/deprecated-shadow-root.ts new file mode 100644 index 000000000..687906dd8 --- /dev/null +++ b/programs/develop/webpack/plugin-extension/feature-scripts/steps/deprecated-shadow-root.ts @@ -0,0 +1,24 @@ +import {type Compiler} from 'webpack' + +export class DeprecatedShadowRoot { + apply(compiler: Compiler) { + // Hook into the beforeRun stage to suppress CSS support warnings + compiler.hooks.beforeRun.tap('deprecated-shadow-root', () => { + // Clear any existing webpack warnings + compiler.options.infrastructureLogging = { + ...compiler.options.infrastructureLogging, + level: 'none' // Use level instead of warnings to control logging + } + + // Also intercept console warnings + const originalWarnings = console.warn + console.warn = function (...args: any[]) { + const message = args.join(' ') + if (message.includes('experiments.css')) { + return + } + originalWarnings.apply(console, args) + } + }) + } +} diff --git a/programs/develop/webpack/plugin-extension/feature-scripts/steps/hmr-accept-code/react.ts b/programs/develop/webpack/plugin-extension/feature-scripts/steps/hmr-accept-code/react.ts new file mode 100644 index 000000000..8b1509c61 --- /dev/null +++ b/programs/develop/webpack/plugin-extension/feature-scripts/steps/hmr-accept-code/react.ts @@ -0,0 +1,102 @@ +// import ReactDOM from 'react-dom/client' +// import ContentScript from './scripts' + +let unmount: () => void +// @ts-expect-error - global reference. +import.meta.webpackHot?.accept() +// @ts-expect-error - global reference. +import.meta.webpackHot?.dispose(() => unmount?.()) + +if (document.readyState === 'complete') { + unmount = getShadowRoot() || (() => {}) +} else { + document.addEventListener('readystatechange', () => { + if (document.readyState === 'complete') + unmount = getShadowRoot() || (() => {}) + }) +} + +async function fetchCSS(stylesheetPath: string) { + // @ts-expect-error - global reference. + const response = await fetch(new URL(stylesheetPath, import.meta.url)) + const text = await response.text() + return response.ok ? text : Promise.reject(text) +} + +export function getShadowRoot( + element?: string, + id?: string, + stylesheets?: string[] + // jsx?: React.ReactNode +) { + // Create a new div element and append it to the document's body + const rootElement = document.createElement(element || 'div') + rootElement.id = id || 'extension-root' + document.body.appendChild(rootElement) + + // Injecting content_scripts inside a shadow dom + // prevents conflicts with the host page's styles. + // This way, styles from the extension won't leak into the host page. + const shadowRoot = rootElement.attachShadow({mode: 'open'}) + + // Create an array to store style elements for hot reloading + const styleElements: HTMLStyleElement[] = [] + + // Create and append style elements for each stylesheet + stylesheets?.forEach((stylesheet) => { + const style = document.createElement('style') + shadowRoot.appendChild(style) + styleElements.push(style) + fetchCSS(stylesheet).then((response) => { + style.textContent = response + }) + }) + + // Set up hot reloading for each stylesheet + stylesheets?.forEach((stylesheet, index) => { + // @ts-expect-error - global reference. + import.meta.webpackHot?.accept(stylesheet, () => { + fetchCSS(stylesheet).then((response) => { + if (styleElements[index]) { + styleElements[index].textContent = response + } + }) + }) + }) + + // const mountingPoint = ReactDOM.createRoot(shadowRoot) + // mountingPoint.render(jsx) + + return () => { + // mountingPoint.unmount() + rootElement.remove() + } +} + +// Usage in a loader +// getShadowRoot('div', 'extension-root', ['./styles.css'], ) + +// Sample user code + +// ```tsx +// 'use shadow-dom' +// +// import ContentApp from './ContentApp' +// // Any imported CSS files will be automatically +// // added the shadow DOM +// import './styles.css' +// +// // Create a root div element with id "'extension-root'" +// // and append it to the document's body. Note that +// // the default export is required for Extension.js to +// // know what element to mount in the shadow root. +// export default function ContentScript() { +// console.log('Extension running...') + +// return ( +//
+// +//
+// ) +// } +// ``` diff --git a/programs/develop/webpack/plugin-extension/feature-scripts/steps/inject-content-css-during-dev.ts b/programs/develop/webpack/plugin-extension/feature-scripts/steps/inject-content-css-during-dev.ts deleted file mode 100644 index 8f7db4253..000000000 --- a/programs/develop/webpack/plugin-extension/feature-scripts/steps/inject-content-css-during-dev.ts +++ /dev/null @@ -1,130 +0,0 @@ -import path from 'path' -import {urlToRequest} from 'loader-utils' -import {validate} from 'schema-utils' -import {type LoaderContext} from '../../../webpack-types' -import {type Schema} from 'schema-utils/declarations/validate' -import {getRelativePath} from '../../../lib/utils' -import {getScriptEntries, getCssEntries} from '../scripts-lib/utils' - -const schema: Schema = { - type: 'object', - properties: { - test: { - type: 'string' - }, - manifestPath: { - type: 'string' - }, - includeList: { - type: 'object' - }, - excludeList: { - type: 'object' - } - } -} - -const beautifulFileContent = `/** - * Welcome to to your content_scripts CSS file during development! - * To speed up the development process, your styles - * are being injected directly into the head of the webpage, - * and will be removed when you build your application, along - * with this file. If you are seeing this file in a production build, - * it means that something is wrong with your build configuration. - */` - -export default function injectContentCssDuringDev( - this: LoaderContext, - source: string -) { - const options = this.getOptions() - - validate(schema, options, { - name: 'scripts:inject-content-css-during-dev', - baseDataPath: 'options' - }) - - const scriptFields = options.includeList || {} - const cssImportPaths: Array<{ - feature: string - scriptPath: string - cssImports: string[] - }> = [] - const scriptEntries = Object.entries(scriptFields).filter( - ([feature, scriptPath]) => feature.startsWith('content') && scriptPath - ) - - if (!scriptEntries.length) return source - - // The goal of this plugin is to enable HMR to standalone content_script.css - // files. To do that, we get all CSS files defined and inject them - // as dynamic imports in the content_script.js file. - for (const contentScript of scriptEntries) { - const [feature, scriptPath] = contentScript - - const scriptImports = [ - ...getScriptEntries(scriptPath, options.excludeList || {}) - ] - // content_scripts-1: ['content_script-a.css', 'content_script-b.css'] - // content_scripts-2: ['content_script-c.css', 'content_script-d.css'] - const cssImports = getCssEntries(scriptPath, options.excludeList || {}) - - // 1 - Since having a .js file is mandatory for HMR to work, if - // during development if user has a content_script.css but not - // a content_script.js file, we create one for each content_script.css - // defined in the manifest. - if (cssImports.length && !scriptImports.length) { - const minimumContentFile = path.resolve( - __dirname, - 'minimum-content-file.mjs' - ) - - scriptImports.push(minimumContentFile) - } - - cssImportPaths.push({ - feature, - scriptPath: scriptImports[0], - cssImports: cssImports.map((cssImport) => { - return getRelativePath(scriptImports[0], cssImport) - }) - }) - } - - const url = urlToRequest(this.resourcePath) - - // 1 - Since we are extracting CSS files from content.css in - // the manifest.json file, we need to have a placeholder - // file to prevent the manifest from looking to a missing file. - cssImportPaths.forEach(({feature, scriptPath, cssImports}) => { - if (url.includes(scriptPath)) { - // Dynamically generate import statements for CSS files - const dynamicImports = cssImports - .map((cssImport) => { - const [, contentName] = feature.split('/') - const index = contentName.split('-')[1] - const filename = path.basename(cssImport) - const chunkName = `web_accessible_resources/resource-${index}/${filename.replace( - '.', - '_' - )}` - // Ensure to resolve the path relative to the manifest or webpack context - // const resolvedPath = getRelativePath(options.manifestPath, cssImport) - // Generate a dynamic import statement for each CSS file - return ( - `import(/* webpackChunkName: "${chunkName}" */ '${cssImport}')` + - `.then(css => console.info('content_script.css loaded', css))` + - `.catch(err => console.error(err));` - ) - }) - .join('\n') - - this.emitFile(`${feature}.css`, beautifulFileContent) - - // Prepend the dynamic imports to the module source - source = `${dynamicImports}\n${source}` - } - }) - - return source -} diff --git a/programs/develop/webpack/plugin-extension/feature-web-resources/index.ts b/programs/develop/webpack/plugin-extension/feature-web-resources/index.ts index 6a8f07bd0..6d924e956 100644 --- a/programs/develop/webpack/plugin-extension/feature-web-resources/index.ts +++ b/programs/develop/webpack/plugin-extension/feature-web-resources/index.ts @@ -91,7 +91,7 @@ export class WebResourcesPlugin { if (webAccessibleResourcesV3.length > 0) { manifest.web_accessible_resources = webAccessibleResourcesV3 as Manifest['web_accessible_resources'] - } + } // else { // // Do nothing // } diff --git a/programs/develop/webpack/plugin-js-frameworks/js-tools/svelte.ts b/programs/develop/webpack/plugin-js-frameworks/js-tools/svelte.ts index d938cb5f3..a40c29026 100644 --- a/programs/develop/webpack/plugin-js-frameworks/js-tools/svelte.ts +++ b/programs/develop/webpack/plugin-js-frameworks/js-tools/svelte.ts @@ -75,13 +75,14 @@ export async function maybeUseSvelte( loader: require.resolve('svelte-loader'), options: { preprocess: sveltePreprocess({ - typescript: true, - postcss: true + typescript: true }), emitCss: true, compilerOptions: { dev: mode === 'development', - } + css: 'injected' // Changed from boolean to 'injected' per new Svelte options + }, + hotReload: mode === 'development' } }, include: projectPath, @@ -100,5 +101,8 @@ export async function maybeUseSvelte( plugins: undefined, loaders: svelteLoaders, alias: undefined + // alias: { + // svelte: path.resolve(projectPath, 'node_modules', 'svelte') + // } } } diff --git a/programs/develop/webpack/plugin-reload/steps/setup-reload-strategy/apply-manifest-dev-defaults/patch-web-resources.ts b/programs/develop/webpack/plugin-reload/steps/setup-reload-strategy/apply-manifest-dev-defaults/patch-web-resources.ts index ab647b9b4..7a4f3f044 100644 --- a/programs/develop/webpack/plugin-reload/steps/setup-reload-strategy/apply-manifest-dev-defaults/patch-web-resources.ts +++ b/programs/develop/webpack/plugin-reload/steps/setup-reload-strategy/apply-manifest-dev-defaults/patch-web-resources.ts @@ -1,7 +1,15 @@ import {type Manifest} from '../../../../../types' function patchWebResourcesV2(manifest: Manifest) { - const defaultResources = ['/*.json', '/*.js', '/*.css'] + const defaultResources = [ + '/*.json', + '/*.js', + '/*.css', + '/*.scss', + '/*.sass', + '/*.less', + '/*.styl' + ] const resources = manifest.web_accessible_resources as string[] | null if (!resources || resources.length === 0) { @@ -20,7 +28,15 @@ function patchWebResourcesV2(manifest: Manifest) { } function patchWebResourcesV3(manifest: Manifest) { - const defaultResources = ['/*.json', '/*.js', '/*.css'] + const defaultResources = [ + '/*.json', + '/*.js', + '/*.css', + '/*.scss', + '/*.sass', + '/*.less', + '/*.styl' + ] return [ ...(manifest.web_accessible_resources || []), { diff --git a/programs/develop/webpack/plugin-reload/steps/setup-reload-strategy/target-web-extension-plugin/webpack-target-webextension/.github/workflows/nodejs.yml b/programs/develop/webpack/plugin-reload/steps/setup-reload-strategy/target-web-extension-plugin/webpack-target-webextension/.github/workflows/nodejs.yml new file mode 100644 index 000000000..411736ebd --- /dev/null +++ b/programs/develop/webpack/plugin-reload/steps/setup-reload-strategy/target-web-extension-plugin/webpack-target-webextension/.github/workflows/nodejs.yml @@ -0,0 +1,27 @@ +name: Node.js CI + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [18.x, 20.x, 22.x] + steps: + - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node-version }} + cache: 'yarn' + - run: yarn install + - run: yarn test + - run: git add . + - run: git restore --source=HEAD --staged --worktree -- tests/snapshot/mv2-hmr-rspack + - run: git restore --source=HEAD --staged --worktree -- tests/snapshot/mv3-hmr-rspack + - run: yarn test-ci diff --git a/programs/develop/webpack/plugin-reload/steps/setup-reload-strategy/target-web-extension-plugin/webpack-target-webextension/.gitignore b/programs/develop/webpack/plugin-reload/steps/setup-reload-strategy/target-web-extension-plugin/webpack-target-webextension/.gitignore new file mode 100644 index 000000000..862ce1ed2 --- /dev/null +++ b/programs/develop/webpack/plugin-reload/steps/setup-reload-strategy/target-web-extension-plugin/webpack-target-webextension/.gitignore @@ -0,0 +1,92 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +playground-* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# next.js build output +.next + +# nuxt.js build output +.nuxt + +# vuepress build output +.vuepress/dist +docs/.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +typedoc + +examples/**/dist/* +examples/hmr/js/ diff --git a/programs/develop/webpack/plugin-reload/steps/setup-reload-strategy/target-web-extension-plugin/webpack-target-webextension/.npmignore b/programs/develop/webpack/plugin-reload/steps/setup-reload-strategy/target-web-extension-plugin/webpack-target-webextension/.npmignore new file mode 100644 index 000000000..56bef8cb3 --- /dev/null +++ b/programs/develop/webpack/plugin-reload/steps/setup-reload-strategy/target-web-extension-plugin/webpack-target-webextension/.npmignore @@ -0,0 +1 @@ +examples/ \ No newline at end of file diff --git a/programs/develop/webpack/plugin-reload/steps/setup-reload-strategy/target-web-extension-plugin/webpack-target-webextension/.prettierrc b/programs/develop/webpack/plugin-reload/steps/setup-reload-strategy/target-web-extension-plugin/webpack-target-webextension/.prettierrc new file mode 100644 index 000000000..cbe842acd --- /dev/null +++ b/programs/develop/webpack/plugin-reload/steps/setup-reload-strategy/target-web-extension-plugin/webpack-target-webextension/.prettierrc @@ -0,0 +1,5 @@ +{ + "printWidth": 120, + "semi": false, + "singleQuote": true +} diff --git a/programs/develop/webpack/plugin-reload/steps/setup-reload-strategy/target-web-extension-plugin/webpack-target-webextension/CHANGELOG.md b/programs/develop/webpack/plugin-reload/steps/setup-reload-strategy/target-web-extension-plugin/webpack-target-webextension/CHANGELOG.md new file mode 100644 index 000000000..4ecfd56eb --- /dev/null +++ b/programs/develop/webpack/plugin-reload/steps/setup-reload-strategy/target-web-extension-plugin/webpack-target-webextension/CHANGELOG.md @@ -0,0 +1,120 @@ +# Changelog + +## 2.1.3 + +- Fix experimental_output stack overflow + +## 2.1.2 + +- Fix experimental_output with HMR chunks +- Fix experimental_output stack overflow + +## 2.1.1 + +- Now rspack can use chunk splitting by `experimental_output`. + +## 2.1.0 + +- Add a try-catch wrapper around the entry file of serviceWorkerEntry so if the initial code throws, you can still open the console of it. + **Enabled by default**, set option `tryCatchWrapper` to `false` to disable it. +- Add `experimental_output` to support service worker/content scripts used with [`splitChunks.chunks`](https://webpack.js.org/plugins/split-chunks-plugin/#splitchunkschunks) or [`optimization.runtimeChunk`](https://webpack.js.org/configuration/optimization/#optimizationruntimechunk). + +## 2.0.1 + +- Fix ServiceWorkerPlugin does not add eager chunks correctly in watch mode. +- Fix `splitChunks: { chunks: 'all', minSize: 1 }` crashes rspack. + +## 2.0.0 + +- Works on rspack now +- (Breaking) Minimal Node.js requirement changed from 14.17.16 to 18.20.5 +- (Breaking) Deprecated option `BackgroundOptions.entry` is removed. Use `pageEntry` and/or `serviceWorkerEntry` instead. +- (Breaking) Deprecated option `BackgroundOptions.manifest` is removed. +- (Breaking) Option `noWarningDynamicEntry` has been renamed to `noDynamicEntryWarning`. +- (Breaking) `background.pageEntry` cannot be the same as `background.serviceWorkerEntry`. +- Now `devServer.hot` is set to `only` by default. +- Now `output.environment.dynamicImport` is set to `true` by default. +- Now `output.hotUpdateChunkFilename` is set to `hot/[id].[fullhash].js` by default. +- Now `output.hotUpdateMainFilename` is set to `hot/[runtime].[fullhash].json` by default. + +## 1.1.2 + +- Support main world content script to be bundled. Also added a guide and example for this. + +## 1.1.1 + +- Add [a workaround](https://github.com/awesome-webextension/webpack-target-webextension/pull/42) for [a Chrome bug of loading content script in a sandboxed iframe](https://github.com/awesome-webextension/webpack-target-webextension/issues/41). +- Fix [compatibility with mini-css-extract-plugin in Manifest v3](https://github.com/awesome-webextension/webpack-target-webextension/issues/43) +- Add a warning if `background.pageEntry` and `background.serviceWorkerEntry` are the same. + + The only chunk loading method `serviceWorkerEntry` supports is `importScripts` ([ES Module is not supported yet](https://github.com/awesome-webextension/webpack-target-webextension/issues/24)) and `pageEntry` only supports `