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.
- package/CHANGELOG.md +54 -0
- package/README.md +16 -11
- package/dist/{blocks-DYU5Ke29.js → blocks-BdJaEmz1.js} +36 -20
- package/dist/cli.js +29 -52
- package/dist/extract.js +9 -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/AGENTS.md +20 -6
- package/dist/template/README.md +5 -0
- package/dist/template/oxlint.config.ts +10 -14
- package/dist/template/package.json +21 -20
- package/dist/template/src/auth/AuthScreen.test.tsx +44 -0
- package/dist/template/src/auth/AuthScreen.tsx +43 -0
- package/dist/template/src/auth/session.test.tsx +64 -0
- package/dist/template/src/auth/session.ts +42 -0
- package/dist/template/src/auth/states.tsx +70 -0
- 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 +6 -11
- package/dist/template/src/{routes/-fixtures.ts → overview-fixtures.ts} +6 -18
- package/dist/template/src/{routes/-hooks.ts → overview-hooks.ts} +1 -1
- package/dist/template/src/overview-sections.tsx +4 -1
- 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.test.tsx +1 -1
- package/dist/template/src/recipes/DataGrid.tsx +7 -21
- package/dist/template/src/routes/__root.tsx +21 -0
- package/dist/template/src/routes/{-index.test.tsx → index.test.tsx} +1 -1
- package/dist/template/src/routes/index.tsx +2 -3
- package/dist/template/src/routes/signed-out.test.tsx +56 -0
- package/dist/template/src/routes/signed-out.tsx +31 -0
- package/dist/template/src/routes/tokens/$id.tsx +2 -2
- package/dist/template/src/routes/tokens/{-index.test.tsx → index.test.tsx} +2 -2
- package/dist/template/src/routes/tokens/index.tsx +2 -3
- package/dist/template/src/routes/tokens/new.tsx +2 -2
- 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/{routes/tokens/-fixtures.test.ts → tokens/fixtures.test.ts} +1 -1
- package/dist/template/src/{routes/tokens/-fixtures.ts → tokens/fixtures.ts} +1 -1
- package/dist/template/src/{routes/tokens/-hooks.ts → tokens/hooks.ts} +2 -2
- package/dist/template/src/{routes/tokens/-nav.ts → tokens/nav.ts} +1 -1
- package/dist/template/vite.config.ts +5 -0
- package/dist/template/vitest.config.ts +2 -5
- package/package.json +13 -12
- /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
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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: "
|
|
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
|
|
147
|
+
* Tracked boilerplate files the template does not carry.
|
|
129
148
|
*
|
|
130
|
-
* - `AGENTS.md`, `README.md`, `.gitignore
|
|
131
|
-
*
|
|
132
|
-
* - `
|
|
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-
|
|
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,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
|
-
* -
|
|
134
|
-
*
|
|
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}
|
|
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
|
-
* -
|
|
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
|
-
* -
|
|
203
|
-
*
|
|
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
|
-
|
|
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:
|
|
276
|
-
*
|
|
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
|
|
306
|
-
* -
|
|
307
|
-
*
|
|
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
|
|
329
|
-
*
|
|
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
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
|
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
|
-
|
|
353
|
-
|
|
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
|
|
454
|
-
*
|
|
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-
|
|
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,16 +49,13 @@ 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>");
|
|
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
|
-
|
|
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";
|