@starci/hfs 5.0.7 → 5.1.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/CHANGELOG.md +21 -0
- package/README.md +7 -8
- package/cache/fake-session.mjs +1 -1
- package/cache/hfs-family.mjs +22 -9
- package/cache/import-graph.d.mts +32 -4
- package/cache/import-graph.mjs +88 -11
- package/cache/lint-family.d.mts +10 -3
- package/cache/lint-family.mjs +65 -37
- package/cache/records.d.mts +7 -0
- package/cache/records.mjs +17 -1
- package/cache/run.d.mts +1 -1
- package/cache/session.mjs +6 -2
- package/cache/side-key.d.mts +2 -0
- package/cache/side-key.mjs +5 -1
- package/cache/structure-memo.d.mts +34 -0
- package/cache/structure-memo.mjs +150 -0
- package/cache/types.d.mts +6 -2
- package/cache/wiring-family.mjs +1 -1
- package/codemod/directive-first.d.mts +48 -0
- package/codemod/directive-first.mjs +136 -0
- package/codemod/explicit-surface-edit.d.mts +14 -0
- package/codemod/explicit-surface-edit.mjs +57 -0
- package/codemod/explicit-surface-names.d.mts +30 -0
- package/codemod/explicit-surface-names.mjs +135 -0
- package/codemod/explicit-surface-render.d.mts +29 -0
- package/codemod/explicit-surface-render.mjs +197 -0
- package/codemod/explicit-surface.d.mts +50 -0
- package/codemod/explicit-surface.mjs +374 -0
- package/codemod/main.d.mts +7 -5
- package/codemod/main.mjs +37 -16
- package/codemod/small-renames.mjs +22 -2
- package/codemod/source-tree.d.mts +8 -0
- package/codemod/source-tree.mjs +23 -0
- package/lint/eslint-list.cjs +55 -0
- package/lint/run.d.mts +35 -61
- package/lint/run.mjs +91 -288
- package/package.json +1 -1
- package/release/lint-report.d.mts +1 -1
- package/release/lint-report.mjs +3 -3
- package/report/sonar.d.mts +1 -1
- package/report/sonar.mjs +3 -3
- package/report/stylelint-findings.d.mts +23 -0
- package/report/stylelint-findings.mjs +9 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +8 -8
- package/runtime/knowledge/hfs/ci-standard.yaml +2 -2
- package/runtime/knowledge/hfs/rules.yaml +56 -5
- package/runtime/knowledge/hfs/slots.yaml +1 -1
- package/runtime/knowledge/sonar-gate.yaml +2 -2
- package/runtime/modules/kernel/failure-codes.yaml +4 -4
- package/runtime/scripts/api/node/run-node.d.mts +5 -0
- package/runtime/scripts/hfs/architecture/test-world-files.d.mts +4 -3
- package/runtime/scripts/hfs/architecture/test-world-files.mjs +26 -7
- package/runtime/scripts/hfs/check.d.mts +16 -1
- package/runtime/scripts/hfs/check.mjs +18 -7
- package/runtime/scripts/hfs/explicit-surface-rule.d.mts +58 -0
- package/runtime/scripts/hfs/explicit-surface-rule.mjs +59 -0
- package/runtime/scripts/hfs/gate-threshold.mjs +1 -1
- package/runtime/scripts/hfs/rule-memo.d.mts +30 -0
- package/runtime/scripts/hfs/rule-memo.mjs +24 -0
- package/runtime/scripts/hfs/rules/a15-findings.d.mts +6 -0
- package/runtime/scripts/hfs/rules/a15-findings.mjs +11 -2
- package/runtime/scripts/hfs/rules/ci-standard.mjs +2 -2
- package/runtime/scripts/hfs/rules/monorepo.d.mts +1 -3
- package/runtime/scripts/hfs/rules/monorepo.mjs +9 -11
- package/runtime/scripts/hfs/rules/pipeline.mjs +13 -9
- package/runtime/scripts/hfs/rules/read.d.mts +5 -0
- package/runtime/scripts/hfs/rules/read.mjs +13 -0
- package/runtime/scripts/hfs/rules/secrets.d.mts +2 -0
- package/runtime/scripts/hfs/rules/secrets.mjs +4 -1
- package/runtime/scripts/hfs/rules/stacks-release.mjs +35 -21
- package/runtime/scripts/lib/declaration-surface.d.mts +35 -0
- package/runtime/scripts/lib/declaration-surface.mjs +112 -0
- package/runtime/scripts/lib/worktree-exclude.mjs +1 -1
- package/scaffold/app.mjs +3 -3
- package/src/check-cache-structure-support.d.mts +52 -0
- package/src/main.d.mts +3 -2
- package/src/main.mjs +123 -109
- package/sync/ci-commands.d.mts +2 -1
- package/sync/ci-commands.mjs +19 -20
- package/sync/index.mjs +2 -2
- package/templates/app/package-scripts/core.json +2 -2
- package/templates/app/task-graph/turbo.json +0 -3
- package/templates/be/inbox/delivery-recorded.event.ts.tpl +2 -2
- package/templates/be/inbox/entities.index.ts.tpl +1 -1
- package/templates/be/inbox/module-definition.ts.tpl +8 -2
- package/templates/be/inbox/platform/event-bus/event-bus.module-definition.ts.tpl +10 -2
- package/templates/be/inbox/platform/inbox/inbox.module-definition.ts.tpl +8 -2
- package/templates/be/patterns/api/feature.module-definition.ts.tpl +9 -3
- package/templates/be/patterns/api/graphql.module-definition.ts.tpl +9 -3
- package/templates/be/patterns/cli/group.module-definition.ts.tpl +9 -3
- package/templates/be/patterns/event-bus/platform/event-bus.module-definition.ts.tpl +8 -3
- package/templates/be/patterns/event-bus/platform/persistence/connection.ts.tpl +3 -2
- package/templates/be/patterns/event-bus/platform/persistence/event-bus.sql.ts.tpl +4 -3
- package/templates/be/patterns/idempotency/platform/inbox/inbox.module-definition.ts.tpl +8 -3
- package/templates/be/patterns/idempotency/platform/inbox/persistence/connection.ts.tpl +3 -2
- package/templates/be/patterns/idempotency/platform/inbox/persistence/inbox.sql.ts.tpl +7 -6
- package/templates/be/patterns/jobs/job.module-definition.ts.tpl +9 -3
- package/templates/be/patterns/jobs/job.processor.ts.tpl +1 -1
- package/templates/be/patterns/jobs/job.queue.module-definition.ts.tpl +9 -3
- package/templates/be/patterns/jobs/platform/jobs.module-definition.ts.tpl +8 -3
- package/templates/be/patterns/jobs/platform/persistence.connection.ts.tpl +3 -2
- package/templates/be/patterns/jobs/platform/persistence.jobs.sql.ts.tpl +5 -4
- package/templates/be/patterns/projections/persistence.connection.ts.tpl +3 -2
- package/templates/be/patterns/projections/projection.module-definition.ts.tpl +8 -3
- package/templates/be/patterns/queues/platform/persistence.connection.ts.tpl +3 -2
- package/templates/be/patterns/queues/platform/persistence.queue.sql.ts.tpl +4 -3
- package/templates/be/patterns/queues/platform/queue.module-definition.ts.tpl +8 -3
- package/templates/be/patterns/queues/queue.module-definition.ts.tpl +9 -3
- package/templates/be/patterns/reactors/consumer.ts.tpl +1 -1
- package/templates/be/patterns/reactors/message.module-definition.ts.tpl +9 -3
- package/templates/be/patterns/reactors/reactor.module-definition.ts.tpl +9 -3
- package/templates/be/patterns/realtime/channel.graphql.module-definition.ts.tpl +9 -3
- package/templates/be/patterns/realtime/channel.sse.module-definition.ts.tpl +9 -3
- package/templates/be/patterns/realtime/channel.websocket.module-definition.ts.tpl +9 -3
- package/templates/be/patterns/saga/saga.done.consumer.ts.tpl +1 -1
- package/templates/be/patterns/saga/saga.failed.consumer.ts.tpl +1 -1
- package/templates/be/patterns/saga/saga.message.module-definition.ts.tpl +9 -3
- package/templates/be/patterns/saga/saga.module-definition.ts.tpl +9 -3
- package/templates/be/patterns/webhooks/webhook.http.module-definition.ts.tpl +9 -3
- package/templates/be/platform/graphql/graphql.module-definition.ts +8 -3
- package/templates/be/skeleton/src/features/api/system-health/system-health.module-definition.ts +9 -3
- package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module-definition.ts +9 -3
- package/templates/be/skeleton/src/features/cli/cli.module-definition.ts +9 -3
- package/templates/be/skeleton/src/features/cli/migrate/migrate.module-definition.ts +9 -3
- package/templates/be/skeleton/src/features/cli/seed/seed.module-definition.ts +9 -3
- package/templates/be/skeleton/src/modules/domain/identity/identity.module-definition.ts +8 -3
- package/templates/be/skeleton/src/modules/domain/liveness/liveness.module-definition.ts +8 -3
- package/templates/be/skeleton/src/modules/domain/note/note.module-definition.ts +8 -3
- package/templates/be/skeleton/src/modules/domain/note/persistence/connection.ts +3 -2
- package/templates/be/skeleton/src/modules/domain/note/persistence/note.sql.ts +2 -1
- package/templates/be/skeleton/src/modules/platform/clock/clock.module-definition.ts +8 -3
- package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.module-definition.ts +8 -3
- package/templates/be/skeleton/src/modules/platform/database/database.module-definition.ts +8 -3
- package/templates/be/skeleton/src/modules/platform/database/index.ts +1 -0
- package/templates/be/skeleton/src/modules/platform/errors/errors.module-definition.ts +10 -4
- package/templates/be/skeleton/src/modules/platform/http/http.module-definition.ts +8 -4
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.module-definition.ts +10 -4
- package/templates/be/skeleton/src/modules/platform/http-security/persistence/connection.ts +3 -2
- package/templates/be/skeleton/src/modules/platform/http-security/persistence/rate-limit.sql.ts +3 -2
- package/templates/be/skeleton/src/modules/platform/i18n/i18n.module-definition.ts +10 -4
- package/templates/be/skeleton/src/modules/platform/ids/ids.module-definition.ts +8 -3
- package/templates/be/skeleton/src/modules/platform/logging/logging.module-definition.ts +8 -3
- package/templates/be/skeleton/src/modules/platform/retry/retry.module-definition.ts +8 -3
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +1 -1
- package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/google.strategy.ts +5 -1
- package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/local.strategy.ts +5 -1
- package/templates/be/skeleton-lite/src/modules/platform/database/database.port.ts +1 -1
- package/templates/be/skeleton-lite/src/modules/platform/http-security/persistence/entities/index.ts +1 -1
- package/templates/be/skeleton-lite/src/tests/fixtures/builders/api.builder.ts +7 -1
- package/templates/be/skeleton-lite/src/tests/fixtures/builders/cli.builder.ts +7 -1
- package/templates/be/table/api/feature.module-definition.ts.tpl +9 -3
- package/templates/be/table/api/graphql.module-definition.ts.tpl +9 -3
- package/templates/be/table/entities.index.ts.tpl +1 -1
- package/templates/be/table/module-definition.ts.tpl +8 -3
- package/templates/be/test-world/fixtures/migrations.sql.ts.tpl +1 -0
- package/templates/be/test-world/world/test-world.config.ts.tpl +8 -1
- package/templates/fe/common/app-layout.tsx.partial +1 -1
- package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSession.ts +2 -1
- package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSignIn.ts +11 -3
- package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSignOut.ts +8 -2
- package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/store.ts +2 -1
- package/templates/fe/skeleton/apps/__app__/src/modules/toast/hooks.ts +1 -1
- package/templates/fe/skeleton/apps/__app__/src/modules/toast/toast.tsx +4 -1
- package/templates/fe/skeleton/apps/__app__/src/redux/hooks.ts +3 -2
- package/templates/fe/skeleton/apps/__app__/src/redux/index.ts +1 -0
- package/templates/fe/skeleton/apps/__app__/src/redux/slices/session.ts +12 -4
- package/templates/fe/skeleton/apps/__app__/src/redux/store.ts +8 -3
- package/templates/fe/skeleton/apps/app/src/app/[locale]/layout.tsx +4 -3
- package/templates/fe/skeleton/apps/app/src/features/layouts/AppLayout/index.tsx +1 -1
- package/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/index.tsx +2 -1
- package/templates/fe/skeleton/apps/app/src/features/pages/AppLoadingPage/index.tsx +2 -1
- package/templates/fe/skeleton/apps/app/src/features/pages/AppNotFoundPage/index.tsx +2 -1
- package/templates/fe/skeleton/apps/app/src/modules/i18n/index.ts +2 -1
- package/templates/fe/skeleton/apps/app/src/modules/i18n/request.ts +4 -1
- package/templates/fe/skeleton/apps/landing/src/app/[locale]/layout.tsx +4 -3
- package/templates/fe/skeleton/apps/landing/src/features/layouts/LandingLayout/index.tsx +2 -2
- package/templates/fe/skeleton/apps/landing/src/features/pages/LandingLoadingPage/index.tsx +2 -1
- package/templates/fe/skeleton/apps/landing/src/features/pages/LandingNotFoundPage/index.tsx +2 -1
- package/templates/fe/skeleton/apps/landing/src/features/pages/LandingPage/index.tsx +2 -1
- package/templates/fe/skeleton/apps/landing/src/modules/i18n/index.ts +2 -1
- package/templates/fe/skeleton/apps/landing/src/modules/i18n/request.ts +4 -1
- package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +19 -1
- package/templates/fe/skeleton/packages/__project__-i18n/src/proxy.ts +1 -1
- package/templates/fe/skeleton/packages/__project__-i18n/src/routing.ts +5 -1
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/layout.tsx +4 -3
- package/templates/fe/skeleton-lite/apps/web/src/features/layouts/AppLayout/index.tsx +2 -2
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/index.tsx +2 -1
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppLoadingPage/index.tsx +2 -1
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppNotFoundPage/index.tsx +2 -1
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/request.ts +3 -1
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/routing.ts +5 -1
- package/templates/fe/table/graphql/useMutateSwr.ts.tpl +2 -1
- package/templates/fe/table/graphql/useQuerySwr.ts.tpl +2 -1
- package/templates/fe/transport/graphql/clients/clients/auth.ts +1 -1
- package/templates/fe/transport/graphql/clients/clients/no-auth.ts +1 -1
- package/templates/fe/transport/graphql/clients/links/attach-access-token.ts +1 -1
- package/templates/fe/transport/graphql/clients/links/error.ts +1 -1
- package/templates/fe/transport/graphql/clients/links/http.ts +1 -1
- package/templates/fe/transport/graphql/clients/links/locale.ts +1 -1
- package/templates/fe/transport/graphql/clients/links/refresh-token.ts +1 -1
- package/templates/fe/transport/graphql/clients/links/retry.ts +1 -1
- package/templates/fe/transport/graphql/clients/links/timeout.ts +1 -1
- package/templates/fe/units/page/index.tsx.tpl +3 -2
- package/templates/fe/units/slot/SlotView.tsx.tpl +1 -1
package/cache/lint-family.mjs
CHANGED
|
@@ -138,17 +138,46 @@ const isFile = (file) => {
|
|
|
138
138
|
return false;
|
|
139
139
|
}
|
|
140
140
|
};
|
|
141
|
+
const isDirectory = (folder) => {
|
|
142
|
+
try {
|
|
143
|
+
return fs.statSync(folder).isDirectory();
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
/**
|
|
150
|
+
* What the import graph reads of the disk, remembered for the build: a side's thousands of files probe the same few hundred folders and
|
|
151
|
+
* candidate paths while resolving their imports, and the tree does not change while the graph is built.
|
|
152
|
+
*/
|
|
153
|
+
function graphHost(ts, sideRoot, hashOf) {
|
|
154
|
+
const files = new Map(), folders = new Map();
|
|
155
|
+
const remembered = (known, ask) => (path) => {
|
|
156
|
+
let answer = known.get(path);
|
|
157
|
+
if (answer === undefined) {
|
|
158
|
+
answer = ask(path);
|
|
159
|
+
known.set(path, answer);
|
|
160
|
+
}
|
|
161
|
+
return answer;
|
|
162
|
+
};
|
|
163
|
+
return { ts, sideRoot, readText, fileExists: remembered(files, isFile), directoryExists: remembered(folders, isDirectory), hashOf };
|
|
164
|
+
}
|
|
141
165
|
/** A record's `data` object, or an empty one. */
|
|
142
166
|
const dataOf = (record) => (record.data !== null && typeof record.data === 'object' && !Array.isArray(record.data) ? record.data : {});
|
|
143
|
-
/**
|
|
144
|
-
|
|
167
|
+
/**
|
|
168
|
+
* The graph findings of every canon that publishes graph rules, merged by file and sorted. A canon whose snapshot the ESLint child handed out (`handed`, by
|
|
169
|
+
* canon name) is not asked to build its graph again; any other canon builds it through its `projectGraph`.
|
|
170
|
+
*/
|
|
171
|
+
export function mergedSnapshot(sideRoot, canons, handed) {
|
|
145
172
|
const merged = {};
|
|
146
173
|
for (const canon of canons) {
|
|
147
174
|
if (!canon.graphRules)
|
|
148
175
|
continue;
|
|
149
|
-
|
|
176
|
+
const given = handed?.[canon.name];
|
|
177
|
+
if (given === undefined && canon.projectGraph === null)
|
|
150
178
|
return null;
|
|
151
|
-
|
|
179
|
+
const { byFile } = given ?? canon.projectGraph({ repoRoot: sideRoot });
|
|
180
|
+
for (const [file, findings] of Object.entries(byFile))
|
|
152
181
|
merged[posix(file)] = [...(merged[posix(file)] ?? []), ...findings];
|
|
153
182
|
}
|
|
154
183
|
for (const list of Object.values(merged))
|
|
@@ -162,7 +191,6 @@ const graphRecordFindings = (side, file, findings) => sortFindings(findings.map(
|
|
|
162
191
|
*/
|
|
163
192
|
export async function lintSide(input) {
|
|
164
193
|
const { session, side, sideRoot, scope, stylesheets } = input;
|
|
165
|
-
const inReport = input.report ?? (() => true);
|
|
166
194
|
const scopeSet = new Set(scope);
|
|
167
195
|
const styleSet = new Set(stylesheets);
|
|
168
196
|
const folder = `lint/${side}`;
|
|
@@ -189,7 +217,7 @@ export async function lintSide(input) {
|
|
|
189
217
|
const validStyle = new Map(reconcile(ofStep(LINT_STEPS.stylelint), hashOf).map((record) => [record.path, record]));
|
|
190
218
|
const meta = ofStep(LINT_STEPS.meta).find((record) => record.path === SIDE_RECORD);
|
|
191
219
|
const metaData = meta === undefined ? {} : dataOf(meta);
|
|
192
|
-
const previousFindings = new Set([...ofStep(LINT_STEPS.eslint), ...ofStep(LINT_STEPS.stylelint)].
|
|
220
|
+
const previousFindings = new Set([...ofStep(LINT_STEPS.eslint), ...ofStep(LINT_STEPS.stylelint)].flatMap((record) => record.findings.map(keyOf)));
|
|
193
221
|
const progress = session.progress.step('lint', `lint ${side}`, scope.length + stylesheets.length);
|
|
194
222
|
// ------------------------------------------------------------------ the plan
|
|
195
223
|
let whole = input.full;
|
|
@@ -242,10 +270,10 @@ export async function lintSide(input) {
|
|
|
242
270
|
const imports = Array.isArray(data['imports']) ? data['imports'].filter((value) => typeof value === 'string') : [];
|
|
243
271
|
const kept = previous.get(record.path);
|
|
244
272
|
if (kept === undefined || record.hash === hashOf(record.path))
|
|
245
|
-
previous.set(record.path, { hash: record.hash, imports, declaresGlobals: data['globals'] === true });
|
|
273
|
+
previous.set(record.path, { hash: record.hash, imports, declaresGlobals: data['globals'] === true, api: typeof data['api'] === 'string' ? data['api'] : undefined, open: typeof data['open'] === 'boolean' ? data['open'] : undefined, refs: Array.isArray(data['refs']) ? data['refs'].filter((value) => typeof value === 'string') : undefined });
|
|
246
274
|
}
|
|
247
275
|
const recordedGraph = previous.size === 0 ? null : previous;
|
|
248
|
-
build = buildImportGraph(
|
|
276
|
+
build = buildImportGraph(graphHost(ts, sideRoot, (file) => hashOf(file) ?? null), scope, recordedGraph, { pathsChanged: metaData['paths'] !== pathsDigest });
|
|
249
277
|
const dirty = dirtySetOf(recordedGraph, build.graph);
|
|
250
278
|
if (dirty.wholeSide !== null)
|
|
251
279
|
whole = dirty.wholeSide;
|
|
@@ -306,14 +334,16 @@ export async function lintSide(input) {
|
|
|
306
334
|
dirtyStyle = new Set(stylesheets);
|
|
307
335
|
}
|
|
308
336
|
// ------------------------------------------------------------------ the run
|
|
309
|
-
const eslintList = [...dirtyEslint].
|
|
310
|
-
const styleList = [...dirtyStyle].
|
|
337
|
+
const eslintList = [...dirtyEslint].sort();
|
|
338
|
+
const styleList = [...dirtyStyle].sort();
|
|
339
|
+
let handedGraph;
|
|
311
340
|
const freshEslint = new Map();
|
|
312
341
|
const freshStyle = new Map();
|
|
313
342
|
const rootOf = (file) => path.relative(sideRoot, file).replace(/\\/g, '/');
|
|
314
343
|
if (eslintList.length > 0) {
|
|
315
344
|
const run = await input.eslint(whole !== null ? null : eslintList);
|
|
316
345
|
errors.push(...run.errors);
|
|
346
|
+
handedGraph = run.graph;
|
|
317
347
|
for (const result of run.results)
|
|
318
348
|
freshEslint.set(posix(rootOf(result.filePath)), result);
|
|
319
349
|
}
|
|
@@ -331,8 +361,6 @@ export async function lintSide(input) {
|
|
|
331
361
|
const styleRecords = [];
|
|
332
362
|
const planHash = (file) => hashOf(file) ?? 'absent';
|
|
333
363
|
for (const file of scope) {
|
|
334
|
-
if (!inReport(file))
|
|
335
|
-
continue;
|
|
336
364
|
const fresh = freshEslint.get(file);
|
|
337
365
|
const reuse = fresh === undefined ? validEslint.get(file) : undefined;
|
|
338
366
|
if (fresh !== undefined) {
|
|
@@ -349,8 +377,6 @@ export async function lintSide(input) {
|
|
|
349
377
|
}
|
|
350
378
|
}
|
|
351
379
|
for (const file of stylesheets) {
|
|
352
|
-
if (!inReport(file))
|
|
353
|
-
continue;
|
|
354
380
|
const fresh = freshStyle.get(file);
|
|
355
381
|
const reuse = fresh === undefined ? validStyle.get(file) : undefined;
|
|
356
382
|
if (fresh !== undefined) {
|
|
@@ -367,38 +393,40 @@ export async function lintSide(input) {
|
|
|
367
393
|
}
|
|
368
394
|
}
|
|
369
395
|
const checked = freshEslint.size + freshStyle.size;
|
|
370
|
-
const units =
|
|
396
|
+
const units = scope.length + stylesheets.length;
|
|
371
397
|
const reused = Math.max(0, units - checked);
|
|
372
398
|
progress.tick(units, reused, checked);
|
|
373
399
|
progress.end();
|
|
374
400
|
// ------------------------------------------------------------------ the records
|
|
375
401
|
let written = false;
|
|
376
|
-
const complete = errors.length === 0 && input.write &&
|
|
402
|
+
const complete = errors.length === 0 && input.write && !sideUnchanged;
|
|
377
403
|
if (complete) {
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
for (const [file, node] of build.graph)
|
|
388
|
-
records.push({ path: file, step: LINT_STEPS.imports, hash: node.hash, findings: [], data: { globals: node.declaresGlobals, imports: node.imports } });
|
|
389
|
-
snapshot ??= mergedSnapshot(sideRoot, input.canons);
|
|
390
|
-
for (const [file, findings] of Object.entries(snapshot ?? {})) {
|
|
391
|
-
if (scopeSet.has(file) && findings.length > 0)
|
|
392
|
-
records.push({ path: file, step: LINT_STEPS.graph, hash: planHash(file), findings: graphRecordFindings(side, file, findings) });
|
|
393
|
-
}
|
|
404
|
+
// A file edited while the run read it gets no record of any step, and the others keep theirs: their hashes are those of the tree the run
|
|
405
|
+
// planned from, so the edited file is then added (no recorded graph entry) and its dependents are judged again, whatever they read of it.
|
|
406
|
+
const moved = new Set([...freshEslint.keys(), ...freshStyle.keys()].filter((file) => hashFile(abs(file)) !== hashOf(file)));
|
|
407
|
+
const records = [...eslintRecords, ...styleRecords].filter((record) => !moved.has(record.path));
|
|
408
|
+
// without an import graph or a project graph the next run lints the side whole; the results themselves are still stored
|
|
409
|
+
try {
|
|
410
|
+
if (build === null) {
|
|
411
|
+
const ts = input.typescript();
|
|
412
|
+
build = buildImportGraph(graphHost(ts, sideRoot, (file) => hashOf(file) ?? null), scope, null);
|
|
394
413
|
}
|
|
395
|
-
|
|
396
|
-
|
|
414
|
+
for (const [file, node] of build.graph) {
|
|
415
|
+
if (!moved.has(file))
|
|
416
|
+
records.push({ path: file, step: LINT_STEPS.imports, hash: node.hash, findings: [], data: { globals: node.declaresGlobals, imports: node.imports, ...(node.api === undefined ? {} : { api: node.api }), open: node.open !== false, ...(node.refs === undefined ? {} : { refs: node.refs }) } });
|
|
397
417
|
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
418
|
+
snapshot ??= mergedSnapshot(sideRoot, input.canons, handedGraph);
|
|
419
|
+
for (const [file, findings] of Object.entries(snapshot ?? {})) {
|
|
420
|
+
if (scopeSet.has(file) && !moved.has(file) && findings.length > 0)
|
|
421
|
+
records.push({ path: file, step: LINT_STEPS.graph, hash: planHash(file), findings: graphRecordFindings(side, file, findings) });
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
catch {
|
|
425
|
+
records.splice(0, records.length, ...records.filter((record) => record.step === LINT_STEPS.eslint || record.step === LINT_STEPS.stylelint));
|
|
401
426
|
}
|
|
427
|
+
records.push({ path: SIDE_RECORD, step: LINT_STEPS.meta, hash: sideKey, findings: [], data: { extra: input.extraKey, files: scope.length, paths: pathsDigest, stylesheets: stylesheets.length } });
|
|
428
|
+
session.replace(folder, records);
|
|
429
|
+
written = true;
|
|
402
430
|
}
|
|
403
431
|
const currentKeys = new Set(current.map(keyOf));
|
|
404
432
|
let knownFindings = 0;
|
package/cache/records.d.mts
CHANGED
|
@@ -4,9 +4,15 @@ export declare class RecordSet {
|
|
|
4
4
|
constructor(initial?: readonly CacheRecord[]);
|
|
5
5
|
/** The record of (path, step) whose hash is `hash`, else undefined: a record of other content is no answer. */
|
|
6
6
|
matching(path: string, step: string, hash: string): CacheRecord | undefined;
|
|
7
|
+
/** The record of (path, step) whatever its hash, for a step that validates the record's own inputs. */
|
|
8
|
+
get(path: string, step: string): CacheRecord | undefined;
|
|
7
9
|
put(record: CacheRecord): void;
|
|
8
10
|
/** Drops every record of `step` whose path is not in `keep`: a deleted path drops every record it owns. */
|
|
9
11
|
retain(step: string, keep: ReadonlySet<string>): void;
|
|
12
|
+
/** Drops every record whose step starts with `prefix` and whose path is not in `keep`. */
|
|
13
|
+
retainPrefix(prefix: string, keep: ReadonlySet<string>): void;
|
|
14
|
+
/** Drops every record whose step starts with `prefix` and is not one of `keep`: a unit step no rule asked for this run belongs to no rule any more. */
|
|
15
|
+
dropPrefixExcept(prefix: string, keep: ReadonlySet<string>): void;
|
|
10
16
|
/** Every finding the records of `step` hold. */
|
|
11
17
|
findings(step: string): Finding[];
|
|
12
18
|
toArray(): CacheRecord[];
|
|
@@ -24,6 +30,7 @@ export declare function scopedStep<T extends JsonValue>(session: CheckSession, r
|
|
|
24
30
|
readonly key: string;
|
|
25
31
|
readonly reuse: boolean;
|
|
26
32
|
readonly inputs: readonly string[];
|
|
33
|
+
readonly captured?: Readonly<Record<string, string>>;
|
|
27
34
|
compute(): T | Promise<T>;
|
|
28
35
|
compact(value: T): Finding[];
|
|
29
36
|
store?(value: T): boolean;
|
package/cache/records.mjs
CHANGED
|
@@ -14,6 +14,10 @@ export class RecordSet {
|
|
|
14
14
|
const record = this.records.get(keyOf(path, step));
|
|
15
15
|
return record?.hash === hash ? record : undefined;
|
|
16
16
|
}
|
|
17
|
+
/** The record of (path, step) whatever its hash, for a step that validates the record's own inputs. */
|
|
18
|
+
get(path, step) {
|
|
19
|
+
return this.records.get(keyOf(path, step));
|
|
20
|
+
}
|
|
17
21
|
put(record) {
|
|
18
22
|
this.records.set(keyOf(record.path, record.step), record);
|
|
19
23
|
}
|
|
@@ -23,6 +27,18 @@ export class RecordSet {
|
|
|
23
27
|
if (record.step === step && !keep.has(record.path))
|
|
24
28
|
this.records.delete(key);
|
|
25
29
|
}
|
|
30
|
+
/** Drops every record whose step starts with `prefix` and whose path is not in `keep`. */
|
|
31
|
+
retainPrefix(prefix, keep) {
|
|
32
|
+
for (const [key, record] of this.records)
|
|
33
|
+
if (record.step.startsWith(prefix) && !keep.has(record.path))
|
|
34
|
+
this.records.delete(key);
|
|
35
|
+
}
|
|
36
|
+
/** Drops every record whose step starts with `prefix` and is not one of `keep`: a unit step no rule asked for this run belongs to no rule any more. */
|
|
37
|
+
dropPrefixExcept(prefix, keep) {
|
|
38
|
+
for (const [key, record] of this.records)
|
|
39
|
+
if (record.step.startsWith(prefix) && !keep.has(record.step))
|
|
40
|
+
this.records.delete(key);
|
|
41
|
+
}
|
|
26
42
|
/** Every finding the records of `step` hold. */
|
|
27
43
|
findings(step) {
|
|
28
44
|
return [...this.records.values()].filter((record) => record.step === step).flatMap((record) => record.findings);
|
|
@@ -48,7 +64,7 @@ export async function scopedStep(session, records, step) {
|
|
|
48
64
|
if (held?.data !== undefined)
|
|
49
65
|
return { value: held.data, reused: true };
|
|
50
66
|
const value = await step.compute();
|
|
51
|
-
if ((step.store?.(value) ?? true) && session.unchanged(step.inputs))
|
|
67
|
+
if ((step.store?.(value) ?? true) && session.unchanged(step.inputs, step.captured))
|
|
52
68
|
records.put({ path: step.path, step: step.step, hash: step.key, findings: step.compact(value), data: value });
|
|
53
69
|
return { value, reused: false };
|
|
54
70
|
}
|
package/cache/run.d.mts
CHANGED
|
@@ -17,7 +17,7 @@ export interface AppRunInput {
|
|
|
17
17
|
readonly presets?: Presets | undefined;
|
|
18
18
|
readonly prettier?: Prettier | undefined;
|
|
19
19
|
readonly edition?: string | undefined;
|
|
20
|
-
/** Limits the formatter to these app-relative files
|
|
20
|
+
/** Limits the formatter to these app-relative files. */
|
|
21
21
|
readonly only?: readonly string[] | undefined;
|
|
22
22
|
/** The ref the baseline database rules compare with, when the caller names one. */
|
|
23
23
|
readonly base?: string | undefined;
|
package/cache/session.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// session.mts - the disk-backed CheckSession of `starci app check
|
|
1
|
+
// session.mts - the disk-backed CheckSession of `starci app check`. It decides per family whether a run is full or diff
|
|
2
2
|
// (the rules of contexts/project/check-cache.md "When a run is full"), serves the trusted records of a folder, takes the records a family
|
|
3
3
|
// replaces and writes them once on flush: one JSON Lines file per area, byte-stable, only the files whose bytes changed. In CI it writes
|
|
4
4
|
// nothing and compares the committed cache with what it inherited or computed instead.
|
|
@@ -127,7 +127,11 @@ export function createDiskSession(options) {
|
|
|
127
127
|
replace(folder, records, replaceOptions) {
|
|
128
128
|
replaced.set(folder, { records, depth: replaceOptions?.depth ?? 2 });
|
|
129
129
|
},
|
|
130
|
-
unchanged: (paths) => paths.every((file) =>
|
|
130
|
+
unchanged: (paths, captured) => paths.every((file) => {
|
|
131
|
+
const now = hashFile(path.join(root, file));
|
|
132
|
+
const taken = captured?.[file];
|
|
133
|
+
return taken === undefined ? now === index.hashOf(file) : (now ?? 'absent') === taken;
|
|
134
|
+
}),
|
|
131
135
|
report: (report) => void reports.push(report),
|
|
132
136
|
verdicts: () => verdictList,
|
|
133
137
|
flush() {
|
package/cache/side-key.d.mts
CHANGED
|
@@ -3,6 +3,8 @@ export interface SideKey {
|
|
|
3
3
|
readonly key: string;
|
|
4
4
|
/** Every path the key covers: tracked paths of the side and the untracked program inputs. */
|
|
5
5
|
readonly inputs: readonly string[];
|
|
6
|
+
/** The hash each input had when the key was computed (`absent` for a missing one): the baseline `session.unchanged` compares an edit-during-the-run against. */
|
|
7
|
+
readonly hashes: Readonly<Record<string, string>>;
|
|
6
8
|
}
|
|
7
9
|
/** The untracked files the side's TypeScript program includes (generated code), side-relative paths prefixed with `side/`. */
|
|
8
10
|
export declare function untrackedProgramInputs(root: string, side: string, tracked: ReadonlySet<string>): string[];
|
package/cache/side-key.mjs
CHANGED
|
@@ -43,5 +43,9 @@ export function sideKey(root, index, side) {
|
|
|
43
43
|
const untracked = untrackedProgramInputs(root, side, new Set(index.paths));
|
|
44
44
|
for (const file of untracked)
|
|
45
45
|
parts[`untracked:${file}`] = hashFile(path.join(root, file)) ?? 'absent';
|
|
46
|
-
|
|
46
|
+
const hashes = {};
|
|
47
|
+
for (const [name, hash] of Object.entries(parts))
|
|
48
|
+
if (name.startsWith('tracked:') || name.startsWith('untracked:'))
|
|
49
|
+
hashes[name.slice(name.indexOf(':') + 1)] = hash;
|
|
50
|
+
return { key: combineHashes(parts), inputs: [...trackedPaths, ...untracked], hashes };
|
|
47
51
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { RuleMemo } from '../runtime/scripts/hfs/rule-memo.mjs';
|
|
2
|
+
import type { RecordSet } from './records.mjs';
|
|
3
|
+
import type { CheckSession } from './types.mjs';
|
|
4
|
+
export declare const FILE_RULES_STEP = "file-rules";
|
|
5
|
+
export declare const RULE_STEP_PREFIX = "rule:";
|
|
6
|
+
export declare const UNIT_STEP_PREFIX = "unit:";
|
|
7
|
+
/** What the memo did, for the family's diagnosis line. */
|
|
8
|
+
export interface MemoCounts {
|
|
9
|
+
/** Files whose per-file rules were judged afresh / taken from a record. */
|
|
10
|
+
filesChecked: number;
|
|
11
|
+
filesReused: number;
|
|
12
|
+
/** Cross-file rules run afresh / taken from a record. */
|
|
13
|
+
keyedChecked: number;
|
|
14
|
+
keyedReused: number;
|
|
15
|
+
/** Per-file units of cross-file rules judged afresh / taken from a record. */
|
|
16
|
+
unitsChecked: number;
|
|
17
|
+
unitsReused: number;
|
|
18
|
+
}
|
|
19
|
+
export interface StructureMemo {
|
|
20
|
+
readonly memo: RuleMemo;
|
|
21
|
+
readonly counts: MemoCounts;
|
|
22
|
+
/** The app paths whose per-file records this run holds (the rest are dropped with their files). */
|
|
23
|
+
readonly files: Set<string>;
|
|
24
|
+
/** The app paths whose unit records this run holds, by unit step. */
|
|
25
|
+
readonly units: Map<string, Set<string>>;
|
|
26
|
+
}
|
|
27
|
+
/** The memo of one structure pass over the app at `root`. `reuse` is false when the family runs in full: the records are then written and never read. */
|
|
28
|
+
export declare function createStructureMemo(input: {
|
|
29
|
+
session: CheckSession;
|
|
30
|
+
records: RecordSet;
|
|
31
|
+
reuse: boolean;
|
|
32
|
+
root: string;
|
|
33
|
+
tracked: readonly string[];
|
|
34
|
+
}): StructureMemo;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// structure-memo.mts - the result cache of the content rules of the structure pass (`--gate hfs`, `--gate secrets`). The rules of the pass used to run over every tracked
|
|
2
|
+
// file on every run. Two kinds are now reused (contexts/project/check-cache.md "Which step reuses what"):
|
|
3
|
+
// - a per-file rule (`FileRule`: plaintext secrets, bypass markers, soft size): one record per file, `file-rules`, keyed on the file's content hash, the hashes of the
|
|
4
|
+
// other files the rule declares it reads (`deps`) and the names of the rules; it holds each rule's findings for the file. The cache folder is the only place a verdict is kept.
|
|
5
|
+
// - a cross-file rule (`memoKeyed`: the documentation freshness): one record per scope, `rule:<id>`, keyed on the exact set of files the rule read through `readText` /
|
|
6
|
+
// `readJson` (recorded when it ran, each with the hash it had then) and on the digest of the tracked path list, which every such rule derives its file sets from.
|
|
7
|
+
// Everything else the pass runs (the slots, the tree, the pins, the managed files, the rules that stat or walk the work tree, git state, untracked entries) stays always-run.
|
|
8
|
+
// A rule that must be judged on the work tree is never given to this memo; a finding that does not carry the path of the file it judges makes the whole batch uncacheable.
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { loadTypescript } from '../runtime/scripts/lib/package-at.mjs';
|
|
12
|
+
import { recordReads } from '../runtime/scripts/hfs/rules/read.mjs';
|
|
13
|
+
import { inCacheDir } from './content-index.mjs';
|
|
14
|
+
import { combineHashes, hashFile, hashText } from './hash.mjs';
|
|
15
|
+
export const FILE_RULES_STEP = 'file-rules';
|
|
16
|
+
export const RULE_STEP_PREFIX = 'rule:';
|
|
17
|
+
export const UNIT_STEP_PREFIX = 'unit:';
|
|
18
|
+
const posix = (value) => value.split(path.sep).join('/');
|
|
19
|
+
const compact = (finding) => ({
|
|
20
|
+
path: finding.path ?? '.', line: finding.line ?? 0, column: finding.column ?? 0, rule: finding.code, message: String(finding.message ?? ''),
|
|
21
|
+
});
|
|
22
|
+
const plain = (value) => JSON.parse(JSON.stringify(value));
|
|
23
|
+
/** The memo of one structure pass over the app at `root`. `reuse` is false when the family runs in full: the records are then written and never read. */
|
|
24
|
+
export function createStructureMemo(input) {
|
|
25
|
+
const { session, records, reuse, root } = input;
|
|
26
|
+
const counts = { filesChecked: 0, filesReused: 0, keyedChecked: 0, keyedReused: 0, unitsChecked: 0, unitsReused: 0 };
|
|
27
|
+
const files = new Set();
|
|
28
|
+
const units = new Map();
|
|
29
|
+
// What this run already judged, so a rule asked twice about one file in one run (the schedule scan is) answers from the first judgement.
|
|
30
|
+
const judged = new Map();
|
|
31
|
+
const textHashes = new Map();
|
|
32
|
+
// The hash of an app path now: the index's for a tracked file (the snapshot of this run), the work tree's for anything else, `absent` for a missing one.
|
|
33
|
+
const hashNow = (appPath) => session.index.hashOf(appPath) ?? hashFile(path.join(root, appPath)) ?? 'absent';
|
|
34
|
+
// What every cross-file rule derives its file sets and its parsing from: the tracked path list, and the TypeScript the rules parse with (the lockfile is a fingerprint part, but an app
|
|
35
|
+
// with no install resolves the compiler from the harness instead).
|
|
36
|
+
let trackedDigest;
|
|
37
|
+
const typescriptVersion = () => loadTypescript(root, path.dirname(fileURLToPath(import.meta.url)))?.version ?? 'none';
|
|
38
|
+
const digestOfTracked = () => (trackedDigest ??= hashText(`${[...input.tracked].sort().join('\n')}\0typescript=${typescriptVersion()}`));
|
|
39
|
+
const prefixOf = (scopeRoot) => {
|
|
40
|
+
const relative = posix(path.relative(root, scopeRoot));
|
|
41
|
+
return relative === '' ? '' : `${relative}/`;
|
|
42
|
+
};
|
|
43
|
+
const filesStep = (scopeRoot, list, rules) => {
|
|
44
|
+
const prefix = prefixOf(scopeRoot);
|
|
45
|
+
const names = rules.map((rule) => rule.id).join('+');
|
|
46
|
+
// The cache folder is rewritten by every run and is outside every step's file set: its files are judged live and never keyed.
|
|
47
|
+
const live = list.filter((file) => inCacheDir(`${prefix}${file}`));
|
|
48
|
+
const liveOut = live.length === 0 ? [] : rules.map((rule) => rule.run([...live]));
|
|
49
|
+
const entries = list.filter((file) => !inCacheDir(`${prefix}${file}`)).map((file) => {
|
|
50
|
+
const deps = [...new Set(rules.flatMap((rule) => rule.deps?.(file) ?? []))].sort().map((dep) => `${prefix}${dep}`);
|
|
51
|
+
const parts = { rules: names, self: hashNow(`${prefix}${file}`) };
|
|
52
|
+
const captured = { [`${prefix}${file}`]: parts['self'] };
|
|
53
|
+
for (const dep of deps) {
|
|
54
|
+
parts[`dep:${dep}`] = hashNow(dep);
|
|
55
|
+
captured[dep] = parts[`dep:${dep}`];
|
|
56
|
+
}
|
|
57
|
+
return { file, app: `${prefix}${file}`, deps, captured, key: combineHashes(parts) };
|
|
58
|
+
});
|
|
59
|
+
const held = new Map();
|
|
60
|
+
for (const entry of entries) {
|
|
61
|
+
const record = reuse ? records.matching(entry.app, FILE_RULES_STEP, entry.key) : undefined;
|
|
62
|
+
if (record?.data !== undefined)
|
|
63
|
+
held.set(entry.file, record.data);
|
|
64
|
+
}
|
|
65
|
+
const missing = entries.filter((entry) => !held.has(entry.file));
|
|
66
|
+
counts.filesReused += entries.length - missing.length;
|
|
67
|
+
counts.filesChecked += missing.length;
|
|
68
|
+
const computed = new Map(missing.map((entry) => [entry.file, {}]));
|
|
69
|
+
if (missing.length > 0) {
|
|
70
|
+
const names = new Set(missing.map((entry) => entry.file));
|
|
71
|
+
for (const rule of rules) {
|
|
72
|
+
for (const finding of rule.run(missing.map((entry) => entry.file))) {
|
|
73
|
+
const own = finding.path !== undefined && names.has(finding.path) ? computed.get(finding.path) : undefined;
|
|
74
|
+
// A finding on a file the batch did not hold is a rule that is not per-file: nothing of it can be kept per file.
|
|
75
|
+
if (own === undefined)
|
|
76
|
+
return rules.flatMap((each) => each.run([...list]));
|
|
77
|
+
(own[rule.id] ??= []).push(finding);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
for (const entry of missing) {
|
|
81
|
+
const result = computed.get(entry.file);
|
|
82
|
+
if (!session.unchanged(Object.keys(entry.captured), entry.captured))
|
|
83
|
+
continue;
|
|
84
|
+
records.put({ path: entry.app, step: FILE_RULES_STEP, hash: entry.key, findings: Object.values(result).flat().map(compact), data: plain(result) });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
for (const entry of entries)
|
|
88
|
+
files.add(entry.app);
|
|
89
|
+
const out = [];
|
|
90
|
+
rules.forEach((rule, at) => {
|
|
91
|
+
for (const entry of entries)
|
|
92
|
+
out.push(...((held.get(entry.file) ?? computed.get(entry.file))?.[rule.id] ?? []));
|
|
93
|
+
out.push(...(liveOut[at] ?? []));
|
|
94
|
+
});
|
|
95
|
+
return out;
|
|
96
|
+
};
|
|
97
|
+
const keyedStep = (scopeRoot, id, run) => {
|
|
98
|
+
const at = prefixOf(scopeRoot).replace(/\/$/, '') || '.';
|
|
99
|
+
const step = `${RULE_STEP_PREFIX}${id}`;
|
|
100
|
+
const digest = digestOfTracked();
|
|
101
|
+
const record = reuse ? records.get(at, step) : undefined;
|
|
102
|
+
const data = record?.data;
|
|
103
|
+
if (data?.tracked === digest && data.reads !== undefined && data.findings !== undefined && Object.entries(data.reads).every(([file, hash]) => hashNow(file) === hash)) {
|
|
104
|
+
counts.keyedReused += 1;
|
|
105
|
+
return data.findings;
|
|
106
|
+
}
|
|
107
|
+
counts.keyedChecked += 1;
|
|
108
|
+
const reads = {};
|
|
109
|
+
const findings = recordReads((from, rel) => {
|
|
110
|
+
const app = posix(path.relative(root, path.join(from, rel)));
|
|
111
|
+
if (app.startsWith('..') || path.isAbsolute(app) || app in reads)
|
|
112
|
+
return;
|
|
113
|
+
reads[app] = hashNow(app);
|
|
114
|
+
}, run);
|
|
115
|
+
if (session.unchanged(Object.keys(reads), reads)) {
|
|
116
|
+
const stored = { path: at, step, hash: combineHashes({ tracked: digest, ...Object.fromEntries(Object.entries(reads).map(([file, hash]) => [`read:${file}`, hash])) }), findings: findings.map(compact), data: plain({ tracked: digest, reads, findings }) };
|
|
117
|
+
records.put(stored);
|
|
118
|
+
}
|
|
119
|
+
return findings;
|
|
120
|
+
};
|
|
121
|
+
const unitStep = (scopeRoot, id, file, extra, empty, compute) => {
|
|
122
|
+
const app = `${prefixOf(scopeRoot)}${file}`;
|
|
123
|
+
const step = `${UNIT_STEP_PREFIX}${id}`;
|
|
124
|
+
let extraHash = textHashes.get(extra);
|
|
125
|
+
if (extraHash === undefined)
|
|
126
|
+
textHashes.set(extra, (extraHash = hashText(extra)));
|
|
127
|
+
const self = hashNow(app);
|
|
128
|
+
const key = combineHashes({ unit: id, extra: extraHash, self, typescript: typescriptVersion() });
|
|
129
|
+
(units.get(step) ?? units.set(step, new Set()).get(step))?.add(app);
|
|
130
|
+
const memory = judged.get(JSON.stringify([step, app, key]));
|
|
131
|
+
if (memory !== undefined)
|
|
132
|
+
return memory;
|
|
133
|
+
const record = reuse ? records.matching(app, step, key) : undefined;
|
|
134
|
+
let value;
|
|
135
|
+
if (record !== undefined) {
|
|
136
|
+
counts.unitsReused += 1;
|
|
137
|
+
value = (record.data ?? empty);
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
counts.unitsChecked += 1;
|
|
141
|
+
value = compute();
|
|
142
|
+
const empties = JSON.stringify(empty) === JSON.stringify(value);
|
|
143
|
+
if (session.unchanged([app], { [app]: self }))
|
|
144
|
+
records.put({ path: app, step, hash: key, findings: [], ...(empties ? {} : { data: plain(value) }) });
|
|
145
|
+
}
|
|
146
|
+
judged.set(JSON.stringify([step, app, key]), value);
|
|
147
|
+
return value;
|
|
148
|
+
};
|
|
149
|
+
return { memo: { files: filesStep, keyed: keyedStep, unit: unitStep }, counts, files, units };
|
|
150
|
+
}
|
package/cache/types.d.mts
CHANGED
|
@@ -91,8 +91,12 @@ export interface CheckSession {
|
|
|
91
91
|
replace(folder: string, records: readonly CacheRecord[], options?: {
|
|
92
92
|
readonly depth?: number;
|
|
93
93
|
}): void;
|
|
94
|
-
/**
|
|
95
|
-
|
|
94
|
+
/**
|
|
95
|
+
* True when none of `paths` changed in the work tree since the run began: a scoped step stores nothing keyed on a file edited during the run.
|
|
96
|
+
* A path the content index does not hold (an untracked program input, generated code) is compared with `captured`, the hash the step's key took
|
|
97
|
+
* of it (`absent` for a missing file); without a captured hash it is compared with the index, so an untracked path is never unchanged.
|
|
98
|
+
*/
|
|
99
|
+
unchanged(paths: readonly string[], captured?: Readonly<Record<string, string>>): boolean;
|
|
96
100
|
report(report: FamilyReport): void;
|
|
97
101
|
readonly progress: ProgressSink;
|
|
98
102
|
flush(): FlushResult;
|
package/cache/wiring-family.mjs
CHANGED
|
@@ -27,7 +27,7 @@ export async function runWiringFamily(session, { repoRoot, edition, files, run,
|
|
|
27
27
|
};
|
|
28
28
|
const step = session.progress.step('wiring', 'boot proof', Math.max(1, key.inputs.length));
|
|
29
29
|
const { value, reused } = await scopedStep(session, records, {
|
|
30
|
-
path: 'be', step: 'boot-proof', key: key.key, reuse: decision.mode === 'diff', inputs: key.inputs,
|
|
30
|
+
path: 'be', step: 'boot-proof', key: key.key, reuse: decision.mode === 'diff', inputs: key.inputs, captured: key.hashes,
|
|
31
31
|
compute: async () => JSON.parse(JSON.stringify(await prove())),
|
|
32
32
|
compact: (found) => found.findings.map(compact),
|
|
33
33
|
store: (found) => !found.findings.some((finding) => finding.code === BOOT_PROOF_TIMEOUT),
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** The TypeScript compiler API of the app's own install (loadTypescript), typed by the version this package develops against. */
|
|
2
|
+
type Ts = typeof import('typescript');
|
|
3
|
+
/** One move of the report: a directive taken from `line` to the top of its module. */
|
|
4
|
+
export interface DirectiveFirstChange {
|
|
5
|
+
file: string;
|
|
6
|
+
line: number;
|
|
7
|
+
column: number;
|
|
8
|
+
code: string;
|
|
9
|
+
from: string;
|
|
10
|
+
to: string;
|
|
11
|
+
}
|
|
12
|
+
/** One case left for a person. */
|
|
13
|
+
export interface DirectiveFirstFinding {
|
|
14
|
+
file: string;
|
|
15
|
+
line: number;
|
|
16
|
+
column: number;
|
|
17
|
+
code: string;
|
|
18
|
+
reason: string;
|
|
19
|
+
}
|
|
20
|
+
/** The report of a migration (`starci app codemod directive-first`). */
|
|
21
|
+
export interface DirectiveFirstReport {
|
|
22
|
+
schema: string;
|
|
23
|
+
root: string;
|
|
24
|
+
write: boolean;
|
|
25
|
+
filesScanned: number;
|
|
26
|
+
filesChanged: string[];
|
|
27
|
+
changes: DirectiveFirstChange[];
|
|
28
|
+
findings: DirectiveFirstFinding[];
|
|
29
|
+
}
|
|
30
|
+
/** Move every `"use client"` / `"use server"` that is not in the prologue of the module to the top of it, as its first statement, with one blank line after. */
|
|
31
|
+
export declare function transformDirectiveFirst({ ts, text, file }: {
|
|
32
|
+
ts: Ts;
|
|
33
|
+
text: string;
|
|
34
|
+
file?: string;
|
|
35
|
+
}): {
|
|
36
|
+
text: string;
|
|
37
|
+
changes: DirectiveFirstChange[];
|
|
38
|
+
findings: DirectiveFirstFinding[];
|
|
39
|
+
};
|
|
40
|
+
/** Preview or apply the moves in one app (`be/`, `fe/`, or `src/` of a single-side app); a conflicting module is kept in the returned report. */
|
|
41
|
+
export declare function migrateDirectiveFirst({ root, write, ts }: {
|
|
42
|
+
root: string;
|
|
43
|
+
write?: boolean;
|
|
44
|
+
ts?: Ts | null;
|
|
45
|
+
}): DirectiveFirstReport;
|
|
46
|
+
/** Render the moves and what is left for a person as Markdown; JSON consumers read the same report object. */
|
|
47
|
+
export declare function directiveFirstMarkdown(report: DirectiveFirstReport): string;
|
|
48
|
+
export {};
|