create-shadow-app
v1.0.4
Published
Create a new app from a starter template, with optional Tailwind, Biome, Vitest, Convex and Clerk
Maintainers
Readme
create-shadow-app
Scaffolds a ShadowJS app. Pick a template, add what you need, and get a project that type-checks, tests, builds and (for the full-stack template) serves on the first run.
spa: a Vite single-page app.render()from@shadow-js/web, optional client routing with@shadow-js/router.dusk: Dusk, the full-stack framework. File routes, a layout, loaders, a server function with input validation, an API route, error and not-found pages, security options and a start script for the deployment target you choose (--adapter, eight of them).content,dashboard,api: starters built on Dusk. A Markdown site with feeds, an admin dashboard (table, queries, forms, server functions) and a JSON API (validation, rate limits, CORS, OpenAPI).- Add-ons, each independent and usable with
spaanddusk(a starter takes the ones that fit it): Tailwind CSS 4, Biome, Vitest, Convex, Clerk, shadow-ui, ESLint and the devtools.
Everything is generated by pure functions that return a map of file paths to text, so the output is
tested without a disk. npm run verify:matrix proves it end to end: it packs this repository,
generates apps from the tarballs, installs, type-checks, tests, lints, builds, starts the dev server
and the production server of each deployment target, and requests them (see
The matrix).
Install
npm create shadow-app@latest my-appnpm create shadow-app runs this package without installing it. Flags go after --:
npm create shadow-app@latest my-app -- --template dusk --tailwind --vitestBy default the project depends on the ShadowJS packages on npm. To try changes in this repository that are not released yet, build the packages and point the scaffolder at the tarballs (see Before ShadowJS is published).
Usage
create-shadow-app - scaffold a ShadowJS app
Usage
npm create shadow-app@latest [directory] [options]
create-shadow-app [directory] [options]
create-shadow-app completion <shell> print a bash, zsh or fish completion script
create-shadow-app codemod <name> [paths] migrate React, Solid or Next.js code to ShadowJS
Template
-t, --template <name> spa (Vite SPA), dusk (full stack), content, dashboard or api [default: spa]
--adapter <name> dusk only: node, bun, deno, cloudflare, vercel, netlify, tarout or static
(the deployment target; default: the Node server, run by dusk start)
--name <package> npm package name [default: derived from the directory]
Add-ons (each: --x to add, --no-x to leave out)
--tailwind Tailwind CSS 4 (@tailwindcss/vite)
--biome Biome lint and format [default: on]
--vitest Vitest, happy-dom, @shadow-js/testing, an example test
--convex Convex: provider, schema, sample functions
--clerk Clerk: provider, sign-in/up, a protected page
--ui shadow-ui scaffolding (components.json, cn(), theme); adds Tailwind
--eslint @shadow-js/eslint-plugin flat config (dusk config for Dusk apps)
--devtools the reactive-graph inspector, in development only
--router spa only: client-side routing (Clerk and Convex imply it)
Behavior
-y, --yes accept the defaults, ask nothing
--no-install do not install dependencies
--no-git do not run git init
--pm <name> npm, pnpm, yarn or bun [default: the one that ran this, else npm]
--force write into a directory that is not empty (overwrites files of the same name)
--dry-run show what would be written; write nothing
--allow-outside permit a directory outside the current one
--json print one JSON document on stdout instead of words; nothing is asked
-h, --help show this help
-v, --version show the version
Before ShadowJS is published
--local <packDir> use the tarballs in <packDir> (written by `npm run pack:local`)
--pack-root <repo> same, with <repo>/.pack
Exit codes 0 done, 1 failed, 2 wrong arguments, 130 canceled (Ctrl-C), 143 terminated (SIGTERM)
Examples
npm create shadow-app@latest my-app
npm create shadow-app@latest my-app -- --template dusk --tailwind --vitest
node dist/cli.js my-app --template dusk --local /path/to/shadowjs/.packThe usage block above is the output of --help, copied as printed. The same text is in
test/snapshots/help.txt, which a test checks against the real output.
On a terminal with no --yes, it asks for the project name, the template, the add-ons (arrows to
move, space to toggle, a for all) and whether to install. A flag on the command line always wins
over an answer; --adapter waits for the template question when the template is not on the command
line. Without a terminal, or with --yes, nothing is asked: the template is spa, Biome is on and
the other add-ons are off unless a flag says otherwise.
What it does, in order: checks the arguments, builds the whole project in memory, refuses a
directory that is not empty (without --force), writes the files, runs git init (when git exists
and the directory is not already in a repository), installs with the package manager, and prints
the next steps. The package manager is npm unless --pm names another one, or the command was
started by another manager or runs next to its lockfile; the README and next steps of the project
then use that manager's commands.
Running it again in the same folder is safe: without --force it refuses a folder that is not empty
and writes nothing; with --force it writes the same files again (identical content).
| Exit code | Meaning | | --- | --- | | 0 | Done. | | 1 | It failed: directory not empty, a file cannot be written (the files this run wrote are removed again), the install failed (the files stay, and the message says what to run), the pack directory is incomplete. | | 2 | The command line was wrong. Nothing was written. | | 130 | You canceled with Ctrl-C. What this run created is removed. | | 143 | The command was terminated (SIGTERM). What this run created is removed. |
Cancellation: Ctrl-C (SIGINT) or SIGTERM stops the command at any point. An open question is dropped
and the terminal is given back; a running install is stopped. The folder this run created is removed
(if it was missing before); in a folder that already existed, the entries this run created are
removed and the files it overwrote with --force keep their new content. On Windows the install
runs through the shell, so a signal stops the command but cannot reach every process it started.
Safety and names: the directory must be inside the current one (--allow-outside lifts that); every
file path is checked before the first write, so a bad path writes nothing; the package name is
validated against npm's rules. A name npm reserves (node_modules, favicon.ico) or a Node built-in
module (http, fs, ...) is refused as an unscoped name, with the way out (--name @you/http). A
folder name that Windows cannot hold is refused on every platform, so a project made on macOS can
be cloned on Windows: the reserved device names (CON, PRN, AUX, NUL, COM1-COM9,
LPT1-LPT9, with or without an extension), the characters < > : " | ? * and the backslash, a
control character, and a trailing dot or space. Spaces are fine (my app becomes the name my-app),
and a scoped folder such as @me/thing is a path whose package name is its last segment. Two file
names that differ only by case are refused, because macOS and Windows would store them as one file.
Output is LF only, and colors follow NO_COLOR, FORCE_COLOR and whether the output is a terminal.
Scripting the command
--json turns the output into one JSON document on stdout, and nothing else is written there. The
words a person would read (Created my-app ..., the install's own output) go to stderr. --json
never asks a question, with or without a terminal, so give it the directory (and --yes to say so
explicitly). The exit code is the same with and without --json.
create-shadow-app my-app --yes --json --no-install --no-git{
"schema": 1,
"command": "create-shadow-app",
"ok": true,
"data": {
"name": "my-app",
"directory": "/work/my-app",
"template": "spa",
"adapter": null,
"addons": ["biome"],
"packageManager": "npm",
"local": false,
"dryRun": false,
"files": [".gitignore", "README.md", "biome.json", "index.html", "package.json", "src/App.tsx", "..."],
"dependencies": { "@shadow-js/core": "^1.0.0", "@shadow-js/web": "^1.0.0" },
"devDependencies": { "@shadow-js/vite": "^1.0.0", "typescript": "7.0.2", "vite": "8.3.3", "@biomejs/biome": "2.5.15" },
"installed": false,
"git": "disabled",
"nextSteps": ["cd my-app", "npm install", "npm run dev"]
},
"warnings": []
}The document always has schema (1), command, ok, data and warnings. ok is true exactly when the
exit code is 0. A failure adds error: { "code": "CREATE_SHADOW_APP_...", "message": "..." }, and data still
holds what is known: after a failed install, the files that were written. A refused command line (an unknown
flag, a missing directory) is a document too.
| Key of data | Meaning |
| --- | --- |
| directory, name, template, adapter, addons, packageManager | What was scaffolded. adapter is null unless --adapter was given. addons includes router for a spa with routing. |
| files | The relative paths of the files, sorted. With --dry-run they are the files that would be written, and dryRun is true. |
| dependencies, devDependencies | The ranges the generated package.json pins. |
| installed | True when the install ran and succeeded. |
| git | initialized, existing-repository, git-missing, failed or disabled (--no-git, and a dry run). |
| nextSteps | The commands to type, in order. |
| Error code | Exit | Cause |
| --- | --- | --- |
| CREATE_SHADOW_APP_UNKNOWN_OPTION | 2 | A flag the command does not have. |
| CREATE_SHADOW_APP_INVALID_OPTION | 2 | A missing or bad value, a second directory, or flags that contradict each other. |
| CREATE_SHADOW_APP_MISSING_DIRECTORY | 2 | No directory, and nothing to ask on (--json, or no terminal). |
| CREATE_SHADOW_APP_INVALID_DIRECTORY, CREATE_SHADOW_APP_INVALID_NAME | 2 | A directory or package name that is refused. |
| CREATE_SHADOW_APP_INVALID_SHELL | 2 | completion without a shell, or with one it does not know. |
| CREATE_SHADOW_APP_UNKNOWN_CODEMOD | 2 | codemod without a name, or with one it does not know. |
| CREATE_SHADOW_APP_DIRECTORY_NOT_EMPTY | 1 | The directory has files and --force was not given. |
| CREATE_SHADOW_APP_LOCAL_PACK | 1 | The --local pack directory is missing or incomplete. |
| CREATE_SHADOW_APP_WRITE_FAILED | 1 | A file could not be written. |
| CREATE_SHADOW_APP_INSTALL_FAILED | 1 | The install failed or could not start. The files stay. |
| CREATE_SHADOW_APP_CANCELED | 130, 143 | Ctrl-C or SIGTERM. What the run created is removed. |
| CREATE_SHADOW_APP_CODEMOD_REFUSED | 1 | codemod was given a path outside the current directory, or one that does not exist. Nothing is written. |
| CREATE_SHADOW_APP_UNEXPECTED | 1 | Anything else. The message says what happened. |
codemod also puts warnings in the document, each { code, message } with the message starting file:line::
CREATE_SHADOW_APP_CODEMOD_UNSUPPORTED (a pattern it left for you), CREATE_SHADOW_APP_CODEMOD_NOTE (a change worth a second
look) and CREATE_SHADOW_APP_CODEMOD_SKIPPED (a file it did not read: too large, inside node_modules or dist, or not valid
syntax, not UTF-8, or nested too deeply). The exit code is 0 for all three.
With jq:
create-shadow-app my-app --yes --json --no-install | jq -r '.data.nextSteps[]'
create-shadow-app my-app --yes --json | jq -e '.ok' > /dev/null || echo "scaffold failed"Shell completion
create-shadow-app completion <bash|zsh|fish> prints a completion script for the flags, the values of
--template, --pm and --adapter, and the directory. Load it from your shell's startup file:
source <(create-shadow-app completion bash)
create-shadow-app completion fish | sourceA project directory called completion is written ./completion. create-shadow-app completion zsh --json
prints { shell, script } as the data of the document.
Migrate code with codemods
create-shadow-app codemod <name> [paths...] [--dry-run | --write] [--json]codemod edits the source of an existing React, Solid or Next.js project, for the mappings that need no judgment. It parses each
file with oxc-parser and changes it by source range, so comments and formatting outside the edited ranges stay as written. It
prints a unified diff and writes nothing unless you pass --write; running it twice changes nothing the second time.
| Name | Changes |
| --- | --- |
| react-to-shadow | useState to useStore (or useSignal when the initial value has no known shape), with the reads of the value rewritten; useReducer, useMemo; useEffect dependency arrays removed ([] becomes onMount); useCallback removed; imports from react moved to @shadow-js/core and @shadow-js/web. |
| preact-to-shadow | The react-to-shadow rules for preact/hooks, preact/compat, and createContext and Fragment from preact; render, hydrate, Component, h and @preact/signals are reported. |
| react-router-to-shadow | Link and NavLink of react-router, react-router-dom and @remix-run/react to A (className, prefetch, activeClassName renamed); Outlet, Navigate, useNavigate, useLocation and useParams moved to @shadow-js/router; the route table and the other names are reported. |
| solid-to-shadow | createSignal, createEffect, createMemo, createSelector, createUniqueId renamed; imports from solid-js, solid-js/web and solid-js/store moved to @shadow-js/core, @shadow-js/web and @shadow-js/web/server. |
| next-router-to-shadow | next/link to A from @shadow-js/router (prefetch={false} to preload={false}); useRouter and its methods; usePathname() to useLocation().pathname. |
paths are files or folders under the current directory (default: the directory itself). A path outside it, by .., an absolute path
or a symbolic link, is refused with exit code 1 and then nothing is written. node_modules, dist and .git are skipped, so are
declaration files and files over 1 MiB. What a codemod will not guess is printed as warning file:line: ... on stderr, and the file
keeps the old import for it. A change worth a second look (a removed dependency array, a store whose setter merges) is a note.
--json prints the files, their diffs and the warnings as one document. See the
guide and the migration guides for React, Solid and Next.js.
The templates
spa
index.html vite.config.ts tsconfig.json package.json README.md .gitignore
src/main.tsx src/App.tsx src/index.css src/vite-env.d.tsScripts: dev, build (type-check, then vite build), preview, typecheck. With the router
(--router, implied by Clerk and Convex) src/App.tsx mounts a RouterProvider, routes live in
src/router.tsx (createAppRouter(history?), so tests can use a memory history) and pages in
src/pages.
dusk
vite.config.ts dusk.config.ts tsconfig.json package.json README.md .env.example .env.public
src/app.tsx src/entry-server.ts src/index.css
src/routes/_layout.tsx index.tsx about.tsx [...rest].tsx posts/index.tsx posts/[id].tsx api/health.ts
src/server/posts.ts db.server.ts schema.ts src/components/NotFound.tsx_layout.tsxis the shell with the navigation and exportserrorandnotFound;[...rest].tsxanswers every unknown URL with status 404 and the not-found page.posts/index.tsxandposts/[id].tsxhave aloader(runs on the server for the first request, in the browser for later navigations) and ahead.src/server/posts.tshas server functions made withserverFn: a GET list, a GET read that callsnotFound(), and a POST create whoseinputschema is checked before the handler runs. The schema is a small Standard Schema written inschema.ts; Zod, Valibot or ArkType can replace it.src/entry-server.tssets the handler options:allowedHosts(production answers only for the host ofAPP_URL), HSTS, and, with Clerk, the middleware. CSRF checks, secure headers and body limits are on by default in Dusk.dusk.config.tsholds the redirect, the header rule and the prerendered route rule. The static target prerenders/and/about; the other targets prerender/about.- The start script depends on the deployment target (
--adapter, defaultnode):noderunsdusk start;bun,denoandtaroutrun their server file;staticrunsserve-static.mjs, which servesdist/staticwith its own server (vite previewwould not answer/, because the Dusk plugin sets Vite'sappTypetocustom);cloudflare,vercelandnetlifyrun their platform's local dev server and have adeployscript. Each README says which output the build writes.
content
The content template is a site of Markdown files on Dusk: a blog, documentation or a portfolio. Every post is a file in content/; the build reads the folder, prerenders /, /posts and /about, follows their links and writes a static page for every post, and serves /rss.xml, /sitemap.xml and /robots.txt.
src/lib/front-matter.tsandsrc/lib/markdown.tsare a small, tested front-matter parser and Markdown reader (headings, lists, code, quotes, links, emphasis). Raw HTML in a post is shown as text and ajavascript:link is dropped. Both files are yours to change, or to replace with a library.- A post's front matter has
titleanddate(required),summary,tags,langanddraft: true. A post withlang: aris drawn right to left inside any page. SITE_LOCALE(for examplear) sets<html lang dir>for the whole site. The stylesheet uses logical properties, so both directions look right.- It takes
--tailwind,--biomeand--devtools. Vitest and ESLint are built in.
dashboard
The dashboard template is an admin dashboard on Dusk: a sidebar shell, an overview, a customers table and the forms that create, edit and delete a customer.
- The overview and the table read through
@shadow-js/query. The table is a@shadow-js/tableover a server-side page: sorting, search, the status filter and paging run in a server function, and the previous page stays on screen while the next one loads. - The form is a
@shadow-js/formsform. The schema lives insrc/lib/customers.tsand runs in the browser and again on the server, so both agree; a problem only the server can see (an email another customer has) is shown on its field. - The data is in memory (
src/server/customers.server.ts). Replace that file with your database; the routes, the components and the server functions keep their signatures. There is no sign-in: guard the routes with@shadow-js/dusk/authbefore real data goes in. - The shell has a skip link, a labeled navigation and one main region. Colors come from the
@shadow-js/uitheme and the classes use logical properties, soAPP_LOCALE=argives a right-to-left dashboard and thedarkclass on<html>gives the dark theme. shadow-uiand Tailwind are built in; it takes--biomeand--devtools. Add more components withnpx shadow-ui add dialog.
api
The api template is a JSON API on Dusk, with no pages: API routes, typed server functions, environment validation, rate limiting, CORS and an OpenAPI description.
src/routes/api/taskshas a paged list (GET),POST, andGET,PATCHandDELETEby id. Input is validated before a handler runs (a wrong body answers 422 with the issues, an oversized one 413), and the tasks are in memory (src/server/tasks.server.ts): replace that file with your database.src/server/limits.tsrate-limits reads and writes with separate buckets and answers 429 withRetry-After. The buckets belong to one process; pass a sharedstorewhen you run several.src/entry-server.tsallows only the host ofAPP_URLin production. A browser app on another origin can call the API only when its origin is inCORS_ORIGINS(comma separated). A request withoutOrigin(curl, a server, a mobile app) is allowed because the API has no cookies; removecsrf.allowMissingOriginif you add sessions.GET /api/openapi.jsonservessrc/api/spec.ts, and a test fails when a route file and the description disagree.- It takes
--biome. Vitest and ESLint are built in.
Add-ons
| Flag | What it adds |
| --- | --- |
| --tailwind | tailwindcss and @tailwindcss/vite, tailwindcss() after the app plugin, src/index.css with @import "tailwindcss";. |
| --biome | biome.json (ignores dist, generated files), lint and format scripts. The generated sources pass biome check. |
| --vitest | Vitest, happy-dom, @shadow-js/testing, vitest.config.ts and vitest.setup.ts as in tools/local/smoke.mjs, a test script and passing example tests (a counter test for spa, a counter and server-function tests for dusk). The example tests are also the accessibility checks of the generated app: one level-1 heading, a name for every button and link (toHaveAccessibleName), and alt text on every image. |
| --convex | ConvexProvider in src/providers.tsx, convex/schema.ts, convex/tasks.ts (a query and a mutation), a Tasks page, VITE_CONVEX_URL in .env.example, convex/_generated (the files convex dev writes, so the app builds first), a convex script. Run convex dev to create the deployment. |
| --clerk | ClerkProvider, sign-in and sign-up pages, a protected /dashboard, VITE_CLERK_PUBLISHABLE_KEY. For dusk: @clerk/backend, a middleware that verifies every request, a gate that redirects signed-out visitors from /dashboard and a requireUser server function the loader calls. With Convex too: ConvexProviderWithAuth and convex/auth.config.ts. |
| --ui | components.json, src/lib/utils.ts (the registry's own file: cn() and the ClassValue type, so shadow-ui add finds it unchanged), the @/ alias (tsconfig and Vite), the @shadow-js/ui theme in src/index.css, and the dependencies the components use. Components are added with npx shadow-ui add button. Implies Tailwind, so --ui --no-tailwind is refused. |
| --eslint | @shadow-js/eslint-plugin with its flat config (configs.dusk for the dusk template, configs.recommended for spa) and the lint:eslint script. The plugin brings a parser that does not use the TypeScript compiler API, so it runs on the TypeScript 7 the app is compiled with: one package.json, one install. |
| --devtools | @shadow-js/devtools, mounted in development only (import.meta.env.DEV); the production build drops it. |
Before ShadowJS is published
# in the ShadowJS repository
npm run pack:local # builds and packs every package into .pack/
# anywhere
node dist/cli.js my-app --template dusk --local /path/to/ShadowJS/.pack
node dist/cli.js my-app --template dusk --pack-root /path/to/ShadowJSThe command reads <packDir>/manifest.json, checks that every tarball exists and that the packages
the project needs (and the ones they depend on) were packed, then writes file: dependencies plus
overrides for every packed package, as tools/local/lib.mjs does. The overrides matter: without
them npm would look up @shadow-js/compiler in the registry. It also writes
.shadowjs-local.json, so npm run local:install -- <app> --restore in the repository can put the
registry ranges back.
The matrix
scripts/verify-matrix.mjs is the end-to-end proof. It packs the repository, then for each cell of
the matrix (scripts/matrix-cells.mjs) generates an app with --local --pm npm, installs it, runs its
type check, tests (the example tests include the accessibility checks), linter, ESLint add-on and
build, starts the dev server and requests it, and for dusk starts the production server of the
target and requests it.
node scripts/verify-matrix.mjs # all 39 cells, 4 at a time
node scripts/verify-matrix.mjs --only spa:plain,dusk[node]:plain
node scripts/verify-matrix.mjs --list # the cell names
node scripts/verify-matrix.mjs --jobs 2 --keep # keep the apps
node scripts/verify-matrix.mjs --pack-dir <dir> # reuse a pack
node scripts/verify-matrix.mjs --axe --only content:plain,dashboard:plain--axe also runs axe-core (WCAG 2.0 and 2.1, levels A and AA) in Chromium on the pages of each served starter and Node dusk
app, at 375 and 1280 px, and fails the cell on any violation, or on a page that does not render a heading and one main. It
needs Chromium (npx playwright-core install chromium); CI runs it on Linux.
The cells are: spa with no add-ons, with everything on, and with the add-ons in rows that cover
every pair of choices (the router included); dusk with no add-ons, with everything on, and sixteen
rows in which every add-on choice appears with every deployment target; each starter (content, dashboard, api) runs with its optional add-ons in the rows that cover them. The exhaustive generator
tests (test/templates.test.ts, test/generated-snapshot.test.ts) cover every subset of the
add-ons without installing anything.
Packages are installed with npm only. The other managers are covered by command-generation tests
(test/package-managers.test.ts): every generated file uses the chosen manager's own commands.
Servers: node, tarout, bun (when Bun is installed), deno (when Deno is installed) and static
are started with their start script and requested. cloudflare, vercel and netlify need their
platform CLI and an account, so their cells check the build and the output folder only. A cell marked
PASS* has such a check skipped and says why; a skipped check is never counted as passed. The run
needs the npm registry for the third-party tools, and nothing else: it never calls Convex or Clerk.
The end-to-end test is npx vitest run test/matrix.e2e.test.ts; SHADOW_E2E_ONLY=spa:plain runs part of the matrix.
Versions
VERSIONS is the single source of the ranges written into projects: ShadowJS packages as ^1.0.0,
every third-party tool as an exact pin (vite 8.3.3, typescript 7.0.2, @biomejs/biome 2.5.15,
tailwindcss and @tailwindcss/vite 4.3.3, vitest 5.0.3, ...). A test compares each pin with what the
repository itself declares and has installed, so a pin cannot drift unnoticed.
Example
The command line and the library share the same generators. projectFiles and buildProject
return the files without writing; scaffold(directory, options) writes them.
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { buildProject, isValidPackageName, scaffold, toPackageName } from "create-shadow-app";
console.log(toPackageName("My App!"));
console.log(isValidPackageName("My App!"));
const project = buildProject({ name: "demo", template: "dusk", tailwind: true });
console.log(project.files.has("serve.mjs"), project.installCommands.join(","));
const parent = mkdtempSync(join(tmpdir(), "shadow-app-"));
try {
const directory = join(parent, "my-app");
const result = scaffold(directory, { name: "my-app", tailwind: false, biome: true });
console.log([...result.files].sort().join(","));
const manifest = JSON.parse(readFileSync(join(directory, "package.json"), "utf8")) as {
devDependencies: Record<string, string>;
};
console.log(manifest.devDependencies["@shadow-js/vite"]);
try {
scaffold(directory, { name: "my-app", tailwind: false, biome: true });
} catch (error) {
const refused = error instanceof Error && error.message.endsWith("is not empty.");
console.log(refused ? "not empty" : "other");
}
} finally {
rmSync(parent, { recursive: true, force: true });
}API
Entry create-shadow-app. The package exports:
| Export | Description |
| --- | --- |
| scaffold | Writes a new project into a directory; throws if the name is invalid or the directory is not empty. Returns { directory, files }. |
| buildProject | Composes a project in memory: { options, files, installCommands, steps, scripts }. |
| projectFiles | The files of the generated project as a Map of relative path to content, without writing anything. |
| writeProject, inspectTarget | Write a file map (every path and every case-distinct name checked first, force for a non-empty directory; a failed write removes what it created) and look at a target directory. |
| resolveOptions, enabledAddons, ADDONS, TEMPLATES, PACKAGE_MANAGERS | The choices, their defaults and what they imply. |
| parseArgs | The command-line parser. |
| applyLocal, overridesFor, fileSpec, parseManifest, localState, overrideField, tarballName | The --local logic. |
| toPackageName, candidateName, isValidPackageName, packageNameProblem, resolveTarget, safeJoin, UsageError | Name and path rules: packageNameProblem says why a name is refused. |
| VERSIONS, THIRD_PARTY_VERSIONS, FRAMEWORK_PACKAGES, FRAMEWORK_RANGE, versionOf | The version table. |
| ScaffoldOptions, ScaffoldResult, ResolvedOptions, Project, LocalPack, LocalManifest | Types. |
The create-shadow-app binary is dist/cli.js. Scaffolding loads nothing but Node: oxc-parser is imported only when codemod runs, so it starts fast.
Known limits
convex/_generatedis a stand-in until the firstconvex dev, which overwrites it.- Clerk's sign-in pages use hash routing (
routing="hash"), so no splat route is needed. - A
duskapp with Convex or Clerk readsVITE_variables (envPrefix: ["VITE_"]invite.config.ts), because that is whatconvex devand Clerk's docs write. VITE_values are read at build time, also on the server. The server bundle of a Dusk app takes them from the environment duringvite build, so build with them set (.env.local); a server started with only runtime variables fails at startup with the message that names the missing one.- Clerk loads in its default script mode: Clerk's bundles come from the frontend API host of the key, so the Clerk add-on installs neither
@clerk/clerk-jsnor@clerk/ui, nor React. Passmode="bundled"toClerkProviderand install both packages for an offline or strict-CSP app (see the Clerk guide in the ShadowJS docs). - The static target serves prerendered pages only:
/and/about. The posts pages read data on the server and need a server target. - Cloudflare, Vercel and Netlify are checked by their build in the matrix, not served: their local dev servers need their CLIs and an account.
- The other package managers are generated and command-checked, not installed by the matrix: the matrix installs with npm.
- The tests of the generated apps are examples, not coverage of your code.
Links
Dependencies
oxc-parser0.153.0, forcodemodonly. It is imported on first use, so scaffolding never loads it. The package declares no peers.- Scaffolded projects get their own dependencies from the templates they write.
Types
- Names from the root:
parseArgs,buildProject,scaffold,writeProject,ParsedArgs,Project,ScaffoldResult,WriteResult,ResolvedOptions.
Support
- Node.js
>=22.12.0. ESM only: useimport;requireis not supported. - Browsers: not applicable; the package has no browser target.
- Versioning: every
@shadow-js/*package releases together at one version. Upgrade them as one set. - Documentation: ShadowJS documentation.
Using, changing and contributing
- Use the package (a person or a coding agent that builds with it): this README, and SKILL.md, the short page an agent reads first.
- Change the package (fix a bug, add a feature): AGENTS.md says how the repository is laid out, how to implement and test a change, and how a pull request is reviewed. It is written for people and coding agents alike.
- Take part: CONTRIBUTING.md is the short guide; SECURITY.md is for vulnerabilities.
