@elench/testkit 0.1.50 → 0.1.52

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/README.md CHANGED
@@ -65,11 +65,14 @@ import {
65
65
  commandStep,
66
66
  defineTestkitSetup,
67
67
  localDatabase,
68
- moduleStep,
69
68
  nextService,
69
+ nodeToolchain,
70
+ schemaSql,
71
+ seedCommand,
72
+ seededDatabaseTemplate,
70
73
  service,
71
- sqlFileStep,
72
74
  tsxService,
75
+ verifyModule,
73
76
  } from "@elench/testkit/setup";
74
77
 
75
78
  export default defineTestkitSetup({
@@ -85,6 +88,13 @@ export default defineTestkitSetup({
85
88
  cacheTtlSeconds: 900,
86
89
  },
87
90
  },
91
+ toolchains: {
92
+ frontendNode: nodeToolchain({
93
+ cwd: "frontend",
94
+ detect: "auto",
95
+ install: "download",
96
+ }),
97
+ },
88
98
  services: {
89
99
  api: service({
90
100
  ...tsxService({
@@ -95,12 +105,12 @@ export default defineTestkitSetup({
95
105
  }),
96
106
  envFiles: [".env.testkit"],
97
107
  database: localDatabase({
98
- template: {
108
+ template: seededDatabaseTemplate({
99
109
  inputs: ["db/schema.sql", "scripts/seed.ts"],
100
- migrate: [sqlFileStep("db/schema.sql")],
101
- seed: [commandStep("npm run db:seed")],
102
- verify: [moduleStep("src/testkit/verify-seed.ts#verifySeed")],
103
- },
110
+ schema: schemaSql("db/schema.sql"),
111
+ seed: seedCommand("npm run db:seed"),
112
+ verify: verifyModule("src/testkit/verify-seed.ts#verifySeed"),
113
+ }),
104
114
  }),
105
115
  runtime: {
106
116
  instances: 1,
@@ -130,6 +140,7 @@ export default defineTestkitSetup({
130
140
  runtime: {
131
141
  instances: 1,
132
142
  maxConcurrentTasks: 2,
143
+ toolchain: "frontendNode",
133
144
  prepare: {
134
145
  inputs: ["frontend/src", "frontend/public", "frontend/package.json"],
135
146
  steps: [commandStep("npm run build", { cwd: "frontend" })],
@@ -164,6 +175,7 @@ for:
164
175
  - multi-service graphs
165
176
  - local runtime instance counts
166
177
  - per-runtime concurrent task caps
178
+ - repo-managed Node toolchains for prepare/start commands
167
179
  - one-time runtime preparation steps for stable shared servers
168
180
  - local DB binding configuration
169
181
  - template database migrate / seed / verify stages
@@ -181,6 +193,60 @@ inputs, and writes cache state under the service runtime directory. This is the
181
193
  right way to move expensive browser targets from `next dev` / watch mode to
182
194
  stable build-and-start flows.
183
195
 
196
+ `database.template` is the database-side equivalent for reusable template DB
197
+ state. It always executes in three explicit phases:
198
+
199
+ - `migrate`
200
+ - `seed`
201
+ - `verify`
202
+
203
+ For most repos, prefer the intent-focused helpers:
204
+
205
+ - `schemaSql(...)`
206
+ - `seedCommand(...)`
207
+ - `seedModule(...)`
208
+ - `verifyCommand(...)`
209
+ - `verifyModule(...)`
210
+ - `seededDatabaseTemplate(...)`
211
+
212
+ Use raw `commandStep(...)`, `sqlFileStep(...)`, and `moduleStep(...)` arrays when
213
+ you need lower-level control over the exact stage layout.
214
+
215
+ `runtime.toolchain` is the first-class way to make those prepare/start commands
216
+ run under the correct Node toolchain instead of whatever `node`/`npm` happened
217
+ to launch `testkit`. Node toolchains support:
218
+
219
+ - host verification mode: `install: "require-host"`
220
+ - cached repo-local provisioning mode: `install: "download"`
221
+ - auto-detection from:
222
+ - `package.json#volta.node`
223
+ - `.nvmrc`
224
+ - `.node-version`
225
+ - `.tool-versions` (`nodejs`)
226
+ - `package.json#engines.node`
227
+ - `package.json#volta.npm`
228
+ - `package.json#packageManager`
229
+ - `package.json#engines.npm`
230
+
231
+ Example:
232
+
233
+ ```ts
234
+ toolchains: {
235
+ frontendNode: nodeToolchain({
236
+ cwd: "frontend",
237
+ detect: "auto",
238
+ install: "download",
239
+ }),
240
+ },
241
+ services: {
242
+ frontend: service({
243
+ runtime: {
244
+ toolchain: "frontendNode",
245
+ },
246
+ }),
247
+ }
248
+ ```
249
+
184
250
  If `reporting.knownFailuresFile` is configured, `testkit` enriches
185
251
  `.testkit/results/latest.json` and `testkit.status.json` with:
186
252
 
@@ -16,6 +16,10 @@ import {
16
16
  normalizeRuntimeInstances,
17
17
  } from "../runner/execution-config.mjs";
18
18
  import { normalizeKnownFailureIssueValidationConfig } from "../known-failures/github.mjs";
19
+ import {
20
+ normalizeRuntimeToolchain,
21
+ normalizeToolchainRegistry,
22
+ } from "../toolchains/index.mjs";
19
23
 
20
24
  const TESTKIT_K6_BIN = "TESTKIT_K6_BIN";
21
25
  const DEFAULT_LOCAL_IMAGE = "pgvector/pgvector:pg16";
@@ -32,6 +36,7 @@ export async function loadConfigs(opts = {}) {
32
36
  const { setup, setupFile } = await loadTestkitSetup(productDir);
33
37
  const execution = normalizeRepoExecution(setup.execution);
34
38
  const reporting = normalizeReportingConfig(setup.reporting);
39
+ const toolchains = normalizeToolchainRegistry(setup.toolchains);
35
40
  const explicitServices = setup.services || {};
36
41
  const discovery = discoverProject(productDir, explicitServices);
37
42
  const serviceNames = new Set([
@@ -49,6 +54,7 @@ export async function loadConfigs(opts = {}) {
49
54
  setupFile,
50
55
  execution,
51
56
  reporting,
57
+ toolchains,
52
58
  explicitService: explicitServices[name] || {},
53
59
  discoveredService: discovery.services[name] || null,
54
60
  suites: discovery.suitesByService[name] || {},
@@ -109,6 +115,7 @@ function normalizeServiceConfig({
109
115
  setupFile,
110
116
  execution,
111
117
  reporting,
118
+ toolchains,
112
119
  explicitService,
113
120
  discoveredService,
114
121
  suites,
@@ -125,7 +132,7 @@ function normalizeServiceConfig({
125
132
  );
126
133
  }
127
134
  const database = normalizeDatabaseConfig(explicitService, name);
128
- const runtime = normalizeRuntimeConfig(explicitService.runtime, name);
135
+ const runtime = normalizeRuntimeConfig(explicitService.runtime, name, toolchains);
129
136
  const skip = normalizeSkipConfig(explicitService.skip, {
130
137
  name,
131
138
  productDir,
@@ -274,7 +281,7 @@ function normalizeDatabaseConfig(explicitService, serviceName) {
274
281
  };
275
282
  }
276
283
 
277
- function normalizeRuntimeConfig(value, serviceName) {
284
+ function normalizeRuntimeConfig(value, serviceName, toolchains) {
278
285
  if (!value) {
279
286
  return {
280
287
  instances: 1,
@@ -283,6 +290,7 @@ function normalizeRuntimeConfig(value, serviceName) {
283
290
  inputs: [],
284
291
  steps: [],
285
292
  },
293
+ toolchain: null,
286
294
  };
287
295
  }
288
296
 
@@ -296,6 +304,11 @@ function normalizeRuntimeConfig(value, serviceName) {
296
304
  `Service "${serviceName}" runtime.maxConcurrentTasks`
297
305
  ),
298
306
  prepare: normalizeRuntimePrepareConfig(value.prepare, serviceName),
307
+ toolchain: normalizeRuntimeToolchain(
308
+ value.toolchain,
309
+ `Service "${serviceName}" runtime.toolchain`,
310
+ toolchains || {}
311
+ ),
299
312
  };
300
313
  }
301
314
 
@@ -739,6 +752,9 @@ function validateServiceConfig({
739
752
  if (local?.cwd) {
740
753
  ensureExistingPath(productDir, local.cwd, `Service "${name}" local.cwd`);
741
754
  }
755
+ if (runtime.toolchain?.cwd) {
756
+ ensureExistingPath(productDir, runtime.toolchain.cwd, `Service "${name}" runtime.toolchain.cwd`);
757
+ }
742
758
  for (const [stageName, steps] of Object.entries(database?.template || {})) {
743
759
  if (stageName === "inputs") continue;
744
760
  for (const step of steps || []) {
@@ -3,6 +3,7 @@ import fs from "fs";
3
3
  import path from "path";
4
4
  import { resolveServiceCwd } from "../config/index.mjs";
5
5
  import { appendFileToHash, appendInputToHash } from "../database/fingerprint.mjs";
6
+ import { announceResolvedToolchain, resolveConfiguredToolchain } from "../toolchains/index.mjs";
6
7
  import { readDatabaseUrl } from "./state-io.mjs";
7
8
  import { buildExecutionEnv } from "./template.mjs";
8
9
  import {
@@ -42,6 +43,7 @@ export async function prepareRuntimeService(config) {
42
43
  }
43
44
 
44
45
  try {
46
+ await announceResolvedToolchain(config, await resolveConfiguredToolchain(config));
45
47
  await runConfiguredSteps({
46
48
  config,
47
49
  steps: prepare.steps,
@@ -61,10 +63,22 @@ export async function prepareRuntimeService(config) {
61
63
  }
62
64
 
63
65
  export async function computeRuntimePrepareFingerprint(config) {
66
+ const resolvedToolchain = await resolveConfiguredToolchain(config);
64
67
  const hash = crypto.createHash("sha256");
65
68
  hash.update(
66
69
  JSON.stringify({
67
70
  prepare: config.testkit.runtime.prepare || null,
71
+ toolchain: resolvedToolchain
72
+ ? {
73
+ kind: resolvedToolchain.kind,
74
+ install: resolvedToolchain.install,
75
+ nodeVersion: resolvedToolchain.nodeVersion,
76
+ npmVersion: resolvedToolchain.npmVersion,
77
+ nodeSource: resolvedToolchain.nodeSource,
78
+ npmSource: resolvedToolchain.npmSource,
79
+ fingerprint: resolvedToolchain.fingerprint,
80
+ }
81
+ : null,
68
82
  serviceEnv: config.testkit.serviceEnv || {},
69
83
  local: config.testkit.local
70
84
  ? {
@@ -1,4 +1,9 @@
1
1
  import { resolveServiceCwd } from "../config/index.mjs";
2
+ import {
3
+ announceResolvedToolchain,
4
+ applyToolchainEnv,
5
+ resolveConfiguredToolchain,
6
+ } from "../toolchains/index.mjs";
2
7
  import { buildExecutionEnv, numericPortFromUrl } from "./template.mjs";
3
8
  import { DEFAULT_READY_TIMEOUT_MS, assertLocalServicePortsAvailable, isPortInUse, waitForReady } from "./readiness.mjs";
4
9
  import { killChildProcess, pipeOutput, startDetachedCommand, stopChildProcess, sleep } from "./processes.mjs";
@@ -23,7 +28,12 @@ export async function startLocalServices(runtimeConfigs, lifecycle) {
23
28
 
24
29
  export async function startLocalService(config, lifecycle) {
25
30
  const cwd = resolveServiceCwd(config.productDir, config.testkit.local.cwd);
26
- const env = buildExecutionEnv(config, config.testkit.local.env, process.env);
31
+ const resolvedToolchain = await resolveConfiguredToolchain(config);
32
+ await announceResolvedToolchain(config, resolvedToolchain);
33
+ const env = applyToolchainEnv(
34
+ buildExecutionEnv(config, config.testkit.local.env, process.env),
35
+ resolvedToolchain
36
+ );
27
37
  const port = config.testkit.local.port || numericPortFromUrl(config.testkit.local.baseUrl);
28
38
  if (port) {
29
39
  env.PORT = String(port);
@@ -0,0 +1,25 @@
1
+ import fs from "fs";
2
+ import { pathToFileURL } from "url";
3
+
4
+ const [, , moduleFile, exportName, contextFile] = process.argv;
5
+
6
+ if (!moduleFile || !exportName || !contextFile) {
7
+ console.error("Usage: node template-step-module-runner.mjs <module-file> <export-name> <context-file>");
8
+ process.exit(1);
9
+ }
10
+
11
+ try {
12
+ const context = JSON.parse(fs.readFileSync(contextFile, "utf8"));
13
+ const moduleRef = await import(pathToFileURL(moduleFile).href);
14
+ const fn = moduleRef[exportName];
15
+ if (typeof fn !== "function") {
16
+ throw new Error(
17
+ `Template module step "${moduleFile}#${exportName}" did not export a function named "${exportName}"`
18
+ );
19
+ }
20
+
21
+ await fn(context);
22
+ } catch (error) {
23
+ console.error(error?.stack || error?.message || String(error));
24
+ process.exit(1);
25
+ }
@@ -3,22 +3,35 @@ import fs from "fs";
3
3
  import path from "path";
4
4
  import { build } from "esbuild";
5
5
  import { execa, execaCommand } from "execa";
6
- import { fileURLToPath, pathToFileURL } from "url";
6
+ import { fileURLToPath } from "url";
7
7
  import { resolveServiceCwd } from "../config/index.mjs";
8
+ import {
9
+ announceResolvedToolchain,
10
+ applyToolchainEnv,
11
+ resolveConfiguredToolchain,
12
+ } from "../toolchains/index.mjs";
8
13
 
9
14
  const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
10
15
  const ROOT_ENTRY = path.join(PACKAGE_ROOT, "lib", "index.mjs");
11
16
  const SETUP_ENTRY = path.join(PACKAGE_ROOT, "lib", "setup", "index.mjs");
12
17
  const RUNTIME_ENTRY = path.join(PACKAGE_ROOT, "lib", "runtime", "index.mjs");
13
18
  const KNOWN_FAILURES_ENTRY = path.join(PACKAGE_ROOT, "lib", "known-failures", "index.mjs");
19
+ const MODULE_RUNNER_ENTRY = path.join(
20
+ PACKAGE_ROOT,
21
+ "lib",
22
+ "runner",
23
+ "template-step-module-runner.mjs"
24
+ );
14
25
 
15
26
  export async function runConfiguredSteps({ config, steps = [], env, labelPrefix }) {
16
27
  if (steps.length === 0) return;
28
+ const resolvedToolchain = await resolveConfiguredToolchain(config);
29
+ await announceResolvedToolchain(config, resolvedToolchain);
17
30
 
18
31
  for (const [index, step] of steps.entries()) {
19
32
  const label = `${labelPrefix}:${config.name}:${index + 1}`;
20
33
  console.log(`\n── ${label} ──`);
21
- await runConfiguredStep(config, step, env);
34
+ await runConfiguredStep(config, step, env, resolvedToolchain);
22
35
  }
23
36
  }
24
37
 
@@ -51,11 +64,14 @@ export function resolveConfiguredPath(productDir, stepCwd, targetPath) {
51
64
  return path.resolve(resolveConfiguredCwd(productDir, stepCwd), targetPath);
52
65
  }
53
66
 
54
- async function runConfiguredStep(config, step, env) {
67
+ async function runConfiguredStep(config, step, env, resolvedToolchain) {
68
+ const runtimeEnv = applyToolchainEnv(env, resolvedToolchain);
69
+ const cwd = resolveConfiguredCwd(config.productDir, step.cwd);
70
+
55
71
  if (step.kind === "command") {
56
72
  await execaCommand(step.cmd, {
57
- cwd: resolveConfiguredCwd(config.productDir, step.cwd),
58
- env,
73
+ cwd,
74
+ env: runtimeEnv,
59
75
  stdio: "inherit",
60
76
  shell: true,
61
77
  });
@@ -66,7 +82,7 @@ async function runConfiguredStep(config, step, env) {
66
82
  await execa(
67
83
  "psql",
68
84
  [
69
- env.DATABASE_URL,
85
+ runtimeEnv.DATABASE_URL,
70
86
  "-v",
71
87
  "ON_ERROR_STOP=1",
72
88
  "-X",
@@ -74,8 +90,8 @@ async function runConfiguredStep(config, step, env) {
74
90
  resolveConfiguredPath(config.productDir, step.cwd, step.path),
75
91
  ],
76
92
  {
77
- cwd: resolveConfiguredCwd(config.productDir, step.cwd),
78
- env,
93
+ cwd,
94
+ env: runtimeEnv,
79
95
  stdio: "inherit",
80
96
  }
81
97
  );
@@ -83,34 +99,41 @@ async function runConfiguredStep(config, step, env) {
83
99
  }
84
100
 
85
101
  if (step.kind === "module") {
86
- const moduleRef = await loadConfiguredModule(config.productDir, step);
102
+ const bundledModule = await bundleConfiguredModule(config.productDir, step);
87
103
  const { exportName } = parseModuleSpecifier(step.specifier);
88
- const fn = moduleRef[exportName];
89
- if (typeof fn !== "function") {
90
- throw new Error(
91
- `Template module step "${step.specifier}" did not export a function named "${exportName}"`
104
+ const context = {
105
+ databaseUrl: runtimeEnv.DATABASE_URL || null,
106
+ productDir: config.productDir,
107
+ cwd,
108
+ serviceName: config.name,
109
+ env: { ...runtimeEnv },
110
+ runtimeId: config.runtimeId || null,
111
+ stateDir: config.stateDir,
112
+ prepareDir: config.testkit.prepareDir || null,
113
+ };
114
+ const contextPath = `${bundledModule.outputFile}.context.json`;
115
+ fs.writeFileSync(contextPath, JSON.stringify(context));
116
+
117
+ try {
118
+ await execa(
119
+ resolvedToolchain?.nodeExecutable || process.execPath,
120
+ [MODULE_RUNNER_ENTRY, bundledModule.outputFile, exportName, contextPath],
121
+ {
122
+ cwd,
123
+ env: runtimeEnv,
124
+ stdio: "inherit",
125
+ }
92
126
  );
127
+ } finally {
128
+ fs.rmSync(contextPath, { force: true });
93
129
  }
94
-
95
- await withProcessContext(resolveConfiguredCwd(config.productDir, step.cwd), env, async () => {
96
- await fn({
97
- databaseUrl: env.DATABASE_URL || null,
98
- productDir: config.productDir,
99
- cwd: resolveConfiguredCwd(config.productDir, step.cwd),
100
- serviceName: config.name,
101
- env: { ...env },
102
- runtimeId: config.runtimeId || null,
103
- stateDir: config.stateDir,
104
- prepareDir: config.testkit.prepareDir || null,
105
- });
106
- });
107
130
  return;
108
131
  }
109
132
 
110
133
  throw new Error(`Unsupported template step kind "${step.kind}"`);
111
134
  }
112
135
 
113
- async function loadConfiguredModule(productDir, step) {
136
+ async function bundleConfiguredModule(productDir, step) {
114
137
  const { modulePath } = parseModuleSpecifier(step.specifier);
115
138
  const absoluteModulePath = resolveConfiguredPath(productDir, step.cwd, modulePath);
116
139
  const bundleDir = path.join(productDir, ".testkit", "_template-steps");
@@ -135,7 +158,10 @@ async function loadConfiguredModule(productDir, step) {
135
158
  plugins: [testkitAliasPlugin()],
136
159
  });
137
160
 
138
- return import(`${pathToFileURL(outputFile).href}?v=${cacheKey}`);
161
+ return {
162
+ outputFile,
163
+ cacheKey,
164
+ };
139
165
  }
140
166
 
141
167
  function buildModuleCacheKey(modulePath) {
@@ -172,20 +198,3 @@ function parseModuleSpecifier(specifier) {
172
198
  exportName: exportName || "default",
173
199
  };
174
200
  }
175
-
176
- async function withProcessContext(cwd, env, fn) {
177
- const previousCwd = process.cwd();
178
- const previousEnv = process.env;
179
- process.chdir(cwd);
180
- process.env = {
181
- ...previousEnv,
182
- ...env,
183
- };
184
-
185
- try {
186
- return await fn();
187
- } finally {
188
- process.chdir(previousCwd);
189
- process.env = previousEnv;
190
- }
191
- }
@@ -1,18 +1,18 @@
1
1
  import type { AuthAdapter, HeaderBuilder, HttpSuiteConfig } from "../index";
2
2
 
3
- export interface LocalDatabaseConfig {
4
- provider: "local";
5
- binding?: "shared" | "per-runtime";
6
- image?: string;
7
- password?: string;
8
- reset?: boolean;
9
- template?: {
10
- inputs?: string[];
11
- migrate?: TemplateLifecycleStepConfig[];
12
- seed?: TemplateLifecycleStepConfig[];
13
- verify?: TemplateLifecycleStepConfig[];
14
- };
15
- user?: string;
3
+ export interface DatabaseTemplateConfig {
4
+ inputs?: string[];
5
+ migrate?: TemplateLifecycleStepConfig[];
6
+ seed?: TemplateLifecycleStepConfig[];
7
+ verify?: TemplateLifecycleStepConfig[];
8
+ }
9
+
10
+ export interface SeededDatabaseTemplateOptions {
11
+ inputs?: string[];
12
+ schema?: string | TemplateSqlFileStepConfig;
13
+ migrate?: TemplateLifecycleStepConfig | TemplateLifecycleStepConfig[];
14
+ seed?: TemplateLifecycleStepConfig | TemplateLifecycleStepConfig[];
15
+ verify?: TemplateLifecycleStepConfig | TemplateLifecycleStepConfig[];
16
16
  }
17
17
 
18
18
  export interface TemplateStepBaseConfig {
@@ -40,6 +40,16 @@ export type TemplateLifecycleStepConfig =
40
40
  | TemplateSqlFileStepConfig
41
41
  | TemplateModuleStepConfig;
42
42
 
43
+ export interface LocalDatabaseConfig {
44
+ provider: "local";
45
+ binding?: "shared" | "per-runtime";
46
+ image?: string;
47
+ password?: string;
48
+ reset?: boolean;
49
+ template?: DatabaseTemplateConfig;
50
+ user?: string;
51
+ }
52
+
43
53
  export interface SkipFileRule {
44
54
  path: string;
45
55
  reason: string;
@@ -62,8 +72,20 @@ export interface RuntimeConfig {
62
72
  inputs?: string[];
63
73
  steps?: TemplateLifecycleStepConfig[];
64
74
  };
75
+ toolchain?: string | NodeToolchainConfig;
65
76
  }
66
77
 
78
+ export interface NodeToolchainConfig {
79
+ kind?: "node";
80
+ cwd?: string;
81
+ detect?: "auto" | "off";
82
+ install?: "require-host" | "download";
83
+ node?: string;
84
+ npm?: string;
85
+ }
86
+
87
+ export type ToolchainConfig = NodeToolchainConfig;
88
+
67
89
  export interface SuiteRequirementRule {
68
90
  selector: string;
69
91
  locks?: string[];
@@ -124,6 +146,7 @@ export interface TestkitSetup {
124
146
  issueValidation?: KnownFailureIssueValidationConfig;
125
147
  };
126
148
  services?: Record<string, ServiceConfig>;
149
+ toolchains?: Record<string, ToolchainConfig>;
127
150
  telemetry?: {
128
151
  enabled?: boolean;
129
152
  endpoint?: string;
@@ -148,6 +171,30 @@ export declare function moduleStep(
148
171
  specifier: string,
149
172
  options?: Omit<TemplateModuleStepConfig, "kind" | "specifier">
150
173
  ): TemplateModuleStepConfig;
174
+ export declare function schemaSql(
175
+ filePath: string,
176
+ options?: Omit<TemplateSqlFileStepConfig, "kind" | "path">
177
+ ): TemplateSqlFileStepConfig;
178
+ export declare function seedCommand(
179
+ cmd: string,
180
+ options?: Omit<TemplateCommandStepConfig, "kind" | "cmd">
181
+ ): TemplateCommandStepConfig;
182
+ export declare function seedModule(
183
+ specifier: string,
184
+ options?: Omit<TemplateModuleStepConfig, "kind" | "specifier">
185
+ ): TemplateModuleStepConfig;
186
+ export declare function verifyCommand(
187
+ cmd: string,
188
+ options?: Omit<TemplateCommandStepConfig, "kind" | "cmd">
189
+ ): TemplateCommandStepConfig;
190
+ export declare function verifyModule(
191
+ specifier: string,
192
+ options?: Omit<TemplateModuleStepConfig, "kind" | "specifier">
193
+ ): TemplateModuleStepConfig;
194
+ export declare function seededDatabaseTemplate(
195
+ options?: SeededDatabaseTemplateOptions
196
+ ): DatabaseTemplateConfig;
197
+ export declare function nodeToolchain(options?: NodeToolchainConfig): NodeToolchainConfig;
151
198
  export declare function goService(options: ServiceConfig["local"] & {
152
199
  command?: string;
153
200
  entrypoint?: string;
@@ -56,6 +56,47 @@ export function moduleStep(specifier, options = {}) {
56
56
  };
57
57
  }
58
58
 
59
+ export function schemaSql(filePath, options = {}) {
60
+ return sqlFileStep(filePath, options);
61
+ }
62
+
63
+ export function seedCommand(cmd, options = {}) {
64
+ return commandStep(cmd, options);
65
+ }
66
+
67
+ export function seedModule(specifier, options = {}) {
68
+ return moduleStep(specifier, options);
69
+ }
70
+
71
+ export function verifyCommand(cmd, options = {}) {
72
+ return commandStep(cmd, options);
73
+ }
74
+
75
+ export function verifyModule(specifier, options = {}) {
76
+ return moduleStep(specifier, options);
77
+ }
78
+
79
+ export function seededDatabaseTemplate(options = {}) {
80
+ const migrate = normalizeTemplateStepList(options.migrate);
81
+ const seed = normalizeTemplateStepList(options.seed);
82
+ const verify = normalizeTemplateStepList(options.verify);
83
+ const schema = normalizeSchemaStep(options.schema);
84
+
85
+ return {
86
+ inputs: Array.isArray(options.inputs) ? [...options.inputs] : undefined,
87
+ migrate: schema ? [schema, ...migrate] : migrate,
88
+ seed,
89
+ verify,
90
+ };
91
+ }
92
+
93
+ export function nodeToolchain(options = {}) {
94
+ return {
95
+ kind: "node",
96
+ ...options,
97
+ };
98
+ }
99
+
59
100
  export function goService(options = {}) {
60
101
  const cwd = options.cwd || ".";
61
102
  const port = requiredNumber(options.port, "goService port");
@@ -211,6 +252,19 @@ function requiredNumber(value, label) {
211
252
  return value;
212
253
  }
213
254
 
255
+ function normalizeTemplateStepList(value) {
256
+ if (value == null) return [];
257
+ return Array.isArray(value) ? [...value] : [value];
258
+ }
259
+
260
+ function normalizeSchemaStep(value) {
261
+ if (value == null) return null;
262
+ if (typeof value === "string") {
263
+ return sqlFileStep(value);
264
+ }
265
+ return value;
266
+ }
267
+
214
268
  function envValue(name) {
215
269
  const env = getRuntimeEnv();
216
270
  const value = env?.rawEnv?.[name] || env?.[name];