@ticket-gate/supabase-next 0.0.0-stage → 0.1.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,482 @@
1
+ #!/usr/bin/env node
2
+ // Is the local Supabase stack healthy enough for the gate steps about to run?
3
+ //
4
+ // The pack form. `stackPreflight(cfg)` binds the project values (services map, CLI command, web
5
+ // env file, anon key file, usage line) and returns the checker; a project binds them in a CLI shim
6
+ // or through the pack's stack-preflight command. Every problem line
7
+ // names the command that fixes it. Dependency-free Node ESM, no build step.
8
+ import { execFile, spawn } from 'node:child_process';
9
+ import { existsSync, readFileSync } from 'node:fs';
10
+ import net from 'node:net';
11
+ import { join } from 'node:path';
12
+
13
+ import { dbContainer } from '../mutations/snapshot.mjs';
14
+
15
+ import { safeKillGroup } from './kill-group.mjs';
16
+
17
+ // Re-exported from kill-group.mjs, a leaf module, so a caller that only kills process groups need
18
+ // not load this file.
19
+ export { killGroup, safeKillGroup } from './kill-group.mjs';
20
+
21
+ export const GRACE_S = 60;
22
+ // Reported as a warning only when unhealthy: no gate step needs them.
23
+ export const NEVER_REQUIRED = [
24
+ 'studio',
25
+ 'storage',
26
+ 'edge_runtime',
27
+ 'analytics',
28
+ 'vector',
29
+ ];
30
+ // Never reported at all (a missing imgproxy or pooler is normal).
31
+ export const SILENT = ['imgproxy', 'pooler'];
32
+ const KNOWN = [
33
+ 'db',
34
+ 'kong',
35
+ 'auth',
36
+ 'rest',
37
+ 'realtime',
38
+ 'inbucket',
39
+ 'pg_meta',
40
+ 'studio',
41
+ 'storage',
42
+ 'edge_runtime',
43
+ 'analytics',
44
+ 'vector',
45
+ 'imgproxy',
46
+ 'pooler',
47
+ ];
48
+
49
+ const API_HEALTH = 'http://127.0.0.1:54321/auth/v1/health';
50
+ const REST_URL = 'http://127.0.0.1:54321/rest/v1/';
51
+ const MAILPIT_URL = 'http://127.0.0.1:54324/livez';
52
+ const PG_URL = '127.0.0.1:54322';
53
+
54
+ /** The Supabase project id (config.toml `project_id`); throws as dbContainer does. */
55
+ export function projectId(configToml) {
56
+ return dbContainer(configToml).replace(/^supabase_db_/, '');
57
+ }
58
+
59
+ function uptimeSeconds(status) {
60
+ const m = /^Up (.*?)(?: \(|$)/.exec(status);
61
+ if (!m) return Infinity;
62
+ const t = m[1].toLowerCase();
63
+ if (t.startsWith('less than a second')) return 0;
64
+ if (t.startsWith('about a minute')) return 60;
65
+ if (t.startsWith('about an hour')) return 3600;
66
+ const n = /^(\d+) (second|minute|hour|day|week|month|year)/.exec(t);
67
+ if (!n) return Infinity;
68
+ const unit = {
69
+ second: 1,
70
+ minute: 60,
71
+ hour: 3600,
72
+ day: 86400,
73
+ week: 604800,
74
+ month: 2592000,
75
+ year: 31536000,
76
+ }[n[2]];
77
+ return Number(n[1]) * unit;
78
+ }
79
+
80
+ /** 'healthy' | 'starting' (may settle by itself) | 'stuck' (needs a human). */
81
+ export function classify(status) {
82
+ const s = String(status);
83
+ if (!/^Up/.test(s) || /\(paused\)/i.test(s)) return 'stuck';
84
+ if (/\(health: starting\)/.test(s)) return 'starting';
85
+ if (/\(unhealthy\)/.test(s)) {
86
+ return uptimeSeconds(s) < GRACE_S ? 'starting' : 'stuck';
87
+ }
88
+ return 'healthy';
89
+ }
90
+
91
+ /** Parse `docker ps` text of `supabase_<svc>_<p>\t<status>` lines. `unknown` = lines that do not fit. */
92
+ export function parseDockerPs(text, pid) {
93
+ const services = new Map();
94
+ const unknown = [];
95
+ const prefix = 'supabase_';
96
+ const suffix = `_${pid}`;
97
+ for (const line of String(text).split('\n')) {
98
+ if (line.trim() === '') continue;
99
+ const tab = line.indexOf('\t');
100
+ const name = tab < 0 ? line : line.slice(0, tab);
101
+ const status = tab < 0 ? '' : line.slice(tab + 1).trim();
102
+ const svc =
103
+ tab >= 0 && name.startsWith(prefix) && name.endsWith(suffix)
104
+ ? name.slice(prefix.length, name.length - suffix.length)
105
+ : null;
106
+ if (svc === null || !KNOWN.includes(svc)) unknown.push(line);
107
+ else services.set(svc, status);
108
+ }
109
+ return { services, unknown };
110
+ }
111
+
112
+ function stateOf(status) {
113
+ if (/^Created/.test(status)) return 'created';
114
+ if (/^Exited/.test(status)) return 'exited';
115
+ if (/^Restarting/.test(status)) return 'restarting';
116
+ if (/^Paused|\(paused\)/i.test(status)) return 'paused';
117
+ if (/\(unhealthy\)/.test(status)) return 'unhealthy';
118
+ return 'not running';
119
+ }
120
+
121
+ const bits = (steps, REQUIRED) => {
122
+ const required = new Set();
123
+ for (const s of steps) for (const svc of REQUIRED[s] ?? []) required.add(svc);
124
+ return required;
125
+ };
126
+
127
+ async function collect({ steps }, io, cfg) {
128
+ const { REQUIRED, cli, webEnvMissing } = cfg;
129
+ const STOP_START = `${cli} stop && ${cli} start`;
130
+ const bad = steps.filter((s) => !(s in REQUIRED));
131
+ if (bad.length > 0) {
132
+ throw new Error(
133
+ `unknown step id ${bad.join(', ')}: use ${Object.keys(REQUIRED).join(', ')}`,
134
+ );
135
+ }
136
+ const pid = io.projectId();
137
+ const ps = await io.dockerPs();
138
+ if (!ps.ok) {
139
+ return {
140
+ items: [
141
+ {
142
+ text: `Docker is not running (docker ps did not answer in 5 s): start Docker, then run \`${cli} start\``,
143
+ },
144
+ ],
145
+ warnings: [],
146
+ };
147
+ }
148
+ const text = ps.text;
149
+ const { services, unknown } = parseDockerPs(text, pid);
150
+ const unrecognised = (t) =>
151
+ `unrecognised containers for project ${pid}:\n${String(t).trim()}`;
152
+ // prettier-ignore
153
+ if (unknown.length > 0) return { problems: [unrecognised(text)], warnings: [], transient: false };
154
+ if (services.size === 0) {
155
+ return {
156
+ items: [{ text: `the stack is stopped: run \`${cli} start\`` }],
157
+ warnings: [],
158
+ };
159
+ }
160
+
161
+ const required = bits(steps, REQUIRED);
162
+ const items = [];
163
+ const warnings = [];
164
+ for (const svc of required) {
165
+ const status = services.get(svc);
166
+ if (status === undefined) {
167
+ items.push({
168
+ text: `service ${svc} is missing (no container): run \`${STOP_START}\``,
169
+ });
170
+ continue;
171
+ }
172
+ const cls = classify(status);
173
+ if (cls === 'healthy') continue;
174
+ if (cls === 'starting') {
175
+ items.push({ starting: { svc, status }, text: '' });
176
+ } else if (svc !== 'db' && /\(unhealthy\)/.test(status)) {
177
+ items.push({
178
+ text: `service ${svc} is unhealthy (${status}): run \`docker restart supabase_${svc}_${pid}\``,
179
+ });
180
+ } else {
181
+ items.push({
182
+ text: `service ${svc} is ${stateOf(status)} (${status}): run \`${STOP_START}\``,
183
+ });
184
+ }
185
+ }
186
+ for (const [svc, status] of services) {
187
+ if (required.has(svc) || SILENT.includes(svc)) continue;
188
+ if (classify(status) === 'stuck' && /\(unhealthy\)/.test(status)) {
189
+ warnings.push(
190
+ `warning: service ${svc} is unhealthy (${status}); no gate step needs it`,
191
+ );
192
+ }
193
+ }
194
+
195
+ const wants = (id) => steps.includes(id);
196
+ const probes = [];
197
+ const add = (name, url, command, check) =>
198
+ probes.push({ name, url, command, check });
199
+ if (wants('db') || wants('seed')) {
200
+ add('Postgres', PG_URL, STOP_START, async () => io.tcp(54322));
201
+ }
202
+ if (wants('integration')) {
203
+ add(
204
+ 'Supabase API',
205
+ API_HEALTH,
206
+ STOP_START,
207
+ async () => (await io.probe(API_HEALTH)) === 200,
208
+ );
209
+ add('REST', REST_URL, STOP_START, async () => {
210
+ const key = io.anonKey();
211
+ return (
212
+ (await io.probe(REST_URL, {
213
+ apikey: key,
214
+ Authorization: `Bearer ${key}`,
215
+ })) === 200
216
+ );
217
+ });
218
+ }
219
+ if (wants('e2e') || wants('mutation-registry')) {
220
+ add(
221
+ 'Mailpit',
222
+ MAILPIT_URL,
223
+ `docker restart supabase_inbucket_${pid}`,
224
+ async () => (await io.probe(MAILPIT_URL)) === 200,
225
+ );
226
+ }
227
+ const answers = await Promise.all(probes.map((p) => p.check()));
228
+ probes.forEach((p, i) => {
229
+ if (!answers[i]) {
230
+ items.push({
231
+ text: `${p.name} (${p.url}) does not answer: ${p.command.startsWith('docker') ? `\`${p.command}\`` : `run \`${p.command}\``}`,
232
+ deferrable: true,
233
+ });
234
+ }
235
+ });
236
+ if (wants('e2e') || wants('mutation-registry')) {
237
+ const holder = await io.portHolder(3000);
238
+ if (holder) {
239
+ items.push({
240
+ text: `port 3000 is held by pid ${holder.pid} (cwd ${holder.cwd}): stop that server; the gate starts its own`,
241
+ });
242
+ }
243
+ if (!io.hasWebEnv()) items.push({ text: webEnvMissing });
244
+ }
245
+ return { items, warnings, pid };
246
+ }
247
+
248
+ const startingLine = (svc, status, pid, afterS) =>
249
+ `service ${svc} is starting (${status})${afterS === undefined ? '' : ` after ${afterS} s`}: run \`docker restart supabase_${svc}_${pid}\``;
250
+
251
+ function shape(c, afterS) {
252
+ if (c.problems) return c;
253
+ const problems = c.items.map((i) =>
254
+ i.starting
255
+ ? startingLine(i.starting.svc, i.starting.status, c.pid, afterS)
256
+ : i.text,
257
+ );
258
+ const transient =
259
+ c.items.length > 0 && c.items.every((i) => i.starting || i.deferrable);
260
+ return { problems, warnings: c.warnings, transient };
261
+ }
262
+
263
+ /** One look: { problems, warnings, transient }. `transient` = every problem may settle by itself. */
264
+ export async function checkStack({ steps }, io, cfg) {
265
+ return shape(await collect({ steps }, io, cfg));
266
+ }
267
+
268
+ /** Poll every `everyMs` while all problems are transient, up to `forS` seconds. */
269
+ export async function waitReady(
270
+ { steps },
271
+ io,
272
+ {
273
+ forS,
274
+ everyMs = 2000,
275
+ sleep = (ms) => new Promise((r) => setTimeout(r, ms)),
276
+ now = Date.now,
277
+ },
278
+ cfg,
279
+ ) {
280
+ const start = now();
281
+ for (;;) {
282
+ const c = await collect({ steps }, io, cfg);
283
+ const r = shape(c);
284
+ if (r.problems.length === 0 || !r.transient) return r;
285
+ if (now() - start >= forS * 1000) return shape(c, forS);
286
+ await sleep(everyMs);
287
+ }
288
+ }
289
+
290
+ /** Run `bash -lc cmd` in its own process group; on `limitS` TERM the group, KILL 5 s later. */
291
+ export function runBounded(cmd, { limitS, cwd = process.cwd() } = {}) {
292
+ return new Promise((resolve) => {
293
+ const child = spawn('bash', ['-lc', cmd], {
294
+ cwd,
295
+ detached: true,
296
+ stdio: ['ignore', 'pipe', 'pipe'],
297
+ });
298
+ let out = '';
299
+ let timedOut = false;
300
+ let done = false;
301
+ let timer;
302
+ let hard;
303
+ const swallow = (sig) => safeKillGroup(child.pid, sig);
304
+ const finish = (code) => {
305
+ if (done) return;
306
+ done = true;
307
+ clearTimeout(timer);
308
+ if (!timedOut) clearTimeout(hard);
309
+ resolve({ code: timedOut ? 124 : (code ?? 1), out, timedOut });
310
+ };
311
+ child.stdout.on('data', (d) => (out += d));
312
+ child.stderr.on('data', (d) => (out += d));
313
+ child.on('error', (err) => {
314
+ out += String(err);
315
+ finish(1);
316
+ });
317
+ child.on('exit', (code) => {
318
+ if (timedOut) finish(124);
319
+ else setTimeout(() => finish(code), 200).unref();
320
+ });
321
+ child.on('close', (code) => finish(code));
322
+ if (limitS !== undefined) {
323
+ timer = setTimeout(() => {
324
+ timedOut = true;
325
+ swallow('SIGTERM');
326
+ hard = setTimeout(() => {
327
+ swallow('SIGKILL');
328
+ finish(124);
329
+ }, 5000);
330
+ hard.unref();
331
+ }, limitS * 1000);
332
+ }
333
+ });
334
+ }
335
+
336
+ /** True when any HTTP response arrives; false on a connection error or after 2 s. */
337
+ export async function httpAnswers(url) {
338
+ return (await httpStatus(url)) !== null;
339
+ }
340
+
341
+ /** HTTP status, or null on a connection error or after 2 s. */
342
+ export async function httpStatus(url, headers = {}) {
343
+ try {
344
+ const res = await fetch(url, {
345
+ headers,
346
+ signal: AbortSignal.timeout(2000),
347
+ });
348
+ return res.status;
349
+ } catch {
350
+ return null;
351
+ }
352
+ }
353
+
354
+ const run = (file, args, timeout) =>
355
+ new Promise((resolve) => {
356
+ execFile(file, args, { timeout }, (err, stdout) =>
357
+ resolve({ ok: !err, text: String(stdout ?? '') }),
358
+ );
359
+ });
360
+
361
+ /** The real io: docker, sockets, lsof, files. webEnv: { file }, anonKey: { file, name }. */
362
+ export function realIo(root, webEnv, anonKey) {
363
+ const pid = () =>
364
+ projectId(readFileSync(join(root, 'supabase/config.toml'), 'utf8'));
365
+ return {
366
+ projectId: pid,
367
+ dockerPs: () =>
368
+ run(
369
+ 'docker',
370
+ [
371
+ 'ps',
372
+ '-a',
373
+ '--filter',
374
+ `name=_${pid()}$`,
375
+ '--format',
376
+ '{{.Names}}\t{{.Status}}',
377
+ ],
378
+ 5000,
379
+ ),
380
+ probe: httpStatus,
381
+ tcp: (port) =>
382
+ new Promise((resolve) => {
383
+ const sock = net.connect({ port, host: '127.0.0.1' });
384
+ sock.setTimeout(2000);
385
+ sock.once('connect', () => (sock.destroy(), resolve(true)));
386
+ sock.once('timeout', () => (sock.destroy(), resolve(false)));
387
+ sock.once('error', () => resolve(false));
388
+ }),
389
+ portHolder: async (port) => {
390
+ const l = await run(
391
+ 'lsof',
392
+ ['-nP', `-iTCP:${port}`, '-sTCP:LISTEN', '-t'],
393
+ 3000,
394
+ );
395
+ const holder = l.text.trim().split('\n')[0];
396
+ if (!holder) return null;
397
+ const c = await run(
398
+ 'lsof',
399
+ ['-a', '-p', holder, '-d', 'cwd', '-Fn'],
400
+ 3000,
401
+ );
402
+ const cwd = /^n(.*)$/m.exec(c.text)?.[1] ?? 'unknown';
403
+ return { pid: Number(holder), cwd };
404
+ },
405
+ hasWebEnv: () => existsSync(join(root, webEnv.file)),
406
+ anonKey: () => {
407
+ const m = new RegExp(`^${anonKey.name}=(.+)$`, 'm').exec(
408
+ readFileSync(join(root, anonKey.file), 'utf8'),
409
+ );
410
+ return m ? m[1].trim() : '';
411
+ },
412
+ };
413
+ }
414
+
415
+ /** Parse the CLI args: { steps, wait } or { error }. */
416
+ export function parseArgs(args, REQUIRED) {
417
+ let steps = null;
418
+ let wait = 0;
419
+ for (const a of args) {
420
+ let m;
421
+ if ((m = /^--steps=(.*)$/.exec(a))) {
422
+ steps = m[1].split(',').filter(Boolean);
423
+ const bad = steps.filter((s) => !(s in REQUIRED));
424
+ if (steps.length === 0 || bad.length > 0) {
425
+ return {
426
+ error: `unknown step id ${bad.join(', ') || '(none)'}: use ${Object.keys(REQUIRED).join(', ')}`,
427
+ };
428
+ }
429
+ } else if ((m = /^--wait=(.*)$/.exec(a))) {
430
+ if (!/^\d+$/.test(m[1])) {
431
+ return { error: `--wait=${m[1]} is not a whole number of seconds` };
432
+ }
433
+ wait = Number(m[1]);
434
+ } else return { error: `unknown argument ${a}` };
435
+ }
436
+ if (steps === null) return { error: '--steps is required' };
437
+ return { steps, wait };
438
+ }
439
+
440
+ export async function main(args, io, out, cfg) {
441
+ const parsed = parseArgs(args, cfg.REQUIRED);
442
+ if (parsed.error) {
443
+ out.error(`${parsed.error}\n${cfg.usage}`);
444
+ return 2;
445
+ }
446
+ const r =
447
+ parsed.wait > 0
448
+ ? await waitReady({ steps: parsed.steps }, io, { forS: parsed.wait }, cfg)
449
+ : await checkStack({ steps: parsed.steps }, io, cfg);
450
+ for (const w of r.warnings) out.log(w);
451
+ if (r.problems.length > 0) {
452
+ for (const p of r.problems) out.error(p);
453
+ return 1;
454
+ }
455
+ out.log(`stack ok for: ${parsed.steps.join(', ')}`);
456
+ return 0;
457
+ }
458
+
459
+ /**
460
+ * Bind the project values. services: step id -> service names. webEnv: { file, missing }.
461
+ * anonKey: { file, name }. usage: the line printed after a parse error. cli: the Supabase CLI
462
+ * command used in every fix line.
463
+ */
464
+ export function stackPreflight({
465
+ services,
466
+ root,
467
+ cli = 'supabase',
468
+ webEnv,
469
+ anonKey,
470
+ usage,
471
+ }) {
472
+ const cfg = { REQUIRED: services, cli, webEnvMissing: webEnv.missing, usage };
473
+ const io = (r = root) => realIo(r, webEnv, anonKey);
474
+ return {
475
+ REQUIRED: services,
476
+ checkStack: ({ steps }, i) => checkStack({ steps }, i, cfg),
477
+ waitReady: ({ steps }, i, opts) => waitReady({ steps }, i, opts, cfg),
478
+ parseArgs: (args) => parseArgs(args, services),
479
+ realIo: io,
480
+ main: (args, i = io(), out = console) => main(args, i, out, cfg),
481
+ };
482
+ }
@@ -0,0 +1,29 @@
1
+ // The Supabase stack descriptor a gate config hands the engine. `services` maps a
2
+ // step id to the services that must be healthy; `health` builds the preflight and ready
3
+ // commands the engine runs before the stack steps. Pure: no I/O, no project literal.
4
+
5
+ /**
6
+ * @param {object} cfg
7
+ * @param {Record<string, string[]>} cfg.services step id -> service names
8
+ * @param {string} cfg.cmd the preflight CLI command (no flags)
9
+ * @param {{ preflight: number, ready: number }} [cfg.waits] seconds each command waits
10
+ * @param {unknown} [cfg.reset] passed through untouched, present only when given
11
+ */
12
+ export function supabaseStack({
13
+ services,
14
+ cmd,
15
+ waits = { preflight: 20, ready: 90 },
16
+ reset,
17
+ }) {
18
+ const stack = {
19
+ services,
20
+ health: {
21
+ step: 'preflight',
22
+ preflight: (ids) =>
23
+ `${cmd} --wait=${waits.preflight} --steps=${ids.join(',')}`,
24
+ ready: (ids) => `${cmd} --wait=${waits.ready} --steps=${ids.join(',')}`,
25
+ },
26
+ };
27
+ if (reset !== undefined) stack.reset = reset;
28
+ return stack;
29
+ }
package/commands.mjs ADDED
@@ -0,0 +1,54 @@
1
+ // The commands the supabase-next pack adds to the engine CLI through packCommands. A command
2
+ // exists only when its option is given to supabaseNext; the check modules load on first use.
3
+ const isText = (v) => typeof v === 'string' && v !== '';
4
+
5
+ const PREFLIGHT_KEYS = [
6
+ ['webEnv', 'file'],
7
+ ['webEnv', 'missing'],
8
+ ['anonKey', 'file'],
9
+ ['anonKey', 'name'],
10
+ ];
11
+
12
+ export const DB_SCHEMA_DEFAULTS = Object.freeze({
13
+ out: 'supabase/schema',
14
+ tool: 'ticket-gate db-schema',
15
+ name: 'db-schema',
16
+ usage: 'usage: ticket-gate db-schema [--dump=PATH] [--cron=PATH] [--out=DIR]',
17
+ start: 'supabase start',
18
+ reset: 'supabase db reset',
19
+ });
20
+
21
+ /** `{ [name]: (args, { root, gate }) => Promise<number> }` for the options that were given. */
22
+ export function packCommandsFor({ stackPreflight: preflight, dbSchema: schema } = {}) {
23
+ const commands = {};
24
+ if (preflight !== undefined) {
25
+ if (!PREFLIGHT_KEYS.every(([k, f]) => isText(preflight[k]?.[f])))
26
+ throw new TypeError(
27
+ 'supabase-next: stackPreflight needs webEnv { file, missing } and anonKey { file, name }',
28
+ );
29
+ commands['stack-preflight'] = async (args, { root, gate }) => {
30
+ const { stackPreflight } = await import('./checks/stack-preflight.mjs');
31
+ const services = gate.stacks?.supabase?.services ?? {};
32
+ return stackPreflight({
33
+ usage: `usage: ticket-gate stack-preflight --steps=<${Object.keys(services).join(',')}> [--wait=<sec>]`,
34
+ ...preflight,
35
+ services,
36
+ root,
37
+ }).main(args);
38
+ };
39
+ }
40
+ if (schema !== undefined) {
41
+ if (!isText(schema.header))
42
+ throw new TypeError(
43
+ 'supabase-next: dbSchema needs header, the first line of every generated file',
44
+ );
45
+ commands['db-schema'] = async (args, { root }) => {
46
+ const { dbSchema } = await import('./checks/db-schema.mjs');
47
+ const r = dbSchema({ ...DB_SCHEMA_DEFAULTS, ...schema, root }).run(args);
48
+ for (const l of r.out) console.log(l);
49
+ for (const l of r.err) console.error(l);
50
+ return r.code;
51
+ };
52
+ }
53
+ return commands;
54
+ }
package/index.mjs ADDED
@@ -0,0 +1,41 @@
1
+ // The supabase-next pack: a factory, so gate.config.mjs hands it the project modules it
2
+ // needs (tools.mjs) and the pack imports nothing outside tools/gate/. The version is a
3
+ // digest input (inputState `packs`): bump it when a step's command, selector or summary
4
+ // changes behaviour, and every reusable step re-runs once.
5
+ //
6
+ // The engine reaches the pack only through these hooks (tools/gate/pack-hooks.mjs); a pack
7
+ // without one is a pack that does not do that:
8
+ // mutationKinds { sql } the mutation kinds this pack contributes
9
+ // mutationHarnesses [nextServed(opts)] or [] the served-test harness, when `mutations` is given
10
+ // nightlyPayloads [supabase-stack] the nightly's Supabase payload, only when `nightlyStack` is given
11
+ // sqlRanges { H1_REASON, migrationFacts, migrationReadRanges } migration read ranges
12
+ // packCommands { 'stack-preflight', 'db-schema' } CLI commands, each only when its option is given
13
+ // It also carries `e2eWhen` for gate.config.mjs's e2e step.
14
+ import { packCommandsFor } from './commands.mjs';
15
+ import { bindTools, tools } from './tools.mjs';
16
+ import {
17
+ mutationHarnesses,
18
+ mutationKinds,
19
+ mutationKindsWith,
20
+ } from './mutations/pack.mjs';
21
+ import {
22
+ H1_REASON,
23
+ migrationFacts,
24
+ migrationReadRanges,
25
+ } from './sql-ranges.mjs';
26
+ import { supabaseStack } from './nightly/stack-payload.mjs';
27
+ import { e2eWhen } from './step-defaults.mjs';
28
+
29
+ export function supabaseNext({ tools: project, mutations, sql, stackPreflight, dbSchema, nightlyStack } = {}) {
30
+ bindTools(project);
31
+ return {
32
+ name: 'supabase-next',
33
+ version: '0.0.0-local',
34
+ mutationKinds: sql ? mutationKindsWith(sql) : mutationKinds,
35
+ mutationHarnesses: mutations ? mutationHarnesses(mutations) : [],
36
+ ...(nightlyStack ? { nightlyPayloads: [supabaseStack(nightlyStack)] } : {}),
37
+ sqlRanges: { H1_REASON, migrationFacts, migrationReadRanges },
38
+ packCommands: packCommandsFor({ stackPreflight, dbSchema }),
39
+ e2eWhen,
40
+ };
41
+ }
@@ -0,0 +1,25 @@
1
+ // The next-served harness: `served(opts)` plus the type
2
+ // check that reports a patch which can never pass `next build` before the
3
+ // clean build, and the `--typecheck` CLI hooks.
4
+ import { ROOT } from '#engine/root.mjs';
5
+ import { served } from '#engine/mutations/served.mjs';
6
+ import {
7
+ tsChecker,
8
+ typecheckMain,
9
+ typecheckPatches,
10
+ typecheckSummary,
11
+ } from './next-typecheck.mjs';
12
+
13
+ export function nextServed(opts) {
14
+ const base = served({ name: 'next-served', order: 1, ...opts });
15
+ return {
16
+ ...base,
17
+ ioMethods: (h) => ({
18
+ ...base.ioMethods(h),
19
+ typecheck: (patches) => typecheckPatches(patches, tsChecker(ROOT)),
20
+ }),
21
+ precheck: async (entries, io) =>
22
+ io.typecheck ? await io.typecheck(entries) : new Map(),
23
+ typecheck: { main: typecheckMain, summary: typecheckSummary },
24
+ };
25
+ }