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(ordeno.json). - Put
pages/,scripts/,public/, andextensions/in that directory, next topackage.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.jsonintosrc/does not move the project root. The scaffolded templates shipsrc/manifest.jsonwithpackage.jsonone level up, and special folders belong on thepackage.jsonlevel.
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/.
Template examples
special-folders-pages

pages/ special folder in action with extra HTML entrypoints.
special-folders-scripts

scripts/ special folder in action with standalone script entrypoints.
Why this matters
The manifest does not directly declare many extension files. These include iframe pages, scripts you inject dynamically withchrome.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.
Root location and emitted path
Two facts trip up mostscripts/ setups.
Location. scripts/ lives beside package.json (or deno.json), at the project root, not inside src/. This holds even when manifest.json lives in src/. The one exception is a project with no package.json and no deno.json, where the manifest directory is the root. See where special folders live.
Emitted path. Inject the compiled file. A scripts/foo.ts source is compiled to scripts/foo.js, so chrome.scripting.executeScript({ files }) and chrome.scripting.registerContentScripts({ js }) must name the .js file. A .ts path builds fine and then fails with a 404 in the browser. Extension.js warns about a compiled source literal at build time. The warning names the emitted path to use.
background.ts
Reference the file path somewhere
Ascripts/ 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 ascripts/ 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.
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').
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.jsonpresent). - A subfolder named for a browser loads only for that browser family:
extensions/chrome/for Chromium targets,extensions/edge/for Edge, andextensions/firefox/for Gecko targets. Root-level subfolders and entries you configure explicitly load everywhere. - Extension.js loads companion extensions alongside your main extension.
- Use this folder to load companion extensions, not to build them into your main artifact.
--extensions CLI flag or the extensions key in extension.config.js:
chromewebstore.google.com (and the legacy chrome.google.com/webstore form), microsoftedge.microsoft.com, and addons.mozilla.org, with or without a scheme or a www. prefix. A downloaded store extension lands under extensions/<browser>/ and loads only for that browser. A link from another host, a bare store id, or an entry that is neither a link nor a path is reported as an error instead of being dropped.
Development behavior (watch mode)
In development watch mode, Extension.js monitorspages/ 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.
Best practices
- Keep shared runtime assets in
public/: Use it for files that must keep exact names and paths in output. - Use
pages/andscripts/for true entrypoints: Keep off-manifest execution paths explicit. - Restart dev server after entrypoint changes: Especially after removing files under
pages/orscripts/. - Keep companion extensions isolated: Treat
extensions/as load-only dependencies for local workflows. - Do not place
manifest.jsoninpublic/: Extension.js blockspublic/manifest.jsonto protect the generated extension output.
Next steps
- Learn more about Page reload and hot module replacement (HMR).
- Understand the mount contract in Content scripts.
- Browse the Templates to scaffold your next extension.

