Scripts
Browser scripts live in a top-level scripts/ directory, and archival builds them into /js/ on your site. TypeScript files have their types stripped and JavaScript files are copied as they are, so there is no build step to set up: no prebuild, no node, and no package.json.
Because this is part of archival itself, scripts build the same way in archival build, in archival run (which rebuilds a script as soon as you save it), and when archival builds and deploys your site.
Folder structure
Every script keeps its path, with scripts/ replaced by /js/ and .ts replaced by .js:
scripts/
main.ts -> /js/main.js
lib/format.ts -> /js/lib/format.js
worker.mts -> /js/worker.mjs
vendor/confetti.js -> /js/vendor/confetti.js (copied as is)
globals.d.ts (declarations only, not built)
In scripts/ |
Built to |
|---|---|
.ts / .mts |
.js / .mjs, with types stripped |
.js / .mjs |
the same name, copied unchanged |
.d.ts / .d.mts |
nothing — declarations are for your editor |
| anything else | nothing — put stylesheets, images and data in public/ |
Dotfiles and anything inside a node_modules folder are skipped. If there's no scripts/ directory, nothing happens.
Loading scripts
Scripts are built as ES modules, so load them with type="module", usually from a layout:
<script type="module" src="/js/main.js"></script>
Modules load each other by relative path. You can name the file you wrote or the file it builds to: archival rewrites ./format.ts to ./format.js in a relative import, so both of these load the same module:
import { formatDate } from "./lib/format.ts";
import { formatDate } from "./lib/format.js";
This covers import, export … from and import("./lazy.ts") with a string literal.
Nothing is bundled. Each file becomes one module, and the browser follows the imports. That means a bare specifier like import confetti from "canvas-confetti" is left alone and will not resolve in the browser. To use a package, import it from a URL (for example, a CDN that serves ES modules), map it with an import map, or copy its ES module build into scripts/vendor/.
TypeScript support
Types are removed rather than compiled. This is the same rule Node applies to .ts files, and the same toolchain carriers use, so code can move between the two. Because types become whitespace, every line and column in the built file matches your source, so browser errors and stack traces point at the right place without source maps.
The few TypeScript features that generate code aren't supported: enum, namespace and constructor parameter properties. Anything that isn't a type, such as a decorator, is left in the output as written. An import used only for types should say import type.
archival strips types but does not check them. To type-check, run tsc --noEmit yourself (for instance in CI). A tsconfig.json like this makes your editor follow the same rules archival does:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"lib": ["ES2023", "DOM", "DOM.Iterable"],
"strict": true,
"noEmit": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"erasableSyntaxOnly": true,
"allowImportingTsExtensions": true
},
"include": ["scripts"]
}
allowImportingTsExtensions is only needed if you import files as ./format.ts.
Errors
A script that doesn't compile is reported with its location:
1 script failed to compile:
scripts/main.ts:12:1: TypeScript enum is not supported in strip-only mode
(set scripts_dir = false in archival.toml to turn off script compilation)
archival buildwrites everything else inpublic/andscripts/, then exits non-zero. With--skip-failuresit prints the error as a warning and goes on to build your pages.archival runprints the error and keeps serving. The broken script keeps its last version that compiled until you fix it.
Configuration
Both settings are optional, and go in archival.toml:
scripts_dir: the directory scripts are built from. Defaultscripts. Set it tofalseto turn script building off.scripts_build_dir: where built scripts go, relative to yourbuild_dir. Defaultjs. An empty string builds them into the root ofbuild_dir. It can't point outsidebuild_dir.
For example, to build src/ into the root of your site:
scripts_dir = "src"
scripts_build_dir = ""
Here src/scripts/main.ts is served at /scripts/main.js.
A built script and a file in public/ can't build to the same path; archival stops with an error naming both. A built script does take precedence over a page that renders to the same path, the same way a file in public/ does.
Passing site data to scripts
A script can import a module that a page renders, which is a simple way to hand it values from your site. Render the module from pages/:
// pages/js/config.js.liquid
export const SITE_URL = "{{ site_url }}";
Then declare it next to the script that imports it, so TypeScript knows its shape. The declaration is not built:
// scripts/config.d.ts
export declare const SITE_URL: string;
// scripts/main.ts
import { SITE_URL } from "./config.js";
Moving off a prebuild step
If your site already compiles TypeScript with a prebuild command (for instance a build.mjs that writes into public/), you can usually replace it:
- Point
scripts_dirat your source directory, and setscripts_build_dirso the URLs stay the same. - Remove the build command from
prebuild, and the script and any packages it needed frompackage.json. - Delete the generated files from
public/. A generated file left behind at a path a script now builds to stops the build with an error, rather than quietly serving the stale copy.
If your build bundles npm packages or uses TypeScript features that generate code, keep your existing build and set scripts_dir = false if it reads from scripts/.
If scripts/ already holds tooling: many projects keep node or shell scripts for maintenance tasks in a scripts/ folder. Any .js, .mjs or .ts files there will be built and published under /js/. Move them somewhere else (for example tools/), or set scripts_dir = false.