create-keboola-app 0.2.0 → 0.2.2
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 +19 -0
- package/dist/{blocks-D_-NWhwg.js → blocks-BdJaEmz1.js} +5 -8
- package/dist/cli.js +27 -53
- package/dist/extract.js +8 -14
- package/dist/{regions-B3r5LhXl.js → regions-BNl4QsPP.js} +12 -22
- package/dist/{sanitize-CKs8pAzZ.js → sanitize-DVFBkv8b.js} +14 -24
- package/dist/sanitize.js +1 -2
- package/dist/template/oxlint.config.ts +10 -14
- package/dist/template/package.json +20 -19
- package/dist/template/src/auth/AuthScreen.tsx +6 -26
- package/dist/template/src/auth/session.ts +7 -23
- package/dist/template/src/auth/states.tsx +5 -23
- package/dist/template/src/components/AppChrome.tsx +26 -74
- package/dist/template/src/components/SelectionFloater.tsx +10 -3
- package/dist/template/src/components/SidebarModeMenu.tsx +8 -13
- package/dist/template/src/nav.ts +5 -10
- package/dist/template/src/overview-fixtures.ts +6 -18
- package/dist/template/src/recipes/Chart.test.tsx +1 -3
- package/dist/template/src/recipes/Chart.tsx +4 -11
- package/dist/template/src/recipes/DataGrid.tsx +5 -19
- package/dist/template/src/routes/__root.tsx +6 -13
- package/dist/template/src/routes/signed-out.tsx +3 -7
- package/dist/template/src/sections/kpi.tsx +3 -7
- package/dist/template/src/slices/assistant.ts +3 -9
- package/dist/template/src/slices/shell.ts +4 -11
- package/dist/template/src/store.ts +9 -24
- package/dist/template/src/tokens/fixtures.ts +1 -1
- package/dist/template/vite.config.ts +1 -4
- package/dist/template/vitest.config.ts +2 -5
- package/package.json +13 -12
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# create-keboola-app
|
|
2
2
|
|
|
3
|
+
## 0.2.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Under `--workspace`, the scaffolded app's `knip:check` runs the monorepo's shared knip runner.
|
|
8
|
+
|
|
9
|
+
- It checks the app with `knip.json`, then in production mode, where an export only tests import fails the check.
|
|
10
|
+
|
|
11
|
+
## 0.2.1
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- Internal code satisfies the `anti-slop` lint rules; public APIs and behaviour are unchanged.
|
|
16
|
+
|
|
17
|
+
- `Reflect.get`, parameters typed `object` and accumulator-copying reducers give way to narrowing, precise types, `filter` and `flatMap`.
|
|
18
|
+
- `@keboola/api-client` keeps its `as unknown as` bridges from OpenAPI to domain types.
|
|
19
|
+
|
|
20
|
+
- Scaffolded apps get route trees from `@tanstack/router-generator` 1.167.46, the generator of the TanStack Router 1.170.46 line.
|
|
21
|
+
|
|
3
22
|
## 0.2.0
|
|
4
23
|
|
|
5
24
|
### Minor Changes
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
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
|
|
147
|
+
* Tracked boilerplate files the template does not carry.
|
|
148
148
|
*
|
|
149
|
-
* - `AGENTS.md`, `README.md`, `.gitignore
|
|
150
|
-
*
|
|
151
|
-
* - `
|
|
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-
|
|
3
|
-
import { a as substitute, i as renderRegions, n as assertNoMarkers, r as blockIdOf } from "./regions-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
* -
|
|
134
|
-
*
|
|
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
|
-
* -
|
|
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);
|
|
@@ -192,7 +182,7 @@ const writeManifest = (opts) => {
|
|
|
192
182
|
...pkg.scripts,
|
|
193
183
|
"gate:ui": gateUiScript(titleCase(appName), blocks),
|
|
194
184
|
...workspace ? {
|
|
195
|
-
"knip:check":
|
|
185
|
+
"knip:check": "node ../../scripts/knip-check.mjs",
|
|
196
186
|
"knip:fix": `pnpm --dir ../.. exec knip --workspace apps/${appName} --fix-type types,exports,files`
|
|
197
187
|
} : {}
|
|
198
188
|
};
|
|
@@ -201,10 +191,8 @@ const writeManifest = (opts) => {
|
|
|
201
191
|
/**
|
|
202
192
|
* `pnpm-workspace.yaml` for a standalone app.
|
|
203
193
|
*
|
|
204
|
-
* -
|
|
205
|
-
*
|
|
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
|
-
|
|
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:
|
|
278
|
-
*
|
|
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
|
|
308
|
-
* -
|
|
309
|
-
*
|
|
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
|
|
332
|
-
*
|
|
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
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
|
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
|
-
|
|
356
|
-
|
|
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
|
|
457
|
-
*
|
|
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-
|
|
2
|
-
import { i as renderRegions, r as blockIdOf } from "./regions-
|
|
3
|
-
import { r as scan, t as formatFindings } from "./sanitize-
|
|
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
|
-
|
|
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`
|
|
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
|
|
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
|
-
* -
|
|
54
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
25
|
+
* Collapses blank-line runs and drops a leading blank line.
|
|
26
26
|
*
|
|
27
|
-
* -
|
|
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
|
-
* -
|
|
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
|
-
* -
|
|
80
|
-
*
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
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
|
-
* -
|
|
16
|
-
*
|
|
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
|
-
*
|
|
73
|
-
* `workspace:` protocol.
|
|
71
|
+
* Findings for any `package.json` dependency still on pnpm's `workspace:` protocol.
|
|
74
72
|
*
|
|
75
|
-
* - Parsed
|
|
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 =
|
|
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
|
|
102
|
+
* Region markers surviving into an emitted app.
|
|
105
103
|
*
|
|
106
|
-
* -
|
|
107
|
-
*
|
|
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
|
|
126
|
+
* Two modes:
|
|
134
127
|
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
14
|
+
// The file that `@import`s "tailwindcss".
|
|
18
15
|
entryPoint: 'src/index.css',
|
|
19
16
|
},
|
|
17
|
+
shadcn: shadcnSettings,
|
|
20
18
|
},
|
|
21
19
|
jsPlugins: [
|
|
22
|
-
//
|
|
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
|
-
|
|
28
|
-
//
|
|
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
|
-
|
|
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'
|
|
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.
|
|
19
|
-
"@keboola/design": "^
|
|
18
|
+
"@keboola/brand-registry": "^3.1.0",
|
|
19
|
+
"@keboola/design": "^17.2.1",
|
|
20
20
|
"@tanstack/react-form": "^1.29.1",
|
|
21
|
-
"@tanstack/react-query": "^5.
|
|
22
|
-
"@tanstack/react-router": "^1.
|
|
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.
|
|
25
|
-
"react": "^19.
|
|
26
|
-
"react-dom": "^19.
|
|
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.
|
|
32
|
-
"@keboola/oxlint-config": "^10.0
|
|
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.
|
|
36
|
-
"@testing-library/jest-dom": "^
|
|
37
|
-
"@testing-library/react": "^16.3.
|
|
38
|
-
"@testing-library/user-event": "^14.6.
|
|
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.
|
|
41
|
-
"@types/react-dom": "^19.
|
|
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.
|
|
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.
|
|
48
|
+
"oxlint": "^1.89.0",
|
|
48
49
|
"tailwindcss": "^4.3.3",
|
|
49
50
|
"typescript": "npm:@typescript/typescript6@^6.0.2",
|
|
50
|
-
"vite": "^8.
|
|
51
|
-
"vitest": "^
|
|
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
|
-
//
|
|
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
|
|
18
|
-
*
|
|
19
|
-
* -
|
|
20
|
-
*
|
|
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 (
|