@fjall/generator 22.0.0 → 24.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,7 +9,7 @@
9
9
  "package.json",
10
10
  "tsconfig.json"
11
11
  ],
12
- "hash": "072da3c52c9b559750731318bd00cee13ef1e254a1b6e8ece9e9f4378f958c06",
12
+ "hash": "f7719f98cd378ddbe28b4db7318ab783f99247be7b80208b9152ecacbdc9a9e1",
13
13
  "files": {
14
14
  "src/ast/astTestHelpers.ts": "026cac849eb88bdb",
15
15
  "src/ast/domain/astDomainParser.ts": "a88b990396db0a46",
@@ -111,7 +111,7 @@
111
111
  "src/codemod/validationGate/gates/schema.ts": "7ed5acaec0620a46",
112
112
  "src/codemod/validationGate/index.ts": "bbe1f460330a11cf",
113
113
  "src/codemod/validationGate/types.ts": "4a1cfa054e3383d5",
114
- "src/detection/index.ts": "88a577f0173028d4",
114
+ "src/detection/index.ts": "cd321fd291b7388b",
115
115
  "src/dns/bindParser.ts": "2b463c4dd1395ae5",
116
116
  "src/dns/bindWriter.ts": "c296911ce5aef77c",
117
117
  "src/dns/domainFileGenerator.ts": "f2d0ba8cca5ab1c9",
@@ -143,7 +143,7 @@
143
143
  "src/planning/resourceAddition.ts": "7d0320620a74fdcd",
144
144
  "src/planning/resourceConnections.ts": "a6b7db8f733e560f",
145
145
  "src/planning/resourcePlanning.ts": "d6bb9fa8e1419908",
146
- "src/planning/staticSitePlanning.ts": "242af1e9618821ab",
146
+ "src/planning/staticSitePlanning.ts": "ee0b100e081061be",
147
147
  "src/presets/clickhouseTierPreset.ts": "1758d991c81d769a",
148
148
  "src/presets/index.ts": "8332c0198b2a6fe9",
149
149
  "src/presets/patternTierPresets.ts": "f4fc368fc623aae4",
@@ -158,7 +158,7 @@
158
158
  "src/projection/types.ts": "68045bc02a36cee9",
159
159
  "src/projection/unresolved.ts": "885c8d3c1624b328",
160
160
  "src/schemas/alarmSchemas.ts": "beb19255d37ed43d",
161
- "src/schemas/applicationSchemas.ts": "d85771a4607cf2da",
161
+ "src/schemas/applicationSchemas.ts": "12ee3bc2654717f0",
162
162
  "src/schemas/baseSchemas.ts": "93d9bd1c0adfcaa0",
163
163
  "src/schemas/buildkiteSchemas.ts": "a28ec3b40f7851ec",
164
164
  "src/schemas/cdnSchemas.ts": "16784f15894c62ba",
@@ -171,7 +171,7 @@
171
171
  "src/schemas/messagingSchemas.ts": "2c1975fac903fd1b",
172
172
  "src/schemas/networkSchemas.ts": "c3a5f30e440dd2f8",
173
173
  "src/schemas/organisationSchemas.ts": "50d793aa7ff7f749",
174
- "src/schemas/patternSchemas.ts": "2ad2ed522c244ac1",
174
+ "src/schemas/patternSchemas.ts": "8b48dfb02852a7ba",
175
175
  "src/schemas/resourceSchemas.ts": "cd050813242ea213",
176
176
  "src/schemas/securityBoundary.ts": "54d216807103bab8",
177
177
  "src/schemas/sharedTypes.ts": "45210e600bea393e",
@@ -181,11 +181,11 @@
181
181
  "src/validation/patterns.ts": "f9629eafdad7ac3e",
182
182
  "src/validation/validationMessages.ts": "b03413b3953c3e79",
183
183
  "src/validation/validationPatterns.ts": "865f3d688975b7c8",
184
- "src/version.ts": "b24668563e688655",
184
+ "src/version.ts": "628659b85d1e2c43",
185
185
  "../scripts/minify-dist.mjs": "6b2e4b0df8aec601",
186
- "package.json": "68bbbd728e286eff",
186
+ "package.json": "097bea1fa43d11c5",
187
187
  "tsconfig.json": "1929cf3f99fe9b1e"
188
188
  },
189
- "outputHash": "b14439370356dd742ad72b81c29bf80df1c5ce3ffbd9470bac6637e6d3d42843",
189
+ "outputHash": "b758bb39fed2cbf694ae9e1df71412a218866466be0c834122143536d0d13cf3",
190
190
  "outputFileCount": 343
191
191
  }
package/dist/.minified CHANGED
@@ -1 +1 @@
1
- 171 files minified at 2026-08-30T00:18:26.718Z
1
+ 171 files minified at 2026-08-30T07:28:36.308Z
@@ -342,7 +342,7 @@ export declare function parseAndConvert(code: string): {
342
342
  command: string;
343
343
  outputDir: string;
344
344
  };
345
- routing?: "multipage" | "spa" | undefined;
345
+ routing?: "multipage" | "directory" | "spa" | undefined;
346
346
  security?: {
347
347
  headers?: boolean | undefined;
348
348
  contentSecurityPolicy?: string | undefined;
@@ -10,7 +10,7 @@
10
10
  * `apps detect --output json` frame and the MCP `detect_app_framework`
11
11
  * tool's `outputSchema`.
12
12
  */
13
- import { type Framework } from "@fjall/util";
13
+ import { type Framework, type StaticSiteRouting } from "@fjall/util";
14
14
  import { z } from "zod";
15
15
  /**
16
16
  * The framework vocabulary now lives in `@fjall/util/patterns`, beside the
@@ -104,24 +104,132 @@ export declare const CreatePrefillSchema: z.ZodObject<{
104
104
  outputDir: z.ZodNullable<z.ZodString>;
105
105
  routing: z.ZodNullable<z.ZodEnum<{
106
106
  multipage: "multipage";
107
+ directory: "directory";
107
108
  spa: "spa";
108
109
  }>>;
109
110
  }, z.core.$strict>;
110
111
  export type CreatePrefill = z.infer<typeof CreatePrefillSchema>;
112
+ export declare const PACKAGE_MANAGER_VALUES: readonly ["npm", "pnpm", "yarn", "bun"];
113
+ export type PackageManager = (typeof PACKAGE_MANAGER_VALUES)[number];
114
+ /**
115
+ * The lockfile that proves each manager.
116
+ *
117
+ * Keyed on the MANAGER, not on the file, so the vocabulary cannot grow a member
118
+ * that no evidence can ever reach — a manager without a lockfile here fails to
119
+ * compile rather than becoming silently undetectable. That is the shape of the
120
+ * bug this table exists to close: the manager vocabulary and the set of files
121
+ * actually probed drifted apart, so pnpm was only recognised through
122
+ * `pnpm-workspace.yaml` and never through its lockfile.
123
+ *
124
+ * The value is a NON-EMPTY tuple. A manager whose evidence list is empty would
125
+ * satisfy a plain `string[]` while being exactly as undetectable as one that is
126
+ * missing, so the type refuses to let detectability be claimed without naming a
127
+ * file that proves it.
128
+ */
129
+ export declare const PACKAGE_MANAGER_LOCKFILES: {
130
+ readonly npm: readonly ["package-lock.json"];
131
+ readonly pnpm: readonly ["pnpm-lock.yaml"];
132
+ readonly yarn: readonly ["yarn.lock"];
133
+ readonly bun: readonly ["bun.lock", "bun.lockb"];
134
+ };
135
+ /**
136
+ * Which package manager runs this repo's scripts.
137
+ *
138
+ * Deliberately orthogonal to `MonorepoTool`. The two were conflated — the build
139
+ * command was chosen by asking `inferMonorepoTool`, whose ladder returns the
140
+ * ORCHESTRATOR in preference to the manager — so turbo-on-pnpm (a common pair,
141
+ * not an exotic one) reported `npm run build`, and so did every yarn repo, every
142
+ * bun repo, and every flat pnpm repo that has a lockfile but no
143
+ * `pnpm-workspace.yaml`. A repo has both a manager and, sometimes, an
144
+ * orchestrator; asking one question cannot answer the other.
145
+ *
146
+ * Absent evidence resolves to npm: it ships with Node, so a repo with no
147
+ * lockfile and no declared manager is npm's by default rather than by guess.
148
+ * Contradictory evidence (two lockfiles committed) is not a tie to break — the
149
+ * declared field decides, and npm is the fallback.
150
+ */
151
+ export declare function inferPackageManager(rootPackageJson: ParsedPackageJson | null, markers: MarkerFiles): PackageManager;
152
+ /**
153
+ * Astro's `build.format` vocabulary — the setting that decides which object
154
+ * each page becomes, and therefore which routing mode serves the site.
155
+ */
156
+ export declare const ASTRO_BUILD_FORMAT_VALUES: readonly ["file", "directory", "preserve"];
157
+ export type AstroBuildFormat = (typeof ASTRO_BUILD_FORMAT_VALUES)[number];
158
+ /**
159
+ * Config filenames Astro will load, in the order the CLI probes them. Owned
160
+ * here beside the parser so the I/O wrapper cannot probe a spelling the
161
+ * parser was never taught about.
162
+ */
163
+ export declare const ASTRO_CONFIG_FILENAMES: readonly ["astro.config.mjs", "astro.config.js", "astro.config.ts", "astro.config.cjs", "astro.config.mts", "astro.config.cts"];
164
+ /**
165
+ * Read `build.format` out of an Astro config's text.
166
+ *
167
+ * A bounded regex over comment-stripped text, not an AST: this runs on the
168
+ * CLI and MCP detect paths, and pulling a TypeScript parser into those
169
+ * bundles for one key is weight no other detection signal pays. The match is
170
+ * bounded on both sides — the key must be `format` and the value must be one
171
+ * of Astro's three format literals — so `format:` keys with other values (an
172
+ * image config, a date format) cannot match. Returns null when the text
173
+ * states no format; callers resolve that to Astro's default ("directory").
174
+ */
175
+ export declare function parseAstroBuildFormat(configText: string): AstroBuildFormat | null;
176
+ /**
177
+ * The routing mode that serves each Astro build format, total over the format
178
+ * vocabulary so a format Astro adds cannot silently fall through to a wrong
179
+ * default.
180
+ *
181
+ * `preserve` maps to null deliberately: it keeps whichever shape each source
182
+ * file already had, so no single rewrite serves the whole site — the gap is
183
+ * left marked for the user rather than filled with a plausible mode.
184
+ */
185
+ export declare const ASTRO_FORMAT_TO_ROUTING: {
186
+ readonly file: "multipage";
187
+ readonly directory: "directory";
188
+ readonly preserve: null;
189
+ };
190
+ /**
191
+ * Routing for an Astro workspace, from its config text (null when no config
192
+ * file exists). Absence of the file, and absence of `build.format` inside
193
+ * it, both mean Astro's default: "directory".
194
+ */
195
+ export declare function inferAstroRouting(configText: string | null): StaticSiteRouting | null;
196
+ /**
197
+ * Re-frame a repo-root-relative workspace path as the `source` value the
198
+ * static-site pattern is actually read in.
199
+ *
200
+ * `source` is not repo-root-relative, though it long claimed to be. deploy-core
201
+ * runs the build in `resolve(appPath, buildCommand.cwd)` and the CDK construct
202
+ * uploads `resolve(this.props.source, build.outputDir)` from a child process
203
+ * whose cwd is that same `appPath` — the app's config directory. Emitting the
204
+ * repo-root path put both one boundary too deep, and neither consumer errors:
205
+ * the value simply resolves somewhere else, which is why this shipped. Every
206
+ * hand-written example in `components/infrastructure` carries the climb
207
+ * (`source: "../.."`), and this makes the generated value agree with them.
208
+ *
209
+ * An absolute path is returned untouched. `relativise` yields one for a
210
+ * workspace that resolves outside the scanned root (a `workspaces` glob may
211
+ * escape it), and an absolute source already resolves correctly from any cwd —
212
+ * prefixing a climb would corrupt a value that was right.
213
+ */
214
+ export declare function toConfigDirRelativeSource(relativePath: string): string;
111
215
  /**
112
216
  * Assemble the create prefill for one detected workspace. Pure: the caller
113
- * supplies the parsed `package.json` and the repo's monorepo tool.
217
+ * supplies the parsed `package.json`, the repo's package manager, and the
218
+ * framework's own routing verdict (`inferAstroRouting` for Astro; null when
219
+ * the framework states none — the gap rides through rather than being
220
+ * defaulted here).
114
221
  */
115
222
  export declare function inferCreatePrefill(input: {
116
223
  framework: Framework;
117
224
  relativePath: string;
118
225
  pkg: ParsedPackageJson | null;
119
- monorepoTool: MonorepoTool | null;
226
+ packageManager: PackageManager;
227
+ routing: StaticSiteRouting | null;
120
228
  }): CreatePrefill | null;
121
229
  export declare const MONOREPO_TOOL_VALUES: readonly ["npm-workspaces", "pnpm", "lerna", "turbo", "nx"];
122
230
  export declare const MonorepoToolSchema: z.ZodEnum<{
123
- "npm-workspaces": "npm-workspaces";
124
231
  pnpm: "pnpm";
232
+ "npm-workspaces": "npm-workspaces";
125
233
  lerna: "lerna";
126
234
  turbo: "turbo";
127
235
  nx: "nx";
@@ -181,6 +289,7 @@ export declare const AppDetectionSchema: z.ZodObject<{
181
289
  outputDir: z.ZodNullable<z.ZodString>;
182
290
  routing: z.ZodNullable<z.ZodEnum<{
183
291
  multipage: "multipage";
292
+ directory: "directory";
184
293
  spa: "spa";
185
294
  }>>;
186
295
  }, z.core.$strict>>;
@@ -189,8 +298,8 @@ export type AppDetection = z.infer<typeof AppDetectionSchema>;
189
298
  export declare const RepositoryDetectionSchema: z.ZodObject<{
190
299
  isMonorepo: z.ZodBoolean;
191
300
  monorepoTool: z.ZodNullable<z.ZodEnum<{
192
- "npm-workspaces": "npm-workspaces";
193
301
  pnpm: "pnpm";
302
+ "npm-workspaces": "npm-workspaces";
194
303
  lerna: "lerna";
195
304
  turbo: "turbo";
196
305
  nx: "nx";
@@ -250,6 +359,7 @@ export declare const RepositoryDetectionSchema: z.ZodObject<{
250
359
  outputDir: z.ZodNullable<z.ZodString>;
251
360
  routing: z.ZodNullable<z.ZodEnum<{
252
361
  multipage: "multipage";
362
+ directory: "directory";
253
363
  spa: "spa";
254
364
  }>>;
255
365
  }, z.core.$strict>>;
@@ -265,12 +375,20 @@ export interface ParsedPackageJson {
265
375
  dependencies?: Record<string, string>;
266
376
  devDependencies?: Record<string, string>;
267
377
  scripts?: Record<string, string>;
378
+ /** Corepack's declared manager, e.g. `"pnpm@9.1.0"`. */
379
+ packageManager?: string;
268
380
  }
269
381
  export interface MarkerFiles {
270
382
  pnpmWorkspaceYaml: boolean;
271
383
  lernaJson: boolean;
272
384
  turboJson: boolean;
273
385
  nxJson: boolean;
386
+ /**
387
+ * Which manager's lockfile is present. Total over `PackageManager` so the
388
+ * probe cannot fall behind the vocabulary: adding a manager fails to compile
389
+ * here and at every collection site until its lockfile is actually stat'd.
390
+ */
391
+ lockfiles: Record<PackageManager, boolean>;
274
392
  }
275
393
  /**
276
394
  * Determine the monorepo tool from root package.json + marker files.
@@ -1 +1 @@
1
- var c=Object.defineProperty;var o=(e,t)=>c(e,"name",{value:t,configurable:!0});import{buildAppConfigPath as p,FRAMEWORK_VALUES as u,PATTERN_TYPE_VALUES as s,STATIC_SITE_ROUTING_VALUES as f,recommendPatternForFramework as d}from"@fjall/util";import{z as r}from"zod";import{FRAMEWORK_VALUES as q,FRAMEWORKS as V,isFramework as I,FRAMEWORK_PATTERN_RECOMMENDATIONS as B,recommendPatternForFramework as G,CREATABLE_FRAMEWORKS as J}from"@fjall/util";const m=r.enum(u),b=["application","library"],k=r.enum(b);function T(e,t){return e!=="unknown"||t?"application":"library"}o(T,"inferAppRole");const g=r.discriminatedUnion("kind",[r.object({kind:r.literal("recommended"),pattern:r.enum(s),rationale:r.string()}).strict(),r.object({kind:r.literal("blocked"),pattern:r.enum(s),reason:r.string()}).strict(),r.object({kind:r.literal("unsupported"),reason:r.string()}).strict()]),w={astro:"dist",nextjs:"out","nextjs+payload":null,payload:null,remix:null,express:null,unknown:null},x=r.object({pattern:r.enum(s),source:r.string().describe("Directory the build runs in, relative to the repo root."),buildCommand:r.string().nullable().describe("Build command, from the workspace's own build script. Null when it declares none."),outputDir:r.string().nullable().describe("Build output directory relative to `source`, by framework convention \u2014 NOT verified against disk. Confirm it exists after building before deploying."),routing:r.enum(f).nullable()}).strict();function h(e){return e==="pnpm"?"pnpm run build":"npm run build"}o(h,"buildCommandFor");function F(e){const t=d(e.framework);if(t.kind!=="recommended")return null;const n=typeof e.pkg?.scripts?.build=="string"&&e.pkg.scripts.build.trim().length>0;return{pattern:t.pattern,source:e.relativePath===""?".":e.relativePath,buildCommand:n?h(e.monorepoTool):null,outputDir:w[e.framework],routing:t.pattern==="staticsite"?"multipage":null}}o(F,"inferCreatePrefill");const A=["npm-workspaces","pnpm","lerna","turbo","nx"],E=r.enum(A),R=r.object({relativePath:r.string().describe("Path relative to repo root. '.' for flat repo, e.g. 'apps/website' for monorepo workspace."),framework:m,packageName:r.string().nullable(),hasDockerfile:r.boolean(),docker:r.object({path:r.string().describe("Path to chosen Dockerfile, relative to relativePath.")}).strict().nullable().describe("Docker build descriptor. Null if no Dockerfile present."),inferredPort:r.number().int().min(1).max(65535),suggestedAppName:r.string().describe("kebab-case app name suggestion derived from packageName or relativePath."),suggestedConfigPath:r.string().describe("Suggested fjall config path relative to repo root, e.g. 'fjall/website' or 'apps/website/fjall/website'."),role:k.describe("'application' if deployable evidence was found (a framework, or a Dockerfile); 'library' otherwise. Do not scaffold libraries."),recommendation:g.describe("What this workspace's framework means for `fjall apps create --pattern`. Pass `pattern` from here; never pass the framework name."),create:x.nullable().describe("Values to pass to `fjall apps create`, read from the repository. Null when no pattern applies.")}).strict(),O=r.object({isMonorepo:r.boolean(),monorepoTool:E.nullable(),rootPackageName:r.string().nullable(),apps:r.array(R).describe("Detected apps. Empty if nothing scaffoldable was found. Single-element for flat repos, multi-element for monorepos."),warnings:r.array(r.string()).describe("Non-fatal observations (e.g. 'apps/legacy has no package.json \u2014 skipped').")}).strict();function _(e,t){return t.turboJson?"turbo":t.nxJson?"nx":t.pnpmWorkspaceYaml?"pnpm":t.lernaJson?"lerna":e?.workspaces?"npm-workspaces":null}o(_,"inferMonorepoTool");function j(e){return e?.workspaces?Array.isArray(e.workspaces)?e.workspaces:e.workspaces.packages??[]:[]}o(j,"extractWorkspaceGlobs");const D=[{framework:"nextjs+payload",required:["next","payload"]},{framework:"nextjs",required:["next"]},{framework:"payload",required:["payload"]},{framework:"remix",required:["@remix-run/node"]},{framework:"astro",required:["astro"]},{framework:"express",required:["express"]}];function M(e){if(!e)return"unknown";const t={...e.dependencies??{},...e.devDependencies??{}};for(const n of D)if(n.required.every(a=>a in t))return n.framework;return"unknown"}o(M,"inferFrameworkFromDeps");const y={nextjs:3e3,"nextjs+payload":3e3,payload:3e3,remix:3e3,astro:4321,express:3e3,unknown:3e3};function C(e){return y[e]}o(C,"inferPortForFramework");function N(e){if(e.length===0)return null;const t=e.find(i=>i==="Dockerfile");if(t)return{name:t,isDev:!1};const n=e.find(i=>i.startsWith("Dockerfile")&&!/\.(dev|local|test)$/i.test(i));if(n)return{name:n,isDev:!1};const a=e.find(i=>/\.dev$/i.test(i));if(a)return{name:a,isDev:!0};const l=e[0];return l===void 0?null:{name:l,isDev:!1}}o(N,"pickPreferredDockerfile");function W(e,t){if(e){const a=e.replace(/^@[^/]+\//,"");if(a.length>0)return a}return t==="."||t===""?"app":t.split("/").pop()??"app"}o(W,"suggestAppName");function L(e,t){return t==="."||t===""?p(e):p(e,t)}o(L,"suggestConfigPath");export{b as APP_ROLE_VALUES,R as AppDetectionSchema,k as AppRoleSchema,J as CREATABLE_FRAMEWORKS,x as CreatePrefillSchema,V as FRAMEWORKS,w as FRAMEWORK_OUTPUT_DIRS,B as FRAMEWORK_PATTERN_RECOMMENDATIONS,q as FRAMEWORK_VALUES,m as FrameworkSchema,A as MONOREPO_TOOL_VALUES,E as MonorepoToolSchema,g as PatternRecommendationSchema,O as RepositoryDetectionSchema,j as extractWorkspaceGlobs,T as inferAppRole,F as inferCreatePrefill,M as inferFrameworkFromDeps,_ as inferMonorepoTool,C as inferPortForFramework,I as isFramework,N as pickPreferredDockerfile,G as recommendPatternForFramework,W as suggestAppName,L as suggestConfigPath};
1
+ var d=Object.defineProperty;var o=(e,r)=>d(e,"name",{value:r,configurable:!0});import{buildAppConfigPath as l,FRAMEWORK_VALUES as b,PATTERN_TYPE_VALUES as p,STATIC_SITE_ROUTING_VALUES as g,recommendPatternForFramework as A}from"@fjall/util";import{z as t}from"zod";import{FRAMEWORK_VALUES as re,FRAMEWORKS as te,isFramework as ne,FRAMEWORK_PATTERN_RECOMMENDATIONS as oe,recommendPatternForFramework as ae,CREATABLE_FRAMEWORKS as se}from"@fjall/util";const k=t.enum(b),x=["application","library"],h=t.enum(x);function K(e,r){return e!=="unknown"||r?"application":"library"}o(K,"inferAppRole");const R=t.discriminatedUnion("kind",[t.object({kind:t.literal("recommended"),pattern:t.enum(p),rationale:t.string()}).strict(),t.object({kind:t.literal("blocked"),pattern:t.enum(p),reason:t.string()}).strict(),t.object({kind:t.literal("unsupported"),reason:t.string()}).strict()]),w={astro:"dist",nextjs:"out","nextjs+payload":null,payload:null,remix:null,express:null,unknown:null},y=t.object({pattern:t.enum(p),source:t.string().describe("Pass verbatim to `--source`. Relative to the app's `fjall/<name>` config directory, NOT the repo root \u2014 that is the frame every consumer resolves it in, so the leading `../..` is part of the value rather than a mistake. Assumes the app is scaffolded at `<repo root>/fjall/<name>`, which is where `apps create` puts it."),buildCommand:t.string().nullable().describe("Build command, from the workspace's own build script. Null when it declares none."),outputDir:t.string().nullable().describe("Build output directory relative to `source`, by framework convention \u2014 NOT verified against disk. Confirm it exists after building before deploying."),routing:t.enum(g).nullable().describe("Pass to `--routing`. Read from the framework's own build configuration (for Astro, `build.format`): 'multipage' when the build emits `about.html`, 'directory' when it emits `about/index.html`. Null when no single mode serves the layout (e.g. Astro's 'preserve') \u2014 ask rather than guess.")}).strict(),c=["npm","pnpm","yarn","bun"],E={npm:"npm run build",pnpm:"pnpm run build",yarn:"yarn run build",bun:"bun run build"},W={npm:["package-lock.json"],pnpm:["pnpm-lock.yaml"],yarn:["yarn.lock"],bun:["bun.lock","bun.lockb"]};function _(e){if(typeof e!="string")return null;const r=e.trim().split("@")[0];return c.find(n=>n===r)??null}o(_,"packageManagerFromField");function I(e,r){const n=_(e?.packageManager);if(n!==null)return n;const a=c.filter(i=>r.lockfiles[i]);return a.length===1?a[0]:"npm"}o(I,"inferPackageManager");function O(e){return E[e]}o(O,"buildCommandFor");const S=["file","directory","preserve"],G=["astro.config.mjs","astro.config.js","astro.config.ts","astro.config.cjs","astro.config.mts","astro.config.cts"];function D(e){const r=e.replace(/\/\*[\s\S]*?\*\//g,"").replace(/\/\/[^\n]*/g,""),n=new RegExp(`format\\s*:\\s*["'](${S.join("|")})["']`).exec(r);return n===null?null:n[1]}o(D,"parseAstroBuildFormat");const F={file:"multipage",directory:"directory",preserve:null};function V(e){const r=e===null?"directory":D(e)??"directory";return F[r]}o(V,"inferAstroRouting");const u=l("probe").split("/").map(()=>"..").join("/");function T(e){const r=e.trim().replace(/^\.\/+/,"").replace(/\/+$/,"");return r.startsWith("/")?r:r===""||r==="."?u:`${u}/${r}`}o(T,"toConfigDirRelativeSource");function B(e){const r=A(e.framework);if(r.kind!=="recommended")return null;const n=typeof e.pkg?.scripts?.build=="string"&&e.pkg.scripts.build.trim().length>0;return{pattern:r.pattern,source:T(e.relativePath),buildCommand:n?O(e.packageManager):null,outputDir:w[e.framework],routing:r.pattern==="staticsite"?e.routing:null}}o(B,"inferCreatePrefill");const j=["npm-workspaces","pnpm","lerna","turbo","nx"],v=t.enum(j),M=t.object({relativePath:t.string().describe("Path relative to repo root. '.' for flat repo, e.g. 'apps/website' for monorepo workspace."),framework:k,packageName:t.string().nullable(),hasDockerfile:t.boolean(),docker:t.object({path:t.string().describe("Path to chosen Dockerfile, relative to relativePath.")}).strict().nullable().describe("Docker build descriptor. Null if no Dockerfile present."),inferredPort:t.number().int().min(1).max(65535),suggestedAppName:t.string().describe("kebab-case app name suggestion derived from packageName or relativePath."),suggestedConfigPath:t.string().describe("Suggested fjall config path relative to repo root, e.g. 'fjall/website' or 'apps/website/fjall/website'."),role:h.describe("'application' if deployable evidence was found (a framework, or a Dockerfile); 'library' otherwise. Do not scaffold libraries."),recommendation:R.describe("What this workspace's framework means for `fjall apps create --pattern`. Pass `pattern` from here; never pass the framework name."),create:y.nullable().describe("Values to pass to `fjall apps create`, read from the repository. Null when no pattern applies.")}).strict(),$=t.object({isMonorepo:t.boolean(),monorepoTool:v.nullable(),rootPackageName:t.string().nullable(),apps:t.array(M).describe("Detected apps. Empty if nothing scaffoldable was found. Single-element for flat repos, multi-element for monorepos."),warnings:t.array(t.string()).describe("Non-fatal observations (e.g. 'apps/legacy has no package.json \u2014 skipped').")}).strict();function J(e,r){return r.turboJson?"turbo":r.nxJson?"nx":r.pnpmWorkspaceYaml?"pnpm":r.lernaJson?"lerna":e?.workspaces?"npm-workspaces":null}o(J,"inferMonorepoTool");function Y(e){return e?.workspaces?Array.isArray(e.workspaces)?e.workspaces:e.workspaces.packages??[]:[]}o(Y,"extractWorkspaceGlobs");const f={"nextjs+payload":["next","payload"],nextjs:["next"],payload:["payload"],remix:["@remix-run/node"],astro:["astro"],express:["express"]},m={"nextjs+payload":0,nextjs:1,payload:2,remix:3,astro:4,express:5},P=Object.keys(f).sort((e,r)=>m[e]-m[r]);function H(e){if(!e)return"unknown";const r={...e.dependencies??{},...e.devDependencies??{}};for(const n of P)if(f[n].every(a=>a in r))return n;return"unknown"}o(H,"inferFrameworkFromDeps");const C={nextjs:3e3,"nextjs+payload":3e3,payload:3e3,remix:3e3,astro:4321,express:3e3,unknown:3e3};function z(e){return C[e]}o(z,"inferPortForFramework");function q(e){if(e.length===0)return null;const r=e.find(s=>s==="Dockerfile");if(r)return{name:r,isDev:!1};const n=e.find(s=>s.startsWith("Dockerfile")&&!/\.(dev|local|test)$/i.test(s));if(n)return{name:n,isDev:!1};const a=e.find(s=>/\.dev$/i.test(s));if(a)return{name:a,isDev:!0};const i=e[0];return i===void 0?null:{name:i,isDev:!1}}o(q,"pickPreferredDockerfile");function Q(e,r){if(e){const a=e.replace(/^@[^/]+\//,"");if(a.length>0)return a}return r==="."||r===""?"app":r.split("/").pop()??"app"}o(Q,"suggestAppName");function X(e,r){return r==="."||r===""?l(e):l(e,r)}o(X,"suggestConfigPath");export{x as APP_ROLE_VALUES,S as ASTRO_BUILD_FORMAT_VALUES,G as ASTRO_CONFIG_FILENAMES,F as ASTRO_FORMAT_TO_ROUTING,M as AppDetectionSchema,h as AppRoleSchema,se as CREATABLE_FRAMEWORKS,y as CreatePrefillSchema,te as FRAMEWORKS,w as FRAMEWORK_OUTPUT_DIRS,oe as FRAMEWORK_PATTERN_RECOMMENDATIONS,re as FRAMEWORK_VALUES,k as FrameworkSchema,j as MONOREPO_TOOL_VALUES,v as MonorepoToolSchema,W as PACKAGE_MANAGER_LOCKFILES,c as PACKAGE_MANAGER_VALUES,R as PatternRecommendationSchema,$ as RepositoryDetectionSchema,Y as extractWorkspaceGlobs,K as inferAppRole,V as inferAstroRouting,B as inferCreatePrefill,H as inferFrameworkFromDeps,J as inferMonorepoTool,I as inferPackageManager,z as inferPortForFramework,ne as isFramework,D as parseAstroBuildFormat,q as pickPreferredDockerfile,ae as recommendPatternForFramework,Q as suggestAppName,X as suggestConfigPath,T as toConfigDirRelativeSource};
@@ -14,7 +14,8 @@ export interface StaticSiteResourceOptions {
14
14
  source: string;
15
15
  /** Build command + output directory (both required — deploy SSoT). */
16
16
  build: StaticSiteBuildConfig;
17
- /** Clean-URL rewriting ("multipage") or SPA fallback ("spa"). */
17
+ /** "multipage" (/about /about.html), "directory" (/about →
18
+ * /about/index.html) or "spa" (index.html fallback). */
18
19
  routing?: StaticSiteRouting;
19
20
  /** Custom domain (auto-creates certificate + DNS). Required for forms. */
20
21
  domain?: string;
@@ -271,6 +271,7 @@ export declare const ApplicationResourcePlanSchema: z.ZodObject<{
271
271
  }, z.core.$strict>;
272
272
  routing: z.ZodOptional<z.ZodEnum<{
273
273
  multipage: "multipage";
274
+ directory: "directory";
274
275
  spa: "spa";
275
276
  }>>;
276
277
  security: z.ZodOptional<z.ZodObject<{
@@ -939,6 +940,7 @@ export declare const ApplicationGeneratorSchema: z.ZodObject<{
939
940
  outputDir: z.ZodOptional<z.ZodString>;
940
941
  routing: z.ZodOptional<z.ZodEnum<{
941
942
  multipage: "multipage";
943
+ directory: "directory";
942
944
  spa: "spa";
943
945
  }>>;
944
946
  formsTo: z.ZodOptional<z.ZodString>;
@@ -412,13 +412,15 @@ export declare const NextJSPatternConfigSchema: z.ZodObject<{
412
412
  environment: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
413
413
  }, z.core.$strict>;
414
414
  /**
415
- * Routing mode for a static site: "multipage" (clean-URL `.html` rewriting)
416
- * or "spa" (single-page-app fallback to index.html). The accept-set is derived
417
- * from `@fjall/util` the CDN construct reads the same tuple, so a third mode
418
- * cannot be added to one side alone.
415
+ * Routing mode for a static site: "multipage" (/about → /about.html),
416
+ * "directory" (/about /about/index.html Astro's default layout) or "spa"
417
+ * (unknown paths fall back to /index.html). The accept-set is derived from
418
+ * `@fjall/util` the CDN construct reads the same tuple, so a mode cannot
419
+ * be added to one side alone.
419
420
  */
420
421
  export declare const StaticSiteRoutingSchema: z.ZodEnum<{
421
422
  multipage: "multipage";
423
+ directory: "directory";
422
424
  spa: "spa";
423
425
  }>;
424
426
  export type StaticSiteRouting = z.infer<typeof StaticSiteRoutingSchema>;
@@ -483,6 +485,7 @@ export declare const StaticSitePatternConfigSchema: z.ZodObject<{
483
485
  }, z.core.$strict>;
484
486
  routing: z.ZodOptional<z.ZodEnum<{
485
487
  multipage: "multipage";
488
+ directory: "directory";
486
489
  spa: "spa";
487
490
  }>>;
488
491
  security: z.ZodOptional<z.ZodObject<{
@@ -757,6 +760,7 @@ export declare const PatternConfigSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
757
760
  }, z.core.$strict>;
758
761
  routing: z.ZodOptional<z.ZodEnum<{
759
762
  multipage: "multipage";
763
+ directory: "directory";
760
764
  spa: "spa";
761
765
  }>>;
762
766
  security: z.ZodOptional<z.ZodObject<{
@@ -1 +1 @@
1
- export declare const GENERATOR_VERSION = "22.0.0";
1
+ export declare const GENERATOR_VERSION = "24.0.0";
@@ -1 +1 @@
1
- const E="22.0.0";export{E as GENERATOR_VERSION};
1
+ const E="24.0.0";export{E as GENERATOR_VERSION};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fjall/generator",
3
- "version": "22.0.0",
3
+ "version": "24.0.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/fjall-tech/fjall.git",
@@ -58,7 +58,7 @@
58
58
  "license": "SEE LICENSE IN LICENSE",
59
59
  "dependencies": {
60
60
  "@babel/parser": "^7.29.8",
61
- "@fjall/util": "^22.0.0",
61
+ "@fjall/util": "^24.0.0",
62
62
  "ast-types": "^0.16.1",
63
63
  "recast": "^0.23.12",
64
64
  "ts-morph": "^28.0.0",