From c0a8d7d42b3651763f76c541e3112e76eca98641 Mon Sep 17 00:00:00 2001 From: Chandler Weiner Date: Tue, 20 Oct 2026 04:34:03 +0000 Subject: [PATCH 1/4] Document TypeScript project configuration and JSONC migration --- content/(dynamic)/crons.mdx | 16 +++-- content/(publish)/git.mdx | 10 +-- content/(reference)/config-file.mdx | 103 +++++++++++++++++++++++---- content/_llms-preamble.md | 4 ++ content/cli/index.mdx | 3 +- content/cli/project.mdx | 15 ++-- scripts/audit-composed-site.mjs | 3 +- scripts/audit-composed-site.test.mjs | 11 +++ scripts/docs-experience.test.mjs | 25 +++++++ scripts/website-dependencies.mjs | 1 + 10 files changed, 157 insertions(+), 34 deletions(-) diff --git a/content/(dynamic)/crons.mdx b/content/(dynamic)/crons.mdx index ee25e15..43fe689 100644 --- a/content/(dynamic)/crons.mdx +++ b/content/(dynamic)/crons.mdx @@ -1,11 +1,11 @@ --- title: Crons -description: Schedule a GET against one of your own paths. Declared in sf.jsonc, applied by publishing, with failures reported in the dashboard and sf crons. +description: Schedule a GET against one of your own paths. Declare crons in project configuration and publish to apply them. sidebar: order: 4 --- -There's no dashboard editor for crons. Declare schedules in `sf.jsonc`, then publish and make that version live to apply them. A preview does not replace the live version's schedules. +There's no dashboard editor for crons. Declare schedules in `spacefast.config.ts` or `sf.jsonc`, then publish and make that version live to apply them. A preview does not replace the live version's schedules. ## How a run happens @@ -23,15 +23,17 @@ A response under 400 is a success and is silent. A response of 400 or worse, or ## Declare it -There is no write API and no dashboard editor. `sf.jsonc` is the only writer, and a publish is the only edit. That is what makes a rollback restore the previous schedules for free. +There is no write API and no dashboard editor. [Project configuration](/config-file) is the only writer, and a publish is the only edit. A rollback restores the previous schedules. -```jsonc sf.jsonc -{ +```ts spacefast.config.ts +import { defineConfig } from "spacefast/config"; + +export default defineConfig({ "crons": [ { "path": "/api/digest", "schedule": "daily" }, { "path": "/api/poll", "schedule": "*/15 * * * *" } ] -} +}); ``` ```bash @@ -92,7 +94,7 @@ The run prints a receipt with the URL, status, duration, content type, byte coun Before the first publish, `sf crons ls` prints: ```text -No scheduled jobs declared. Add a `crons` block to sf.jsonc. +No scheduled jobs declared. Add a `crons` block to spacefast.config.ts. ``` ## Failures diff --git a/content/(publish)/git.mdx b/content/(publish)/git.mdx index 226821f..5e5ebc9 100644 --- a/content/(publish)/git.mdx +++ b/content/(publish)/git.mdx @@ -108,10 +108,12 @@ The platform builds your repository and publishes the output. It detects the fra Detected presets include Astro, Next, Nuxt, SvelteKit, Remix, TanStack Start, Vite, Solid Start, Qwik, Angular, Ember, Gatsby, Docusaurus, Eleventy, VitePress, Nextra, Hugo, Jekyll, Zola, MkDocs, and Sphinx. See [Frameworks](/frameworks). -Override any of it in `sf.jsonc` or on the connection. +Repository builds read `spacefast.config.ts` in the isolated build environment. Override build settings there or on the connection. `sf.jsonc` remains supported; TypeScript takes precedence when both files exist. See [Project configuration](/config-file) for imports, migration, and file precedence. -```jsonc sf.jsonc -{ +```ts spacefast.config.ts +import { defineConfig } from "spacefast/config"; + +export default defineConfig({ "build": { "rootDirectory": "apps/web", "installCommand": "pnpm install", @@ -119,7 +121,7 @@ Override any of it in `sf.jsonc` or on the connection. "outputDirectory": "dist", "timeoutSeconds": 900 } -} +}); ``` The dashboard's **Build settings** section holds the same four fields as **Root directory**, **Install command**, **Build command**, and **Output directory**, each with an **Auto** placeholder. Leave a field empty and the builder decides from the detected framework. **Detect build settings** re-runs detection and shows candidates with a confidence, which you can **Apply**. diff --git a/content/(reference)/config-file.mdx b/content/(reference)/config-file.mdx index cbc05dc..f71fd91 100644 --- a/content/(reference)/config-file.mdx +++ b/content/(reference)/config-file.mdx @@ -1,11 +1,85 @@ --- -title: sf.jsonc reference -description: Every key sf.jsonc accepts, what it does, and which defaults the finalizer actually computes +title: Project configuration +description: Configure a project with spacefast.config.ts or sf.jsonc, migrate between formats, and set serving and build defaults. sidebar: order: 1 --- -`sf.jsonc` is the project config Spacefast reads when a version finalizes. This page lists every key the schema accepts. After it you can write the file by hand and know which defaults are real. +`spacefast.config.ts` is the preferred project configuration for new projects. `sf init` creates it. `sf.jsonc` and its JSON/JSONC aliases remain permanently supported; they are not deprecated. + +Both formats declare the settings below. The CLI evaluates TypeScript during a build and freezes the result into JSON configuration for serving. The server does not execute TypeScript configuration on visitor requests. + +## TypeScript configuration + +```ts spacefast.config.ts +import { defineConfig } from "spacefast/config"; + +export default defineConfig({ + name: "My App", + build: { buildCommand: "npm run build", outputDirectory: "dist" }, + crons: [{ path: "/api/digest", schedule: "daily" }], +}); +``` + +The module must have a default export. It can be an object, a promise of an object, or a function returning either. Async functions and local imports work. Package imports resolve from your project, so install those dependencies there. + +`spacefast/config` exports `defineConfig`, `SpacefastConfig`, and `SpacefastConfigInput`. `defineConfig` preserves type inference and checks the input type; the CLI validates the resolved configuration when it loads it. `SpacefastConfig` describes the object, while `SpacefastConfigInput` also accepts promises and factories. You can use the object type without the helper: + +```ts spacefast.config.ts +import type { SpacefastConfig } from "spacefast/config"; + +export default { + name: "My App", + cleanUrls: true, +} satisfies SpacefastConfig; +``` + +The installed CLI supplies `defineConfig` when it loads the file, including global and standalone installations. For editor type checking, add `spacefast` as a project development dependency: + +```bash +npm install --save-dev spacefast +``` + +The resolved object must contain JSON-compatible values. Optional properties with `undefined` values are omitted when the CLI writes JSON. Configuration is executable code: only build projects you trust, and do not put secrets in settings that become part of a published artifact. + +## Migrate from sf.jsonc + +Migration is optional. There is no `sf migrate config` command; `sf migrate` publishes a site from a public URL. + +1. Create `spacefast.config.ts` beside your existing `sf.jsonc`. +2. Add the `defineConfig` import and wrap the existing object in `export default defineConfig(...)`. JSON property names and comments can stay. Remove the optional `$schema` editor pointer; TypeScript uses the exported types instead. +3. Keep all your settings, including `runtime`, routing rules, crons, and build options. Do not replace your file with an empty starter. +4. Run `sf build` from the project root to validate and build without publishing. Fix any configuration or build errors. +5. Remove the old `sf.jsonc` after the build succeeds. Commit the TypeScript file and any dependency changes. + +For example, this JSONC: + +```jsonc sf.jsonc +{ + "$schema": "https://spacefast.com/schemas/sf.json", + "name": "My App", + "cleanUrls": true +} +``` + +becomes: + +```ts spacefast.config.ts +import { defineConfig } from "spacefast/config"; + +export default defineConfig({ + name: "My App", + cleanUrls: true, +}); +``` + +Keep `.spacefast/space.json` if you have linked the project. Changing the config format does not replace that link. + +## Repository builds and uploads + +Connected Git repository builds read `spacefast.config.ts` too. Commit it with your source; evaluation happens in the isolated build environment. The CLI also accepts it through `--config` (or `SPACEFAST_CONFIG`) on commands that expose that option, including Git build configuration. Do not depend on files or environment variables that exist only on your laptop. + +Source builds preserve the authored TypeScript file and produce resolved JSON configuration in the build output. Raw file uploads and hosted MCP inline publishing cannot execute it. For those paths, build locally with `sf build`, then publish the resulting output directory with `sf publish --prebuilt`. ## A full example @@ -68,19 +142,20 @@ sidebar: ## Where the file lives -The canonical name is `sf.jsonc` at the root of the directory you publish. Six aliases are accepted too. The first file found wins, in this order. +Put the config at the project root. During project builds, the first file found wins, in this order: -1. `sf.jsonc` -2. `spacefast.jsonc` -3. `spacefast.json` -4. `sf.json` -5. `.sf/sf.json` -6. `.sf/config.jsonc` -7. `.sf/config.json` +1. `spacefast.config.ts` +2. `sf.jsonc` +3. `spacefast.jsonc` +4. `spacefast.json` +5. `sf.json` +6. `.sf/sf.json` +7. `.sf/config.jsonc` +8. `.sf/config.json` -All seven are private files. They are never served. Keep exactly one, since two config files in the same tree is an error. +All eight are private files. They are never served. Keep one authored config: if several exist, the CLI warns that it ignored the lower-priority files. An invalid or unreadable winning file fails the command; it does not fall back to another file. An explicit `--config` selects that file instead of discovery. -The file is JSON with comments, capped at 256 KiB. Point `$schema` at `https://spacefast.com/schemas/sf.json` for editor completion and validation. +The JSONC format accepts comments and is capped at 256 KiB. Point `$schema` at [the JSON Schema](https://spacefast.com/schemas/sf.json) for editor completion and validation. That schema describes JSON configuration, not executable TypeScript modules. ## Identity @@ -206,7 +281,7 @@ See [Functions](/functions). | `crons[].path` | string, up to 512 chars | none | Path to GET. Starts with `/`, no query, no wildcards, no `:placeholders` | | `crons[].schedule` | string | none | A named schedule, a shorthand, or a five-field crontab | -`sf.jsonc` is the only writer. See [Crons](/crons) for the schedule grammar. +Project configuration is the only writer. Use `spacefast.config.ts` or `sf.jsonc`. See [Crons](/crons) for the schedule grammar. ## Access diff --git a/content/_llms-preamble.md b/content/_llms-preamble.md index e729ec2..764e7df 100644 --- a/content/_llms-preamble.md +++ b/content/_llms-preamble.md @@ -32,6 +32,10 @@ Fetch https://spacefast.com/setup.md That page teaches a single-request publish flow that needs no CLI and no MCP server. +For project settings, read [Project configuration](https://spacefast.com/docs/config-file). +New projects use `spacefast.config.ts` with `defineConfig` from `spacefast/config`. +`sf.jsonc` remains supported. The [JSON Schema](https://spacefast.com/schemas/sf.json) describes the JSON format, not TypeScript modules. + For a fuller setup, install the CLI and let it configure the rest: ```bash diff --git a/content/cli/index.mdx b/content/cli/index.mdx index b8a2a01..1704bfe 100644 --- a/content/cli/index.mdx +++ b/content/cli/index.mdx @@ -181,7 +181,8 @@ Other files the CLI owns: | `~/.spacefast/credentials/profile-*.json` | One credential file per profile | | `.spacefast/space.json` | Committable link: which Space, team, and API base URL | | `.spacefast/state.json` | Local cache and credentials. Never commit it, and `sf` adds it to `.gitignore` for you | -| `sf.jsonc` | Project config written by `sf init` | +| `spacefast.config.ts` | Typed project config written by `sf init`; see [Project configuration](/config-file) | +| `sf.jsonc` | Permanently supported JSONC project config; TypeScript takes precedence when both exist | ## Update checks diff --git a/content/cli/project.mdx b/content/cli/project.mdx index 2b1df5b..8037547 100644 --- a/content/cli/project.mdx +++ b/content/cli/project.mdx @@ -49,11 +49,12 @@ sf init my-app --runtime zero --template guestbook ### What it writes -`sf init` writes `sf.jsonc`, commented out down to the keys you are likely to want: +For a new static project, `sf init` writes `spacefast.config.ts`. Add the settings you need inside `defineConfig`: -```jsonc sf.jsonc -{ - "$schema": "https://spacefast.com/schemas/sf.json", +```ts spacefast.config.ts +import { defineConfig } from "spacefast/config"; + +export default defineConfig({ // Project link: .spacefast/space.json (written by sf init --space, sf link, or sf publish). // "index": "index.html", // "fallback": "/index.html", // SPA fallback; or { "path": "/404.html", "status": 404 } @@ -62,19 +63,19 @@ sf init my-app --runtime zero --template guestbook // "meta": { "title": "", "description": "", "image": "/og.png", "favicon": "/favicon.svg" }, // "templates": [], // files that receive {{ vars.NAME }} substitution // "build": { "rootDirectory": ".", "buildCommand": "npm run build", "outputDirectory": "dist" }, -} +}); ``` With `--space` it also writes `.spacefast/space.json` and refreshes `.spacefast/state.json`. If the cache write fails you get a warning, not a failure, because the committed link is the source of truth. -Other config filenames still work, in this precedence order: `sf.jsonc`, `spacefast.jsonc`, `spacefast.json`, `sf.json`, `.sf/sf.json`, `.sf/config.jsonc`, `.sf/config.json`. The highest one present wins and the rest produce a warning naming the winner. +`spacefast.config.ts` takes precedence over `sf.jsonc` and the other JSON/JSONC aliases. Those formats remain permanently supported. The highest-priority file present wins; the CLI warns about ignored files and does not fall back if the winning file is invalid. See [Project configuration](/config-file) for the exported types, full precedence, and migration steps. `sf init` refuses to overwrite an existing config without `--force`, and refuses a symlinked or non-regular config outright. `sf init ` removes the directory it created if init fails. ### Output ```text -{ "data": { "created": true, "configPath": "…/sf.jsonc" } } +{ "data": { "created": true, "configPath": "…/spacefast.config.ts" } } ``` With `--space`, `data` also carries `linkPath`, `space`, `statePath`, and `stateSaved`. diff --git a/scripts/audit-composed-site.mjs b/scripts/audit-composed-site.mjs index a29f444..96472e8 100644 --- a/scripts/audit-composed-site.mjs +++ b/scripts/audit-composed-site.mjs @@ -24,7 +24,8 @@ function isComposedDependency(diagnostic, dependencyUrls, dependencies) { (url) => (diagnostic.message.startsWith(`Link to ${url} resolves to `) && diagnostic.message.endsWith(", which the build does not serve.")) || - diagnostic.message === `Link to ${url}${outsideBase}`, + diagnostic.message === `Link to ${url}${outsideBase}` || + diagnostic.message === `Link to ${url} is missing deployment.base (/docs), so the deployed site does not serve it.`, ) || dependencies.some( (dependency) => diff --git a/scripts/audit-composed-site.test.mjs b/scripts/audit-composed-site.test.mjs index ca02a5b..8f3b629 100644 --- a/scripts/audit-composed-site.test.mjs +++ b/scripts/audit-composed-site.test.mjs @@ -56,3 +56,14 @@ test("allows only exact Website-owned composition dependencies", () => { diagnostics[6], ]); }); + +test("allows the declared JSON Schema with Blume's direct-link diagnostic", () => { + const diagnostic = (url) => ({ + code: "BLUME_AUDIT_LINK_TO_BROKEN", + severity: "error", + message: `Link to ${url} is missing deployment.base (/docs), so the deployed site does not serve it.`, + }); + const schema = diagnostic("https://spacefast.com/schemas/sf.json"); + const missing = diagnostic("https://spacefast.com/schemas/missing.json"); + assert.deepEqual(unexpectedAuditErrors([schema, missing], ["/schemas/sf.json"]), [missing]); +}); diff --git a/scripts/docs-experience.test.mjs b/scripts/docs-experience.test.mjs index 1d14f69..6784aba 100644 --- a/scripts/docs-experience.test.mjs +++ b/scripts/docs-experience.test.mjs @@ -22,6 +22,31 @@ function opening(page) { return paragraph.replace(/\s+/gu, " "); } +test("project configuration explains both supported formats and migration", async () => { + const config = await builtPage("/config-file"); + assert.match(config, /sf\.jsonc.*permanently supported; they are not deprecated/u); + for (const symbol of ["defineConfig", "SpacefastConfig", "SpacefastConfigInput"]) { + assert.ok(config.includes(symbol), `configuration must explain ${symbol}`); + } + assert.match(config, /1\. `spacefast\.config\.ts`\s+2\. `sf\.jsonc`/u); + assert.match(config, /does not fall back to another file/u); + assert.match(config, /There is no `sf migrate config` command/u); + assert.match(config, /Run `sf build` from the project root/u); + assert.match(config, /Connected Git repository builds read `spacefast\.config\.ts` too/u); + assert.match(config, /hosted MCP inline publishing cannot execute it/u); + assert.match(config, /https:\/\/spacefast\.com\/schemas\/sf\.json/u); + + const project = await builtPage("/cli/project"); + assert.match(project, /`sf init` writes `spacefast\.config\.ts`/u); + assert.doesNotMatch(project, /`sf init` writes `sf\.jsonc`/u); + const crons = await builtPage("/crons"); + assert.match(crons, /```ts spacefast\.config\.ts/u); + const git = await builtPage("/git"); + assert.match(git, /Repository builds read `spacefast\.config\.ts`/u); + const index = await readFile(path.join(dist, "llms.txt"), "utf8"); + assert.match(index, /Project configuration.*https:\/\/spacefast\.com\/docs\/config-file/u); +}); + test("every authored page opens with its subject instead of a page promise", async () => { const offenders = []; const manifest = JSON.parse(await readFile(path.join(root, ".blume", "blume.manifest.json"), "utf8")); diff --git a/scripts/website-dependencies.mjs b/scripts/website-dependencies.mjs index a28f0e2..d89bc16 100644 --- a/scripts/website-dependencies.mjs +++ b/scripts/website-dependencies.mjs @@ -11,6 +11,7 @@ export const websiteDependencies = [ "/cookie-banner.js", "/help", "/recipes", + "/schemas/sf.json", "/setup", ]; From 9302a80a78aa13830c0ca8f83f2d03e20424ee6b Mon Sep 17 00:00:00 2001 From: Chandler Weiner Date: Tue, 20 Oct 2026 04:52:00 +0000 Subject: [PATCH 2/4] Preserve existing configuration in cron and prebuilt guidance --- content/(dynamic)/crons.mdx | 6 +----- content/(dynamic)/functions.mdx | 2 +- content/(reference)/config-file.mdx | 2 +- content/(serve)/routing.mdx | 10 ++++++---- content/cli/project.mdx | 13 ++++--------- scripts/docs-experience.test.mjs | 13 +++++++------ 6 files changed, 20 insertions(+), 26 deletions(-) diff --git a/content/(dynamic)/crons.mdx b/content/(dynamic)/crons.mdx index 43fe689..03eca10 100644 --- a/content/(dynamic)/crons.mdx +++ b/content/(dynamic)/crons.mdx @@ -91,11 +91,7 @@ The run prints a receipt with the URL, status, duration, content type, byte coun `sf crons run` cannot send the platform's own `x-spacefast-cron` header. A handler that gates on it will refuse a manual run, which is why the example above passes a shared secret explicitly. ::: -Before the first publish, `sf crons ls` prints: - -```text -No scheduled jobs declared. Add a `crons` block to spacefast.config.ts. -``` +Before the first publish, `sf crons ls` reports no scheduled jobs. Add a `crons` block to your existing project configuration, either `spacefast.config.ts` or `sf.jsonc`, then publish. Do not create a second configuration file just to add crons. ## Failures diff --git a/content/(dynamic)/functions.mdx b/content/(dynamic)/functions.mdx index 8f96dcb..20e6064 100644 --- a/content/(dynamic)/functions.mdx +++ b/content/(dynamic)/functions.mdx @@ -82,7 +82,7 @@ export default { }; ``` -`sf init my-app --runtime functions` writes exactly that file plus the matching `sf.jsonc`. +`sf init my-app --runtime functions` writes exactly that file plus the matching `spacefast.config.ts`. Requests and responses are the standard `Request` and `Response`. There is no Spacefast wrapper around either. diff --git a/content/(reference)/config-file.mdx b/content/(reference)/config-file.mdx index f71fd91..9008e18 100644 --- a/content/(reference)/config-file.mdx +++ b/content/(reference)/config-file.mdx @@ -79,7 +79,7 @@ Keep `.spacefast/space.json` if you have linked the project. Changing the config Connected Git repository builds read `spacefast.config.ts` too. Commit it with your source; evaluation happens in the isolated build environment. The CLI also accepts it through `--config` (or `SPACEFAST_CONFIG`) on commands that expose that option, including Git build configuration. Do not depend on files or environment variables that exist only on your laptop. -Source builds preserve the authored TypeScript file and produce resolved JSON configuration in the build output. Raw file uploads and hosted MCP inline publishing cannot execute it. For those paths, build locally with `sf build`, then publish the resulting output directory with `sf publish --prebuilt`. +Source builds preserve the authored TypeScript file and put the resolved JSON configuration in the build archive. Raw file uploads and hosted MCP inline publishing cannot execute it. To publish prebuilt content without losing configuration, run `sf build`, then `sf publish .spacefast/build-output.tgz --prebuilt`. If you set `sf build --output`, publish that archive instead. Plain `sf publish` from the project root also loads the project configuration. ## A full example diff --git a/content/(serve)/routing.mdx b/content/(serve)/routing.mdx index 3e2344c..b66eaf6 100644 --- a/content/(serve)/routing.mdx +++ b/content/(serve)/routing.mdx @@ -1,6 +1,6 @@ --- title: Redirects, rewrites, and headers -description: The _redirects and _headers files, the routing rules in sf.jsonc, how they are ordered, and the limits they run under +description: The _redirects and _headers files, project configuration routing rules, their order, and their limits. sidebar: { order: 3 } --- @@ -14,13 +14,13 @@ Nothing here is evaluated per request. Spacefast compiles your rules at publish | --- | --- | | `_redirects` | Line-based redirect, rewrite, proxy, and 404 rules | | `_headers` | Block-based response header rules | -| `sf.jsonc` | Typed `redirects`, `rewrites`, and `headers` arrays | +| `spacefast.config.ts` or `sf.jsonc` | Typed `redirects`, `rewrites`, and `headers` arrays | They merge into one list in that order: `_redirects` rules first, then `redirects` from the config, then its `rewrites`. Within a list, rules match in the order you wrote them and the first match wins. A config rule whose source is already claimed by an unconditional file rule never runs, and publish reports `redirect_shadowed_by_file`: `_redirects already routes "{source}", so this rule never runs.` -The config file is `sf.jsonc`. These names also work, in this precedence order: `spacefast.jsonc`, `spacefast.json`, `sf.json`, `.sf/sf.json`, `.sf/config.jsonc`, `.sf/config.json`. +`spacefast.config.ts` takes precedence over `sf.jsonc`. JSON and JSONC formats remain supported. See [Project configuration](/config-file) for the full discovery order and explicit file selection. Add routing rules to your existing configuration rather than creating a second file. `_redirects` and `_headers` are published but never served as content. Requesting one returns 404. @@ -89,7 +89,9 @@ Header names must match `^[A-Za-z0-9-]+$`. Some names are owned by the platform `_headers` matchers cannot include a port. They match the raw pathname, so they are sensitive to a trailing slash where `_redirects` is not. Header rules also apply to the URL the visitor asked for, not to the path a rewrite landed on. -## Rules in `sf.jsonc` +## Rules in project configuration + +The examples below use JSONC. The same arrays work inside `defineConfig` in `spacefast.config.ts`. ```jsonc sf.jsonc { diff --git a/content/cli/project.mdx b/content/cli/project.mdx index 8037547..bd9f332 100644 --- a/content/cli/project.mdx +++ b/content/cli/project.mdx @@ -49,20 +49,15 @@ sf init my-app --runtime zero --template guestbook ### What it writes -For a new static project, `sf init` writes `spacefast.config.ts`. Add the settings you need inside `defineConfig`: +`sf init` writes `spacefast.config.ts`. Without a runtime option, it starts with these commented examples: ```ts spacefast.config.ts import { defineConfig } from "spacefast/config"; export default defineConfig({ - // Project link: .spacefast/space.json (written by sf init --space, sf link, or sf publish). - // "index": "index.html", - // "fallback": "/index.html", // SPA fallback; or { "path": "/404.html", "status": 404 } - // "listing": false, - // "access": "public", // whole live space public; or { "public": ["/assets/**", "!/assets/private/**"] } - // "meta": { "title": "", "description": "", "image": "/og.png", "favicon": "/favicon.svg" }, - // "templates": [], // files that receive {{ vars.NAME }} substitution - // "build": { "rootDirectory": ".", "buildCommand": "npm run build", "outputDirectory": "dist" }, + // fallback: "/index.html", + // meta: { title: "", description: "", image: "/og.png" }, + // build: { buildCommand: "npm run build", outputDirectory: "dist" }, }); ``` diff --git a/scripts/docs-experience.test.mjs b/scripts/docs-experience.test.mjs index 6784aba..4e40ab5 100644 --- a/scripts/docs-experience.test.mjs +++ b/scripts/docs-experience.test.mjs @@ -28,12 +28,13 @@ test("project configuration explains both supported formats and migration", asyn for (const symbol of ["defineConfig", "SpacefastConfig", "SpacefastConfigInput"]) { assert.ok(config.includes(symbol), `configuration must explain ${symbol}`); } - assert.match(config, /1\. `spacefast\.config\.ts`\s+2\. `sf\.jsonc`/u); - assert.match(config, /does not fall back to another file/u); - assert.match(config, /There is no `sf migrate config` command/u); - assert.match(config, /Run `sf build` from the project root/u); - assert.match(config, /Connected Git repository builds read `spacefast\.config\.ts` too/u); - assert.match(config, /hosted MCP inline publishing cannot execute it/u); + assert.match(config, /first file found wins[\s\S]*`spacefast\.config\.ts`[\s\S]*`sf\.jsonc`/u); + assert.match(config, /does not fall back/u); + assert.match(config, /no `sf migrate config`/u); + assert.match(config, /`sf build`[^\n]*validat/u); + assert.match(config, /Git repository builds[^\n]*`spacefast\.config\.ts`/u); + assert.match(config, /hosted MCP[^\n]*cannot execute/u); + assert.match(config, /sf publish \.spacefast\/build-output\.tgz --prebuilt/u); assert.match(config, /https:\/\/spacefast\.com\/schemas\/sf\.json/u); const project = await builtPage("/cli/project"); From ac60665f3c660890a154b84eaeb42afaa31df714 Mon Sep 17 00:00:00 2001 From: Chandler Weiner Date: Tue, 20 Oct 2026 04:55:23 +0000 Subject: [PATCH 3/4] Keep configuration examples in the existing project file --- content/(concepts)/spaces.mdx | 2 +- content/(dynamic)/database.mdx | 4 ++-- content/(dynamic)/environment-variables.mdx | 2 +- content/(dynamic)/zero-runtime.mdx | 6 +++--- content/(serve)/customization.mdx | 8 ++++---- content/cli/db.mdx | 2 +- content/cli/publish.mdx | 2 +- content/cli/share.mdx | 2 +- content/platforms/partner-api/configuration.mdx | 2 +- content/troubleshooting.mdx | 2 +- 10 files changed, 16 insertions(+), 16 deletions(-) diff --git a/content/(concepts)/spaces.mdx b/content/(concepts)/spaces.mdx index 0734f01..cf3caa0 100644 --- a/content/(concepts)/spaces.mdx +++ b/content/(concepts)/spaces.mdx @@ -128,7 +128,7 @@ What changes: - The old slug stays reserved for this Space. - Version URLs do not move. They were computed once and stored. -One thing blocks a rename. While `name` is set in your `sf.jsonc`, the API refuses to rename the Space — the file would win back the old name on the next publish. Remove the key or rename it in the file. See [Config file](/config-file). +One thing blocks a rename. While `name` is set in your project configuration, the API refuses to rename the Space — the file would win back the old name on the next publish. Remove the key or rename it in the file. See [Config file](/config-file). ## Deleting and restoring diff --git a/content/(dynamic)/database.mdx b/content/(dynamic)/database.mdx index c7b2a60..073049a 100644 --- a/content/(dynamic)/database.mdx +++ b/content/(dynamic)/database.mdx @@ -8,7 +8,7 @@ sidebar: There's no connection string for this database — your code reaches it through `ctx.db` or `env.DB`, or through a single-use SQL console when you need raw SQL. :::note[Don't have a database yet?] -For [Zero](/zero-runtime), start with `sf init --runtime zero`, or add the full [runtime block](/zero-runtime#declare-it) to `sf.jsonc`, including `kind: "zero"`, `server`, and `client` entry paths. The server file is required; Zero can generate a client shell. Publish the app before querying its live database. A [Functions](/functions#what-is-on-env) worker also gets a database through `env.DB`, but the Zero table commands do not apply to it. +For [Zero](/zero-runtime), start with `sf init --runtime zero`, or add the full [runtime block](/zero-runtime#declare-it) to your existing `spacefast.config.ts` or `sf.jsonc`, including `kind: "zero"`, `server`, and `client` entry paths. The server file is required; Zero can generate a client shell. Publish the app before querying its live database. A [Functions](/functions#what-is-on-env) worker also gets a database through `env.DB`, but the Zero table commands do not apply to it. ::: ## What it is @@ -150,7 +150,7 @@ sf db migrate --rename | `drop_table`, `drop_column`, `drop_index` | `--drop` | | `rename_table`, `rename_column` | `--rename` | -`sf db migrate` decides Zero-ness from your local `sf.jsonc` before it contacts the API. A static or Functions project fails with `runtime_kind_mismatch` and exit code `30`, having sent no request. `--previous-artifact` diffs against a named artifact instead of the live schema. +`sf db migrate` decides Zero-ness from your local project configuration before it contacts the API. A static or Functions project fails with `runtime_kind_mismatch` and exit code `30`, having sent no request. `--previous-artifact` diffs against a named artifact instead of the live schema. `sf db`, `sf db dump`, and `sf db migrate` are Zero-only. A worker declares no table set, so they answer `runtime_kind_mismatch` for one. diff --git a/content/(dynamic)/environment-variables.mdx b/content/(dynamic)/environment-variables.mdx index 4dcd062..f3009f2 100644 --- a/content/(dynamic)/environment-variables.mdx +++ b/content/(dynamic)/environment-variables.mdx @@ -61,7 +61,7 @@ So a brand new variable with no flag is write-only. Pass `--no-secret` when you ### Build time, `{{ vars.NAME }}` -List a file under `templates` in `sf.jsonc`. Every `{{ vars.NAME }}` token in it is substituted when the version finalizes. +List a file under `templates` in your existing `spacefast.config.ts` or `sf.jsonc`. Every `{{ vars.NAME }}` token in it is substituted when the version finalizes. ```jsonc sf.jsonc { diff --git a/content/(dynamic)/zero-runtime.mdx b/content/(dynamic)/zero-runtime.mdx index 572cf1c..d210e61 100644 --- a/content/(dynamic)/zero-runtime.mdx +++ b/content/(dynamic)/zero-runtime.mdx @@ -5,7 +5,7 @@ sidebar: order: 0 --- -Zero is available on every account, with [usage allowances and metered rates](/usage). To use it, declare the full [runtime block](#declare-it) in `sf.jsonc` and provide the server entry. +Zero is available on every account, with [usage allowances and metered rates](/usage). To use it, declare the full [runtime block](#declare-it) in your existing project configuration (`spacefast.config.ts` or `sf.jsonc`) and provide the server entry. ## What Zero is @@ -50,7 +50,7 @@ sf publish | Endpoints | Plain HTTP routes in your capsule, for webhooks and callers outside the app. | This page | | Queries, mutations, and actions | The read, transactional write, and outbound-work paths your client calls. Queries are live. | This page | | Environment variables | `ctx.env`, from `.env.server` or `sf env`. | [Environment variables](/environment-variables) | -| Crons | Scheduled `GET` requests to your own paths, declared in `sf.jsonc`. | [Crons](/crons) | +| Crons | Scheduled `GET` requests to your own paths, declared in project configuration. | [Crons](/crons) | | Storage | Runtime object storage for visitor uploads. | [Storage](/object-storage) | | Logs | What your handlers wrote, plus every request the edge served. | [Logs](/logs) | @@ -278,7 +278,7 @@ sf dev Workers, the `functions/` file router, and OpenNext. - + Every key the config schema accepts. diff --git a/content/(serve)/customization.mdx b/content/(serve)/customization.mdx index fdd33ac..68c2247 100644 --- a/content/(serve)/customization.mdx +++ b/content/(serve)/customization.mdx @@ -3,17 +3,17 @@ title: Customization description: Theme the pages Spacefast draws, set the title and image links unfurl with, add an analytics tag, and run one site-wide script --- -Everything you set here lives on the Space, not a version — it survives every publish, and it wins over whatever `theme` or `meta` your `sf.jsonc` declares. +Everything you set here lives on the Space, not a version — it survives every publish, and it wins over whatever `theme` or `meta` your project configuration declares. Open the Space and go to **Customization**. Four tabs, in order: **Theme**, **Search & sharing**, **Google Analytics**, and **Custom JS**. A large live preview sits beside the controls, labelled **Visitor sees**, painted from your unsaved draft. ## What lives on the Space, not the version -Everything on this page is stored on the Space rather than on a version, so it survives a publish and a rollback alike. It also wins over the published `sf.jsonc`: the Space's `theme` replaces the file's rather than merging with it. So a project that sets `theme` in its config, then edits Theme here, keeps seeing the dashboard's answer until you clear it. The same holds for `meta`. +Everything on this page is stored on the Space rather than on a version, so it survives a publish and a rollback alike. It also wins over the published configuration: the Space's `theme` replaces the file's rather than merging with it. So a project that sets `theme` in its config, then edits Theme here, keeps seeing the dashboard's answer until you clear it. The same holds for `meta`. Theme and share metadata apply without republishing. Save, and the next request gets them. **Custom JS** is the exception, and its own field note says so: a script change takes effect on the next publish. -Everything the four tabs write is `meta`, `theme`, and `superpowers` in the Space's config. The same keys are documented in the [`sf.jsonc` reference](/config-file), and the [`updateSpace`](/api/reference/spaces/updatespace) operation is what the dashboard calls. +Everything the four tabs write is `meta`, `theme`, and `superpowers` in the Space's config. The same keys are documented in the [Project configuration reference](/config-file), and the [`updateSpace`](/api/reference/spaces/updatespace) operation is what the dashboard calls. ## Theme @@ -112,6 +112,6 @@ The header's **Copy as spacefast.jsonc** button, titled "Theme and share metadat } ``` -Paste it into your project's `sf.jsonc` and those two keys travel with the repository. `spacefast.jsonc` is the button's wording and a legacy filename the loader still accepts; the canonical name is `sf.jsonc`. The rest of the file is unaffected, and both keys work under either name. +Merge these two keys into your existing project configuration: inside `defineConfig` for `spacefast.config.ts`, or into the object in `sf.jsonc`. Keep the other settings. Do not create a second configuration file. `spacefast.jsonc` is the button's wording and a supported JSONC alias; it does not take precedence over TypeScript. One thing to know before you paste it: as long as the Space also carries a `theme`, the Space keeps winning. Clear the Theme tab's fields if you want the file to be the source of truth. diff --git a/content/cli/db.mdx b/content/cli/db.mdx index b82a52e..25f9cda 100644 --- a/content/cli/db.mdx +++ b/content/cli/db.mdx @@ -221,7 +221,7 @@ sf crons ls --space docs ### Output -One row per job with its key, path, schedule, and recent failures. A Space with none prints "No scheduled jobs declared. Add a `crons` block to sf.jsonc." +One row per job with its key, path, schedule, and recent failures. A Space with none prints "No scheduled jobs declared. Add a `crons` block to sf.jsonc." Add that block to the project's existing configuration, whether it uses `spacefast.config.ts` or `sf.jsonc`; do not create a second file. With `--json`, `data` is `{ versionId, crons }`, where each cron carries `key`, `path`, `schedule`, `recentFailures[]`, and `unattributedFailures[]`. Run output is never echoed, because it can contain response bodies and secrets. diff --git a/content/cli/publish.mdx b/content/cli/publish.mdx index 4f06009..8a957d3 100644 --- a/content/cli/publish.mdx +++ b/content/cli/publish.mdx @@ -44,7 +44,7 @@ These apply when the publish creates the Space. On an existing Space with `--tar | `--access=private\|team\|public` | team default | Set initial root access when this publish creates a team-owned Space. Requires a login or `--token`, and fails with `space_selection_conflict` if a Space is already selected | | `--hostname-scope=team\|global` | server default | Managed hostname scope for newly created Spaces | | `--spa=auto\|true\|false` | `auto` | Control single-page app fallback detection | -| `--mode=website\|files` | `website` | Serve the Space as a website or as a raw file listing. Usually set in `sf.jsonc` | +| `--mode=website\|files` | `website` | Serve the Space as a website or as a raw file listing. Usually set in project configuration | `--mode files` with `--spa true` is rejected: a file listing has no SPA fallback to serve. diff --git a/content/cli/share.mdx b/content/cli/share.mdx index 2c83c80..dc18204 100644 --- a/content/cli/share.mdx +++ b/content/cli/share.mdx @@ -117,7 +117,7 @@ sf share grant --to public --role viewer --path '/**' --target all-versions ## sf share revoke -`sf share revoke ID` revokes one managed Grant by its ID from `sf share list`. Grants that come from `sf.jsonc` are changed there instead, and credential-backed Grants normally revoke through their own credential command. +`sf share revoke ID` revokes one managed Grant by its ID from `sf share list`. Grants that come from project configuration are changed there instead, and credential-backed Grants normally revoke through their own credential command. ```bash sf share revoke gnt_123 diff --git a/content/platforms/partner-api/configuration.mdx b/content/platforms/partner-api/configuration.mdx index 9b5904c..530e4ff 100644 --- a/content/platforms/partner-api/configuration.mdx +++ b/content/platforms/partner-api/configuration.mdx @@ -21,7 +21,7 @@ Use the current ID instead of `null` when replacing an existing system Space. A ## Publish the declaration -Add a `system` block to the system Space's `sf.jsonc`. Keep the other project settings and content in the same publish. +Add a `system` block to the system Space's existing project configuration (`spacefast.config.ts` or `sf.jsonc`). Keep the other project settings and content in the same publish. ```json { diff --git a/content/troubleshooting.mdx b/content/troubleshooting.mdx index fdcda0d..2794249 100644 --- a/content/troubleshooting.mdx +++ b/content/troubleshooting.mdx @@ -67,7 +67,7 @@ Full policy on [Caching](/caching). Three rules decide this, and your local dev server implements none of them. -Clean URLs are on by default, so `about.html` answers `/about` and `/about/` redirects to `/about` with a 308. A directory serves its `index.html`. A single-page app needs an explicit `fallback` in `sf.jsonc`. The fallback is skipped for around 110 asset extensions, so a missing `.js` returns 404 instead of your HTML shell. +Clean URLs are on by default, so `about.html` answers `/about` and `/about/` redirects to `/about` with a 308. A directory serves its `index.html`. A single-page app needs an explicit `fallback` in your existing [project configuration](/config-file). The fallback is skipped for around 110 asset extensions, so a missing `.js` returns 404 instead of your HTML shell. If two files would answer the same URL after that resolution, the publish stops with `publish_path_collision` rather than letting lookup order decide. See [Redirects, rewrites, and headers](/routing). From e6e49da6aba6de03de49c76baa13105e123c96a3 Mon Sep 17 00:00:00 2001 From: Chandler Weiner Date: Tue, 20 Oct 2026 04:56:36 +0000 Subject: [PATCH 4/4] Name both supported Git build configuration formats --- content/cli/git.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/content/cli/git.mdx b/content/cli/git.mdx index 2144a9c..68859a7 100644 --- a/content/cli/git.mdx +++ b/content/cli/git.mdx @@ -45,7 +45,7 @@ sf git connect [--space ] [--provider github|gitlab|bitbucket|generic] | `--default-branch=` | none | Repository default branch | | `--production-branch=` | none | Branch that publishes to production | | `--ref=` | none | Ref, branch, tag, or commit to sync or build after connecting. Env `SPACEFAST_BUILD_REF` | -| `--config=` | none | Path to an `sf.jsonc` with repository build settings. Env `SPACEFAST_CONFIG` | +| `--config=` | none | Path to a `spacefast.config.ts` or `sf.jsonc` with repository build settings. Env `SPACEFAST_CONFIG` | | `--auto-deploy-production` / `--no-auto-deploy-production` | `true` | Automatically build pushes to the production branch | | `--auto-deploy-previews` / `--no-auto-deploy-previews` | `true` | Automatically build preview branches and pull requests | | `--root-directory=` | none | App root directory | @@ -122,7 +122,7 @@ sf git update [--space ] [--connection-type connected|hosted] ... | `--clear-credential` | `false` | Clear the stored generic Git clone token | | `--auto-deploy-production` / `--no-auto-deploy-production` | unchanged | Automatically build pushes to the production branch | | `--auto-deploy-previews` / `--no-auto-deploy-previews` | unchanged | Automatically build preview branches and pull requests | -| `--config=` | none | Path to an `sf.jsonc` with repository build settings. Env `SPACEFAST_CONFIG` | +| `--config=` | none | Path to a `spacefast.config.ts` or `sf.jsonc` with repository build settings. Env `SPACEFAST_CONFIG` | | `--root-directory=` | unchanged | App root directory | | `--install-directory=` | unchanged | Dependency install directory | | `--install-command=` | unchanged | Install command | @@ -157,7 +157,7 @@ sf git build [--space ] [--connection-type connected|hosted] [--ref ` | none | Branch name for preview alias and production branch selection. Env `SPACEFAST_GIT_BRANCH` | | `--commit=` | none | Commit SHA to build. Env `SPACEFAST_GIT_COMMIT` | | `--pull-request=` | none | Pull request number for preview builds. Minimum 1. Env `SPACEFAST_PULL_REQUEST` | -| `--config=` | none | Path to an `sf.jsonc` with repository build settings. Env `SPACEFAST_CONFIG` | +| `--config=` | none | Path to a `spacefast.config.ts` or `sf.jsonc` with repository build settings. Env `SPACEFAST_CONFIG` | | `--target=` | from the branch | Force the build target | | `--root-directory=` | saved setting | Override the app root directory for this build | | `--install-directory=` | saved setting | Override the dependency install directory for this build |