create-keboola-app 0.2.0 → 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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
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
+
3
14
  ## 0.2.0
4
15
 
5
16
  ### Minor Changes
@@ -1,4 +1,4 @@
1
- //#region src/blocks.ts
1
+ // src/blocks.ts
2
2
  const BLOCK_IDS = [
3
3
  "kpi",
4
4
  "chart",
@@ -144,13 +144,11 @@ const SHELL_FILES = [
144
144
  "vitest.config.ts"
145
145
  ];
146
146
  /**
147
- * Tracked boilerplate files the template deliberately does not carry.
147
+ * Tracked boilerplate files the template does not carry.
148
148
  *
149
- * - `AGENTS.md`, `README.md`, `.gitignore` — replaced by this package's `templates/`, because
150
- * the boilerplate's versions point at monorepo paths.
151
- * - `turbo.json` — there is no turbo outside the monorepo.
152
- * - `src/routeTree.gen.ts` — regenerated per selection by the CLI; the committed one lists
153
- * 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.
154
152
  */
155
153
  const NOT_IN_TEMPLATE = [
156
154
  ".gitignore",
@@ -162,5 +160,4 @@ const NOT_IN_TEMPLATE = [
162
160
  const OWNER = new Map([...SHELL_FILES.map((file) => [file, "shell"]), ...BLOCK_IDS.flatMap((id) => BLOCKS[id].files.map((file) => [file, id]))]);
163
161
  /** The block that owns `path`, `'shell'`, or `null` when nothing claims it. */
164
162
  const ownerOf = (path) => NOT_IN_TEMPLATE.includes(path) ? "shell" : OWNER.get(path) ?? null;
165
- //#endregion
166
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-D_-NWhwg.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,13 +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 an invocation for it, briefed entirely by the block. The
136
- * conformance axis is graded against this text, so a shared suffix here would grade every
137
- * routed block against one page's qualities.
138
- * - Not wired into `test`: it needs a model key, and a first run that fails on a missing
139
- * 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.
140
134
  */
141
135
  const gateUiScript = (appTitle, blocks) => {
142
136
  const clauses = blocks.filter((id) => BLOCKS[id].route === null).map((id) => BLOCKS[id].brief);
@@ -158,11 +152,7 @@ const walk = (root, dir, out) => {
158
152
  /**
159
153
  * `_gitignore` → `.gitignore`: npm strips a real `.gitignore` out of every tarball.
160
154
  *
161
- * - Applied only to `TEMPLATE_DOCS`, the files this package's `templates/` contributes. The
162
- * rule exists for them and nothing else.
163
- * - Not a bare "basename starts with `_`" rule: TanStack's root route is `src/routes/__root.tsx`,
164
- * which such a rule renames to `.root.tsx` — the route tree then has no root and the emitted
165
- * app fails to generate one.
155
+ * - `TEMPLATE_DOCS` only: a bare leading-`_` rule would rename `src/routes/__root.tsx`.
166
156
  */
167
157
  const outputPath = (file) => {
168
158
  const name = basename(file);
@@ -201,10 +191,8 @@ const writeManifest = (opts) => {
201
191
  /**
202
192
  * `pnpm-workspace.yaml` for a standalone app.
203
193
  *
204
- * - It is the only file pnpm 11 reads `overrides` from; the `pnpm` field in `package.json` is
205
- * ignored with a warning.
206
- * - Written only outside a monorepo. Under `--workspace` the app inherits the root's file, and a
207
- * 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.
208
196
  */
209
197
  const writeWorkspaceConfig = (targetDir) => {
210
198
  writeFileSync(join(targetDir, "pnpm-workspace.yaml"), `# pnpm reads \`overrides\` from this file only — the \`pnpm\` field in package.json is ignored.
@@ -268,16 +256,13 @@ const scaffold = (opts) => {
268
256
  files: written.sort()
269
257
  };
270
258
  };
271
- //#endregion
272
- //#region src/cli.ts
259
+ // src/cli.ts
273
260
  const TEMPLATE_DIR = fileURLToPath(new URL("./template/", import.meta.url));
274
261
  /**
275
262
  * What to do when there is nobody to ask.
276
263
  *
277
- * - A missing `--name` is an error: there is no sane default for it, and a prompt an agent
278
- * cannot answer is worse than a non-zero exit.
279
- * - A missing `--blocks` defaults to every block, which is the shape `apps/boilerplate` has and
280
- * 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`.
281
266
  */
282
267
  const resolveNonInteractive = (options) => {
283
268
  if (options.name === null) return {
@@ -304,11 +289,9 @@ const nextSteps = (result, workspace) => workspace ? [
304
289
  /**
305
290
  * Writes `src/routeTree.gen.ts` for the routes that were actually copied.
306
291
  *
307
- * - Same code path the Vite plugin takes, so the tree matches what `pnpm dev` would write.
308
- * - Run now rather than left to the first build, so the emitted app type-checks before it is
309
- * ever built.
310
- * - A failure is fatal: an app whose `pnpm type-check` fails on a missing route tree is not a
311
- * 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.
312
295
  */
313
296
  const generateRouteTree = async (appDir) => {
314
297
  try {
@@ -328,32 +311,29 @@ const generateRouteTree = async (appDir) => {
328
311
  /**
329
312
  * Adds the new app to the monorepo's knip config.
330
313
  *
331
- * - knip keys its per-workspace entry and project globs by path, so an unregistered app
332
- * inherits knip's defaults and reports its postcss config as unused.
333
- * - 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.
334
316
  */
335
317
  const registerKnipWorkspace = (repoRoot, appName) => {
336
318
  const knipPath = join(repoRoot, "knip.json");
337
319
  if (!existsSync(knipPath)) return;
338
320
  const config = JSON.parse(readFileSync(knipPath, "utf8"));
339
- const workspaces = Reflect.get(config, "workspaces");
340
- if (typeof workspaces !== "object" || workspaces === null) return;
341
- const source = Reflect.get(workspaces, "apps/boilerplate");
342
- if (source === void 0) return;
343
- 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"]);
344
325
  writeFileSync(knipPath, `${JSON.stringify(config, null, 2)}\n`);
345
326
  };
346
327
  /**
347
328
  * The monorepo root's own `engines`, for the emitted app to inherit under `--workspace`.
348
329
  *
349
- * - syncpack (`pnpm deps:check`) compares `engines.node` across every workspace, so an app
350
- * 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.
351
331
  * - `undefined` when the root declares none — `scaffold` then keeps the template's value.
352
332
  */
353
333
  const monorepoEngines = (root) => {
354
334
  const manifest = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
355
- const engines = Reflect.get(manifest, "engines");
356
- if (typeof engines !== "object" || engines === null) return void 0;
335
+ if (!isRecord(manifest) || !isRecord(manifest.engines)) return void 0;
336
+ const { engines } = manifest;
357
337
  return Object.fromEntries(Object.entries(engines).filter((entry) => typeof entry[1] === "string"));
358
338
  };
359
339
  const promptFor = async (options) => {
@@ -453,13 +433,8 @@ const main = async () => {
453
433
  /**
454
434
  * Whether this module is the process entry point.
455
435
  *
456
- * - `realpathSync` on both sides, because the installed `bin` is a symlink: npm and pnpm invoke
457
- * `node_modules/.bin/create-keboola-app`, so `argv[1]` is the link, not `dist/cli.js`. An
458
- * `endsWith('cli.js')` check — which is enough for `extract.ts` and `sanitize.ts`, both
459
- * invoked by path — would answer `false` for every real install.
460
- * - Guarded at all so importing the module does not run it: `cli.test.ts` imports
461
- * `resolveNonInteractive` and `nextSteps`, and an unguarded `main()` would scaffold, then
462
- * `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()`.
463
438
  */
464
439
  const isEntryPoint = () => {
465
440
  const entry = argv[1];
@@ -477,5 +452,4 @@ if (isEntryPoint()) try {
477
452
  stderr.write(`✗ ${error instanceof Error ? error.message : String(error)}\n`);
478
453
  exit(1);
479
454
  }
480
- //#endregion
481
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-D_-NWhwg.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,12 +49,8 @@ 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>");
@@ -140,5 +135,4 @@ const main = () => {
140
135
  console.log(`✓ extracted ${files.length} files into dist/template (${workspaceDeps.length} @keboola deps pinned to published ranges)`);
141
136
  };
142
137
  if (process.argv[1]?.endsWith("extract.js") === true) main();
143
- //#endregion
144
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";
@@ -1,41 +1,37 @@
1
1
  import { defineConfig } from 'oxlint';
2
2
 
3
3
  import keboolaShared from '@keboola/oxlint-config';
4
+ import keboolaShadcn, { shadcnSettings } from '@keboola/oxlint-config/shadcn';
4
5
  import keboolaTailwind from '@keboola/oxlint-config/tailwind';
5
6
 
6
7
  export default defineConfig({
7
- // `keboolaTailwind` opts into the v4-native Tailwind lint layer
8
- // (eslint-plugin-better-tailwindcss). It carries the plugin + rules; the
9
- // `entryPoint` below is required — oxlint only reads `settings` from the
10
- // root config, so it can't live inside the shared fragment.
11
- extends: [keboolaShared, keboolaTailwind],
8
+ extends: [keboolaShared, keboolaTailwind, keboolaShadcn],
12
9
  env: {
13
10
  node: true,
14
11
  },
15
12
  settings: {
16
13
  'better-tailwindcss': {
17
- // This app's Tailwind v4 CSS entry (the file that `@import`s "tailwindcss").
14
+ // The file that `@import`s "tailwindcss".
18
15
  entryPoint: 'src/index.css',
19
16
  },
17
+ shadcn: shadcnSettings,
20
18
  },
21
19
  jsPlugins: [
22
- // Local design-system + brand-safety lint rules. Aliased to `design` so rule
23
- // IDs read `design/<rule>` in lint output.
20
+ // Aliased to `design`, so rule IDs read `design/<rule>`.
24
21
  { name: 'design', specifier: '@keboola/eslint-plugin-design' },
25
22
  ],
26
23
  rules: {
27
- // Shipped at 'warn' for the first pass, matching the plugin convention —
28
- // promote to 'error' once generated apps are consistently clean. See
29
- // packages/eslint-plugin-design/README.md.
24
+ 'design/max-comment-lines': 'error',
25
+ // 'warn' until generated apps are consistently clean.
30
26
  'design/no-layout-primitives': 'warn',
31
- 'design/no-inline-color': 'warn',
27
+ // `design/no-inline-color` off: `shadcn/no-inline-styles` and `no-arbitrary-values` cover it.
32
28
  'design/no-primitive-tw-color': 'warn',
33
29
  'design/no-native-form-controls': 'warn',
34
30
  'design/no-raw-heading-on-page': 'warn',
35
31
  'design/prefer-design-modal': 'warn',
32
+ 'design/htmlfor-points-at-nothing': 'error',
36
33
 
37
- // 'error' from the start: this app is the scaffold template, so a
38
- // locale-implicit format here is copied into every generated app.
34
+ // 'error': the scaffold copies any locale-implicit format into every generated app.
39
35
  'design/no-implicit-locale': 'error',
40
36
  },
41
37
  });
@@ -15,39 +15,40 @@
15
15
  "type-check": "tsc"
16
16
  },
17
17
  "dependencies": {
18
- "@keboola/brand-registry": "^1.9.4",
19
- "@keboola/design": "^11.5.0",
18
+ "@keboola/brand-registry": "^3.1.0",
19
+ "@keboola/design": "^17.2.0",
20
20
  "@tanstack/react-form": "^1.29.1",
21
- "@tanstack/react-query": "^5.96.2",
22
- "@tanstack/react-router": "^1.120.4",
21
+ "@tanstack/react-query": "^5.104.5",
22
+ "@tanstack/react-router": "^1.170.46",
23
23
  "@tanstack/react-table": "^8.21.3",
24
- "@tanstack/router-plugin": "^1.120.4",
25
- "react": "^19.2.7",
26
- "react-dom": "^19.2.7",
24
+ "@tanstack/router-plugin": "^1.168.48",
25
+ "react": "^19.3.1",
26
+ "react-dom": "^19.3.1",
27
27
  "zod": "^4.3.6",
28
28
  "zustand": "^5.0.8"
29
29
  },
30
30
  "devDependencies": {
31
- "@keboola/eslint-plugin-design": "^0.4.3",
32
- "@keboola/oxlint-config": "^10.0.2",
31
+ "@keboola/eslint-plugin-design": "^0.8.0",
32
+ "@keboola/oxlint-config": "^10.3.0",
33
33
  "@keboola/tsconfig": "^0.2.1",
34
+ "@shadcn/lint": "^0.1.5",
34
35
  "@tailwindcss/vite": "^4.3.3",
35
- "@tanstack/react-query-devtools": "^5.96.2",
36
- "@testing-library/jest-dom": "^6.8.0",
37
- "@testing-library/react": "^16.3.2",
38
- "@testing-library/user-event": "^14.6.1",
36
+ "@tanstack/react-query-devtools": "^5.104.5",
37
+ "@testing-library/jest-dom": "^7.0.1",
38
+ "@testing-library/react": "^16.3.3",
39
+ "@testing-library/user-event": "^14.6.8",
39
40
  "@types/node": "24.12.0",
40
- "@types/react": "^19.2.15",
41
- "@types/react-dom": "^19.2.3",
41
+ "@types/react": "^19.3.0",
42
+ "@types/react-dom": "^19.3.0",
42
43
  "@typescript/native": "npm:typescript@^7.0.2",
43
- "@vitejs/plugin-react": "^6.0.0",
44
+ "@vitejs/plugin-react": "^6.1.2",
44
45
  "eslint-plugin-better-tailwindcss": "^4.7.0",
45
46
  "jsdom": "^26.1.0",
46
47
  "oxfmt": "^0.66.0",
47
- "oxlint": "^1.81.0",
48
+ "oxlint": "^1.89.0",
48
49
  "tailwindcss": "^4.3.3",
49
50
  "typescript": "npm:@typescript/typescript6@^6.0.2",
50
- "vite": "^8.1.5",
51
- "vitest": "^4.1.10"
51
+ "vite": "^8.3.5",
52
+ "vitest": "^5.0.5"
52
53
  }
53
54
  }
@@ -4,37 +4,17 @@ import { BrandLogo, Card } from '@keboola/design';
4
4
 
5
5
  import { ColorModeToggle } from '../components/ColorModeToggle';
6
6
 
7
- // The product name beside the brand mark.
8
- //
9
- // - `create-keboola-app` rewrites this literal to the generated app's title while extracting
10
- // the template (`parameterise` in the package's `extract.ts`), so it has to stay a top-level
11
- // single-quoted `const`.
7
+ // Must stay a top-level single-quoted `const`: `create-keboola-app` (`parameterise` in
8
+ // `extract.ts`) rewrites it to the generated app's title.
12
9
  const PRODUCT = '__APP_TITLE__';
13
10
 
14
11
  /**
15
12
  * The chrome every pre-project screen shares: brand lockup, one centred card, no navigation.
16
13
  *
17
- * - Renders *instead of* `AppChrome`, not inside it — `routes/__root.tsx` picks one from
18
- * `useIsAuthRoute`. A visitor with no session gets no rail, no breadcrumb and no history.
19
- * - `tailwindcss-preflight` and `text-foreground` are set here rather than inherited. A real app
20
- * moves this screen ahead of the router, because the session decides whether the router mounts
21
- * at all, and then nothing above it sets either: without the preflight scope the card's
22
- * controls pick up UA chrome, and without the ink `BrandLogo`'s inline SVG inherits
23
- * `currentColor` and goes near-black on the dark page.
24
- * - `header` and `main` are landmarks, not decoration. Outside one, axe reports these controls as
25
- * unreachable by landmark navigation — which is why the colour-scheme control sits inside
26
- * `header` even though it is positioned away from the lockup.
27
- * - `Card.Title as="h1"` — the card carries the page's only heading, and the default `h3` opens
28
- * the outline three levels down.
29
- * - `variant="mark"` beside the product name as text. The wordmark already spells the brand, so
30
- * pairing it with a product name states the brand twice; the mark reads as one lockup. It also
31
- * degrades in the right order — `BrandLogo` falls back to the wordmark for a brand that ships
32
- * no mark and returns `null` for one that ships neither, which is why the name is text and not
33
- * part of the image.
34
- * - `h-8`, not the navbar's `h-6`: with no chrome around it the lockup is the only thing naming
35
- * the product, and at bar size it reads as chrome that lost its bar.
36
- * - Every pre-project state reuses this chrome — see `AUTH_STATES` in `states.tsx` — so the
37
- * common path does not jump when one state replaces another.
14
+ * - Renders instead of `AppChrome`; `routes/__root.tsx` picks via `useIsAuthRoute`.
15
+ * - Sets `tailwindcss-preflight` and `text-foreground` itself: a real app mounts it above the router.
16
+ * - The toggle sits in `header` so axe finds it inside a landmark.
17
+ * - `variant="mark"` + text name: the wordmark would repeat the brand; the name survives a `null` logo.
38
18
  */
39
19
  export function AuthScreen({ title, children }: { title: string; children: ReactNode }) {
40
20
  return (