Use special folders when your extension needs entrypoints or assets that do not fit cleanly in manifest.json.
Handle extra pages, runtime-injected scripts, static assets with exact paths, and companion extensions for local development without splitting your project structure.
Where special folders live
Special folders resolve from the project root, and only from there. The rule:
- The project root is the directory that contains your
package.json (or deno.json).
- Put
pages/, scripts/, public/, and extensions/ in that directory, next to package.json.
src/scripts/ is not special. Extension.js ignores nested copies of the special folders, so files there never become entrypoints or copied assets.
- Moving
manifest.json into src/ does not move the project root. The scaffolded templates ship src/manifest.json with package.json one level up, and special folders belong on the package.json level.
The one src-rooted exception: a project with no package.json and no deno.json. There the directory that contains manifest.json becomes the project root. When that manifest sits in src/, src/ is the root, and special folders (and dist/) live inside src/.
My scripts/ folder does not appear in dist/. This almost always maps
to the rule above: the folder sits under src/ instead of next to
package.json. Move it to the project root. If it is already there, confirm
that something in your project references the file
path.
Template examples
special-folders-pages
See the pages/ special folder in action with extra HTML entrypoints.
Repository: extension-js/examples/special-folders-pages
special-folders-scripts
See the scripts/ special folder in action with standalone script entrypoints.
Repository: extension-js/examples/special-folders-scripts
Why this matters
The manifest does not directly declare many extension files. These include iframe pages, scripts you inject dynamically with chrome.scripting.executeScript, and static vendor assets. You may also need companion extensions during development. Special folders make all of these first-class in the build pipeline.
How it works
Each special folder has a specific role:
pages/: additional HTML entrypoints
Use pages/ for extra extension pages such as sandbox iframes, diagnostics pages, or internal tools.
Extension.js treats each .html file in pages/ as an entrypoint and compiles it like manifest-declared pages.
For a sandboxed iframe example, see the Chrome Sandbox Sample.
scripts/: standalone script entrypoints
Use scripts/ for executable scripts that you load dynamically and that do not tie to an HTML page entry.
Extension.js compiles files in scripts/ as entrypoints using the same extension resolution pipeline as the rest of your project.
Reference the file path somewhere
A scripts/ entry is kept only when its project-relative path appears somewhere in your
source: the manifest, an HTML file, or a JavaScript or TypeScript string such as the argument
you pass to chrome.scripting.executeScript. That is what makes runtime-only injection work,
since the path never reaches the manifest.
A file nothing mentions is treated as dead code and is dropped from the build without a
warning, so check dist/<browser>/scripts/ after your first build with a new entry.
Important contract
When you use a scripts/ entry as a content-script-like runtime entry, follow the content script initialization pattern. This is the default-export contract Extension.js expects for safe hot-reload of injected scripts:
- Export a default function.
- Perform setup inside that function.
- Optionally return a synchronous cleanup.
This matters most during development, where Extension.js remounts content-script-like entries safely instead of using full page reloads.
Node.js scripts are not allowed in scripts/
Extension.js wraps every file inside scripts/ with a browser content-script mount runtime. If you place a Node.js-only file there (for example, a CLI launcher or build helper), the wrapper breaks the file.
The shebang is no longer on line 1, and Node-only APIs are unavailable in the browser context.
Extension.js detects two Node.js indicators and throws an error at build time:
- A shebang (
#!/usr/bin/env node) on line 1.
- An import from the
node: protocol (for example, import fs from 'node:fs').
If you see scripts/ is a reserved folder in Extension.js, move the file to a
different folder at the project root, for example, bin/, tools/, ops/,
tasks/, or ci-scripts/.
For dynamic injection examples, see the Chrome Scripting Sample.
public/: copy-only static assets
Use public/ when you need stable file paths and no bundling/transformation.
Extension.js copies everything under public/ to the output root 1:1.
Important public/ guard
Do not place manifest.json at public/manifest.json. Extension.js prevents this to avoid overwriting the generated manifest during compilation.
extensions/: companion extensions (load-only)
When you use companion extensions (for example, DevTools helpers), Extension.js supports an extensions/ folder as a load-only source in dev/preview/start flows.
At a high level:
- Scans subfolders under
extensions/ for unpacked extension roots (manifest.json present).
- Extension.js loads companion extensions alongside your main extension.
- Use this folder to load companion extensions, not to build them into your main artifact.
You can also load companion extensions via the --extensions CLI flag or the extensions key in extension.config.js:
Extension.js automatically downloads, unpacks, and loads store URLs alongside your extension.
Development behavior (watch mode)
In development watch mode, Extension.js monitors pages/ and scripts/ for file set changes:
- Adding supported files triggers a warning (you can keep working).
- Removing a supported file triggers a compilation error. Restart the dev server to recover.
This protects the running compilation graph from stale or broken entrypoints.
Best practices
- Keep shared runtime assets in
public/: Use it for files that must keep exact names and paths in output.
- Use
pages/ and scripts/ for true entrypoints: Keep off-manifest execution paths explicit.
- Restart dev server after entrypoint changes: Especially after removing files under
pages/ or scripts/.
- Keep companion extensions isolated: Treat
extensions/ as load-only dependencies for local workflows.
- Do not place
manifest.json in public/: Extension.js blocks public/manifest.json to protect the generated extension output.
Next steps
See the template run