@ultimat3/cli 22.14.0 → 23.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +25 -8
- package/README.md +28 -5
- package/package.json +29 -28
- package/src/admin-catalog-keys.ts +16 -0
- package/src/admin-registration.ts +192 -0
- package/src/api-registration.ts +36 -11
- package/src/app-boundaries.ts +6 -20
- package/src/app-load.ts +55 -11
- package/src/app-manifest.ts +14 -0
- package/src/app-permissions.ts +0 -0
- package/src/app-reload-graph.ts +7 -30
- package/src/app-transport.ts +219 -0
- package/src/boundary-findings.ts +16 -4
- package/src/browser-transport.ts +160 -0
- package/src/budgets.ts +35 -3
- package/src/cmd-build-spec.ts +10 -3
- package/src/cmd-build.ts +56 -9
- package/src/cmd-db.ts +60 -11
- package/src/cmd-dev.ts +2 -2
- package/src/cmd-doctor.ts +35 -11
- package/src/cmd-generate.ts +118 -17
- package/src/cmd-jobs-spec.ts +19 -3
- package/src/cmd-jobs.ts +120 -3
- package/src/cmd-new.ts +15 -1
- package/src/cmd-routes.ts +15 -1
- package/src/cmd-secrets-ring.ts +76 -0
- package/src/cmd-secrets-spec.ts +10 -2
- package/src/cmd-secrets.ts +48 -12
- package/src/cmd-tasks.ts +84 -15
- package/src/cmd-verify.ts +10 -2
- package/src/comment-safe.ts +16 -0
- package/src/coverage-floor.ts +334 -0
- package/src/coverage-lcov.ts +193 -0
- package/src/coverage-source.ts +136 -0
- package/src/db-object-drift.ts +30 -0
- package/src/db-replay-engine.ts +115 -0
- package/src/db-schema-dump.ts +150 -0
- package/src/db-schema-refresh.ts +122 -0
- package/src/dev-dashboard.ts +2 -2
- package/src/dev-route-table.ts +30 -6
- package/src/doctor-coverage.ts +39 -0
- package/src/doctor-guards.ts +54 -0
- package/src/doctor-sealed.ts +132 -0
- package/src/document-graph.ts +82 -0
- package/src/document-options.ts +64 -0
- package/src/error-codes.ts +32 -0
- package/src/framework-schema.ts +15 -0
- package/src/generate-files.ts +76 -10
- package/src/generate-grant-findings.ts +76 -0
- package/src/generate-grants.ts +77 -3
- package/src/generate-kinds.ts +19 -0
- package/src/generate-write.ts +15 -27
- package/src/generated-imports.ts +59 -0
- package/src/guard-sources.ts +104 -0
- package/src/guards.ts +23 -7
- package/src/handle-registration.ts +217 -0
- package/src/i18n-registration.ts +36 -0
- package/src/image-prepare.ts +51 -8
- package/src/import-closure.ts +165 -0
- package/src/index.ts +11 -2
- package/src/island-store.ts +24 -16
- package/src/jobs-drain.ts +14 -7
- package/src/jobs-json.ts +50 -0
- package/src/jobs-report.ts +3 -0
- package/src/json-merge.ts +46 -0
- package/src/mcp-errors.ts +29 -1
- package/src/messages.ts +33 -0
- package/src/metrics-endpoint.ts +71 -24
- package/src/migration-extensions.ts +29 -0
- package/src/module-imports.ts +47 -0
- package/src/output.ts +18 -9
- package/src/page-speculation.ts +124 -0
- package/src/permission-grants.ts +41 -9
- package/src/prerender.ts +9 -5
- package/src/publish-closure.ts +101 -0
- package/src/role-load.ts +143 -0
- package/src/role-start.ts +18 -1
- package/src/role-wake.ts +45 -0
- package/src/runtime-admin.ts +141 -0
- package/src/runtime-hooks.ts +5 -2
- package/src/runtime-queue.ts +8 -1
- package/src/runtime-render.ts +36 -60
- package/src/runtime-services.ts +2 -1
- package/src/scaffold-typecheck.ts +1 -0
- package/src/schema-dump-drift.ts +64 -0
- package/src/serve-boot.ts +85 -147
- package/src/serve-prebuilt-paths.ts +19 -0
- package/src/serve-prebuilt.ts +84 -0
- package/src/serve-web.ts +164 -0
- package/src/serve.ts +6 -9
- package/src/server-barrels.ts +54 -0
- package/src/source-edit.ts +49 -0
- package/src/static-document.ts +108 -0
- package/src/tasks-facts.ts +4 -1
- package/src/templates/action.ts +144 -11
- package/src/templates/admin-catalog.ts +58 -0
- package/src/templates/admin-page.ts +36 -8
- package/src/templates/admin.ts +57 -17
- package/src/templates/backfill.ts +121 -87
- package/src/templates/entity.ts +140 -44
- package/src/templates/guard-animated-layout-property.ts +4 -11
- package/src/templates/guard-bare-error.ts +9 -13
- package/src/templates/guard-focus-visible.ts +4 -11
- package/src/templates/guard-image-dimensions.ts +6 -6
- package/src/templates/guard-island-without-states.ts +2 -4
- package/src/templates/guard-raw-breakpoint.ts +210 -0
- package/src/templates/guard-raw-colour.ts +4 -11
- package/src/templates/guard-raw-length.ts +375 -0
- package/src/templates/guard-raw-motion.ts +213 -0
- package/src/templates/guard-raw-shadow.ts +187 -0
- package/src/templates/guard-raw-z-index.ts +166 -0
- package/src/templates/guard-repo-raw-sql.ts +345 -0
- package/src/templates/guard-semantic-interactive.ts +6 -6
- package/src/templates/guard-undeclared-custom-property.ts +217 -0
- package/src/templates/guard-undefined-style-class.ts +215 -0
- package/src/templates/guard-untranslated-string.ts +6 -6
- package/src/templates/guard-unzoned-date.ts +9 -13
- package/src/templates/index.ts +3 -2
- package/src/templates/island.ts +34 -51
- package/src/templates/job-body-test.ts +121 -0
- package/src/templates/job.ts +23 -8
- package/src/templates/policy.ts +16 -9
- package/src/templates/query.ts +242 -26
- package/src/templates/resource-create.ts +62 -18
- package/src/templates/resource-form-island.ts +58 -80
- package/src/templates/resource-page.ts +269 -0
- package/src/templates/resource-service.ts +96 -0
- package/src/templates/resource.ts +86 -61
- package/src/templates/route.ts +38 -29
- package/src/templates/scaffold-api.ts +55 -1
- package/src/templates/scaffold-app.ts +171 -51
- package/src/templates/scaffold-auth.ts +83 -19
- package/src/templates/scaffold-browser-client.ts +90 -0
- package/src/templates/scaffold-container.ts +16 -0
- package/src/templates/scaffold-dashboard-bare.ts +64 -2
- package/src/templates/scaffold-dashboard-example.ts +124 -31
- package/src/templates/scaffold-dashboard-shared.ts +1 -20
- package/src/templates/scaffold-dashboard.ts +1 -2
- package/src/templates/scaffold-db-client.ts +142 -0
- package/src/templates/scaffold-db-package.ts +90 -8
- package/src/templates/scaffold-docs.ts +50 -5
- package/src/templates/scaffold-guards.ts +54 -11
- package/src/templates/scaffold-http.ts +9 -7
- package/src/templates/scaffold-repo.ts +52 -2
- package/src/templates/scaffold-roles.ts +56 -9
- package/src/templates/scaffold-shell.ts +36 -70
- package/src/templates/scaffold-site.ts +45 -19
- package/src/templates/scaffold-ui-package.ts +18 -0
- package/src/templates/slice-foundation.ts +46 -8
- package/src/test-counts.ts +18 -2
- package/src/transport-calls.ts +105 -0
- package/src/unscanned-admin.ts +54 -0
- package/src/verify-checks.ts +26 -7
- package/src/verify-coverage-run.ts +190 -0
- package/src/verify-deadline.ts +256 -0
- package/src/verify-errors.ts +15 -6
- package/src/verify-floor.ts +36 -9
- package/src/verify-merge.ts +117 -24
- package/src/verify-progress.ts +25 -0
- package/src/verify-role-load-probe.ts +54 -0
- package/src/verify-role-load.ts +148 -0
- package/src/verify-run.ts +93 -35
- package/src/verify-shard.ts +20 -9
- package/src/verify-step.ts +42 -1
- package/src/verify-tests.ts +78 -6
- package/src/workspace-checks.ts +50 -8
- package/src/workspace-dep-edit.ts +127 -0
- package/src/cmd-dev-fixture.ts +0 -204
- package/src/dev-live-fixture.ts +0 -122
- package/src/policy-fixture.ts +0 -78
- package/src/realtime-browser-probe-fixture.ts +0 -9
- package/src/role-start-fixture.ts +0 -78
- package/src/scaffold-fixture.ts +0 -160
- package/src/templates/island-fixture.ts +0 -76
package/src/guards.ts
CHANGED
|
@@ -11,8 +11,8 @@ import { join } from 'node:path';
|
|
|
11
11
|
import { pathToFileURL } from 'node:url';
|
|
12
12
|
import { ERROR_DOCS_URL, renderCauseValue, renderThrowable } from '@ultimat3/core';
|
|
13
13
|
import { fixProblem } from './error-contract';
|
|
14
|
+
import { type GuardSources, guardSources } from './guard-sources';
|
|
14
15
|
import type { Finding } from './output';
|
|
15
|
-
import type { HostCheck } from './verify-step';
|
|
16
16
|
|
|
17
17
|
/** The directory IS the registration. An app-side list is a list an app can forget to add to. */
|
|
18
18
|
export const GUARD_DIR = 'guards';
|
|
@@ -24,8 +24,13 @@ export interface Guard {
|
|
|
24
24
|
* The rule, over the app root. Returns findings — it never prints, never decides an exit code
|
|
25
25
|
* and never throws for a normal result, because `--json`, the step table and the exit code are
|
|
26
26
|
* all projections of what it returns (axiom 2).
|
|
27
|
+
*
|
|
28
|
+
* `sources` is the run's ONE read of the app (`guard-sources.ts`): a guard that asks it for
|
|
29
|
+
* files or a compiled stylesheet shares the walk, the read and the compile with every other
|
|
30
|
+
* guard in the directory. A guard that reads something else — a migration, a lockfile — still
|
|
31
|
+
* may, from `root`.
|
|
27
32
|
*/
|
|
28
|
-
check(root: string): Promise<readonly Finding[]> | readonly Finding[];
|
|
33
|
+
check(root: string, sources: GuardSources): Promise<readonly Finding[]> | readonly Finding[];
|
|
29
34
|
}
|
|
30
35
|
|
|
31
36
|
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
@@ -129,7 +134,11 @@ const findingInvalid = (path: string, cause: string): Finding => ({
|
|
|
129
134
|
const messageOf = (error: unknown): string => renderThrowable(error);
|
|
130
135
|
|
|
131
136
|
/** One guard: import it, run it, and hold what it returns to the contract. Never throws. */
|
|
132
|
-
async function runGuard(
|
|
137
|
+
async function runGuard(
|
|
138
|
+
root: string,
|
|
139
|
+
path: string,
|
|
140
|
+
sources: GuardSources,
|
|
141
|
+
): Promise<readonly Finding[]> {
|
|
133
142
|
let loaded: unknown;
|
|
134
143
|
try {
|
|
135
144
|
loaded = await import(pathToFileURL(join(root, path)).href);
|
|
@@ -149,7 +158,7 @@ async function runGuard(root: string, path: string): Promise<readonly Finding[]>
|
|
|
149
158
|
}
|
|
150
159
|
let returned: unknown;
|
|
151
160
|
try {
|
|
152
|
-
returned = await exported.check(root);
|
|
161
|
+
returned = await exported.check(root, sources);
|
|
153
162
|
} catch (error) {
|
|
154
163
|
return [failed(path, `${path} ("${exported.summary}") threw: ${messageOf(error)}`)];
|
|
155
164
|
}
|
|
@@ -173,14 +182,21 @@ async function runGuard(root: string, path: string): Promise<readonly Finding[]>
|
|
|
173
182
|
}
|
|
174
183
|
|
|
175
184
|
/**
|
|
176
|
-
* Every guard this app declares, as findings the step it rides on adds to its own.
|
|
185
|
+
* Every guard this app declares, as findings the step it rides on adds to its own. Shaped as a
|
|
177
186
|
* `HostCheck` because that is exactly the seam's shape — a rule the repo enforces on itself,
|
|
178
187
|
* contributed to a step that already exists. A guard can never add, remove, reorder or skip a
|
|
179
188
|
* step, which is what keeps "green" meaning one thing (axiom 5) while the app still gets to make
|
|
180
189
|
* its own convention a build error.
|
|
181
190
|
*/
|
|
182
|
-
export const guardFindings
|
|
191
|
+
export const guardFindings = async (
|
|
192
|
+
root: string,
|
|
193
|
+
/** Files the step already read (`guardSources`' seed), so no guard reads one a second time. */
|
|
194
|
+
seed: readonly { readonly path: string; readonly source: string }[] = [],
|
|
195
|
+
): Promise<readonly Finding[]> => {
|
|
183
196
|
const findings: Finding[] = [];
|
|
184
|
-
|
|
197
|
+
const sources = guardSources(root, seed);
|
|
198
|
+
for (const path of await guardPaths(root)) {
|
|
199
|
+
findings.push(...(await runGuard(root, path, sources)));
|
|
200
|
+
}
|
|
185
201
|
return findings;
|
|
186
202
|
};
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// `x g entity` / `x g resource` hand the entity they wrote to the app's typed database handle. The
|
|
2
|
+
// generated `repo.ts` reads `db.<table>`, and a table is on the handle only when its entity is in
|
|
3
|
+
// the set `packages/db/src/client.ts` passes to `database()` — an edit nothing made, so the repo
|
|
4
|
+
// did not compile. Performed here; where the file is not the scaffold's shape, the finding names
|
|
5
|
+
// the two lines.
|
|
6
|
+
|
|
7
|
+
import { ERROR_DOCS_URL, stripComments } from '@ultimat3/core';
|
|
8
|
+
import { containedPath } from './generate-write';
|
|
9
|
+
import type { Finding } from './output';
|
|
10
|
+
import { holdsKey, withKeyedLine, withSortedImport } from './source-edit';
|
|
11
|
+
import type { GeneratedFile } from './templates/naming';
|
|
12
|
+
import { plural } from './templates/naming';
|
|
13
|
+
import type { HandleEntry } from './templates/scaffold-db-client';
|
|
14
|
+
import {
|
|
15
|
+
dbClientSource,
|
|
16
|
+
HANDLE_FILE,
|
|
17
|
+
handleEntryLine,
|
|
18
|
+
handleImportLine,
|
|
19
|
+
PLACEHOLDER_DB_MODULE,
|
|
20
|
+
webModuleOf,
|
|
21
|
+
} from './templates/scaffold-db-client';
|
|
22
|
+
import type { PlannedEdit } from './workspace-dep-edit';
|
|
23
|
+
import { planWorkspaceDependency, readWorkspace } from './workspace-dep-edit';
|
|
24
|
+
|
|
25
|
+
/** The two workspaces the handle joins: it lives in one and imports entities from the other. */
|
|
26
|
+
export const DB_WORKSPACE = 'packages/db';
|
|
27
|
+
const WEB_WORKSPACE = 'apps/web';
|
|
28
|
+
const INDEX_FILE = `${DB_WORKSPACE}/src/index.ts`;
|
|
29
|
+
|
|
30
|
+
const ENTITY_PATH = /^apps\/web\/(?<rest>[^/]+\/[^/]+)\/entity\.ts$/;
|
|
31
|
+
const ENTITY_EXPORT = /\bexport const ([A-Za-z_$][\w$]*) = entity\(/;
|
|
32
|
+
const IDENTIFIER = '[A-Za-z_$][\\w$]*';
|
|
33
|
+
|
|
34
|
+
/** The `@<app>/db` a generated `repo.ts` imports the handle from; absent when the app names none. */
|
|
35
|
+
export const resolveDbModule = async (root: string): Promise<string | undefined> =>
|
|
36
|
+
(await readWorkspace(root, DB_WORKSPACE))?.name;
|
|
37
|
+
|
|
38
|
+
/** The finding for an entity the handle does not serve, carrying the edit that makes it. */
|
|
39
|
+
export const handleUnregisteredFinding = (entry: HandleEntry, reason: string): Finding => ({
|
|
40
|
+
code: 'X_DB_HANDLE_UNREGISTERED',
|
|
41
|
+
cause: `${HANDLE_FILE} ${reason}, so db.${entry.key} does not exist and the generated repo.ts does not compile`,
|
|
42
|
+
fix: `add ${handleImportLine(entry)} and the line "${handleEntryLine(entry).trim()}" to the entity set passed to database() in ${HANDLE_FILE}`,
|
|
43
|
+
docs: ERROR_DOCS_URL,
|
|
44
|
+
at: HANDLE_FILE,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
/** The object literal `database(<name>, …)` is called with: where it opens and where it closes. */
|
|
48
|
+
function entitySetOf(
|
|
49
|
+
source: string,
|
|
50
|
+
): { readonly open: number; readonly close: number } | undefined {
|
|
51
|
+
const call = new RegExp(`\\bdatabase\\(\\s*(${IDENTIFIER})\\s*[,)]`).exec(source);
|
|
52
|
+
if (call === null) return undefined;
|
|
53
|
+
const declared = new RegExp(`\\bconst ${call[1]}\\s*=\\s*\\{`).exec(source);
|
|
54
|
+
if (declared === null) return undefined;
|
|
55
|
+
const open = declared.index + declared[0].length;
|
|
56
|
+
const close = source.indexOf('}', open);
|
|
57
|
+
return close === -1 ? undefined : { open, close };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* `source` with each entry imported and listed. An entry whose key the set already holds is left
|
|
62
|
+
* alone; a file with no `const <set> = { … }` handed to `database()` is returned untouched with
|
|
63
|
+
* every entry in `missing`.
|
|
64
|
+
*/
|
|
65
|
+
export function insertHandleEntries(
|
|
66
|
+
source: string,
|
|
67
|
+
entries: readonly HandleEntry[],
|
|
68
|
+
): { readonly source: string; readonly missing: readonly HandleEntry[] } {
|
|
69
|
+
let next = source;
|
|
70
|
+
const missing: HandleEntry[] = [];
|
|
71
|
+
for (const entry of entries) {
|
|
72
|
+
const set = entitySetOf(next);
|
|
73
|
+
if (set === undefined) {
|
|
74
|
+
missing.push(entry);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const body = next.slice(set.open, set.close);
|
|
78
|
+
if (holdsKey(stripComments(body), entry.key)) continue;
|
|
79
|
+
const listed = withKeyedLine(body, entry.key, handleEntryLine(entry), ' ');
|
|
80
|
+
next = withSortedImport(
|
|
81
|
+
`${next.slice(0, set.open)}${listed}${next.slice(set.close)}`,
|
|
82
|
+
handleImportLine(entry),
|
|
83
|
+
entry.specifier,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
return { source: next, missing };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The entry an `entity.ts` implies, named by what the file exports; `undefined` for any other path. */
|
|
90
|
+
function entryOf(path: string, source: string, webModule: string): HandleEntry | undefined {
|
|
91
|
+
const rest = ENTITY_PATH.exec(path)?.groups?.['rest'];
|
|
92
|
+
const binding = ENTITY_EXPORT.exec(source)?.[1];
|
|
93
|
+
if (rest === undefined || binding === undefined) return undefined;
|
|
94
|
+
return { key: plural(binding), binding, specifier: `${webModule}/${rest}/entity` };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The same registration over a file LIST, for a caller that composes generators without a disk —
|
|
99
|
+
* the scaffold fixture runs every generator on top of `x new` and compiles the result, and a
|
|
100
|
+
* handle that listed only the example entity would fail that compile on every other repo.
|
|
101
|
+
*/
|
|
102
|
+
export function withHandleEntries(
|
|
103
|
+
files: readonly GeneratedFile[],
|
|
104
|
+
dbModule: string,
|
|
105
|
+
): readonly GeneratedFile[] {
|
|
106
|
+
const entries = files.flatMap((file) => {
|
|
107
|
+
if (typeof file.contents !== 'string') return [];
|
|
108
|
+
const entry = entryOf(file.path, file.contents, webModuleOf(dbModule));
|
|
109
|
+
return entry === undefined ? [] : [entry];
|
|
110
|
+
});
|
|
111
|
+
return files.map((file) =>
|
|
112
|
+
file.path === HANDLE_FILE && typeof file.contents === 'string' && file.merge === undefined
|
|
113
|
+
? { ...file, contents: insertHandleEntries(file.contents, entries).source }
|
|
114
|
+
: file,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** The entries the written files imply: one per `entity.ts` this run put on disk. */
|
|
119
|
+
async function entriesFor(
|
|
120
|
+
root: string,
|
|
121
|
+
written: readonly string[],
|
|
122
|
+
webModule: string,
|
|
123
|
+
): Promise<readonly HandleEntry[]> {
|
|
124
|
+
const entries: HandleEntry[] = [];
|
|
125
|
+
for (const path of written) {
|
|
126
|
+
if (!ENTITY_PATH.test(path)) continue;
|
|
127
|
+
const entry = entryOf(path, await Bun.file(containedPath(root, path)).text(), webModule);
|
|
128
|
+
if (entry !== undefined) entries.push(entry);
|
|
129
|
+
}
|
|
130
|
+
return entries;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface HandleRegistration {
|
|
134
|
+
/** App-root-relative paths this rewrote or created. */
|
|
135
|
+
readonly edited: readonly string[];
|
|
136
|
+
readonly findings: readonly Finding[];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** The handle exists but the package does not export it: the repo's import reaches something else. */
|
|
140
|
+
async function indexFindings(root: string, dbModule: string): Promise<readonly Finding[]> {
|
|
141
|
+
const file = Bun.file(containedPath(root, INDEX_FILE));
|
|
142
|
+
const source = (await file.exists()) ? await file.text() : '';
|
|
143
|
+
if (/from\s+'\.\/client'/.test(stripComments(source))) return [];
|
|
144
|
+
return [
|
|
145
|
+
{
|
|
146
|
+
code: 'X_DB_HANDLE_UNREGISTERED',
|
|
147
|
+
cause: `${INDEX_FILE} does not export the typed handle, so "import { db } from '${dbModule}'" in the generated repo.ts is not database()'s`,
|
|
148
|
+
fix: `add export { db, driver, selectDriver } from './client'; to ${INDEX_FILE} and drop db from its '@ultimat3/db' re-export`,
|
|
149
|
+
docs: ERROR_DOCS_URL,
|
|
150
|
+
at: INDEX_FILE,
|
|
151
|
+
},
|
|
152
|
+
];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Performs the registration for every entity among `written`: the handle's set and import, then
|
|
157
|
+
* the manifest lines the new edges need. Creates the handle in an app that has none.
|
|
158
|
+
*
|
|
159
|
+
* PLANNED, then written — all of it or none. A handle with no `const <set> = { … }` refuses the
|
|
160
|
+
* entry, and a run that then went on to edit `packages/db/package.json` left a manifest line for
|
|
161
|
+
* an import nobody wrote: `packages/db` depending on the app, behind a finding that said nothing
|
|
162
|
+
* was registered.
|
|
163
|
+
*
|
|
164
|
+
* And the edge from the db package to the app is declared only for the import that needs it. The
|
|
165
|
+
* handle imports each entity from the web workspace, so that edge is real exactly when the handle
|
|
166
|
+
* this run leaves names that workspace; the other one — the app's `repo.ts` importing the handle —
|
|
167
|
+
* is every entity's.
|
|
168
|
+
*/
|
|
169
|
+
export async function registerGeneratedEntities(
|
|
170
|
+
root: string,
|
|
171
|
+
written: readonly string[],
|
|
172
|
+
dbModule: string | undefined,
|
|
173
|
+
): Promise<HandleRegistration> {
|
|
174
|
+
const web = await readWorkspace(root, WEB_WORKSPACE);
|
|
175
|
+
const module = dbModule ?? PLACEHOLDER_DB_MODULE;
|
|
176
|
+
const webModule = web?.name ?? webModuleOf(module);
|
|
177
|
+
const entries = await entriesFor(root, written, webModule);
|
|
178
|
+
if (entries.length === 0) return { edited: [], findings: [] };
|
|
179
|
+
const planned: PlannedEdit[] = [];
|
|
180
|
+
const file = Bun.file(containedPath(root, HANDLE_FILE));
|
|
181
|
+
const before = (await file.exists()) ? await file.text() : undefined;
|
|
182
|
+
const { source, missing } =
|
|
183
|
+
before === undefined
|
|
184
|
+
? { source: dbClientSource(entries), missing: [] }
|
|
185
|
+
: insertHandleEntries(before, entries);
|
|
186
|
+
if (missing.length > 0) {
|
|
187
|
+
// Refused: nothing this registrar would have written is written.
|
|
188
|
+
return {
|
|
189
|
+
edited: [],
|
|
190
|
+
findings: missing.map((entry) =>
|
|
191
|
+
handleUnregisteredFinding(entry, 'passes database() no "const <set> = { … }" to add to'),
|
|
192
|
+
),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
if (source !== before) planned.push({ path: HANDLE_FILE, contents: source });
|
|
196
|
+
const findings: Finding[] = [];
|
|
197
|
+
if (dbModule === undefined) {
|
|
198
|
+
findings.push({
|
|
199
|
+
code: 'X_DB_HANDLE_UNREGISTERED',
|
|
200
|
+
cause: `${DB_WORKSPACE}/package.json supplies no "name", so the generated repo.ts imports the placeholder ${PLACEHOLDER_DB_MODULE}`,
|
|
201
|
+
fix: `add "name": "@<app>/db" to ${DB_WORKSPACE}/package.json, then re-run this command with --force`,
|
|
202
|
+
docs: ERROR_DOCS_URL,
|
|
203
|
+
at: `${DB_WORKSPACE}/package.json`,
|
|
204
|
+
});
|
|
205
|
+
} else findings.push(...(await indexFindings(root, dbModule)));
|
|
206
|
+
const importsWeb = stripComments(source).includes(`from '${webModule}/`);
|
|
207
|
+
const edges = [
|
|
208
|
+
...(importsWeb ? [[DB_WORKSPACE, WEB_WORKSPACE] as const] : []),
|
|
209
|
+
[WEB_WORKSPACE, DB_WORKSPACE] as const,
|
|
210
|
+
];
|
|
211
|
+
for (const [from, to] of edges) {
|
|
212
|
+
const manifest = await planWorkspaceDependency(root, from, to);
|
|
213
|
+
if (manifest !== undefined) planned.push(manifest);
|
|
214
|
+
}
|
|
215
|
+
for (const edit of planned) await Bun.write(containedPath(root, edit.path), edit.contents);
|
|
216
|
+
return { edited: planned.map((edit) => edit.path), findings };
|
|
217
|
+
}
|
package/src/i18n-registration.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
pluralVariantsOf,
|
|
19
19
|
registeredLocales,
|
|
20
20
|
} from '@ultimat3/i18n';
|
|
21
|
+
import { mountedAdminKeys } from './admin-catalog-keys';
|
|
21
22
|
import { loadApp } from './app-load';
|
|
22
23
|
import type { DuplicateProbe } from './duplicate-packages';
|
|
23
24
|
import { duplicateCause, duplicateFinding, findDuplicateInstalls } from './duplicate-packages';
|
|
@@ -50,6 +51,8 @@ export interface RegistrationInput {
|
|
|
50
51
|
readonly load?: AppLoader;
|
|
51
52
|
/** The duplicate-install probe; `findDuplicateInstalls` is the production value. */
|
|
52
53
|
readonly duplicates?: DuplicateProbe;
|
|
54
|
+
/** Keys a mounted admin derives, read after the load; `mountedAdminKeys` is the production value. */
|
|
55
|
+
readonly adminKeys?: () => Promise<readonly string[]>;
|
|
53
56
|
}
|
|
54
57
|
|
|
55
58
|
export interface RegistrationReport {
|
|
@@ -126,6 +129,18 @@ export async function checkRegistration(input: RegistrationInput): Promise<Regis
|
|
|
126
129
|
}
|
|
127
130
|
}
|
|
128
131
|
|
|
132
|
+
// A key `defineAdmin` derives is spelled in no source file, so the source audit cannot see it.
|
|
133
|
+
// Judged against the REGISTRY — the framework's base layer answers `admin.group.data` in `en` —
|
|
134
|
+
// and only for a locale that registered: an unregistered one is already a finding above.
|
|
135
|
+
const unregisteredSet = new Set(gaps.map((gap) => gap.locale));
|
|
136
|
+
const adminKeys = await (input.adminKeys ?? mountedAdminKeys)();
|
|
137
|
+
for (const locale of Object.keys(input.catalogs)
|
|
138
|
+
.filter((tag) => !unregisteredSet.has(tag))
|
|
139
|
+
.sort()) {
|
|
140
|
+
const missing = unanswered(catalogFor(locale), adminKeys);
|
|
141
|
+
if (missing.length > 0) findings.push(adminKeysFinding(locale, missing));
|
|
142
|
+
}
|
|
143
|
+
|
|
129
144
|
return {
|
|
130
145
|
ok: findings.length === 0,
|
|
131
146
|
// The load's own findings ride along ONLY when something is unregistered, and that condition is
|
|
@@ -140,6 +155,27 @@ export async function checkRegistration(input: RegistrationInput): Promise<Regis
|
|
|
140
155
|
};
|
|
141
156
|
}
|
|
142
157
|
|
|
158
|
+
/** Keys the catalog does not define, or defines with a placeholder. Labels are never plurals. */
|
|
159
|
+
const unanswered = (catalog: Catalog, keys: readonly string[]): readonly string[] =>
|
|
160
|
+
keys.filter((key) => !Object.hasOwn(catalog, key) || isLoudMiss(catalog[key]));
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* `X_CATALOG_MISSING_KEYS` — the same fact, a catalog without keys it is asked for — with the
|
|
164
|
+
* cause naming who asks and a fix that is an EDIT: `x i18n sync`, the code's own fix, seeds the
|
|
165
|
+
* keys SOURCE uses and cannot know these.
|
|
166
|
+
*/
|
|
167
|
+
function adminKeysFinding(locale: Locale, missing: readonly string[]): Finding {
|
|
168
|
+
const path = catalogPath(locale);
|
|
169
|
+
const shown = missing.slice(0, 12).map((key) => `"${key}"`);
|
|
170
|
+
const more = missing.length > shown.length ? `, +${missing.length - shown.length} more` : '';
|
|
171
|
+
return {
|
|
172
|
+
...findingFrom(catalogMissingKeys(locale, missing)),
|
|
173
|
+
cause: `${path} is missing ${missing.length} key(s) the mounted admin renders (defineAdmin derives them, so no source file names one): ${shown.join(', ')}${more}`,
|
|
174
|
+
fix: `add to ${path}, each with its real label: ${shown.join(', ')}${more} — then x i18n check --json`,
|
|
175
|
+
at: path,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
143
179
|
/**
|
|
144
180
|
* `packages/i18n/src/index.ts` as text, or `undefined` where the app has no i18n package. Read
|
|
145
181
|
* here and nowhere lower down: `@ultimat3/i18n` states that it never reads a file, and the CLI is
|
package/src/image-prepare.ts
CHANGED
|
@@ -1,14 +1,57 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// The two halves of an image build that are this framework's. On the host, `x build --target
|
|
2
|
+
// docker` stamps the image with the manifest's build id. INSIDE the image build, `x build --target
|
|
3
|
+
// prebuilt` writes what every web pod would otherwise make on every boot: the island chunks and
|
|
4
|
+
// the compiled stylesheets (`serve-prebuilt-paths.ts` names the store and why it is where it is).
|
|
4
5
|
|
|
6
|
+
// why: Bun ships no recursive remove; a previous store must not survive beside the new one.
|
|
7
|
+
import { rm } from 'node:fs/promises';
|
|
8
|
+
// why: Bun ships no path-join primitive.
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { sassCompilations, setSassCacheDir } from '@ultimat3/render/server';
|
|
11
|
+
import { scanAppModules } from './app-load';
|
|
5
12
|
import { appManifest } from './app-manifest';
|
|
6
13
|
import { buildIslands } from './island-bundle';
|
|
7
14
|
import { writeIslandStore } from './island-store';
|
|
15
|
+
import type { Finding } from './output';
|
|
16
|
+
import { PREBUILT_DIR, PREBUILT_SASS_DIR } from './serve-prebuilt-paths';
|
|
8
17
|
|
|
9
|
-
/**
|
|
10
|
-
export async function
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
18
|
+
/** The build id `docker build` is handed as `--build-arg BUILD_ID=…`. */
|
|
19
|
+
export async function imageBuildId(root: string): Promise<string> {
|
|
20
|
+
return (await appManifest(root)).manifest.buildId;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface PrebuiltImage {
|
|
24
|
+
/** App-root-relative, POSIX. */
|
|
25
|
+
readonly dir: string;
|
|
26
|
+
/** Island chunks written, shared chunks included. */
|
|
27
|
+
readonly islands: number;
|
|
28
|
+
/** Stylesheets compiled into the store. */
|
|
29
|
+
readonly stylesheets: number;
|
|
30
|
+
/** A module that would not import: its stylesheets are not in the store. */
|
|
31
|
+
readonly findings: readonly Finding[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Where it runs is the design. The store is valid for one Bun, one framework version and one set
|
|
36
|
+
* of absolute paths — a chunk is verified against the runtime that built it, and a Sass entry is
|
|
37
|
+
* keyed by the sheet's path — so it is written by the image's own Bun, at the image's own
|
|
38
|
+
* `WORKDIR`, after the source is in place. Built on a laptop and copied in, it was a store for a
|
|
39
|
+
* different runtime; and under `.x/` it was dropped by the ignore file before that mattered.
|
|
40
|
+
*
|
|
41
|
+
* The app is imported the way the web role imports it (`scanAppModules`, every module), because
|
|
42
|
+
* importing a page is what compiles its stylesheet: what this scan compiles is what a pod reads.
|
|
43
|
+
*/
|
|
44
|
+
export async function prebuildImage(root: string): Promise<PrebuiltImage> {
|
|
45
|
+
await rm(join(root, PREBUILT_DIR), { recursive: true, force: true });
|
|
46
|
+
setSassCacheDir(join(root, PREBUILT_SASS_DIR));
|
|
47
|
+
const compiled = sassCompilations();
|
|
48
|
+
const scan = await scanAppModules(root, { track: false });
|
|
49
|
+
const bundle = await buildIslands(root);
|
|
50
|
+
await writeIslandStore(root, bundle);
|
|
51
|
+
return {
|
|
52
|
+
dir: PREBUILT_DIR,
|
|
53
|
+
islands: bundle.chunks.length + bundle.shared.length,
|
|
54
|
+
stylesheets: sassCompilations() - compiled,
|
|
55
|
+
findings: scan.findings,
|
|
56
|
+
};
|
|
14
57
|
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// The modules a set of entry files can EXECUTE, followed name by name through barrels. A package
|
|
2
|
+
// barrel re-exports its whole surface by name (the house forbids `export *`), so a closure taken
|
|
3
|
+
// module by module reaches every server file of every package an island touches — `@ultimat3/action`'s
|
|
4
|
+
// barrel reaches `http.ts` — while a bundler with honest `sideEffects` keeps only the modules the
|
|
5
|
+
// imported NAMES live in. This follows the names, which is what makes "browser-reachable" a
|
|
6
|
+
// question with a useful answer instead of "everything".
|
|
7
|
+
//
|
|
8
|
+
// Pure over an injected reader, so a test's fixture tree is a Map rather than files on disk.
|
|
9
|
+
|
|
10
|
+
import { stripComments } from '@ultimat3/core';
|
|
11
|
+
|
|
12
|
+
/** What a module asks of another: named bindings, or the whole module (`*`). */
|
|
13
|
+
export type Wanted = '*' | ReadonlySet<string>;
|
|
14
|
+
|
|
15
|
+
export interface ModuleShape {
|
|
16
|
+
/** `import … from`, `import('x')` — followed only when this module's body runs. */
|
|
17
|
+
readonly imports: readonly { readonly spec: string; readonly names: Wanted }[];
|
|
18
|
+
/** `export { local as exported } from 'x'` — followed only for the names somebody wants. */
|
|
19
|
+
readonly reexports: readonly {
|
|
20
|
+
readonly spec: string;
|
|
21
|
+
readonly map: ReadonlyMap<string, string>;
|
|
22
|
+
}[];
|
|
23
|
+
/** `export * from 'x'` — searched for a wanted name this module does not name itself. */
|
|
24
|
+
readonly stars: readonly string[];
|
|
25
|
+
/** `import 'x'` — runs whenever this module is touched at all, which is what a barrel's is for. */
|
|
26
|
+
readonly effects: readonly string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ClosureHost {
|
|
30
|
+
/** The file's text, or `undefined` when there is no such file. Paths are root-relative POSIX. */
|
|
31
|
+
read(path: string): string | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* A bare specifier's file (`@ultimat3/core` → `packages/core/src/index.ts`), or `undefined` —
|
|
34
|
+
* which ENDS the walk there: an installed package is past the boundary this closure reads.
|
|
35
|
+
*/
|
|
36
|
+
alias(spec: string, from: string): string | undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const IMPORT_FROM = /\bimport\s+(?!type\s)([^;'"]*?)\s*from\s*(['"])([^'"]+)\2/g;
|
|
40
|
+
const IMPORT_BARE = /\bimport\s*(['"])([^'"]+)\1/g;
|
|
41
|
+
const IMPORT_DYNAMIC = /\bimport\s*\(\s*(['"])([^'"]+)\1\s*\)/g;
|
|
42
|
+
const EXPORT_FROM = /\bexport\s+(?!type\s)\{([^}]*)\}\s*from\s*(['"])([^'"]+)\2/g;
|
|
43
|
+
const EXPORT_STAR = /\bexport\s+\*\s*(?:as\s+[\w$]+\s*)?from\s*(['"])([^'"]+)\1/g;
|
|
44
|
+
|
|
45
|
+
/** `a, type b, c as d` → the value names, `type` members dropped. */
|
|
46
|
+
const members = (list: string): readonly (readonly [string, string])[] =>
|
|
47
|
+
list
|
|
48
|
+
.split(',')
|
|
49
|
+
.map((part) => part.trim())
|
|
50
|
+
.filter((part) => part.length > 0 && !part.startsWith('type '))
|
|
51
|
+
.map((part) => {
|
|
52
|
+
const [local = part, exported = local] = part.split(/\s+as\s+/);
|
|
53
|
+
return [local.trim(), exported.trim()] as const;
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
/** The names an import clause binds: `default`, the braced members, or `*` for a namespace. */
|
|
57
|
+
function clauseNames(clause: string): Wanted {
|
|
58
|
+
if (/\*\s*as\s/.test(clause)) return '*';
|
|
59
|
+
const names = new Set<string>();
|
|
60
|
+
const braced = /\{([^}]*)\}/.exec(clause);
|
|
61
|
+
for (const [local] of members(braced?.[1] ?? '')) names.add(local);
|
|
62
|
+
const head = clause
|
|
63
|
+
.replace(/\{[^}]*\}/, '')
|
|
64
|
+
.replace(/,/g, ' ')
|
|
65
|
+
.trim();
|
|
66
|
+
if (head.length > 0) names.add('default');
|
|
67
|
+
return names;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function parseModule(source: string): ModuleShape {
|
|
71
|
+
const code = stripComments(source);
|
|
72
|
+
const imports: { spec: string; names: Wanted }[] = [];
|
|
73
|
+
for (const found of code.matchAll(IMPORT_FROM)) {
|
|
74
|
+
const names = clauseNames(found[1] ?? '');
|
|
75
|
+
// `import type { A }` is caught above; `import { type A }` alone binds nothing at runtime.
|
|
76
|
+
if (names !== '*' && names.size === 0) continue;
|
|
77
|
+
imports.push({ spec: found[3] ?? '', names });
|
|
78
|
+
}
|
|
79
|
+
const effects = [...code.matchAll(IMPORT_BARE)].map((found) => found[2] ?? '');
|
|
80
|
+
for (const found of code.matchAll(IMPORT_DYNAMIC)) {
|
|
81
|
+
imports.push({ spec: found[2] ?? '', names: '*' });
|
|
82
|
+
}
|
|
83
|
+
const reexports = [...code.matchAll(EXPORT_FROM)].map((found) => ({
|
|
84
|
+
spec: found[3] ?? '',
|
|
85
|
+
map: new Map(members(found[1] ?? '').map(([local, exported]) => [exported, local])),
|
|
86
|
+
}));
|
|
87
|
+
const stars = [...code.matchAll(EXPORT_STAR)].map((found) => found[2] ?? '');
|
|
88
|
+
return { imports, reexports, stars, effects };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const CANDIDATES = ['', '.ts', '.tsx', '/index.ts', '/index.tsx'];
|
|
92
|
+
|
|
93
|
+
/** A relative specifier against the importing file, extension-less as TypeScript writes it. */
|
|
94
|
+
function resolveRelative(host: ClosureHost, from: string, spec: string): string | undefined {
|
|
95
|
+
const base = from.split('/').slice(0, -1);
|
|
96
|
+
for (const part of spec.split('/')) {
|
|
97
|
+
if (part === '.' || part === '') continue;
|
|
98
|
+
if (part === '..') base.pop();
|
|
99
|
+
else base.push(part);
|
|
100
|
+
}
|
|
101
|
+
const joined = base.join('/').replace(/\.js$/, '');
|
|
102
|
+
for (const suffix of CANDIDATES) {
|
|
103
|
+
const path = `${joined}${suffix}`;
|
|
104
|
+
if (/\.tsx?$/.test(path) && host.read(path) !== undefined) return path;
|
|
105
|
+
}
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function resolveSpec(host: ClosureHost, from: string, spec: string): string | undefined {
|
|
110
|
+
return spec.startsWith('.') ? resolveRelative(host, from, spec) : host.alias(spec, from);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Every module whose BODY an entry set can run. A module reached only for names it re-exports is
|
|
115
|
+
* passed through, not included: its own statements are the re-export lines and nothing else.
|
|
116
|
+
*/
|
|
117
|
+
export function importClosure(host: ClosureHost, entries: readonly string[]): readonly string[] {
|
|
118
|
+
const shapes = new Map<string, ModuleShape>();
|
|
119
|
+
const bodies = new Set<string>();
|
|
120
|
+
const asked = new Map<string, Set<string>>();
|
|
121
|
+
const queue: { path: string; names: Wanted }[] = entries.map((path) => ({ path, names: '*' }));
|
|
122
|
+
|
|
123
|
+
const shapeOf = (path: string): ModuleShape | undefined => {
|
|
124
|
+
const known = shapes.get(path);
|
|
125
|
+
if (known !== undefined) return known;
|
|
126
|
+
const source = host.read(path);
|
|
127
|
+
if (source === undefined) return undefined;
|
|
128
|
+
const shape = parseModule(source);
|
|
129
|
+
shapes.set(path, shape);
|
|
130
|
+
return shape;
|
|
131
|
+
};
|
|
132
|
+
const follow = (from: string, spec: string, names: Wanted): void => {
|
|
133
|
+
const path = resolveSpec(host, from, spec);
|
|
134
|
+
if (path !== undefined) queue.push({ path, names });
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
for (let next = queue.shift(); next !== undefined; next = queue.shift()) {
|
|
138
|
+
const { path, names } = next;
|
|
139
|
+
const shape = shapeOf(path);
|
|
140
|
+
if (shape === undefined) continue;
|
|
141
|
+
let needsBody = names === '*';
|
|
142
|
+
const seen = asked.get(path) ?? new Set<string>();
|
|
143
|
+
if (!asked.has(path)) for (const effect of shape.effects) follow(path, effect, '*');
|
|
144
|
+
asked.set(path, seen);
|
|
145
|
+
for (const name of names === '*' ? [] : names) {
|
|
146
|
+
if (seen.has(name)) continue;
|
|
147
|
+
seen.add(name);
|
|
148
|
+
const source = shape.reexports.find((entry) => entry.map.has(name));
|
|
149
|
+
if (source !== undefined) {
|
|
150
|
+
follow(path, source.spec, new Set([source.map.get(name) ?? name]));
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
needsBody = true;
|
|
154
|
+
for (const star of shape.stars) follow(path, star, new Set([name]));
|
|
155
|
+
}
|
|
156
|
+
if (names === '*') {
|
|
157
|
+
for (const entry of shape.reexports) follow(path, entry.spec, new Set(entry.map.values()));
|
|
158
|
+
for (const star of shape.stars) follow(path, star, '*');
|
|
159
|
+
}
|
|
160
|
+
if (!needsBody || bodies.has(path)) continue;
|
|
161
|
+
bodies.add(path);
|
|
162
|
+
for (const entry of shape.imports) follow(path, entry.spec, entry.names);
|
|
163
|
+
}
|
|
164
|
+
return [...bodies].sort();
|
|
165
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -8,11 +8,10 @@
|
|
|
8
8
|
import './error-codes';
|
|
9
9
|
|
|
10
10
|
/** The app's own API over HTTP — the one table `x dev` and a container both mount. */
|
|
11
|
-
export { apiRoutes } from './api-routes';
|
|
11
|
+
export { apiMountRoutes, apiRoutes } from './api-routes';
|
|
12
12
|
export type { SourceFile } from './app-boundaries';
|
|
13
13
|
export {
|
|
14
14
|
appImportGraph,
|
|
15
|
-
checkAppBoundaries,
|
|
16
15
|
readAppSources,
|
|
17
16
|
resolveSpecifier,
|
|
18
17
|
scanRuntimeImports,
|
|
@@ -46,6 +45,11 @@ export type { BranchRow } from './db-branch';
|
|
|
46
45
|
export { BRANCH_SUBCOMMANDS, branchDatabaseName, branchNameOf } from './db-branch';
|
|
47
46
|
export type { GeneratedFiles } from './db-generate';
|
|
48
47
|
export { generateAppMigration } from './db-generate';
|
|
48
|
+
// The schema dump's two verbs for a caller that holds an app root and no command context —
|
|
49
|
+
// `scripts/schema-dumps.ts` regenerates and checks every tracked app's through them.
|
|
50
|
+
export { SCHEMA_DUMP_DIR } from './db-schema-dump';
|
|
51
|
+
export type { SchemaDumpRefresh } from './db-schema-refresh';
|
|
52
|
+
export { refreshSchemaDump } from './db-schema-refresh';
|
|
49
53
|
export { QuerySubscribesUnknownError, replicaIdentityTables } from './db-subscribes';
|
|
50
54
|
export { dispatch } from './dispatch';
|
|
51
55
|
export { checkSourceDrift, reconcileSchemaHash, writeSchemaHash } from './drift';
|
|
@@ -99,6 +103,10 @@ export { checkFlagReads } from './flag-reads';
|
|
|
99
103
|
// The framework's own tables, as data. Exported so `scripts/` can read the applier's list without
|
|
100
104
|
// re-deriving it — the shape a ratchet over declared-but-never-applied DDL needs.
|
|
101
105
|
export { FRAMEWORK_SCHEMA } from './framework-schema';
|
|
106
|
+
// What a guard's `check(root, sources)` is handed: the run's one read of the app. Types an app's
|
|
107
|
+
// `guards/*.ts` names, and the constructor a test of one needs.
|
|
108
|
+
export type { GuardFile, GuardSources, GuardStylesheet } from './guard-sources';
|
|
109
|
+
export { guardSources } from './guard-sources';
|
|
102
110
|
export type { Guard } from './guards';
|
|
103
111
|
export { findingProblem, guardFindings, guardPaths } from './guards';
|
|
104
112
|
export { ICON_BASE_PATH, ICON_SOURCE } from './icon-assets';
|
|
@@ -134,6 +142,7 @@ export type { RunningServices } from './runtime-services';
|
|
|
134
142
|
export { startServices } from './runtime-services';
|
|
135
143
|
// The drift a hash cannot see, and the composition both the gate step and `x doctor` read.
|
|
136
144
|
export { checkMigrationDrift, checkSnapshotDrift } from './schema-drift';
|
|
145
|
+
export { checkSchemaDump } from './schema-dump-drift';
|
|
137
146
|
export { FrameworkSchemaFailedError } from './schema-errors';
|
|
138
147
|
export type { MigratedApp, ServeOptions } from './serve';
|
|
139
148
|
export {
|