@assembly-js/app-foundation 0.11.0 → 0.12.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 +3 -0
- package/bin/check-crons.mjs +56 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -19,6 +19,9 @@ A patch release never changes what an app's build requires; a minor release may
|
|
|
19
19
|
not by hand; see `changes/README.md`. CI fails a PR that changes a check script without a
|
|
20
20
|
`bump: minor` change file.
|
|
21
21
|
|
|
22
|
+
## 0.12.0
|
|
23
|
+
- [behavior] `check:crons` on a platform update turn now only warns about a cron route the app already had without a schedule; a route added this turn, or one that lost its schedule, still fails.
|
|
24
|
+
|
|
22
25
|
## 0.11.0
|
|
23
26
|
- [behavior] `check:record-urls` now checks each surface on its own: a record opened on the client portal needs its own `/c` route or `useUrlState`, and an internal dynamic route no longer exempts it. It also catches a list/detail view switch held in `useState`, so a client's request page keeps its own URL that can be reloaded or shared.
|
|
24
27
|
- [guidance] `assembly-app-inner-location` says the record URL rule applies to the client portal too, with `app/c/[id]/page.tsx` or `?id=` through `useUrlState` as the two valid shapes.
|
package/bin/check-crons.mjs
CHANGED
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
// as warnings: those paths have no agent who can act on a failure, so failing
|
|
17
17
|
// them would only strand an app that already worked. A below-floor schedule
|
|
18
18
|
// the app shipped before the turn stays a warning even then (see
|
|
19
|
-
// baselineCronSchedules)
|
|
19
|
+
// baselineCronSchedules), as does a cron route the app shipped with no schedule
|
|
20
|
+
// at all (see baselineUnscheduledRoutes).
|
|
20
21
|
import { readFileSync, readdirSync, existsSync } from 'node:fs';
|
|
21
22
|
import { spawnSync } from 'node:child_process';
|
|
22
23
|
import { join, relative, sep } from 'node:path';
|
|
@@ -42,7 +43,8 @@ const TURN_BASELINE_ENV = 'ASSEMBLY_TURN_BASELINE_SHA';
|
|
|
42
43
|
// Set by the turn program on a platform update turn, where the update itself is
|
|
43
44
|
// the mandate to fix whatever is wrong, so findings fail the build whether or
|
|
44
45
|
// not the turn touched the cron wiring.
|
|
45
|
-
const
|
|
46
|
+
const UPDATE_TURN = process.env.ASSEMBLY_UPDATE_TURN === '1';
|
|
47
|
+
const STRICT = process.argv.includes('--strict') || UPDATE_TURN;
|
|
46
48
|
const CRON_WIRING_PATHS = ['vercel.json', 'app/api/cron/'];
|
|
47
49
|
|
|
48
50
|
function report(lines, { fatal }) {
|
|
@@ -92,6 +94,44 @@ function baselineCronSchedules() {
|
|
|
92
94
|
return schedules;
|
|
93
95
|
}
|
|
94
96
|
|
|
97
|
+
// Cron route files (repo-relative, posix) that existed at the turn baseline with
|
|
98
|
+
// no vercel.json entry invoking them. An update turn has no mandate to schedule
|
|
99
|
+
// work the app never ran, so these only warn. An unreadable baseline yields an
|
|
100
|
+
// empty set, which keeps every unscheduled route fatal.
|
|
101
|
+
function baselineUnscheduledRoutes() {
|
|
102
|
+
const baseline = (process.env[TURN_BASELINE_ENV] ?? '').trim();
|
|
103
|
+
const unscheduled = new Set();
|
|
104
|
+
if (!baseline || !UPDATE_TURN) return unscheduled;
|
|
105
|
+
const listing = spawnSync('git', ['ls-tree', '-r', '--name-only', baseline, '--', 'app'], {
|
|
106
|
+
cwd: ROOT,
|
|
107
|
+
encoding: 'utf8',
|
|
108
|
+
});
|
|
109
|
+
if (listing.status !== 0) return unscheduled;
|
|
110
|
+
// Route groups can hold cron routes too, so list all of app/ and filter by the URL served.
|
|
111
|
+
const routePaths = listing.stdout
|
|
112
|
+
.split('\n')
|
|
113
|
+
.filter((p) => ROUTE_FILE.test(p.split('/').pop()))
|
|
114
|
+
.filter((p) => ('/' + urlSegments(p.split('/').slice(1)).join('/') + '/').startsWith(CRON_ROUTE_PREFIX));
|
|
115
|
+
const shown = spawnSync('git', ['show', `${baseline}:vercel.json`], { cwd: ROOT, encoding: 'utf8' });
|
|
116
|
+
let baselineCrons = [];
|
|
117
|
+
if (shown.status === 0) {
|
|
118
|
+
try {
|
|
119
|
+
const parsed = JSON.parse(shown.stdout).crons;
|
|
120
|
+
if (Array.isArray(parsed)) baselineCrons = parsed;
|
|
121
|
+
} catch {
|
|
122
|
+
// An unparseable baseline file scheduled nothing.
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const scheduledPaths = baselineCrons
|
|
126
|
+
.filter((c) => typeof c?.path === 'string')
|
|
127
|
+
.map((c) => c.path.replace(/\/+$/, '').split('/').filter(Boolean));
|
|
128
|
+
for (const path of routePaths) {
|
|
129
|
+
const segments = urlSegments(path.split('/').slice(1));
|
|
130
|
+
if (!scheduledPaths.some((pathSegments) => routeServes(segments, pathSegments))) unscheduled.add(path);
|
|
131
|
+
}
|
|
132
|
+
return unscheduled;
|
|
133
|
+
}
|
|
134
|
+
|
|
95
135
|
function cronKey(entry) {
|
|
96
136
|
return `${entry?.path}\n${typeof entry?.schedule === 'string' ? entry.schedule.trim() : ''}`;
|
|
97
137
|
}
|
|
@@ -146,8 +186,11 @@ function routeFiles(dir) {
|
|
|
146
186
|
// The URL segments a route file serves; route groups `(group)` and parallel
|
|
147
187
|
// slots `@slot` never appear in the URL.
|
|
148
188
|
function urlSegmentsForRouteFile(file) {
|
|
149
|
-
return relative(APP_DIR, file)
|
|
150
|
-
|
|
189
|
+
return urlSegments(relative(APP_DIR, file).split(sep));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function urlSegments(pathParts) {
|
|
193
|
+
return pathParts
|
|
151
194
|
.slice(0, -1)
|
|
152
195
|
.filter((seg) => !(seg.startsWith('(') && seg.endsWith(')')) && !seg.startsWith('@'));
|
|
153
196
|
}
|
|
@@ -259,6 +302,7 @@ const entries = Array.isArray(crons) ? crons : [];
|
|
|
259
302
|
const coveredRoutes = new Set();
|
|
260
303
|
const shippedSchedules = baselineCronSchedules();
|
|
261
304
|
const preexistingIntervals = [];
|
|
305
|
+
const unscheduledAtBaseline = baselineUnscheduledRoutes();
|
|
262
306
|
|
|
263
307
|
entries.forEach((entry, i) => {
|
|
264
308
|
const label = `vercel.json crons[${i}]`;
|
|
@@ -296,6 +340,14 @@ entries.forEach((entry, i) => {
|
|
|
296
340
|
for (const route of cronRoutes) {
|
|
297
341
|
const url = '/' + route.segments.join('/');
|
|
298
342
|
if (!coveredRoutes.has(route.file)) {
|
|
343
|
+
const rel = relative(ROOT, route.file).split(sep).join('/');
|
|
344
|
+
if (unscheduledAtBaseline.has(rel)) {
|
|
345
|
+
preexistingIntervals.push(
|
|
346
|
+
`${rel} serves ${url} but no vercel.json cron entry invokes it. The app shipped this route unscheduled, ` +
|
|
347
|
+
`and the update does not add a schedule, so leave it as it is.`,
|
|
348
|
+
);
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
299
351
|
problems.push(
|
|
300
352
|
`${relative(ROOT, route.file)} serves ${url} but no vercel.json cron entry invokes it — it will never run ` +
|
|
301
353
|
`after publish. Add { "path": "${url}", "schedule": "*/30 * * * *" } to "crons" or remove the route.`,
|