create-keboola-app 0.1.1 → 0.2.1

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.
Files changed (49) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +16 -11
  3. package/dist/{blocks-DYU5Ke29.js → blocks-BdJaEmz1.js} +36 -20
  4. package/dist/cli.js +29 -52
  5. package/dist/extract.js +9 -14
  6. package/dist/{regions-B3r5LhXl.js → regions-BNl4QsPP.js} +12 -22
  7. package/dist/{sanitize-CKs8pAzZ.js → sanitize-DVFBkv8b.js} +14 -24
  8. package/dist/sanitize.js +1 -2
  9. package/dist/template/AGENTS.md +20 -6
  10. package/dist/template/README.md +5 -0
  11. package/dist/template/oxlint.config.ts +10 -14
  12. package/dist/template/package.json +21 -20
  13. package/dist/template/src/auth/AuthScreen.test.tsx +44 -0
  14. package/dist/template/src/auth/AuthScreen.tsx +43 -0
  15. package/dist/template/src/auth/session.test.tsx +64 -0
  16. package/dist/template/src/auth/session.ts +42 -0
  17. package/dist/template/src/auth/states.tsx +70 -0
  18. package/dist/template/src/components/AppChrome.tsx +26 -74
  19. package/dist/template/src/components/SelectionFloater.tsx +10 -3
  20. package/dist/template/src/components/SidebarModeMenu.tsx +8 -13
  21. package/dist/template/src/nav.ts +6 -11
  22. package/dist/template/src/{routes/-fixtures.ts → overview-fixtures.ts} +6 -18
  23. package/dist/template/src/{routes/-hooks.ts → overview-hooks.ts} +1 -1
  24. package/dist/template/src/overview-sections.tsx +4 -1
  25. package/dist/template/src/recipes/Chart.test.tsx +1 -3
  26. package/dist/template/src/recipes/Chart.tsx +4 -11
  27. package/dist/template/src/recipes/DataGrid.test.tsx +1 -1
  28. package/dist/template/src/recipes/DataGrid.tsx +7 -21
  29. package/dist/template/src/routes/__root.tsx +21 -0
  30. package/dist/template/src/routes/{-index.test.tsx → index.test.tsx} +1 -1
  31. package/dist/template/src/routes/index.tsx +2 -3
  32. package/dist/template/src/routes/signed-out.test.tsx +56 -0
  33. package/dist/template/src/routes/signed-out.tsx +31 -0
  34. package/dist/template/src/routes/tokens/$id.tsx +2 -2
  35. package/dist/template/src/routes/tokens/{-index.test.tsx → index.test.tsx} +2 -2
  36. package/dist/template/src/routes/tokens/index.tsx +2 -3
  37. package/dist/template/src/routes/tokens/new.tsx +2 -2
  38. package/dist/template/src/sections/kpi.tsx +3 -7
  39. package/dist/template/src/slices/assistant.ts +3 -9
  40. package/dist/template/src/slices/shell.ts +4 -11
  41. package/dist/template/src/store.ts +9 -24
  42. package/dist/template/src/{routes/tokens/-fixtures.test.ts → tokens/fixtures.test.ts} +1 -1
  43. package/dist/template/src/{routes/tokens/-fixtures.ts → tokens/fixtures.ts} +1 -1
  44. package/dist/template/src/{routes/tokens/-hooks.ts → tokens/hooks.ts} +2 -2
  45. package/dist/template/src/{routes/tokens/-nav.ts → tokens/nav.ts} +1 -1
  46. package/dist/template/vite.config.ts +5 -0
  47. package/dist/template/vitest.config.ts +2 -5
  48. package/package.json +13 -12
  49. /package/dist/template/src/{routes/tokens/-types.ts → tokens/types.ts} +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,59 @@
1
1
  # create-keboola-app
2
2
 
3
+ ## 0.2.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Internal code satisfies the `anti-slop` lint rules; public APIs and behaviour are unchanged.
8
+
9
+ - `Reflect.get`, parameters typed `object` and accumulator-copying reducers give way to narrowing, precise types, `filter` and `flatMap`.
10
+ - `@keboola/api-client` keeps its `as unknown as` bridges from OpenAPI to domain types.
11
+
12
+ - Scaffolded apps get route trees from `@tanstack/router-generator` 1.167.46, the generator of the TanStack Router 1.170.46 line.
13
+
14
+ ## 0.2.0
15
+
16
+ ### Minor Changes
17
+
18
+ - New `auth` block: a generated app can now arrive with its signed-out screen already built.
19
+
20
+ Four apps in the platform hand-build the same pre-auth screen, and a Storybook pattern does not
21
+ stop a fifth from doing it again — new apps come from this generator, so that is where the screen
22
+ has to arrive. Pick `auth` and the app gets `/signed-out`, rendered without the app chrome: brand
23
+ lockup from `BrandLogo`, one centred card, a colour-scheme control, correct landmarks and heading
24
+ level, and the four pre-project states — signed out, restoring, expired, service unreachable —
25
+ under one piece of chrome, so the common path does not jump when one replaces another. The
26
+ emitted `gate:ui` scores that route as well as `/`.
27
+
28
+ The block is the screen and nothing behind it: no dependency, no network. Only the app knows what
29
+ it authenticates against, so wiring it up is two edits — the form's `onSubmit`, and where the
30
+ state comes from instead of the `?state=` search param. `--blocks none` still yields an app that
31
+ builds, type-checks and passes its own gate.
32
+
33
+ A routed block now brings the whole brief for its own route rather than a shared suffix. The
34
+ suffix described a searchable table, which graded a signed-out screen against qualities it is
35
+ supposed not to have.
36
+
37
+ - A generated app keeps `src/routes/` for route files. Everything else — hooks, fixtures, types, a
38
+ module's rail entry — arrives in a domain directory beside it.
39
+
40
+ TanStack Router excludes a file from the route tree when its name starts with `-`, and the
41
+ emitted layout leaned on that to keep a module's code inside its route folder. It does not scale:
42
+ one app grew to six route files and fifty-four dashed ones, and route folders that own domain code
43
+ end up importing across each other, which is the coupling the co-location was meant to prevent. A
44
+ marker on nine files out of ten marks nothing.
45
+
46
+ So the crud block now emits `routes/<kebab>/{index,new,$id}.tsx` and `<kebab>/{hooks,fixtures,
47
+ types,nav}.ts`, and the overview's hooks and fixtures sit at `src/overview-*.ts`. Nothing emitted
48
+ carries the `-` prefix.
49
+
50
+ A route's test is the one thing that stays with its route, because that is the file it exercises.
51
+ It exports no `Route`, so the emitted `vite.config.ts` declares
52
+ `routeFileIgnorePattern: '\\.test\\.tsx?$'`. Without it the generator prints a
53
+ `does not export a Route` warning for every such file on every build, which is what makes the
54
+ prefix feel obligatory in the first place. The CLI passes the same pattern when it writes the
55
+ first `routeTree.gen.ts`, which happens before the app has a Vite build to read the config from.
56
+
3
57
  ## 0.1.1
4
58
 
5
59
  ### Patch Changes
package/README.md CHANGED
@@ -25,22 +25,27 @@ npm create keboola-app -- --name my-app --blocks kpi,grid --yes --json
25
25
 
26
26
  ## Blocks
27
27
 
28
- | Block | What you get |
29
- | ----------- | -------------------------------------------------------------------------------------- |
30
- | `kpi` | A KPI card row on the overview. |
31
- | `chart` | A token-themed line chart card on the overview. |
32
- | `grid` | An editable per-row budget grid (TanStack Table + design `Table`). |
33
- | `crud` | A list → detail → form slice over `/tokens`, exercising router params, Query and Form. |
34
- | `assistant` | A docked assistant rail, a page composer and an explain-on-selection floater. |
28
+ | Block | What you get |
29
+ | ----------- | ----------------------------------------------------------------------------------------- |
30
+ | `kpi` | A KPI card row on the overview. |
31
+ | `chart` | A token-themed line chart card on the overview. |
32
+ | `grid` | An editable per-row budget grid (TanStack Table + design `Table`). |
33
+ | `crud` | A list → detail → form slice over `/tokens`, exercising router params, Query and Form. |
34
+ | `assistant` | A docked assistant rail, a page composer and an explain-on-selection floater. |
35
+ | `auth` | A signed-out screen at `/signed-out`, with the restoring, expired and unreachable states. |
35
36
 
36
37
  Every block is independent — none imports another, so any combination compiles. Blocks are
37
38
  additive: pick what you need rather than taking everything and deleting, since an unpicked
38
39
  block costs no files and pulls none of its npm dependencies.
39
40
 
40
- Blocks join the shell through three files the generator writes rather than patches:
41
- `src/nav.ts` (the nav tuple), `src/overview-sections.tsx` (the ordered section registry) and
42
- `src/store.ts` (the shell slice plus the selected block slices). Add a section to a generated
43
- app by editing those, not the app chrome.
41
+ Blocks join the shell through four files the generator writes rather than patches: `src/nav.ts`
42
+ (the nav tuple), `src/overview-sections.tsx` (the ordered section registry), `src/store.ts` (the
43
+ shell slice plus the selected block slices) and `src/routes/__root.tsx` (whether a route gets the
44
+ app chrome). Add a section to a generated app by editing those, not the app chrome.
45
+
46
+ `auth` is the screen and nothing behind it. It renders with no dependency and reaches no network,
47
+ because only your app knows what it authenticates against — swap the form's `onSubmit` and pick
48
+ the state from your session instead of from the `?state=` search param.
44
49
 
45
50
  ## Naming
46
51
 
@@ -1,10 +1,11 @@
1
- //#region src/blocks.ts
1
+ // src/blocks.ts
2
2
  const BLOCK_IDS = [
3
3
  "kpi",
4
4
  "chart",
5
5
  "grid",
6
6
  "crud",
7
- "assistant"
7
+ "assistant",
8
+ "auth"
8
9
  ];
9
10
  const isBlockId = (value) => BLOCK_IDS.includes(value);
10
11
  const BLOCKS = {
@@ -55,19 +56,19 @@ const BLOCKS = {
55
56
  label: "CRUD slice",
56
57
  summary: "List → detail → form over `/tokens`: router params, Query and Form in one flow.",
57
58
  files: [
58
- "src/routes/tokens/-fixtures.test.ts",
59
- "src/routes/tokens/-fixtures.ts",
60
- "src/routes/tokens/-hooks.ts",
61
- "src/routes/tokens/-index.test.tsx",
62
- "src/routes/tokens/-nav.ts",
63
- "src/routes/tokens/-types.ts",
64
59
  "src/routes/tokens/$id.tsx",
60
+ "src/routes/tokens/index.test.tsx",
65
61
  "src/routes/tokens/index.tsx",
66
- "src/routes/tokens/new.tsx"
62
+ "src/routes/tokens/new.tsx",
63
+ "src/tokens/fixtures.test.ts",
64
+ "src/tokens/fixtures.ts",
65
+ "src/tokens/hooks.ts",
66
+ "src/tokens/nav.ts",
67
+ "src/tokens/types.ts"
67
68
  ],
68
69
  deps: ["@tanstack/react-form", "zod"],
69
70
  glue: ["src/nav.ts"],
70
- brief: "a list page reachable from the rail",
71
+ brief: "tokens list page: on-brand @keboola/design shell, a searchable table of API tokens, no runtime or a11y errors",
71
72
  route: "/tokens"
72
73
  },
73
74
  assistant: {
@@ -87,6 +88,24 @@ const BLOCKS = {
87
88
  glue: ["src/store.ts"],
88
89
  brief: "a prompt input and a docked assistant rail",
89
90
  route: null
91
+ },
92
+ auth: {
93
+ id: "auth",
94
+ label: "Signed-out screen",
95
+ summary: "The pre-project screen: brand lockup, one centred card, and the signed-out, restoring, expired and unreachable states.",
96
+ files: [
97
+ "src/auth/AuthScreen.test.tsx",
98
+ "src/auth/AuthScreen.tsx",
99
+ "src/auth/session.test.tsx",
100
+ "src/auth/session.ts",
101
+ "src/auth/states.tsx",
102
+ "src/routes/signed-out.test.tsx",
103
+ "src/routes/signed-out.tsx"
104
+ ],
105
+ deps: [],
106
+ glue: ["src/routes/__root.tsx"],
107
+ brief: "signed-out screen: a brand lockup over one centred sign-in card on a muted page, no navigation rail and no breadcrumb, a colour-scheme control in the corner, no runtime or a11y errors",
108
+ route: "/signed-out"
90
109
  }
91
110
  };
92
111
  /** Files every emitted app carries, whatever the selection. */
@@ -103,12 +122,12 @@ const SHELL_FILES = [
103
122
  "src/main.tsx",
104
123
  "src/nav.test.ts",
105
124
  "src/nav.ts",
125
+ "src/overview-fixtures.ts",
126
+ "src/overview-hooks.ts",
106
127
  "src/overview-sections.test.tsx",
107
128
  "src/overview-sections.tsx",
108
- "src/routes/-fixtures.ts",
109
- "src/routes/-hooks.ts",
110
- "src/routes/-index.test.tsx",
111
129
  "src/routes/__root.tsx",
130
+ "src/routes/index.test.tsx",
112
131
  "src/routes/index.tsx",
113
132
  "src/slices/shell.ts",
114
133
  "src/store.test.tsx",
@@ -125,13 +144,11 @@ const SHELL_FILES = [
125
144
  "vitest.config.ts"
126
145
  ];
127
146
  /**
128
- * Tracked boilerplate files the template deliberately does not carry.
147
+ * Tracked boilerplate files the template does not carry.
129
148
  *
130
- * - `AGENTS.md`, `README.md`, `.gitignore` — replaced by this package's `templates/`, because
131
- * the boilerplate's versions point at monorepo paths.
132
- * - `turbo.json` — there is no turbo outside the monorepo.
133
- * - `src/routeTree.gen.ts` — regenerated per selection by the CLI; the committed one lists
134
- * routes a shell-only app does not have.
149
+ * - `AGENTS.md`, `README.md`, `.gitignore`: replaced by `templates/` (the originals cite monorepo paths).
150
+ * - `turbo.json`: no turbo outside the monorepo.
151
+ * - `src/routeTree.gen.ts`: regenerated per selection by the CLI.
135
152
  */
136
153
  const NOT_IN_TEMPLATE = [
137
154
  ".gitignore",
@@ -143,5 +160,4 @@ const NOT_IN_TEMPLATE = [
143
160
  const OWNER = new Map([...SHELL_FILES.map((file) => [file, "shell"]), ...BLOCK_IDS.flatMap((id) => BLOCKS[id].files.map((file) => [file, id]))]);
144
161
  /** The block that owns `path`, `'shell'`, or `null` when nothing claims it. */
145
162
  const ownerOf = (path) => NOT_IN_TEMPLATE.includes(path) ? "shell" : OWNER.get(path) ?? null;
146
- //#endregion
147
163
  export { isBlockId as a, SHELL_FILES as i, BLOCK_IDS as n, ownerOf as o, NOT_IN_TEMPLATE as r, BLOCKS as t };
package/dist/cli.js CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import { a as isBlockId, n as BLOCK_IDS, o as ownerOf, t as BLOCKS } from "./blocks-DYU5Ke29.js";
3
- import { a as substitute, i as renderRegions, n as assertNoMarkers, r as blockIdOf } from "./regions-B3r5LhXl.js";
2
+ import { a as isBlockId, n as BLOCK_IDS, o as ownerOf, t as BLOCKS } from "./blocks-BdJaEmz1.js";
3
+ import { a as substitute, i as renderRegions, n as assertNoMarkers, o as isRecord, r as blockIdOf } from "./regions-BNl4QsPP.js";
4
4
  import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, statSync, writeFileSync } from "node:fs";
5
5
  import { basename, dirname, join, relative, resolve, sep } from "node:path";
6
6
  import { argv, cwd, exit, stderr, stdin, stdout } from "node:process";
7
7
  import { createInterface } from "node:readline/promises";
8
8
  import { fileURLToPath } from "node:url";
9
- //#region src/args.ts
9
+ // src/args.ts
10
10
  /**
11
11
  * Accepted app names: kebab-case, starting with a letter, not ending in `-`.
12
12
  *
@@ -113,8 +113,7 @@ const parseArgs = (argv) => {
113
113
  options
114
114
  };
115
115
  };
116
- //#endregion
117
- //#region src/scaffold.ts
116
+ // src/scaffold.ts
118
117
  /** Template files that describe the template rather than belong to the app. */
119
118
  const TEMPLATE_METADATA = /* @__PURE__ */ new Set(["workspace-deps.json", "package.json"]);
120
119
  /** Files this package's `templates/` contributes, which the boilerplate manifest does not claim. */
@@ -130,11 +129,8 @@ const evalCommand = (route, brief) => `npx --yes @keboola/ui-gen-bench eval --se
130
129
  /**
131
130
  * The emitted `gate:ui` script: one `ui-gen-bench eval` per scored route.
132
131
  *
133
- * - The overview's brief names the blocks that are actually on the page, because the score's
134
- * brief-conformance axis marks down a page for missing what the brief promises.
135
- * - A block with a `route` adds a second invocation for it.
136
- * - Not wired into `test`: it needs a model key, and a first run that fails on a missing
137
- * credential is a worse welcome than an opt-in gate.
132
+ * - Each brief names only its route's blocks: the brief-conformance axis grades against it.
133
+ * - Not wired into `test`: it needs a model key.
138
134
  */
139
135
  const gateUiScript = (appTitle, blocks) => {
140
136
  const clauses = blocks.filter((id) => BLOCKS[id].route === null).map((id) => BLOCKS[id].brief);
@@ -143,7 +139,7 @@ const gateUiScript = (appTitle, blocks) => {
143
139
  ...clauses,
144
140
  "no runtime or a11y errors"
145
141
  ].join(", ");
146
- const extras = blocks.filter((id) => BLOCKS[id].route !== null).map((id) => evalCommand(BLOCKS[id].route ?? "/", `${appTitle} ${BLOCKS[id].brief}: on-brand @keboola/design shell, searchable table, no runtime or a11y errors`));
142
+ const extras = blocks.filter((id) => BLOCKS[id].route !== null).map((id) => evalCommand(BLOCKS[id].route ?? "/", `${appTitle} ${BLOCKS[id].brief}`));
147
143
  return [evalCommand("/", overview), ...extras].join(" && ");
148
144
  };
149
145
  const walk = (root, dir, out) => {
@@ -156,11 +152,7 @@ const walk = (root, dir, out) => {
156
152
  /**
157
153
  * `_gitignore` → `.gitignore`: npm strips a real `.gitignore` out of every tarball.
158
154
  *
159
- * - Applied only to `TEMPLATE_DOCS`, the files this package's `templates/` contributes. The
160
- * rule exists for them and nothing else.
161
- * - Not a bare "basename starts with `_`" rule: TanStack's root route is `src/routes/__root.tsx`,
162
- * which such a rule renames to `.root.tsx` — the route tree then has no root and the emitted
163
- * app fails to generate one.
155
+ * - `TEMPLATE_DOCS` only: a bare leading-`_` rule would rename `src/routes/__root.tsx`.
164
156
  */
165
157
  const outputPath = (file) => {
166
158
  const name = basename(file);
@@ -199,10 +191,8 @@ const writeManifest = (opts) => {
199
191
  /**
200
192
  * `pnpm-workspace.yaml` for a standalone app.
201
193
  *
202
- * - It is the only file pnpm 11 reads `overrides` from; the `pnpm` field in `package.json` is
203
- * ignored with a warning.
204
- * - Written only outside a monorepo. Under `--workspace` the app inherits the root's file, and a
205
- * nested one would declare a second workspace root inside the first.
194
+ * - pnpm 11 reads `overrides` only from it.
195
+ * - Skipped under `--workspace`: a nested one would declare a second workspace root.
206
196
  */
207
197
  const writeWorkspaceConfig = (targetDir) => {
208
198
  writeFileSync(join(targetDir, "pnpm-workspace.yaml"), `# pnpm reads \`overrides\` from this file only — the \`pnpm\` field in package.json is ignored.
@@ -266,16 +256,13 @@ const scaffold = (opts) => {
266
256
  files: written.sort()
267
257
  };
268
258
  };
269
- //#endregion
270
- //#region src/cli.ts
259
+ // src/cli.ts
271
260
  const TEMPLATE_DIR = fileURLToPath(new URL("./template/", import.meta.url));
272
261
  /**
273
262
  * What to do when there is nobody to ask.
274
263
  *
275
- * - A missing `--name` is an error: there is no sane default for it, and a prompt an agent
276
- * cannot answer is worse than a non-zero exit.
277
- * - A missing `--blocks` defaults to every block, which is the shape `apps/boilerplate` has and
278
- * the most useful thing to hand someone who did not choose.
264
+ * - A missing `--name` is an error: no sane default, and an unanswerable prompt is worse.
265
+ * - A missing `--blocks` defaults to every block, the shape of `apps/boilerplate`.
279
266
  */
280
267
  const resolveNonInteractive = (options) => {
281
268
  if (options.name === null) return {
@@ -302,11 +289,9 @@ const nextSteps = (result, workspace) => workspace ? [
302
289
  /**
303
290
  * Writes `src/routeTree.gen.ts` for the routes that were actually copied.
304
291
  *
305
- * - Same code path the Vite plugin takes, so the tree matches what `pnpm dev` would write.
306
- * - Run now rather than left to the first build, so the emitted app type-checks before it is
307
- * ever built.
308
- * - A failure is fatal: an app whose `pnpm type-check` fails on a missing route tree is not a
309
- * deliverable, and the message says how to recover.
292
+ * - Same code path as the Vite plugin, so it matches what `pnpm dev` writes.
293
+ * - Runs now so the emitted app type-checks before its first build.
294
+ * - A failure is fatal.
310
295
  */
311
296
  const generateRouteTree = async (appDir) => {
312
297
  try {
@@ -314,7 +299,8 @@ const generateRouteTree = async (appDir) => {
314
299
  await new Generator({
315
300
  config: getConfig({
316
301
  quoteStyle: "single",
317
- semicolons: true
302
+ semicolons: true,
303
+ routeFileIgnorePattern: "\\.test\\.tsx?$"
318
304
  }, appDir),
319
305
  root: appDir
320
306
  }).run();
@@ -325,32 +311,29 @@ const generateRouteTree = async (appDir) => {
325
311
  /**
326
312
  * Adds the new app to the monorepo's knip config.
327
313
  *
328
- * - knip keys its per-workspace entry and project globs by path, so an unregistered app
329
- * inherits knip's defaults and reports its postcss config as unused.
330
- * - The boilerplate's block is copied as-is: every scaffolded app has the same `src/` layout.
314
+ * - Unregistered, knip's defaults report the app's postcss config as unused.
315
+ * - Copies the boilerplate's entry as-is: every scaffolded app shares its `src/` layout.
331
316
  */
332
317
  const registerKnipWorkspace = (repoRoot, appName) => {
333
318
  const knipPath = join(repoRoot, "knip.json");
334
319
  if (!existsSync(knipPath)) return;
335
320
  const config = JSON.parse(readFileSync(knipPath, "utf8"));
336
- const workspaces = Reflect.get(config, "workspaces");
337
- if (typeof workspaces !== "object" || workspaces === null) return;
338
- const source = Reflect.get(workspaces, "apps/boilerplate");
339
- if (source === void 0) return;
340
- Reflect.set(workspaces, `apps/${appName}`, JSON.parse(JSON.stringify(source)));
321
+ if (!isRecord(config) || !isRecord(config.workspaces)) return;
322
+ const { workspaces } = config;
323
+ if (!("apps/boilerplate" in workspaces)) return;
324
+ workspaces[`apps/${appName}`] = structuredClone(workspaces["apps/boilerplate"]);
341
325
  writeFileSync(knipPath, `${JSON.stringify(config, null, 2)}\n`);
342
326
  };
343
327
  /**
344
328
  * The monorepo root's own `engines`, for the emitted app to inherit under `--workspace`.
345
329
  *
346
- * - syncpack (`pnpm deps:check`) compares `engines.node` across every workspace, so an app
347
- * carrying the template's standalone floor makes the whole repo's check red.
330
+ * - syncpack compares `engines.node` across workspaces; the template's floor would fail it.
348
331
  * - `undefined` when the root declares none — `scaffold` then keeps the template's value.
349
332
  */
350
333
  const monorepoEngines = (root) => {
351
334
  const manifest = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
352
- const engines = Reflect.get(manifest, "engines");
353
- if (typeof engines !== "object" || engines === null) return void 0;
335
+ if (!isRecord(manifest) || !isRecord(manifest.engines)) return void 0;
336
+ const { engines } = manifest;
354
337
  return Object.fromEntries(Object.entries(engines).filter((entry) => typeof entry[1] === "string"));
355
338
  };
356
339
  const promptFor = async (options) => {
@@ -450,13 +433,8 @@ const main = async () => {
450
433
  /**
451
434
  * Whether this module is the process entry point.
452
435
  *
453
- * - `realpathSync` on both sides, because the installed `bin` is a symlink: npm and pnpm invoke
454
- * `node_modules/.bin/create-keboola-app`, so `argv[1]` is the link, not `dist/cli.js`. An
455
- * `endsWith('cli.js')` check — which is enough for `extract.ts` and `sanitize.ts`, both
456
- * invoked by path — would answer `false` for every real install.
457
- * - Guarded at all so importing the module does not run it: `cli.test.ts` imports
458
- * `resolveNonInteractive` and `nextSteps`, and an unguarded `main()` would scaffold, then
459
- * `exit(1)` out of the test worker.
436
+ * - `realpathSync` on both sides: the installed `bin` is a symlink, so `argv[1]` is the link.
437
+ * - Guarded so `cli.test.ts` can import the module without running `main()`.
460
438
  */
461
439
  const isEntryPoint = () => {
462
440
  const entry = argv[1];
@@ -474,5 +452,4 @@ if (isEntryPoint()) try {
474
452
  stderr.write(`✗ ${error instanceof Error ? error.message : String(error)}\n`);
475
453
  exit(1);
476
454
  }
477
- //#endregion
478
455
  export { nextSteps, resolveNonInteractive };
package/dist/extract.js CHANGED
@@ -1,10 +1,10 @@
1
- import { a as isBlockId, i as SHELL_FILES, n as BLOCK_IDS, r as NOT_IN_TEMPLATE, t as BLOCKS } from "./blocks-DYU5Ke29.js";
2
- import { i as renderRegions, r as blockIdOf } from "./regions-B3r5LhXl.js";
3
- import { r as scan, t as formatFindings } from "./sanitize-CKs8pAzZ.js";
1
+ import { a as isBlockId, i as SHELL_FILES, n as BLOCK_IDS, r as NOT_IN_TEMPLATE, t as BLOCKS } from "./blocks-BdJaEmz1.js";
2
+ import { i as renderRegions, r as blockIdOf } from "./regions-BNl4QsPP.js";
3
+ import { r as scan, t as formatFindings } from "./sanitize-DVFBkv8b.js";
4
4
  import { cpSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
5
5
  import { dirname, join } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
- //#region src/extract.ts
7
+ // src/extract.ts
8
8
  /** The scripts the emitted app keeps, keyed the way its `package.json` will hold them. */
9
9
  const APP_SCRIPTS = {
10
10
  build: "vite build",
@@ -36,10 +36,9 @@ const workspaceVersions = (packagesDir) => new Map(readdirSync(packagesDir).flat
36
36
  /**
37
37
  * The decision applied to every region while extracting.
38
38
  *
39
- * - `internal` is dropped: it is the marker for text that is true in the monorepo only.
39
+ * - `internal` (monorepo-only text) is dropped.
40
40
  * - `block:<id>` is kept **with** its markers — the CLI decides per scaffold.
41
- * - Anything else throws, so a typo in a marker fails the build instead of shipping a region
42
- * nobody will ever keep.
41
+ * - Anything else throws, so a marker typo fails the build.
43
42
  */
44
43
  const decideRegion = (regionId) => {
45
44
  if (regionId === "internal") return "drop";
@@ -50,16 +49,13 @@ const decideRegion = (regionId) => {
50
49
  /**
51
50
  * Per-file rewrites that turn this app's identity into placeholders.
52
51
  *
53
- * - Every rewrite has a matching assertion in `extract.test.ts`, and the sanitize scan fails
54
- * the build on any surviving mention of this app's name — a rewrite that stops matching is
55
- * a red build, not a scaffold that quietly names itself after the template.
56
- * - Match structurally, not by the literal name. The name must not appear here: the packed
57
- * tarball is scanned too, and a rewrite that spells its own needle makes the scanner match
58
- * its own implementation.
52
+ * - Each has an assertion in `extract.test.ts`; the sanitize scan fails on any surviving name.
53
+ * - Match structurally, never by the literal name: the tarball is scanned too.
59
54
  */
60
55
  const parameterise = (file, text) => {
61
56
  if (file === "index.html") return text.replace(/<title>[^<]*<\/title>/, "<title>__APP_TITLE__</title>");
62
57
  if (file === "src/components/AppChrome.tsx") return text.replace(/(<Link to="\/">)[^<]*(<\/Link>)/, "$1__APP_TITLE__$2");
58
+ if (file === "src/auth/AuthScreen.tsx") return text.replace(/^const PRODUCT = '[^']*';$/m, "const PRODUCT = '__APP_TITLE__';");
63
59
  if (file === "src/store.ts") return text.replaceAll(/'boilerplate\/([^']+)'/g, "'__APP_NAME__/$1'");
64
60
  return text;
65
61
  };
@@ -139,5 +135,4 @@ const main = () => {
139
135
  console.log(`✓ extracted ${files.length} files into dist/template (${workspaceDeps.length} @keboola deps pinned to published ranges)`);
140
136
  };
141
137
  if (process.argv[1]?.endsWith("extract.js") === true) main();
142
- //#endregion
143
138
  export { extractTemplate };
@@ -1,14 +1,14 @@
1
- //#region src/regions.ts
1
+ // src/isRecord.ts
2
+ /** A non-null object, readable by key — for parsed JSON. */
3
+ const isRecord = (value) => typeof value === "object" && value !== null;
4
+ // src/regions.ts
2
5
  const MARKER = String.raw`#(region|endregion)\s+(\S+)`;
3
6
  new RegExp(String.raw`^\s*(?://\s*|\{/\*\s*|<!--\s*)#region\s+(\S+)`);
4
7
  const MARKER_LINE_RE = new RegExp(String.raw`^\s*(?://\s*|\{/\*\s*|<!--\s*)${MARKER}`);
5
8
  /**
6
9
  * The block name in a region id, or `null` for any other shape.
7
10
  *
8
- * - Only an exact `block:<name>` matches. `block:kpi:extra` is a typo, not the `kpi` block —
9
- * resolving it to the block whose name it happens to start with would let a malformed
10
- * marker decide a real region, which is how a marker leaks into an emitted app.
11
- * - `internal` and every other id are the caller's business; this answers one question.
11
+ * - Only an exact `block:<name>` matches; `block:kpi:extra` is a typo, not the `kpi` block.
12
12
  */
13
13
  const blockIdOf = (regionId) => /^block:([^:\s]+)$/.exec(regionId)?.[1] ?? null;
14
14
  const markerOf = (line) => {
@@ -22,11 +22,9 @@ const markerOf = (line) => {
22
22
  };
23
23
  };
24
24
  /**
25
- * Removes the blank-line debris a dropped region leaves.
25
+ * Collapses blank-line runs and drops a leading blank line.
26
26
  *
27
- * - Runs of two or more blank lines collapse to one: dropping an import region between two
28
- * groups otherwise leaves a double gap, and the emitted app's `oxfmt --check` fails on it.
29
- * - A leading blank line is dropped for the same reason.
27
+ * - Dropped regions leave that debris, and the emitted app's `oxfmt --check` fails on it.
30
28
  */
31
29
  const tidy = (lines) => {
32
30
  const out = [];
@@ -68,21 +66,14 @@ const renderRegions = (source, decide) => {
68
66
  /**
69
67
  * Any marker, in any dialect, anywhere on a line.
70
68
  *
71
- * - Deliberately looser than `markerOf`: this is the containment check, so an *unrecognised*
72
- * marker — misspelt dialect, indented into a continuation line, left inside a string — has to
73
- * match too. `markerOf` would skip exactly those and let them through.
69
+ * - Looser than `markerOf` on purpose: containment must catch unrecognised markers too.
74
70
  */
75
71
  const MARKER_RE = /#(?:region|endregion)\b/;
76
72
  /**
77
- * Fails when rendered text still carries a marker.
73
+ * Fails when rendered text still carries a marker: no `#region` may reach an emitted app.
78
74
  *
79
- * - The invariant: no `#region block:` may reach an emitted app. Markers are a build-time
80
- * mechanism, and one surviving into generated source is how that mechanism fails silently —
81
- * the app compiles, the region is dead text, and nobody finds out until a reader asks.
82
- * - Called on every file the scaffolder writes, so a stray, mis-nested or unknown-block marker
83
- * aborts the scaffold instead of shipping.
84
- * - Not called on the extract's output: that keeps its markers on purpose, for the CLI to
85
- * decide per scaffold.
75
+ * - Called on every file the scaffolder writes.
76
+ * - Not on the extract's output, which keeps markers for the CLI to decide per scaffold.
86
77
  */
87
78
  const assertNoMarkers = (file, text) => {
88
79
  const line = text.split("\n").findIndex((candidate) => MARKER_RE.test(candidate));
@@ -91,5 +82,4 @@ const assertNoMarkers = (file, text) => {
91
82
  };
92
83
  /** Replaces `__KEY__` with `vars[KEY]`. An unlisted placeholder is left in place. */
93
84
  const substitute = (source, vars) => Object.entries(vars).reduce((acc, [key, value]) => acc.replaceAll(`__${key}__`, value), source);
94
- //#endregion
95
- export { substitute as a, renderRegions as i, assertNoMarkers as n, blockIdOf as r, MARKER_RE as t };
85
+ export { substitute as a, renderRegions as i, assertNoMarkers as n, isRecord as o, blockIdOf as r, MARKER_RE as t };
@@ -1,8 +1,8 @@
1
- import { t as MARKER_RE } from "./regions-B3r5LhXl.js";
1
+ import { o as isRecord, t as MARKER_RE } from "./regions-BNl4QsPP.js";
2
2
  import { readFileSync, readdirSync, statSync } from "node:fs";
3
3
  import { join, relative, sep } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
- //#region src/sanitize.ts
5
+ // src/sanitize.ts
6
6
  const PATTERNS_PATH = fileURLToPath(new URL("../sanitize-patterns.json", import.meta.url));
7
7
  const SKIP_DIRS = /* @__PURE__ */ new Set([
8
8
  "node_modules",
@@ -12,9 +12,8 @@ const SKIP_DIRS = /* @__PURE__ */ new Set([
12
12
  /**
13
13
  * The pattern table, compiled.
14
14
  *
15
- * - Read from JSON on purpose: the table is not in `files[]`, so no published copy of this
16
- * scanner carries the patterns it looks for.
17
- * - Absent means the caller is running from an installed package, which the gates never do.
15
+ * - JSON on purpose: it is not in `files[]`, so no published scanner carries its patterns.
16
+ * - Absent only when run from an installed package, which the gates never do.
18
17
  */
19
18
  const loadPatterns = () => {
20
19
  let raw;
@@ -69,11 +68,9 @@ const scanTree = (root, patterns = loadPatterns()) => {
69
68
  });
70
69
  };
71
70
  /**
72
- * Structural check on every `package.json` under `root`: no dependency may keep pnpm's
73
- * `workspace:` protocol.
71
+ * Findings for any `package.json` dependency still on pnpm's `workspace:` protocol.
74
72
  *
75
- * - Parsed rather than grepped, so the rule is about specifiers and not about the string
76
- * appearing anywhere in a file. The CLI legitimately holds that literal for `--workspace`.
73
+ * - Parsed, not grepped: the CLI legitimately holds that literal for `--workspace`.
77
74
  */
78
75
  const scanManifests = (root) => {
79
76
  const files = [];
@@ -86,8 +83,9 @@ const scanManifests = (root) => {
86
83
  ];
87
84
  return files.filter((file) => file === "package.json" || file.endsWith("/package.json")).flatMap((file) => {
88
85
  const manifest = JSON.parse(readFileSync(join(root, file), "utf8"));
86
+ if (!isRecord(manifest)) return [];
89
87
  return groups.flatMap((group) => {
90
- const deps = Reflect.get(manifest, group);
88
+ const deps = manifest[group];
91
89
  if (typeof deps !== "object" || deps === null) return [];
92
90
  return Object.entries(deps).filter(([, range]) => typeof range === "string" && range.startsWith("workspace:")).map(([name, range]) => ({
93
91
  file,
@@ -101,15 +99,10 @@ const scanManifests = (root) => {
101
99
  };
102
100
  const scan = (root) => [...scanTree(root), ...scanManifests(root)];
103
101
  /**
104
- * Region markers surviving into emitted source.
102
+ * Region markers surviving into an emitted app.
105
103
  *
106
- * - Separate from `scan`, and never part of it: the tarball's own `dist/template/` carries
107
- * markers on purpose, so a pattern-table rule for them would fail every run.
108
- * - Run against an **emitted app** instead — the one place the invariant applies. A marker
109
- * there is dead text that compiles, which is how the mechanism fails without telling anyone.
110
- * - `assertNoMarkers` already aborts a scaffold in-process; this is the same rule applied to
111
- * output on disk, so the gates can prove it about a real `pnpm pack` → scaffold run rather
112
- * than trusting the code path.
104
+ * - Never part of `scan`: the tarball's `dist/template/` carries markers on purpose.
105
+ * - The on-disk twin of `assertNoMarkers`, so gates prove it on a real `pnpm pack` → scaffold.
113
106
  */
114
107
  const scanMarkers = (root) => {
115
108
  const files = [];
@@ -130,12 +123,10 @@ const scanMarkers = (root) => {
130
123
  };
131
124
  const formatFindings = (findings) => findings.map((f) => ` ${f.file}:${f.line} [${f.pattern}] ${f.match}\n ${f.why}`).join("\n");
132
125
  /**
133
- * Two modes, because the two things being proved are opposites.
126
+ * Two modes:
134
127
  *
135
- * 1. Default — internal references and credential shapes under a directory. Run on `pnpm pack`
136
- * output, which still holds the marked-up template.
137
- * 2. `--markers` — region markers under a directory. Run on an **emitted app**, where a marker
138
- * must never appear.
128
+ * - Default — internal references and credential shapes; run on `pnpm pack` output.
129
+ * - `--markers` — region markers; run on an emitted app.
139
130
  */
140
131
  const main = () => {
141
132
  const args = process.argv.slice(2);
@@ -154,5 +145,4 @@ const main = () => {
154
145
  console.log(markersOnly ? `✓ no region markers under ${root}` : `✓ no internal references or credential shapes under ${root}`);
155
146
  };
156
147
  if (process.argv[1]?.endsWith("sanitize.js") === true) main();
157
- //#endregion
158
148
  export { scanMarkers as a, scanManifests as i, loadPatterns as n, scanText as o, scan as r, scanTree as s, formatFindings as t };
package/dist/sanitize.js CHANGED
@@ -1,2 +1 @@
1
- import { a as scanMarkers, i as scanManifests, n as loadPatterns, o as scanText, r as scan, s as scanTree, t as formatFindings } from "./sanitize-CKs8pAzZ.js";
2
- export { formatFindings, loadPatterns, scan, scanManifests, scanMarkers, scanText, scanTree };
1
+ export { t as formatFindings, n as loadPatterns, r as scan, i as scanManifests, a as scanMarkers, o as scanText, s as scanTree } from "./sanitize-DVFBkv8b.js";