kizlo 0.13.0 → 0.14.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.
- package/dist/cli/commands/_setup.js +91 -55
- package/dist/cli/commands/_tasks.js +152 -0
- package/dist/cli/commands/_wiring.js +11 -10
- package/dist/cli/commands/create.js +270 -87
- package/dist/cli/commands/init.js +122 -61
- package/dist/cli/daemon/config.js +1 -1
- package/dist/cli/presets/source.js +13 -8
- package/dist/cli/presets/template.js +255 -50
- package/dist/cli/utils.js +149 -80
- package/dist/integrations/tanstack-start/api.d.ts +15 -0
- package/dist/integrations/tanstack-start/api.js +21 -0
- package/dist/integrations/tanstack-start/kizlo.client.d.ts +20 -0
- package/dist/integrations/tanstack-start/kizlo.client.js +18 -0
- package/dist/integrations/tanstack-start/kizlo.server.d.ts +14 -0
- package/dist/integrations/tanstack-start/kizlo.server.js +15 -0
- package/dist/integrations/tanstack-start/manifest.d.ts +9 -0
- package/dist/integrations/tanstack-start/manifest.js +11 -0
- package/dist/integrations/tanstack-start/metadata.d.ts +49 -0
- package/dist/integrations/tanstack-start/metadata.js +112 -0
- package/dist/integrations/tanstack-start/robots.d.ts +8 -0
- package/dist/integrations/tanstack-start/robots.js +11 -0
- package/dist/integrations/tanstack-start/root-head.d.ts +15 -0
- package/dist/integrations/tanstack-start/root-head.js +54 -0
- package/dist/integrations/tanstack-start/sitemap.d.ts +16 -0
- package/dist/integrations/tanstack-start/sitemap.js +20 -0
- package/dist/integrations/tanstack-start/utils.d.ts +20 -0
- package/dist/integrations/tanstack-start/utils.js +17 -0
- package/dist/package.js +1 -1
- package/dist/router.d.ts +5 -5
- package/dist/tanstack-start/server.d.ts +11 -0
- package/dist/tanstack-start/server.js +12 -0
- package/dist/tanstack-start.d.ts +4 -0
- package/dist/tanstack-start.js +5 -0
- package/package.json +22 -1
|
@@ -38,10 +38,18 @@ import fs from "node:fs";
|
|
|
38
38
|
* patch may instead set `mode: "note"` to always print its instructions rather than edit the file —
|
|
39
39
|
* for a change too shaped-unlike an import/export to auto-merge (Astro's `defineConfig` output/adapter).
|
|
40
40
|
*/
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
/**
|
|
42
|
+
* The template's wiring config — how Kizlo addresses the files it lays down, rewritten to the project's
|
|
43
|
+
* own layout on apply (used by both `create` and `init`, unlike the init-only preconditions). The
|
|
44
|
+
* template writes its files against its own {@link configSchema.shape.kizloPath} and
|
|
45
|
+
* {@link configSchema.shape.alias}; on apply the Kizlo dir is retargeted to the project's chosen location,
|
|
46
|
+
* every alias import to the project's import style, and every other `src/`-rooted path to the project's
|
|
47
|
+
* `src/` convention (kept when the project uses `src/`, stripped when it doesn't).
|
|
48
|
+
*/
|
|
49
|
+
const configSchema = z.object({
|
|
50
|
+
apiPath: z.string().optional(),
|
|
51
|
+
alias: z.string(),
|
|
52
|
+
kizloPath: z.string()
|
|
45
53
|
});
|
|
46
54
|
const fileSchema = z.object({
|
|
47
55
|
kind: z.literal("file"),
|
|
@@ -65,12 +73,131 @@ const patchSchema = z.object({
|
|
|
65
73
|
})).default([]),
|
|
66
74
|
note: z.string().optional()
|
|
67
75
|
});
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
76
|
+
/**
|
|
77
|
+
* The `command` half of {@link createSchema}: the full invocation that scaffolds the base app with the
|
|
78
|
+
* framework's own CLI, written exactly as it would be typed — e.g. `{{pm}} create next-app@latest {{name}}
|
|
79
|
+
* --ts …`. The CLI imposes no shape: it tokenizes the string, substitutes the tokens below, and runs the
|
|
80
|
+
* argv verbatim, so a template can use any invocation (a `create-*` initializer, `{{pm}} dlx <cli> create`,
|
|
81
|
+
* a subcommand after the package spec) without the CLI knowing anything framework-specific. The template is
|
|
82
|
+
* fully authoritative, which means it also owns any package-manager quirks (npm's `--` separator, yarn's
|
|
83
|
+
* `@latest` handling).
|
|
84
|
+
*
|
|
85
|
+
* The core tokens substituted before the command runs:
|
|
86
|
+
* - `{{pm}}` — the chosen package manager (`pnpm`/`npm`/`yarn`/`bun`).
|
|
87
|
+
* - `{{name}}` — the project name (or path) the user gave.
|
|
88
|
+
* - `{{dlx}}` — the manager's run-a-package command (`pnpm dlx`/`npx`/`yarn dlx`/`bunx`), for a CLI that
|
|
89
|
+
* isn't a `create-*` package (e.g. `{{dlx}} @tanstack/cli@latest create {{name}}`); a `create-*`
|
|
90
|
+
* package invokes more simply as `{{pm}} create <initializer> {{name}}`.
|
|
91
|
+
*
|
|
92
|
+
* Beyond those, a `{{<token>}}` for every prompt in the same `create` block (see {@link promptSchema}) is
|
|
93
|
+
* substituted with the CLI fragment the user's answer maps to — the mechanism that lets a template
|
|
94
|
+
* surface a real framework choice (which linter, turbopack on/off) instead of hard-coding the flag.
|
|
95
|
+
* A fragment may be empty (contributes nothing) or several flags; each is spliced in as plain text and
|
|
96
|
+
* re-tokenized, so the CLI still learns nothing framework-specific.
|
|
97
|
+
*
|
|
98
|
+
* The command must produce a project whose shape matches {@link configSchema} (directory layout,
|
|
99
|
+
* import alias) so the wiring lands where the manifest expects. Simple single/double quotes group a
|
|
100
|
+
* token that contains spaces; there is no shell expansion.
|
|
101
|
+
*/
|
|
102
|
+
const commandSchema = z.string();
|
|
103
|
+
/**
|
|
104
|
+
* A prompt the template asks during `create`, its answer mapped to a fragment of the {@link commandSchema}
|
|
105
|
+
* command via the prompt's `{{token}}`. This is how a template exposes a framework CLI choice it would
|
|
106
|
+
* otherwise have to hard-code (which linter, turbopack on/off, a custom import alias) without the Kizlo
|
|
107
|
+
* CLI knowing anything framework-specific: the manifest owns both the question and the flag it produces.
|
|
108
|
+
*
|
|
109
|
+
* Three shapes, discriminated on `kind`:
|
|
110
|
+
* - `select` — a single choice from {@link promptOptionSchema} options; the chosen option's `arg` is the
|
|
111
|
+
* fragment. `default` names the option value pre-selected (and used non-interactively); absent → the
|
|
112
|
+
* first option.
|
|
113
|
+
* - `confirm` — a yes/no; `arg` is the fragment when yes, `argFalse` when no. `default` is the answer used
|
|
114
|
+
* non-interactively.
|
|
115
|
+
* - `text` — free input spliced into `arg` at its `{{value}}` placeholder; the value is shell-quoted so it
|
|
116
|
+
* stays a single argv token. Empty input quotes to nothing, so with the default `{{value}}` arg it
|
|
117
|
+
* contributes nothing (a wrapping arg like `--flag {{value}}` still emits the flag). `default` is the
|
|
118
|
+
* value used non-interactively.
|
|
119
|
+
*
|
|
120
|
+
* Every prompt owns a unique `token` that must not shadow a core token (`pm`/`name`/`dlx`); the bootstrap
|
|
121
|
+
* schema rejects collisions and duplicates.
|
|
122
|
+
*/
|
|
123
|
+
const promptOptionSchema = z.object({
|
|
124
|
+
label: z.string(),
|
|
125
|
+
value: z.string(),
|
|
126
|
+
arg: z.string().default("")
|
|
127
|
+
});
|
|
128
|
+
const selectPromptSchema = z.object({
|
|
129
|
+
kind: z.literal("select"),
|
|
130
|
+
token: z.string(),
|
|
131
|
+
message: z.string(),
|
|
132
|
+
default: z.string().optional(),
|
|
133
|
+
options: z.array(promptOptionSchema).min(1)
|
|
134
|
+
});
|
|
135
|
+
const confirmPromptSchema = z.object({
|
|
136
|
+
kind: z.literal("confirm"),
|
|
137
|
+
token: z.string(),
|
|
138
|
+
message: z.string(),
|
|
139
|
+
default: z.boolean().default(true),
|
|
140
|
+
arg: z.string().default(""),
|
|
141
|
+
argFalse: z.string().default("")
|
|
142
|
+
});
|
|
143
|
+
const textPromptSchema = z.object({
|
|
144
|
+
kind: z.literal("text"),
|
|
145
|
+
token: z.string(),
|
|
146
|
+
message: z.string(),
|
|
147
|
+
placeholder: z.string().optional(),
|
|
148
|
+
default: z.string().optional(),
|
|
149
|
+
arg: z.string().default("{{value}}")
|
|
150
|
+
});
|
|
151
|
+
const promptSchema = z.discriminatedUnion("kind", [
|
|
152
|
+
selectPromptSchema,
|
|
153
|
+
confirmPromptSchema,
|
|
154
|
+
textPromptSchema
|
|
155
|
+
]);
|
|
156
|
+
/** The core bootstrap tokens a prompt token must never shadow. */
|
|
157
|
+
const RESERVED_TOKENS = new Set([
|
|
158
|
+
"pm",
|
|
159
|
+
"name",
|
|
160
|
+
"dlx"
|
|
161
|
+
]);
|
|
162
|
+
/**
|
|
163
|
+
* How `create` scaffolds the base app: the framework CLI `command` to run, plus the `prompts` whose answers
|
|
164
|
+
* fill its `{{token}}` placeholders — kept together because a prompt is meaningless without the command it
|
|
165
|
+
* customizes. Absent on templates that only `init` supports; `create` refuses a template whose manifest
|
|
166
|
+
* declares no `create` block. A `superRefine` rejects a prompt `token` that shadows a core token
|
|
167
|
+
* (`pm`/`name`/`dlx`) or repeats another prompt's, so a mis-authored manifest fails loudly at parse time.
|
|
168
|
+
*/
|
|
169
|
+
const createSchema = z.object({
|
|
170
|
+
command: commandSchema,
|
|
171
|
+
prompts: z.array(promptSchema).default([])
|
|
172
|
+
}).superRefine((create, ctx) => {
|
|
173
|
+
const seen = /* @__PURE__ */ new Set();
|
|
174
|
+
for (const prompt of create.prompts) {
|
|
175
|
+
if (RESERVED_TOKENS.has(prompt.token)) ctx.addIssue({
|
|
176
|
+
code: "custom",
|
|
177
|
+
message: `Prompt token "${prompt.token}" is reserved (pm/name/dlx).`,
|
|
178
|
+
path: ["prompts"]
|
|
179
|
+
});
|
|
180
|
+
if (seen.has(prompt.token)) ctx.addIssue({
|
|
181
|
+
code: "custom",
|
|
182
|
+
message: `Duplicate prompt token "${prompt.token}".`,
|
|
183
|
+
path: ["prompts"]
|
|
184
|
+
});
|
|
185
|
+
seen.add(prompt.token);
|
|
186
|
+
}
|
|
71
187
|
});
|
|
72
188
|
const changeSchema = z.discriminatedUnion("kind", [fileSchema, patchSchema]);
|
|
73
189
|
/**
|
|
190
|
+
* The changes a template lays down, split by the one question that decides whether a command may safely
|
|
191
|
+
* apply them — *where is this safe to land?* No command reads another's section: `base` is additive and
|
|
192
|
+
* safe on any project (both commands apply it), `create` overwrites framework-scaffolded files (only
|
|
193
|
+
* `create`), `init` patches files the user owns (only `init`). See the section doc above and {@link changesFor}.
|
|
194
|
+
*/
|
|
195
|
+
const changesSchema = z.object({
|
|
196
|
+
base: z.array(changeSchema).default([]),
|
|
197
|
+
create: z.array(changeSchema).default([]),
|
|
198
|
+
init: z.array(changeSchema).default([])
|
|
199
|
+
});
|
|
200
|
+
/**
|
|
74
201
|
* The `.env` key names the scaffold writes and the pinned runtime later reads — the one part of the
|
|
75
202
|
* wiring contract that can drift when the scaffold-time CLI and the pinned runtime disagree. Declared
|
|
76
203
|
* here as data so the scaffold writes whatever names the pinned runtime expects. Semantic slots only;
|
|
@@ -95,21 +222,6 @@ const envSchema = z.object({
|
|
|
95
222
|
/** A package name → version-range map, package.json's own vocabulary. */
|
|
96
223
|
const depsSchema = z.record(z.string(), z.string());
|
|
97
224
|
/**
|
|
98
|
-
* The package names whose presence in a project's dependencies identify this framework — how `init`
|
|
99
|
-
* recognizes which template to apply to an existing project (e.g. `["next"]`, `["astro"]`). Detection is
|
|
100
|
-
* data, so a community template declares its own signal and needs no CLI change. See {@link detectTemplate}.
|
|
101
|
-
*/
|
|
102
|
-
const detectSchema = z.object({ dependencies: z.array(z.string()).default([]) });
|
|
103
|
-
/**
|
|
104
|
-
* An `init` precondition on the project's shape: at least one of `anyDir` must exist in the project,
|
|
105
|
-
* else `init` stops with `message`. Replaces framework-specific `if` branches — e.g. Next.js needs an
|
|
106
|
-
* `app` or `src/app` directory (the App Router), which the Pages Router lacks. Dormant when absent.
|
|
107
|
-
*/
|
|
108
|
-
const requiresSchema = z.object({
|
|
109
|
-
anyDir: z.array(z.string()),
|
|
110
|
-
message: z.string()
|
|
111
|
-
});
|
|
112
|
-
/**
|
|
113
225
|
* A post-setup manual step `init` prints after wiring — the piece Kizlo can't auto-wire into a file the
|
|
114
226
|
* user owns (e.g. Astro's SEO tags render through a `.astro` component, not a patchable export). `create`
|
|
115
227
|
* owns files whole, so it never prints these. `body` may carry one token, `{{importPrefix}}`, substituted
|
|
@@ -119,24 +231,95 @@ const noteSchema = z.object({
|
|
|
119
231
|
title: z.string(),
|
|
120
232
|
body: z.string()
|
|
121
233
|
});
|
|
234
|
+
/**
|
|
235
|
+
* A single precondition `init` checks against the project before applying — data in place of the old
|
|
236
|
+
* framework-specific `if` branches, so a community template declares its own without a CLI change.
|
|
237
|
+
* Discriminated on `kind`:
|
|
238
|
+
*
|
|
239
|
+
* - `dep` — package(s) in the project's dependencies. Doubles as the framework *detection* signal (see
|
|
240
|
+
* {@link detectTemplates}): a template's match score is how many of its `dep` `values` the project has,
|
|
241
|
+
* read across every candidate template before one is chosen.
|
|
242
|
+
* - `dir` — director(ies) in the project tree, e.g. Next's `app`/`src/app` App Router (the Pages Router
|
|
243
|
+
* lacks it).
|
|
244
|
+
*
|
|
245
|
+
* `match` decides whether `any` (default) or `all` of `values` must be present; `message` is the abort
|
|
246
|
+
* text shown when the requirement is unmet, defaulting to `""` → a generated fallback. See {@link unmetRequirement}.
|
|
247
|
+
*/
|
|
248
|
+
const requiresFields = {
|
|
249
|
+
values: z.array(z.string()).min(1),
|
|
250
|
+
match: z.enum(["any", "all"]).default("any"),
|
|
251
|
+
message: z.string().default("")
|
|
252
|
+
};
|
|
253
|
+
const requiresDepSchema = z.object({
|
|
254
|
+
kind: z.literal("dep"),
|
|
255
|
+
...requiresFields
|
|
256
|
+
});
|
|
257
|
+
const requiresDirSchema = z.object({
|
|
258
|
+
kind: z.literal("dir"),
|
|
259
|
+
...requiresFields
|
|
260
|
+
});
|
|
261
|
+
const requirementSchema = z.discriminatedUnion("kind", [requiresDepSchema, requiresDirSchema]);
|
|
262
|
+
/** Everything the `init` command needs of the project: the preconditions it checks and the steps it prints. */
|
|
263
|
+
const initSchema = z.object({
|
|
264
|
+
requires: z.array(requirementSchema).default([]),
|
|
265
|
+
notes: z.array(noteSchema).default([])
|
|
266
|
+
});
|
|
122
267
|
const manifestSchema = z.object({
|
|
123
|
-
|
|
268
|
+
id: z.string(),
|
|
124
269
|
name: z.string().optional(),
|
|
125
270
|
env: envSchema.optional(),
|
|
126
271
|
dependencies: depsSchema.optional(),
|
|
127
272
|
devDependencies: depsSchema.optional(),
|
|
128
273
|
minCli: z.string().optional(),
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
274
|
+
config: configSchema,
|
|
275
|
+
init: initSchema.default({
|
|
276
|
+
requires: [],
|
|
277
|
+
notes: []
|
|
278
|
+
}),
|
|
279
|
+
create: createSchema.optional(),
|
|
280
|
+
changes: changesSchema.default({
|
|
281
|
+
base: [],
|
|
282
|
+
create: [],
|
|
283
|
+
init: []
|
|
284
|
+
})
|
|
138
285
|
});
|
|
139
286
|
/**
|
|
287
|
+
* Wrap a text prompt's answer so it survives {@link tokenizeCommand} as exactly one argv token: an empty
|
|
288
|
+
* value stays empty (contributes nothing), otherwise it's double-quoted with any inner `"` escaped. This is
|
|
289
|
+
* what lets a free-text value with spaces (`--import-alias "my alias"`) splice cleanly into the bootstrap
|
|
290
|
+
* command without the tokenizer splitting it.
|
|
291
|
+
*/
|
|
292
|
+
function quoteValue(value) {
|
|
293
|
+
if (value === "") return "";
|
|
294
|
+
return `"${value.replaceAll("\"", "\\\"")}"`;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* The CLI fragment a prompt answer maps to — the single place answer→flag translation lives, shared by the
|
|
298
|
+
* interactive collector and the non-interactive default path. `answer` is the chosen option `value`
|
|
299
|
+
* (select), the boolean (confirm), or the raw input (text). Returns the fragment spliced into the bootstrap
|
|
300
|
+
* command for this prompt's `{{token}}`; a select answer with no matching option (a stale `default`) yields
|
|
301
|
+
* the first option's fragment.
|
|
302
|
+
*/
|
|
303
|
+
function promptFragment(prompt, answer) {
|
|
304
|
+
switch (prompt.kind) {
|
|
305
|
+
case "select": return (prompt.options.find((opt) => opt.value === answer) ?? prompt.options[0])?.arg ?? "";
|
|
306
|
+
case "confirm": return answer ? prompt.arg : prompt.argFalse;
|
|
307
|
+
case "text": return prompt.arg.replaceAll("{{value}}", quoteValue(String(answer)));
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* The answer used non-interactively (`--yes`, non-TTY) — the prompt's declared default, or the first
|
|
312
|
+
* option / empty string when none is set. Returns both the answer (for the choices summary) and the
|
|
313
|
+
* {@link promptFragment} it maps to, so the caller shows a value and splices a flag from one call.
|
|
314
|
+
*/
|
|
315
|
+
function resolvePromptDefault(prompt) {
|
|
316
|
+
const answer = prompt.kind === "confirm" ? prompt.default : prompt.kind === "select" ? prompt.default ?? prompt.options[0]?.value ?? "" : prompt.default ?? "";
|
|
317
|
+
return {
|
|
318
|
+
answer,
|
|
319
|
+
arg: promptFragment(prompt, answer)
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
140
323
|
* Render a manual-step note for display, substituting the `{{importPrefix}}` token with the project's
|
|
141
324
|
* source-root import prefix — the configured alias (normalized to a trailing slash, e.g. `@/`) when one
|
|
142
325
|
* is set, else the relative `../`. Reproduces the old `alias ? "@/…" : "../…"` behavior exactly, so a
|
|
@@ -227,9 +410,26 @@ function minCliError(manifest) {
|
|
|
227
410
|
* section: init's opt-in examples are the additive ones in `base`, never create's homepage overwrite.
|
|
228
411
|
*/
|
|
229
412
|
function changesFor(manifest, command, opts = {}) {
|
|
230
|
-
const changes = [...manifest.base, ...manifest[command]];
|
|
413
|
+
const changes = [...manifest.changes.base, ...manifest.changes[command]];
|
|
231
414
|
return opts.includeExamples ? changes : changes.filter((change) => !isExample(change));
|
|
232
415
|
}
|
|
416
|
+
/**
|
|
417
|
+
* The first `init` precondition the project fails, as a ready-to-print abort message, or `undefined` when
|
|
418
|
+
* every requirement passes. `deps` is the project's merged dependencies; `dirExists` tests a
|
|
419
|
+
* project-relative directory. A `dep` requirement checks membership in `deps`, a `dir` requirement checks
|
|
420
|
+
* the tree, each honoring its `match` (`any`/`all`). An unmet requirement yields its own `message`, or a
|
|
421
|
+
* generated fallback when it declares none (the common case for `dep` entries, which detection filters
|
|
422
|
+
* out first). See {@link requirementSchema}.
|
|
423
|
+
*/
|
|
424
|
+
function unmetRequirement(requires, deps, dirExists) {
|
|
425
|
+
for (const req of requires) {
|
|
426
|
+
const has = req.kind === "dep" ? (value) => value in deps : dirExists;
|
|
427
|
+
if (req.match === "all" ? req.values.every(has) : req.values.some(has)) continue;
|
|
428
|
+
if (req.message) return req.message;
|
|
429
|
+
const noun = req.kind === "dep" ? "package" : "directory";
|
|
430
|
+
return `Kizlo needs ${req.match === "all" ? "all of" : "one of"} these ${noun}s: ${req.values.join(", ")}.`;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
233
433
|
/** The whole-file changes in a resolved change list. */
|
|
234
434
|
function fileEntries(changes) {
|
|
235
435
|
return changes.filter((change) => change.kind === "file");
|
|
@@ -253,11 +453,11 @@ function escapeRegExp(value) {
|
|
|
253
453
|
* `src`/root heuristic when the template declares no matching alias, staying consistent with the rest of
|
|
254
454
|
* the alias handling.
|
|
255
455
|
*/
|
|
256
|
-
function templateAliasBase(templateDir,
|
|
257
|
-
const alias =
|
|
456
|
+
function templateAliasBase(templateDir, config) {
|
|
457
|
+
const alias = config.alias.replace(/\/+$/, "");
|
|
258
458
|
const mapping = readTsconfigPaths(templateDir)?.[`${alias}/*`]?.[0];
|
|
259
459
|
if (mapping) return mapping.replace(/^\.\//, "").replace(/\/\*$/, "").replace(/\/+$/, "");
|
|
260
|
-
return
|
|
460
|
+
return config.kizloPath.startsWith("src/") ? "src" : "";
|
|
261
461
|
}
|
|
262
462
|
/**
|
|
263
463
|
* Rewrite every template-alias import in a file to the project's chosen import style — the one place that
|
|
@@ -269,23 +469,28 @@ function templateAliasBase(templateDir, conventions) {
|
|
|
269
469
|
* Only quoted specifiers that begin with the template alias are touched, so an unrelated string can't be
|
|
270
470
|
* corrupted. Subsumes the server-entry import: it is simply the alias import that lands in the Kizlo dir.
|
|
271
471
|
*/
|
|
272
|
-
function rewriteAliasImports(body, fromDir, base,
|
|
273
|
-
const alias =
|
|
472
|
+
function rewriteAliasImports(body, fromDir, base, config, ctx) {
|
|
473
|
+
const alias = config.alias.replace(/\/+$/, "");
|
|
274
474
|
if (!alias) return body;
|
|
275
475
|
const pattern = new RegExp(`(["'])${escapeRegExp(alias)}/([^"']+)\\1`, "g");
|
|
276
476
|
return body.replace(pattern, (_match, quote, rest) => {
|
|
277
|
-
const targetRel = adaptTemplatePath(base ? `${base}/${rest}` : rest,
|
|
477
|
+
const targetRel = adaptTemplatePath(base ? `${base}/${rest}` : rest, config, ctx);
|
|
278
478
|
return `${quote}${ctx.importFrom(targetRel, fromDir)}${quote}`;
|
|
279
479
|
});
|
|
280
480
|
}
|
|
281
481
|
/**
|
|
282
|
-
* Adapt a template path
|
|
283
|
-
*
|
|
482
|
+
* Adapt a template path to the project's real layout. The Kizlo home dir is retargeted to wherever the
|
|
483
|
+
* user put it (`config.kizloPath` → `ctx.kizloPath`). Every other path is `src/`-rooted as authored, so it
|
|
484
|
+
* only needs to match the project's `src/` convention: a project that keeps source under `src/` gets it
|
|
485
|
+
* verbatim, one that doesn't gets the leading `src/` stripped (so `src/app/...` lands at `app/...`, config
|
|
486
|
+
* files at the root are untouched). This makes the one project-wide decision — `src/` or not — instead of
|
|
487
|
+
* tracking a per-framework route directory.
|
|
284
488
|
*/
|
|
285
|
-
function adaptTemplatePath(relPath,
|
|
489
|
+
function adaptTemplatePath(relPath, config, ctx) {
|
|
286
490
|
const normalized = relPath.split(path.sep).join("/");
|
|
287
|
-
|
|
288
|
-
if (normalized ===
|
|
491
|
+
const { kizloPath } = config;
|
|
492
|
+
if (normalized === kizloPath || normalized.startsWith(`${kizloPath}/`)) return `${ctx.kizloPath}${normalized.slice(kizloPath.length)}`;
|
|
493
|
+
if (!ctx.hasSrcDir && normalized.startsWith("src/")) return normalized.slice(4);
|
|
289
494
|
return normalized;
|
|
290
495
|
}
|
|
291
496
|
/**
|
|
@@ -296,12 +501,12 @@ function adaptTemplatePath(relPath, conventions, ctx) {
|
|
|
296
501
|
* the template uses are rewritten, not a loose directory substring, so an unrelated occurrence can't be
|
|
297
502
|
* corrupted. This is the job the old build-time extractor did; it moves to runtime and stays as precise.
|
|
298
503
|
*/
|
|
299
|
-
function adaptFile(templateDir, entry,
|
|
504
|
+
function adaptFile(templateDir, entry, config, ctx) {
|
|
300
505
|
const abs = path.join(templateDir, entry.path);
|
|
301
506
|
if (!fs.existsSync(abs)) throw new Error(`Template file "${entry.path}" is listed in the manifest but does not exist in the template.`);
|
|
302
507
|
const body = fs.readFileSync(abs, "utf8");
|
|
303
|
-
const relPath = adaptTemplatePath(entry.path,
|
|
304
|
-
const contents = rewriteAliasImports(body, path.posix.dirname(relPath), templateAliasBase(templateDir,
|
|
508
|
+
const relPath = adaptTemplatePath(entry.path, config, ctx);
|
|
509
|
+
const contents = rewriteAliasImports(body, path.posix.dirname(relPath), templateAliasBase(templateDir, config), config, ctx);
|
|
305
510
|
return {
|
|
306
511
|
label: ROLE_LABELS[entry.role] ?? entry.role,
|
|
307
512
|
relPath,
|
|
@@ -309,8 +514,8 @@ function adaptFile(templateDir, entry, conventions, ctx) {
|
|
|
309
514
|
};
|
|
310
515
|
}
|
|
311
516
|
/** Adapt a patch's path prefix and server-import specifier to the project's real directories. */
|
|
312
|
-
function resolvePatch(entry,
|
|
313
|
-
const relPath = adaptTemplatePath(entry.path,
|
|
517
|
+
function resolvePatch(entry, config, ctx) {
|
|
518
|
+
const relPath = adaptTemplatePath(entry.path, config, ctx);
|
|
314
519
|
const fromDir = path.posix.dirname(relPath);
|
|
315
520
|
const imports = entry.imports.map((imp) => ({
|
|
316
521
|
module: imp.module.replaceAll("{{serverImport}}", ctx.serverImport(fromDir)),
|
|
@@ -327,4 +532,4 @@ function resolvePatch(entry, conventions, ctx) {
|
|
|
327
532
|
}
|
|
328
533
|
|
|
329
534
|
//#endregion
|
|
330
|
-
export { adaptFile, changesFor, fileEntries, isOlderVersion, minCliError, patchEntries, readManifest, renderNote, resolveDependencies, resolvePatch };
|
|
535
|
+
export { adaptFile, changesFor, fileEntries, isOlderVersion, minCliError, patchEntries, promptFragment, readManifest, renderNote, resolveDependencies, resolvePatch, resolvePromptDefault, unmetRequirement };
|