Feat/1101 support iife content scripts#1158
Merged
Merged
Conversation
Add support for bundling content scripts as self-contained IIFE files. Content scripts named with .iife.ts extension are automatically detected and built as single files with all dependencies inlined. This is useful for: - MAIN world content scripts (chrome.scripting.executeScript) - Avoiding loader overhead - Single-file distribution The IIFE plugin uses Vite's library mode to build each .iife.ts file separately, producing a single bundled output per content script.
- Update manifest plugin to skip emitting .iife.ts files (handled by IIFE plugin) - Update content scripts plugin to skip IIFE type processing - Update dynamic content scripts plugin to auto-detect .iife.ts files - Add enforce: 'post' to dynamic scripts build hook for correct ordering
🦋 Changeset detectedLatest commit: e3c75f9 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
c9aa0b8 to
319d4ab
Compare
- Add unit tests for isIifeContentScript filename detection - Add E2E build test verifying IIFE output format and inlined imports - Add E2E serve test verifying .iife.ts files work in dev mode - Test both manifest-declared and dynamic (?script) content scripts
319d4ab to
0f729b4
Compare
Member
Contributor
Author
|
I'd love to see what @jacksteamdev thinks about this solution |
Contributor
Author
Initially when I tested 1102 it didn't do what I needed it to do. I see that you ended up updating the PR and got it working (I missed the update). I would say that this PR does the same thing as #1102 does, but also adds support for IIFE in the manifest (by naming .iife). I just added a comment to your PR while testing it out: |
Backports the ergonomic `?iife` import syntax from crxjs#1102 as a small extension on top of crxjs#1158. - Add `declare module '*?iife'` (alias to `'*?script&iife'`) in client.d.ts for TypeScript support. - Update dynamic content scripts loader to accept bare `?iife` (in addition to `?script&iife` and the `.iife.*` filename convention). - In build replacement, output IIFE paths without leading `/` (registerContentScripts requirement). This keeps full compatibility with the `.iife.*` + manifest support and auto-detection introduced in crxjs#1158 / crxjs#1101. Refs: crxjs#1102, crxjs#1158, crxjs#1101
…?iife) content scripts Ports and adapts the dev IIFE support from crxjs#1102 so that `type: 'iife'` scripts (from bare `?iife`, `?script&iife`, or `.iife.*?script`) are actually emitted as self-contained IIFE bundles during `vite serve`, not just at build time. - Add `bundleIife` (using `viteBuild` with `configFile: false` + copied resolve settings to avoid Vite 6+ WeakMap/plugin reuse issues, same fix as in crxjs#1102). - Add `prepIifeScript` operator (inlines all deps, writes extra assets like sourcemaps). - Branch in `prepScript` / `prepFileData` so IIFE goes through the dedicated bundler instead of normal transform + es-module-lexer rewrite. - Wire in `plugin-contentScripts` (dev `contentScripts.change$` subscription): call `add({ type: 'iife', id })` instead of skipping (the skip comment was for the build-only IIFE plugin). - Update error message in `getViteUrl`. This enables proper MAIN world dynamic scripts (via `registerContentScripts({ world: 'MAIN' })`) and other IIFE use cases to work in development, including rebuilds. Build-time IIFE (the dedicated `plugin-contentScripts_iife`) from crxjs#1158 is untouched. Refs: crxjs#1102, crxjs#1158, crxjs#1101
Ports the IIFE-specific hot update handling from crxjs#1102. - In `handleHotUpdate`, compute `changedFilePath` from the raw `file` (IIFE entries are not part of Vite's module graph, so normal `modules` / `relFiles` detection misses them). - For any `contentScripts` entry with `type === 'iife'` whose source matches the changed file: call `update()`, wait for the rebuild promise, then send `crx:runtime-reload` (if liveReload enabled). - Includes debug logging. Reason: Chrome caches content scripts registered with `registerContentScripts` / `executeScript`, so source changes require a full extension reload even in dev. This was already necessary for declared MAIN world scripts and is now generalized. Complements the dev bundling from the previous commit. Refs: crxjs#1102, crxjs#1158
…cripts Adds the test coverage from crxjs#1102 (adapted to run on top of the crxjs#1158 base): - `tests/e2e/mv3-dynamic-script-iife/`: full end-to-end test for dynamic IIFE via `?iife` (bare) + `registerContentScripts({ world: 'MAIN' })`. Includes rebuild-on-change test (copies src1 → src → src2 and verifies runtime reload + updated content after page reload). - `tests/out/dynamic-script-iife/`: build and serve output snapshots for dynamic IIFE scripts. These tests specifically exercise: - Bare `?iife` import alias - Dev-time IIFE bundling (self-contained output) - HMR → rebuild → `crx:runtime-reload` flow for IIFE - Correct path format (no leading `/`) for registerContentScripts The existing `mv3-content-script-iife` e2e from crxjs#1158 (manifest + dynamic with `.iife.*` naming) remains. Refs: crxjs#1102, crxjs#1158
…ame convention + query alias) - In the mv3-content-script-iife e2e (filename convention + ?script on .iife files): add explicit bare `?iife` import on a normal-named file. This exercises the query alias alongside the filename convention in the same project. - In the mv3-dynamic-script-iife e2e (bare ?iife + MAIN world): add a `*.iife.ts` file + import via `?script` to also exercise the filename convention in the test for the query approach. - Added a small convention-named file so both styles are used in the same fixture. Now the end-to-end tests on this combined branch verify that **both approaches work** (and can even be mixed in one extension): - Filename convention: naming `foo.iife.ts` (auto-detected for manifest and dynamic `?script`) - Query alias: any file + bare `?iife` or `?script&iife` query Refs: crxjs#1102, crxjs#1158
…st (Salmin approach via config) Adds coverage for Salmin's IIFE filename convention when the manifest is provided via defineManifest in manifest.config.ts (instead of static JSON). This exercises the path where the manifest is a function (resolved in plugin-contentScripts_iife and plugin-manifest), the content_scripts are read from it, and isIifeContentScript detection is applied to the listed JS files. Previously the e2e only tested the static manifest.json case for declared IIFE content scripts. Refs: crxjs#1158
…Scripts.standalone option Adds support for declaring IIFE/standalone content scripts without requiring the .iife.* filename convention. - New option: contentScripts.standalone: string[] in crx() plugin options (or vite.config). - Normal .ts/.js files listed in manifest.content_scripts + marked in standalone will be built as self-contained IIFE. - Updated detection in plugin-manifest (skip normal emit), plugin-contentScripts_iife (collect and build), and dynamic plugin (auto iife type for ?script on standalone files). - Updated the mv3-content-script-iife e2e test to use defineManifest + standalone for a normal-named file (content-standalone.ts), in addition to the .iife convention. - Tests now cover explicit 'define standalone files' in config (as discussed for Salmin's approach). This allows keeping regular filenames while getting IIFE behavior for MAIN world etc. Refs: crxjs#1158 (extension)
…cussion - All references now use the nested form under contentScripts for grouping with other content script options (preamble, hmrTimeout, injectCss). - This makes sense because 'standalone' treatment is a content script processing mode (IIFE instead of loader), not a completely separate top-level concept. - Updated types, all plugins, comments, and the e2e test config. - The feature still allows normal-named files in defineManifest without .iife suffix by listing them in standaloneFiles.
…s + fix paths + clean ts comments - Add dedicated normal-named file exercised via bare ?iife query string - Register it dynamically and assert runtime marker (full coverage for filename convention, standaloneFiles config, and ?iife import) - Fix leading '/' in non-IIFE dynamic script paths returned from ?script (required for chrome.scripting.registerContentScripts) - Remove obsolete @ts-expect-error comments now that client.d.ts ambient declarations make the virtual imports type-check cleanly - Update output snapshots for changed dynamic script filenames and IIFE output locations This makes the e2e test actually validate that all three ways of marking content scripts as IIFE produce working, injectable scripts.
Prefer code simplicity over deduplicating output files for pure IIFE cases. The separate IIFE artifact is still produced and used for registration/injection. Pure IIFE dynamic scripts now leave their (unused at runtime) ESM chunk in assets/, but mixed use (IIFE registration + normal static import) works automatically without any reference scanning or conditional logic. This reverts the dedup approach for simplicity as discussed.
There are three supported ways for a content script to become an IIFE bundle: 1. Filename convention: *.iife.ts / *.iife.js etc. 2. Query string on dynamic imports: ?iife (or ?script on an IIFE/standalone file). 3. Declarative: contentScripts.standaloneFiles in the crx() options inside the user's Vite defineConfig (or defineManifest-style config). The previous attempt had dropped support for (3) in the IIFE plugin and re-introduced fragile ESM chunk removal logic. All explanatory comments (three modes + why we intentionally do not remove the intermediate chunks emitted for dynamic IIFE cases) are now in a single JSDoc block on pluginContentScriptsIife. Also includes a minor comment cleanup in getViteUrl for the iife case.
The mv3-content-script-iife and mv3-dynamic-script-iife fixtures prepare
the src/ directory (copying src1, later switching files for rebuild tests).
fs.remove + plain copy was prone to ENOTEMPTY (on rmdir) and EEXIST (on mkdir)
when the full test matrix runs in one vitest process. Previous serve() calls
leave Vite watchers (with usePolling) and temp state that interfere with
immediate cleanup of the source dir for the next test.
Changed to fs.emptyDir + copy with { overwrite: true, recursive: true }.
This is the fs-extra recommended pattern for "reset this dir to a known state".
Note: this does not make the tests bulletproof (full isolation via per-test
temp dirs would be better), but reduces the flakiness for the exact tests
that exercise the new IIFE feature (standaloneFiles, .iife filenames, and
?iife query strings).
045e4dd to
c1fc77c
Compare
9732180 to
e3c75f9
Compare
This was referenced Jun 7, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Hey guys!
Full disclosure:
I used Agents to heavily assist with this PR. This feature is something we've needed at my company for a while, but I never had enough time to learn CrxJS enough to be able to it without help from AI. My strategy was to write tests to make sure we didn't break any existing functionality.
This feature would solve a number of open issues (some of which would need to accept the iife lifestyle)
#1102
#1101
#1152
#1119
The following below is what the AI generated to describe this PR.
feat: Add IIFE content script bundling
Summary
This PR adds support for bundling content scripts as self-contained IIFE (Immediately Invoked Function Expression) files. Content scripts named with the
.iife.tsextension are automatically detected and built as single files with all dependencies inlined.Motivation
When injecting content scripts into the
MAINworld usingchrome.scripting.executeScriptorchrome.scripting.registerContentScripts, ES modules are not supported. This requires content scripts to be bundled as IIFE files with no external imports.Previously, developers had to manually configure separate build steps or use workarounds. This PR adds first-class support for IIFE content scripts with zero configuration.
Usage
1. Manifest-declared content scripts
Name your content script with
.iife.tsextension:src/content/injected.iife.ts
manifest.config.ts
2. Dynamic content scripts (chrome.scripting API)
src/background.ts
Output
Regular content script (main.ts):
assets/main.ts-loader-xxx.js+ chunksIIFE content script (
main.iife.ts):src/content/main.iife.jsBenefits
.iife.tschrome.scripting.executeScript?scriptimports - Auto-detected for dynamic content scriptsTrade-offs
Testing