> ## Documentation Index
> Fetch the complete documentation index at: https://extension.js.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Special folders for pages and scripts

> Use special folders for extra pages, injected scripts, static assets, and companion extensions that do not fit in manifest.json entrypoints.

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/`.

<Warning>
  **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, check
  that the file is not a Node.js helper, which [the build leaves
  out](#the-folder-contract).
</Warning>

## Template examples

### `special-folders-pages`

<img src="https://mintcdn.com/extensionjs/VCnDd7fX2Nza24SE/images/examples/special-folders-pages/screenshot.png?fit=max&auto=format&n=VCnDd7fX2Nza24SE&q=85&s=34efd0e8532ce82108345f817c2b2b6e" alt="special-folders-pages template screenshot" width="2400" height="1800" data-path="images/examples/special-folders-pages/screenshot.png" />

See the `pages/` special folder in action with extra HTML entrypoints.

<CodeGroup>
  ```bash npm theme={null}
  npx extension@latest create my-extension --template=special-folders-pages
  ```

  ```bash pnpm theme={null}
  pnpx extension@latest create my-extension --template=special-folders-pages
  ```

  ```bash yarn 2+ theme={null}
  yarn dlx extension@latest create my-extension --template=special-folders-pages
  ```

  ```bash bun theme={null}
  bunx extension@latest create my-extension --template=special-folders-pages
  ```

  ```bash deno theme={null}
  deno run -A npm:extension@latest create my-extension --template=special-folders-pages
  ```
</CodeGroup>

Repository: [extension-js/examples/special-folders-pages](https://github.com/extension-js/examples/tree/main/examples/special-folders-pages)

### `special-folders-scripts`

<img src="https://mintcdn.com/extensionjs/VCnDd7fX2Nza24SE/images/examples/special-folders-scripts/screenshot.png?fit=max&auto=format&n=VCnDd7fX2Nza24SE&q=85&s=5cc497256988344c504ed3c9f79dae25" alt="special-folders-scripts template screenshot" width="2400" height="1800" data-path="images/examples/special-folders-scripts/screenshot.png" />

See the `scripts/` special folder in action with standalone script entrypoints.

<CodeGroup>
  ```bash npm theme={null}
  npx extension@latest create my-extension --template=special-folders-scripts
  ```

  ```bash pnpm theme={null}
  pnpx extension@latest create my-extension --template=special-folders-scripts
  ```

  ```bash yarn 2+ theme={null}
  yarn dlx extension@latest create my-extension --template=special-folders-scripts
  ```

  ```bash bun theme={null}
  bunx extension@latest create my-extension --template=special-folders-scripts
  ```

  ```bash deno theme={null}
  deno run -A npm:extension@latest create my-extension --template=special-folders-scripts
  ```
</CodeGroup>

Repository: [extension-js/examples/special-folders-scripts](https://github.com/extension-js/examples/tree/main/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:

| Folder name | Description |
| - | - |
| `pages/` | Adds HTML pages to compilation as entrypoints, even when the manifest does not list them. |
| `scripts/` | Adds every script file to compilation as an entrypoint, whether or not the manifest or your code names its path. Node.js helpers are left out. |
| `public/` | Copies static assets to the output root as-is (`public/**` → `dist/**`) without bundling or transformation. |
| `extensions/` | Provides a conventional location for load-only companion extensions in dev/preview/start workflows. |

## `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](https://github.com/GoogleChrome/chrome-extensions-samples/tree/main/api-samples/sandbox/sandbox).

## `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 most `scripts/` 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](#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.

```ts background.ts theme={null}
// Source: scripts/foo.ts
// Emitted: scripts/foo.js

chrome.scripting.executeScript({
  target: { tabId },
  files: ["scripts/foo.js"],
});

chrome.scripting.registerContentScripts([
  { id: "foo", matches: ["<all_urls>"], js: ["scripts/foo.js"] },
]);
```

### The folder contract

Every file that you put in `scripts/` or `pages/` ships at a predictable output path. `scripts/foo.ts` becomes `dist/<browser>/scripts/foo.js`, and `pages/extra.html` becomes `dist/<browser>/pages/extra.html`. Nothing has to declare the file. A runtime string such as `chrome.runtime.getURL("pages/extra.html")` or `chrome.scripting.executeScript({ files: ["scripts/foo.js"] })` finds it. A file that nothing references ships too, and the build prints no warning about it. The build leaves out Node.js helpers: a file with a shebang, a Node.js import, or a path that a `package.json` script runs. To turn a folder off, set its key to `false` in `extension.config.js`:

```js extension.config.js theme={null}
export default {
  folders: {
    scripts: false,
    pages: false,
  },
};
```

See [special folder locations](/docs/features/extension-configuration#special-folder-locations) for the full `folders` key.

### 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 leaves the file out of the build:

* A shebang (`#!/usr/bin/env node`) on line 1.
* An import from the `node:` protocol (for example, `import fs from 'node:fs'`).

<Warning>
  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/`.
</Warning>

For dynamic injection examples, see the [Chrome Scripting Sample](https://github.com/GoogleChrome/chrome-extensions-samples/tree/main/api-samples/scripting).

## `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.

<Frame>
  <iframe className="w-full aspect-video rounded-xl" src="https://www.youtube-nocookie.com/embed/ZAsyjtiUbNM?rel=0" title="Extension.js: the public folder" loading="lazy" allow="encrypted-media; picture-in-picture; fullscreen" allowFullScreen />
</Frame>

### 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).
* A subfolder named for a browser loads only for that browser family: `extensions/chrome/` for Chromium targets, `extensions/edge/` for Edge, and `extensions/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.

You can also load companion extensions via the `--extensions` CLI flag or the `extensions` key in `extension.config.js`:

```bash theme={null}
# Load from a local folder
extension dev --extensions ./path/to/companion

# Load from Chrome Web Store or Firefox Add-ons
extension dev --extensions "https://chromewebstore.google.com/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi"
```

Extension.js automatically downloads, unpacks, and loads store URLs alongside your extension.

Accepted store links are `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 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

* Learn more about [Page reload and hot module replacement (HMR)](/docs/features/reload-and-hmr).
* Understand the mount contract in [Content scripts](/docs/implementation-guide/content-scripts).
* Browse the [Templates](/docs/getting-started/templates) to scaffold your next extension.

## See the template run

<Frame>
  <iframe className="w-full aspect-video rounded-xl" src="https://www.youtube-nocookie.com/embed/P8Q8TDQqbQE?rel=0" title="Extension.js: Special Folders Scripts template" loading="lazy" allow="encrypted-media; picture-in-picture; fullscreen" allowFullScreen />
</Frame>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.