create-zudo-doc 5.18.1 → 5.19.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/CHANGELOG.md CHANGED
@@ -4,6 +4,31 @@ All notable changes to `create-zudo-doc` are documented in this file.
4
4
 
5
5
  The format is based on Keep a Changelog, and release notes are generated from the changelog MDX pages.
6
6
 
7
+ ## [5.19.0] - 2026-09-07
8
+
9
+ ### Features
10
+
11
+ - The first positional argument is now a **destination path**, not just a project name. `create-zudo-doc sub/ref-doc` creates `sub/ref-doc` and derives the project name from the final segment (`ref-doc`); previously it failed with a project-name validation error because the one argument meant both the directory to create and the name written into the generated `package.json`. Relative (`../ref-doc`) and absolute destinations are accepted; `.`, `..`, and a filesystem root are rejected because they name no final segment. `--name` still takes a bare package name and now acts as an explicit override of the derived name while the positional continues to supply the directory. The resolved destination is followed consistently by dependency install, `git init`, and the printed `cd <dest>` hint. The programmatic `CreateOptions` API is unchanged — there `projectName` still doubles as the directory. (5a928d51e)
12
+
13
+ ### Bug Fixes
14
+
15
+ - The printed `cd` target is quoted when the destination contains whitespace, which a path destination may now legally do, and an empty destination no longer resolves the target directory to the current working directory. (134248eba)
16
+
17
+ ### Other Changes
18
+
19
+ - The EN and JA `create-zudo-doc` reference pages and the package README document the destination argument: an accepted-forms table, both verbatim error messages, the `--name` override, a scaffolding-into-a-subdirectory example, and a note that the programmatic API has no `destination`. (dc83c4615)
20
+
21
+ ## [5.18.2] - 2026-09-06
22
+
23
+ ### Bug Fixes
24
+
25
+ - `@takazudo/zdtp` is added to a generated project only when the Design Token Panel is enabled. It had been an unconditional direct dependency of every scaffold — which is how generated projects avoided the `@takazudo/zudo-doc` bundling bug fixed in this same release — so every project without the panel installed and carried a package it never used. With that bug fixed upstream, the dependency is now gated on `designTokenPanel`. (a93ff2293)
26
+ - Six feature flags now default on, matching the FEATURES catalog: `--sidebar-resizer`, `--sidebar-toggle`, `--toc-toggle`, `--doc-history`, `--llms-txt`, and `--asset-viewer`. A scaffold created without explicit flags now includes these features; pass the matching `--no-` flag to opt out. (6d0d3bfb9)
27
+
28
+ ### Other Changes
29
+
30
+ - The CLI reference table was contradicted by the change above and has been corrected: the default column now matches the catalog, and the previously-missing `--[no-]asset-viewer` row was added in catalog position. The package README's feature list was corrected the same way. (c908f7f9a, f006d4ada)
31
+
7
32
  ## [5.18.1] - 2026-09-06
8
33
 
9
34
  ### Other Changes
package/README.md CHANGED
@@ -42,6 +42,42 @@ pnpm create zudo-doc my-docs \
42
42
  --install
43
43
  ```
44
44
 
45
+ ### Destination paths
46
+
47
+ The positional argument is the **destination** — the directory to scaffold
48
+ into, not just a bare name. It may be a path (relative, `../`-prefixed, or
49
+ absolute), and its **last segment** becomes the project name written to the
50
+ generated `package.json`:
51
+
52
+ ```bash
53
+ # Creates ./sub/ref-doc with the package name "ref-doc"
54
+ pnpm create zudo-doc sub/ref-doc --yes
55
+ ```
56
+
57
+ Only that last segment has to satisfy the project-name grammar (starts with a
58
+ lowercase letter or digit; lowercase letters, digits, dots, underscores, and
59
+ hyphens only; 214 characters max) — the directories leading up to it are just a
60
+ path. `.`, `..`, and a filesystem root are rejected, because they name no last
61
+ segment to derive the project name from.
62
+
63
+ Use `--name` to override the derived name while the positional argument keeps
64
+ supplying the directory:
65
+
66
+ ```bash
67
+ # Creates ./sub/My-Docs with the package name "ref-doc"
68
+ pnpm create zudo-doc sub/My-Docs --name ref-doc --yes
69
+ ```
70
+
71
+ This makes side-by-side scaffolding straightforward — for example, comparing
72
+ two template versions by generating both into one scratch directory and
73
+ diffing them:
74
+
75
+ ```bash
76
+ pnpm create zudo-doc@5.17.0 /tmp/tpl-check/v5.17 --yes
77
+ pnpm create zudo-doc@5.18.0 /tmp/tpl-check/v5.18 --yes
78
+ diff -r /tmp/tpl-check/v5.17 /tmp/tpl-check/v5.18
79
+ ```
80
+
45
81
  ### Locales and translations
46
82
 
47
83
  `--lang` selects the primary locale. Its pages use the unprefixed
@@ -84,7 +120,8 @@ Built-in UI translations resolve in this order:
84
120
 
85
121
  | Flag | Description | Default |
86
122
  |------|-------------|---------|
87
- | `[project-name]` | Project name (positional arg or `--name`) | prompted |
123
+ | `[destination]` | Directory to scaffold into; may be a path whose last segment becomes the project name | prompted |
124
+ | `--name <name>` | Project name written to `package.json`; overrides the name derived from the destination | destination's last segment |
88
125
  | `--lang <code>` | Default language: `en`, `ja`, `zh-cn`, `zh-tw`, `ko`, `es`, `fr`, `de`, `pt` | `en` |
89
126
  | `--additional-langs <a,b>` | Ordered additional locale codes; implies i18n and replaces a preset list | none |
90
127
  | `--pm <manager>` | Package manager: `pnpm`, `npm`, `yarn`, `bun` | detected |
@@ -113,18 +150,19 @@ Each feature has a `--[no-]<flag>` form. Passing `--feature` enables it; `--no-f
113
150
  | `--[no-]search` | Full-text search | on |
114
151
  | `--[no-]sidebar-filter` | Real-time sidebar filter | on |
115
152
  | `--[no-]image-enlarge` | Click-to-enlarge for oversized images | on |
116
- | `--[no-]asset-viewer` | Viewer pages for files under `public/assets/` | off |
153
+ | `--[no-]asset-viewer` | Viewer pages for files under `public/assets/` | on |
117
154
  | `--[no-]tag-governance` | Vocabulary-aware tag audit + suggest scripts | off |
118
155
  | `--[no-]claude-resources` | Auto-generate Claude Code docs from `.claude/` | off |
119
156
  | `--[no-]codex-resources` | Auto-generate Codex docs from `.codex/` + `AGENTS.md` | off |
120
157
  | `--[no-]claude-skills` | Ship zudo-doc Claude Code skills (design-system, translate, version-bump) | off |
121
158
  | `--[no-]design-token-panel` | Interactive panel for tweaking spacing, font, color tokens | off |
122
- | `--[no-]sidebar-resizer` | Draggable sidebar width handle | off |
123
- | `--[no-]sidebar-toggle` | Show/hide desktop sidebar button | off |
159
+ | `--[no-]sidebar-resizer` | Draggable sidebar width handle | on |
160
+ | `--[no-]sidebar-toggle` | Show/hide desktop sidebar button | on |
161
+ | `--[no-]toc-toggle` | Show/hide the desktop table of contents (edge chevron, xl screens) | on |
124
162
  | `--[no-]versioning` | Multi-version documentation support | off |
125
- | `--[no-]doc-history` | Show per-page document edit history | off |
163
+ | `--[no-]doc-history` | Show per-page document edit history | on |
126
164
  | `--[no-]body-foot-util` | Right-aligned strip below each doc (history trigger + GitHub source link) | off |
127
- | `--[no-]llms-txt` | Generate `llms.txt` for LLM consumption | off |
165
+ | `--[no-]llms-txt` | Generate `llms.txt` for LLM consumption | on |
128
166
  | `--[no-]skill-symlinker` | Symlink documentation skills into `.claude/` | off |
129
167
  | `--[no-]tauri` | Tauri desktop app — Mode 1 offline reader | off |
130
168
  | `--[no-]tauri-dev` | Tauri dev wrapper — Mode 2 configurable dev wrapper | off |
package/dist/api.js CHANGED
@@ -1,8 +1,7 @@
1
- import path from "path";
2
1
  import { SINGLE_SCHEMES, THEME_PACKS } from "./constants.js";
3
2
  import { parseChangelogPackages, validateChangelogPackages, validateHeaderRightItems, validateMetaTags, } from "./preset.js";
4
3
  import { scaffold } from "./scaffold.js";
5
- import { initGitRepo, installDependencies, validateProjectName } from "./utils.js";
4
+ import { initGitRepo, installDependencies, resolveTargetDir, validateProjectName, } from "./utils.js";
6
5
  import { resolveLocalePlan } from "./locale-plan.js";
7
6
  export { resolveLocalePlan, } from "./locale-plan.js";
8
7
  export async function createZudoDoc(options) {
@@ -69,7 +68,9 @@ export async function createZudoDoc(options) {
69
68
  changelogPackages,
70
69
  };
71
70
  await scaffold(choices);
72
- const targetDir = path.resolve(process.cwd(), choices.projectName);
71
+ // No `destination` on CreateOptions — the programmatic API keeps the
72
+ // project name doubling as the directory, so the resolver falls back to it.
73
+ const targetDir = resolveTargetDir(choices);
73
74
  if (install) {
74
75
  installDependencies(targetDir, choices.packageManager);
75
76
  }
package/dist/cli.d.ts CHANGED
@@ -1,5 +1,14 @@
1
1
  export interface CliArgs {
2
+ /**
3
+ * The `--name` flag: a bare package name. Kept separate from `destination`
4
+ * so the locked project-name grammar still rejects a path here.
5
+ */
2
6
  name?: string;
7
+ /**
8
+ * The first positional argument: the directory to scaffold into. May be a
9
+ * path; its final segment becomes the project name unless `--name` overrides.
10
+ */
11
+ destination?: string;
3
12
  lang?: string;
4
13
  additionalLangs?: string[];
5
14
  colorSchemeMode?: "single" | "light-dark";
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@ import minimist from "minimist";
2
2
  import pc from "picocolors";
3
3
  import { FEATURES, SINGLE_SCHEMES, SUPPORTED_LANGS, THEME_PACKS } from "./constants.js";
4
4
  import { parseChangelogPackages, validateChangelogPackages, } from "./preset.js";
5
- import { validateProjectName } from "./utils.js";
5
+ import { normalizeDestination, splitDestination, validateProjectName, } from "./utils.js";
6
6
  import { resolveLocalePlan } from "./locale-plan.js";
7
7
  /** Presets are the base layer; an explicitly supplied CLI list replaces it. */
8
8
  export function layerAdditionalLangs(presetValue, cliValue) {
@@ -39,12 +39,12 @@ export function parseArgs(argv = process.argv.slice(2)) {
39
39
  return argv.some((a) => a === `--${flag}` || a === `--no-${flag}`);
40
40
  }
41
41
  const args = {};
42
- // Project name: first positional arg or --name
43
- if (raw.name) {
42
+ // Destination: first positional arg. Project name: --name (a bare name), or
43
+ // the destination's final segment when --name is absent.
44
+ if (raw.name)
44
45
  args.name = raw.name;
45
- }
46
- else if (raw._.length > 0 && typeof raw._[0] === "string") {
47
- args.name = raw._[0];
46
+ if (raw._.length > 0 && typeof raw._[0] === "string") {
47
+ args.destination = raw._[0];
48
48
  }
49
49
  if (raw.lang)
50
50
  args.lang = raw.lang;
@@ -100,10 +100,15 @@ export function printHelp() {
100
100
  const themePackList = THEME_PACKS.map((t) => t.slug).join(", ");
101
101
  const featureHelp = FEATURES.map((f) => ` --[no-]${f.cliFlag.padEnd(22)} ${f.hint}`).join("\n");
102
102
  console.log(`
103
- ${pc.bold("Usage:")} create-zudo-doc [project-name] [options]
103
+ ${pc.bold("Usage:")} create-zudo-doc [destination] [options]
104
+
105
+ ${pc.dim(" destination")} Directory to create the project in; may be a path
106
+ (e.g. sub/my-docs). Its last segment becomes the
107
+ project name. Default: my-docs
104
108
 
105
109
  ${pc.bold("Options:")}
106
- --name <name> Project name (or first positional arg)
110
+ --name <name> Project name written to package.json. Overrides
111
+ the name derived from the destination's last segment
107
112
  --lang <code> Default language (${langList})
108
113
  Default: en
109
114
  --additional-langs <a,b> Additional locale codes (ordered; implies i18n)
@@ -137,6 +142,9 @@ ${pc.bold("Examples:")}
137
142
  ${pc.dim("# Fully specified")}
138
143
  create-zudo-doc my-docs --lang ja --scheme "Default Dark" --no-i18n --pm pnpm --install
139
144
 
145
+ ${pc.dim("# Scaffold into a subdirectory (package name: ref-doc)")}
146
+ create-zudo-doc sub/ref-doc --yes
147
+
140
148
  ${pc.dim("# Per-package changelog pages")}
141
149
  create-zudo-doc my-docs --changelog-packages core,cli --yes
142
150
  `);
@@ -203,5 +211,15 @@ export function validateArgs(args) {
203
211
  if (nameError)
204
212
  return nameError;
205
213
  }
214
+ if (args.destination !== undefined) {
215
+ // With --name present the derived segment is overridden, so the final
216
+ // segment only has to be a legal *directory* name — but a destination that
217
+ // names no final segment at all (".", "..", "/") is still refused.
218
+ const result = args.name
219
+ ? normalizeDestination(args.destination)
220
+ : splitDestination(args.destination);
221
+ if (!result.ok)
222
+ return result.error;
223
+ }
206
224
  return null;
207
225
  }
package/dist/constants.js CHANGED
@@ -251,21 +251,21 @@ export const FEATURES = [
251
251
  value: "sidebarResizer",
252
252
  label: "Sidebar resizer",
253
253
  hint: "Draggable sidebar width",
254
- default: false,
254
+ default: true,
255
255
  cliFlag: "sidebar-resizer",
256
256
  },
257
257
  {
258
258
  value: "sidebarToggle",
259
259
  label: "Sidebar toggle",
260
260
  hint: "Show/hide desktop sidebar",
261
- default: false,
261
+ default: true,
262
262
  cliFlag: "sidebar-toggle",
263
263
  },
264
264
  {
265
265
  value: "tocToggle",
266
266
  label: "TOC toggle",
267
267
  hint: "Show/hide the desktop table of contents",
268
- default: false,
268
+ default: true,
269
269
  cliFlag: "toc-toggle",
270
270
  },
271
271
  {
@@ -279,7 +279,7 @@ export const FEATURES = [
279
279
  value: "docHistory",
280
280
  label: "Document history",
281
281
  hint: "Show document edit history",
282
- default: false,
282
+ default: true,
283
283
  cliFlag: "doc-history",
284
284
  },
285
285
  {
@@ -293,7 +293,7 @@ export const FEATURES = [
293
293
  value: "llmsTxt",
294
294
  label: "llms.txt",
295
295
  hint: "Generate llms.txt for LLM consumption",
296
- default: false,
296
+ default: true,
297
297
  cliFlag: "llms-txt",
298
298
  },
299
299
  {
@@ -335,7 +335,7 @@ export const FEATURES = [
335
335
  value: "assetViewer",
336
336
  label: "Asset viewer",
337
337
  hint: "Viewer pages for files under public/assets",
338
- default: false,
338
+ default: true,
339
339
  cliFlag: "asset-viewer",
340
340
  },
341
341
  {
package/dist/index.js CHANGED
@@ -1,4 +1,3 @@
1
- import path from "path";
2
1
  import * as p from "@clack/prompts";
3
2
  import pc from "picocolors";
4
3
  import { layerAdditionalLangs, parseArgs, printHelp, validateArgs, } from "./cli.js";
@@ -6,7 +5,7 @@ import { FEATURES } from "./constants.js";
6
5
  import { loadPreset } from "./preset.js";
7
6
  import { runPrompts } from "./prompts.js";
8
7
  import { scaffold } from "./scaffold.js";
9
- import { installDependencies, initGitRepo, pmRunCommand } from "./utils.js";
8
+ import { destinationLabel, initGitRepo, installDependencies, normalizeDestination, pmRunCommand, resolveTargetDir, } from "./utils.js";
10
9
  async function main() {
11
10
  const args = parseArgs();
12
11
  // Handle --help
@@ -36,6 +35,17 @@ async function main() {
36
35
  }
37
36
  }
38
37
  // CLI args override preset values
38
+ if (args.destination !== undefined) {
39
+ // validateArgs() already rejected a destination with no usable final
40
+ // segment, so this only fails on an input that never reaches here.
41
+ const dest = normalizeDestination(args.destination);
42
+ if (dest.ok) {
43
+ prefilled.destination = dest.destination;
44
+ prefilled.projectName = dest.finalSegment;
45
+ }
46
+ }
47
+ // --name is a deliberate override of the name derived from the destination's
48
+ // last segment; the positional keeps supplying the directory either way.
39
49
  if (args.name)
40
50
  prefilled.projectName = args.name;
41
51
  if (args.lang)
@@ -111,7 +121,11 @@ async function main() {
111
121
  prefilled.features = { ...featureDefaults, ...prefilled.features };
112
122
  }
113
123
  const choices = await runPrompts(prefilled);
114
- const targetDir = path.resolve(process.cwd(), choices.projectName);
124
+ const targetDir = resolveTargetDir(choices);
125
+ const destination = destinationLabel(choices);
126
+ // A destination may now contain spaces (only its LAST segment goes through
127
+ // the project-name grammar), so the `cd` we print has to stay copy-pastable.
128
+ const cdTarget = /\s/.test(destination) ? `"${destination}"` : destination;
115
129
  const s = p.spinner();
116
130
  s.start("Scaffolding project...");
117
131
  try {
@@ -138,7 +152,7 @@ async function main() {
138
152
  initialValue: true,
139
153
  });
140
154
  if (p.isCancel(result)) {
141
- p.outro(`Done! cd ${choices.projectName} and install dependencies manually.`);
155
+ p.outro(`Done! cd ${cdTarget} and install dependencies manually.`);
142
156
  return;
143
157
  }
144
158
  shouldInstall = result;
@@ -179,10 +193,10 @@ async function main() {
179
193
  break;
180
194
  }
181
195
  }
182
- p.outro(`${pc.green("Done!")} Your project is ready at ${pc.cyan(choices.projectName)}`);
196
+ p.outro(`${pc.green("Done!")} Your project is ready at ${pc.cyan(destination)}`);
183
197
  console.log();
184
198
  console.log(` ${pc.bold("Next steps:")}`);
185
- console.log(` cd ${choices.projectName}`);
199
+ console.log(` cd ${cdTarget}`);
186
200
  console.log(` ${pmRunCommand(choices.packageManager, "dev")}`);
187
201
  console.log();
188
202
  }
package/dist/prompts.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { type PresetHeaderRightItem, type PresetMetaTagsConfig } from "./preset.js";
2
2
  export interface UserChoices {
3
3
  projectName: string;
4
+ destination?: string;
4
5
  defaultLang: string;
5
6
  additionalLangs?: string[];
6
7
  colorSchemeMode: "single" | "light-dark";
@@ -22,6 +23,8 @@ export interface UserChoices {
22
23
  }
23
24
  export interface PartialChoices {
24
25
  projectName?: string;
26
+ /** See `UserChoices.destination` — CLI-supplied, never prompted for. */
27
+ destination?: string;
25
28
  defaultLang?: string;
26
29
  additionalLangs?: string[];
27
30
  colorSchemeMode?: "single" | "light-dark";
package/dist/prompts.js CHANGED
@@ -240,6 +240,7 @@ export async function runPrompts(prefilled = {}) {
240
240
  : features.filter((feature) => feature !== "i18n");
241
241
  return {
242
242
  projectName,
243
+ destination: prefilled.destination,
243
244
  defaultLang: localePlan.defaultLang,
244
245
  additionalLangs: prefilled.additionalLangs === undefined
245
246
  ? undefined
@@ -31,5 +31,5 @@ export declare function deriveDocSkillName(projectName: string): string;
31
31
  *
32
32
  * Bumped in lockstep by scripts/release-create-zudo-doc.sh.
33
33
  */
34
- export declare const ZUDO_DOC_PIN = "^5.18.1";
34
+ export declare const ZUDO_DOC_PIN = "^5.19.0";
35
35
  export declare function scaffold(choices: UserChoices): Promise<void>;
package/dist/scaffold.js CHANGED
@@ -5,7 +5,7 @@ import { generateZfbConfig } from "./zfb-config-gen.js";
5
5
  import { generateCLAUDEFile } from "./claude-md-gen.js";
6
6
  import { composeFeatures } from "./compose.js";
7
7
  import { featureModules } from "./features/index.js";
8
- import { capitalize, hasAncestorPnpmWorkspace, pmRunCommand, } from "./utils.js";
8
+ import { capitalize, destinationLabel, hasAncestorPnpmWorkspace, pmRunCommand, resolveTargetDir, } from "./utils.js";
9
9
  import { resolveLocalePlan, } from "./locale-plan.js";
10
10
  // Kept as a compatibility export from the pre-list API. Internal scaffold
11
11
  // emitters consume the resolved LocalePlan directly; see utils.ts for the
@@ -47,7 +47,7 @@ export function deriveDocSkillName(projectName) {
47
47
  *
48
48
  * Bumped in lockstep by scripts/release-create-zudo-doc.sh.
49
49
  */
50
- export const ZUDO_DOC_PIN = "^5.18.1";
50
+ export const ZUDO_DOC_PIN = "^5.19.0";
51
51
  /**
52
52
  * Files in `templates/base/**` that must not be copied by the unconditional
53
53
  * base mirror. Each entry is matched against the path relative to
@@ -262,11 +262,11 @@ export async function scaffold(choices) {
262
262
  if (localePlan.overridesExplicitDisable) {
263
263
  console.warn("additional-langs requires i18n; enabling it despite --no-i18n");
264
264
  }
265
- const targetDir = path.resolve(process.cwd(), choices.projectName);
265
+ const targetDir = resolveTargetDir(choices);
266
266
  if (await fs.pathExists(targetDir)) {
267
267
  const contents = await fs.readdir(targetDir);
268
268
  if (contents.length > 0) {
269
- throw new Error(`Directory "${choices.projectName}" already exists and is not empty`);
269
+ throw new Error(`Directory "${destinationLabel(choices)}" already exists and is not empty`);
270
270
  }
271
271
  }
272
272
  // body-foot-util-area.tsx ships the DocHistory component inline (byte-
@@ -821,26 +821,20 @@ function generatePackageJson(choices, localePlan) {
821
821
  // missing copy produces no `pnpm install` warning — which is why this gap
822
822
  // shipped silently and only surfaced at build time.
823
823
  diff: "^8.0.3",
824
- // @takazudo/zdtp SAME "unconditional at build time despite being an
825
- // optional peer" class as `diff` above, discovered empirically while
826
- // verifying this generator's own barebone (designTokenPanel: OFF) output
827
- // (epic zudolab/zudo-doc#2651, #2660 self-review a genuine package-level
828
- // coupling, not a generator bug; flagged loudly on #2660's completion
829
- // comment for a package-side follow-up). Since the #2658 gate-2 fix,
830
- // `chrome/derive.tsx`'s `deriveBodyEndIslands` statically imports the REAL
831
- // `DesignTokenPanelBootstrap` as the default for EVERY `createChrome`
832
- // consumer (so the island auto-mounts with zero host wiring when the
833
- // setting is on) and that component imports `@takazudo/zdtp` at module
834
- // scope. So every page that goes through `createChrome` — which is every
835
- // page in this project — pulls the zdtp import into the esbuild bundle
836
- // graph, `designTokenPanel` setting or not; only RENDERING is gated on
837
- // the setting, not the import. Without this dep, `zfb build` fails with
838
- // "Could not resolve '@takazudo/zdtp'" even on a fully barebone project.
839
- // `preact ^10.29.1` (see the floor comment above) is required for the
840
- // same reason. This is the ACCEPTED, permanent contract per #2668 — see
841
- // the "@takazudo/zdtp dep implication" note in
842
- // packages/zudo-doc/docs/adr/route-injection-seam.md.
843
- "@takazudo/zdtp": "0.5.1",
824
+ // (@takazudo/zdtp is NOT here it is gated on the designTokenPanel
825
+ // feature, see the block below. It WAS unconditional from #2660 through
826
+ // #4018: `chrome/derive.tsx`'s `deriveBodyEndIslands` statically imports
827
+ // the REAL `DesignTokenPanelBootstrap` for EVERY `createChrome` consumer
828
+ // (zero host wiring when the setting is on), and that component used to
829
+ // import `@takazudo/zdtp/constants` at module scope so a barebone
830
+ // designTokenPanel:OFF project still died with "Could not resolve
831
+ // '@takazudo/zdtp/constants'". #4018 vendored those constants into
832
+ // @takazudo/zudo-doc, leaving only a rejection-handled
833
+ // `import("@takazudo/zdtp")` that esbuild tolerates when the package is
834
+ // absent, so the dep is now genuinely conditional (#4009).
835
+ // `preact` stays unconditional (the app needs it regardless); its
836
+ // ^10.29.1 floor is still chosen so a designTokenPanel-ON project shares
837
+ // one preact instance with zdtp see the floor comment above.)
844
838
  // (@takazudo/zudo-doc-history-server is NOT here — it is gated on the
845
839
  // docHistory or assetViewer features, see the block below. It was briefly unconditional
846
840
  // (#3080) to work around doc-history-area importing its `/exclude` subpath
@@ -868,6 +862,15 @@ function generatePackageJson(choices, localePlan) {
868
862
  // search ships as @takazudo/zudo-doc's own self-contained generated
869
863
  // search-widget script (custom word-match scorer) — no third-party search
870
864
  // engine dependency is needed here. Do not re-add minisearch/pagefind.
865
+ if (choices.features.includes("designTokenPanel")) {
866
+ // Only the design-token-panel feature graph reaches @takazudo/zdtp: the
867
+ // panel payload loads through a rejection-handled `import("@takazudo/zdtp")`
868
+ // at first toggle, and this feature's global.css injection is what pulls in
869
+ // `@takazudo/zdtp/styles.css` (see features/design-token-panel.ts). Both are
870
+ // no-ops with the feature off, so an OFF project must not carry the dep
871
+ // (#4009 / #4018 — it was unconditional until then, see the `deps` block).
872
+ deps["@takazudo/zdtp"] = "0.5.1";
873
+ }
871
874
  if (choices.features.includes("docHistory") ||
872
875
  choices.features.includes("assetViewer")) {
873
876
  // (`diff` remains an unconditional base dep — see the `deps` block above:
@@ -889,7 +892,7 @@ function generatePackageJson(choices, localePlan) {
889
892
  // `/exclude` at module scope from the always-bundled chrome graph; #3110
890
893
  // moved compileExclude into @takazudo/zudo-doc, so projects with both
891
894
  // docHistory and assetViewer off no longer need the package at all.
892
- deps["@takazudo/zudo-doc-history-server"] = "^5.18.1";
895
+ deps["@takazudo/zudo-doc-history-server"] = "^5.19.0";
893
896
  // tsx is no longer needed here: the relocated package plugin imports the
894
897
  // runner directly (no `tsx -e` spawn) since the package ships compiled
895
898
  // dist/ — package-first migration #2321 (#2337).
package/dist/utils.d.ts CHANGED
@@ -6,6 +6,63 @@
6
6
  * programmatic API.
7
7
  */
8
8
  export declare function validateProjectName(name: string): string | null;
9
+ export type DestinationPath = {
10
+ ok: true;
11
+ destination: string;
12
+ finalSegment: string;
13
+ } | {
14
+ ok: false;
15
+ error: string;
16
+ };
17
+ export type DestinationSplit = {
18
+ ok: true;
19
+ destination: string;
20
+ projectName: string;
21
+ } | {
22
+ ok: false;
23
+ error: string;
24
+ };
25
+ /**
26
+ * Normalize a destination path and expose its final segment.
27
+ *
28
+ * Upward traversal (`../ref-doc`) is **allowed** — `..` is simply a relative
29
+ * way to name a directory outside the cwd, and absolute destinations are
30
+ * accepted too, so rejecting it would be inconsistent. `.`, `..`, and a
31
+ * filesystem root are rejected because they name no final segment.
32
+ *
33
+ * `path.resolve()` is deliberately NOT applied before extracting the segment:
34
+ * resolving `"."` first would turn the *current* directory's basename into an
35
+ * apparently valid project name and scaffold into the cwd.
36
+ */
37
+ export declare function normalizeDestination(raw: string): DestinationPath;
38
+ /**
39
+ * Split a CLI destination argument into the directory to scaffold into and the
40
+ * package name written to the generated `package.json`.
41
+ *
42
+ * The final path segment becomes the project name and goes through the
43
+ * unchanged `validateProjectName`; everything before it is just a directory
44
+ * path and is deliberately not validated as a package name. Callers that
45
+ * supply the name separately (`--name`) use `normalizeDestination` instead, so
46
+ * a directory segment that is not a legal package name stays acceptable.
47
+ */
48
+ export declare function splitDestination(raw: string): DestinationSplit;
49
+ /** The two destination-bearing fields every target-dir consumer needs. */
50
+ export interface DestinationChoices {
51
+ projectName: string;
52
+ /**
53
+ * Directory to scaffold into, as given on the CLI. Absent on the
54
+ * programmatic/preset paths, where the project name is also the directory.
55
+ */
56
+ destination?: string;
57
+ }
58
+ /** The destination as the user expressed it — for messages and `cd` output. */
59
+ export declare function destinationLabel(choices: DestinationChoices): string;
60
+ /**
61
+ * The single source of truth for the absolute directory a scaffold is written
62
+ * to. Every caller that needs it (scaffold.ts, api.ts, index.ts) must use this
63
+ * rather than re-deriving it — that is how the three sites used to drift.
64
+ */
65
+ export declare function resolveTargetDir(choices: DestinationChoices): string;
9
66
  export declare function installDependencies(dir: string, pm: string): void;
10
67
  export type GitInitResult = {
11
68
  status: "ok";
package/dist/utils.js CHANGED
@@ -3,8 +3,13 @@ import fs from "fs-extra";
3
3
  import path from "path";
4
4
  import { resolveLocalePlan } from "./locale-plan.js";
5
5
  // Project-name grammar (locked by F4 — S4 #2013):
6
- // /^[a-z0-9][a-z0-9._-]*$/, max 214 chars, unscoped, used as both directory
7
- // name and package name. Mirrors npm's unscoped-name rules + max path safety.
6
+ // /^[a-z0-9][a-z0-9._-]*$/, max 214 chars, unscoped. Mirrors npm's
7
+ // unscoped-name rules + max path safety.
8
+ //
9
+ // This grammar covers the *package name* only — the value written to the
10
+ // generated package.json's `name`. The directory the project is written to is
11
+ // a separate concept (`UserChoices.destination`, see `splitDestination` below)
12
+ // and may be any path; only its final segment is checked against this grammar.
8
13
  const PROJECT_NAME_RE = /^[a-z0-9][a-z0-9._-]*$/;
9
14
  const PROJECT_NAME_MAX = 214;
10
15
  /**
@@ -27,6 +32,81 @@ export function validateProjectName(name) {
27
32
  }
28
33
  return null;
29
34
  }
35
+ /** Trailing path separators, platform-aware (Windows accepts both). */
36
+ const TRAILING_SEPARATORS = path.sep === "\\" ? /[\\/]+$/ : /\/+$/;
37
+ /**
38
+ * Normalize a destination path and expose its final segment.
39
+ *
40
+ * Upward traversal (`../ref-doc`) is **allowed** — `..` is simply a relative
41
+ * way to name a directory outside the cwd, and absolute destinations are
42
+ * accepted too, so rejecting it would be inconsistent. `.`, `..`, and a
43
+ * filesystem root are rejected because they name no final segment.
44
+ *
45
+ * `path.resolve()` is deliberately NOT applied before extracting the segment:
46
+ * resolving `"."` first would turn the *current* directory's basename into an
47
+ * apparently valid project name and scaffold into the cwd.
48
+ */
49
+ export function normalizeDestination(raw) {
50
+ const given = raw.trim();
51
+ if (given === "") {
52
+ return { ok: false, error: "Destination path is required" };
53
+ }
54
+ // `sub/ref-doc/` and `sub/ref-doc` name the same directory; keep the rest of
55
+ // the string as the user typed it so messages and `cd` output echo it back.
56
+ const destination = given.replace(TRAILING_SEPARATORS, "") || given;
57
+ const finalSegment = path.basename(path.normalize(destination));
58
+ if (finalSegment === "" || finalSegment === "." || finalSegment === "..") {
59
+ return {
60
+ ok: false,
61
+ error: `Destination "${given}" has no final path segment to name the project. ` +
62
+ `Pass a destination whose last segment names the project, e.g. "sub/my-docs"`,
63
+ };
64
+ }
65
+ return { ok: true, destination, finalSegment };
66
+ }
67
+ /**
68
+ * Split a CLI destination argument into the directory to scaffold into and the
69
+ * package name written to the generated `package.json`.
70
+ *
71
+ * The final path segment becomes the project name and goes through the
72
+ * unchanged `validateProjectName`; everything before it is just a directory
73
+ * path and is deliberately not validated as a package name. Callers that
74
+ * supply the name separately (`--name`) use `normalizeDestination` instead, so
75
+ * a directory segment that is not a legal package name stays acceptable.
76
+ */
77
+ export function splitDestination(raw) {
78
+ const normalized = normalizeDestination(raw);
79
+ if (!normalized.ok)
80
+ return normalized;
81
+ const nameError = validateProjectName(normalized.finalSegment);
82
+ if (nameError) {
83
+ return {
84
+ ok: false,
85
+ error: `Invalid project name "${normalized.finalSegment}" — the last segment of ` +
86
+ `destination "${raw.trim()}" is used as the package name. ${nameError}`,
87
+ };
88
+ }
89
+ return {
90
+ ok: true,
91
+ destination: normalized.destination,
92
+ projectName: normalized.finalSegment,
93
+ };
94
+ }
95
+ /** The destination as the user expressed it — for messages and `cd` output. */
96
+ export function destinationLabel(choices) {
97
+ // `||`, not `??`: an empty destination is as absent as an undefined one, and
98
+ // letting `""` through would resolve the target directory to the cwd and
99
+ // scaffold on top of it.
100
+ return choices.destination || choices.projectName;
101
+ }
102
+ /**
103
+ * The single source of truth for the absolute directory a scaffold is written
104
+ * to. Every caller that needs it (scaffold.ts, api.ts, index.ts) must use this
105
+ * rather than re-deriving it — that is how the three sites used to drift.
106
+ */
107
+ export function resolveTargetDir(choices) {
108
+ return path.resolve(process.cwd(), destinationLabel(choices));
109
+ }
30
110
  export function installDependencies(dir, pm) {
31
111
  const commands = {
32
112
  pnpm: "pnpm install",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-zudo-doc",
3
- "version": "5.18.1",
3
+ "version": "5.19.0",
4
4
  "description": "Create a new zudo-doc documentation site",
5
5
  "license": "MIT",
6
6
  "author": "Takeshi Takatsudo",