@prisma/composer 0.1.0 → 0.2.0-dev.10
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/dist/app-config-CVyObaOe-PF94uVBK.d.mts +419 -0
- package/dist/app-config-CVyObaOe-PF94uVBK.d.mts.map +1 -0
- package/dist/assertions.d.mts +1 -1
- package/dist/assertions.d.mts.map +1 -0
- package/dist/bin.mjs +220 -258
- package/dist/bin.mjs.map +1 -1
- package/dist/casts.d.mts +1 -1
- package/dist/casts.d.mts.map +1 -0
- package/dist/config-BP2DrIX_.d.mts +1 -0
- package/dist/config.d.mts +3 -2
- package/dist/config.mjs +2 -1
- package/dist/config.mjs.map +1 -1
- package/dist/container-transport-DKmKg5JQ-DKWs0ubK.mjs +45 -0
- package/dist/container-transport-DKmKg5JQ-DKWs0ubK.mjs.map +1 -0
- package/dist/deploy-BP2DrIX_.d.mts +1 -0
- package/dist/deploy.d.mts +3 -2
- package/dist/deploy.mjs +181 -23
- package/dist/deploy.mjs.map +1 -1
- package/dist/{dist-zBU8ASQW.mjs → dist-BkSJOwTm.mjs} +16 -4
- package/dist/dist-BkSJOwTm.mjs.map +1 -0
- package/dist/{graph-BYdCQKya-BI0njTow.mjs → graph-CP16cJyH-D5IW87Y-.mjs} +118 -21
- package/dist/graph-CP16cJyH-D5IW87Y-.mjs.map +1 -0
- package/dist/graph-DUSgjmzA-DhtkTqoz.d.mts +18 -0
- package/dist/graph-DUSgjmzA-DhtkTqoz.d.mts.map +1 -0
- package/dist/{config-ob5OhCSP-sP3GW3uu.d.mts → graph-types-BcbVhojU-C3fhxjPK.d.mts} +205 -114
- package/dist/graph-types-BcbVhojU-C3fhxjPK.d.mts.map +1 -0
- package/dist/{index-CZSc9drz.d.mts → index-BFBNz7Aw.d.mts} +5 -20
- package/dist/index-BFBNz7Aw.d.mts.map +1 -0
- package/dist/index.d.mts +4 -3
- package/dist/index.mjs +3 -3
- package/dist/{index-Dh4Zro0y.d.mts → nextjs-DLyeRR7M-DwnBjryZ.d.mts} +6 -5
- package/dist/nextjs-DLyeRR7M-DwnBjryZ.d.mts.map +1 -0
- package/dist/nextjs-control.d.mts +6 -5
- package/dist/nextjs-control.d.mts.map +1 -0
- package/dist/nextjs-control.mjs +9 -11
- package/dist/nextjs-control.mjs.map +1 -1
- package/dist/nextjs.d.mts +2 -2
- package/dist/nextjs.mjs.map +1 -1
- package/dist/node-control.d.mts +5 -3
- package/dist/node-control.d.mts.map +1 -0
- package/dist/node-control.mjs +115 -24
- package/dist/node-control.mjs.map +1 -1
- package/dist/node.d.mts +18 -5
- package/dist/node.d.mts.map +1 -0
- package/dist/node.mjs +2 -1
- package/dist/node.mjs.map +1 -1
- package/dist/report.d.mts +17 -0
- package/dist/report.d.mts.map +1 -0
- package/dist/report.mjs +79 -0
- package/dist/report.mjs.map +1 -0
- package/dist/service-rpc.d.mts +61 -0
- package/dist/service-rpc.d.mts.map +1 -0
- package/dist/service-rpc.mjs +353 -0
- package/dist/service-rpc.mjs.map +1 -0
- package/dist/testing.d.mts +2 -3
- package/dist/testing.d.mts.map +1 -0
- package/package.json +20 -18
- package/src/exports/assertions.ts +1 -0
- package/src/exports/casts.ts +1 -0
- package/src/exports/config.ts +1 -0
- package/src/exports/deploy.ts +1 -0
- package/src/exports/index.ts +1 -0
- package/src/exports/nextjs-control.ts +1 -0
- package/src/exports/nextjs.ts +2 -0
- package/src/exports/node-control.ts +1 -0
- package/src/exports/node.ts +2 -0
- package/src/exports/report.ts +1 -0
- package/src/exports/service-rpc.ts +1 -0
- package/src/exports/testing.ts +1 -0
- package/dist/app-config-BUqyK6N6-CVq3uvHF.d.mts +0 -188
- package/dist/config-BVVgDSdq.d.mts +0 -1
- package/dist/deploy-BVVgDSdq.d.mts +0 -1
- package/dist/dist-zBU8ASQW.mjs.map +0 -1
- package/dist/graph-BYdCQKya-BI0njTow.mjs.map +0 -1
- package/dist/rpc.d.mts +0 -43
- package/dist/rpc.mjs +0 -132
- package/dist/rpc.mjs.map +0 -1
- package/dist/tsdown.d.mts +0 -9
- package/dist/tsdown.mjs +0 -35
- package/dist/tsdown.mjs.map +0 -1
package/dist/bin.mjs
CHANGED
|
@@ -2,19 +2,9 @@
|
|
|
2
2
|
import { Cli, Command, Option, UsageError } from "clipanion";
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import * as path from "node:path";
|
|
5
|
-
import { spawnSync } from "node:child_process";
|
|
6
|
-
import * as Layer from "effect/Layer";
|
|
7
|
-
import { createManagementApiClient } from "@prisma/management-api-sdk";
|
|
8
|
-
import * as Context from "effect/Context";
|
|
9
|
-
import * as Effect from "effect/Effect";
|
|
10
|
-
import * as Redacted from "effect/Redacted";
|
|
11
|
-
import * as Config from "effect/Config";
|
|
12
|
-
import * as Data from "effect/Data";
|
|
13
|
-
import * as Provider from "alchemy/Provider";
|
|
14
|
-
import { Resource } from "alchemy";
|
|
15
|
-
import * as Schedule from "effect/Schedule";
|
|
16
5
|
import * as c12 from "c12";
|
|
17
6
|
import { pathToFileURL } from "node:url";
|
|
7
|
+
import { spawnSync } from "node:child_process";
|
|
18
8
|
//#region ../../0-framework/3-tooling/assemble/dist/index.mjs
|
|
19
9
|
/**
|
|
20
10
|
* A user-facing assembly failure with a message that already names the fix
|
|
@@ -31,49 +21,26 @@ var AssembleError = class extends Error {
|
|
|
31
21
|
}
|
|
32
22
|
};
|
|
33
23
|
/**
|
|
34
|
-
* The
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* patterns — it needs one general rule for what an assembler inlines into
|
|
39
|
-
* the wrapper build besides `@prisma/*` (already the assemblers' own
|
|
40
|
-
* default).
|
|
41
|
-
*
|
|
42
|
-
* The rule: inline everything except the runtime's own modules — `bun`,
|
|
43
|
-
* `bun:*` (e.g. bun:sqlite), and `node:*` builtins — which resolve inside
|
|
44
|
-
* the deploy VM at runtime and must never be bundled. Verified (both
|
|
45
|
-
* `@prisma/composer/node/control` and `@prisma/composer/nextjs/control` set
|
|
46
|
-
* `external: ['bun']` ahead of `noExternal` in their tsdown call, and tsdown/
|
|
47
|
-
* rolldown's explicit `external` wins over a `noExternal` match — proven by
|
|
48
|
-
* building an app's wrapper with this exact pattern: the
|
|
49
|
-
* `bun` import stays external while everything else, including the app's own
|
|
50
|
-
* workspace deps, gets inlined) — see this slice's final report for the
|
|
51
|
-
* storefront-auth wrapper proof (arktype + `@storefront-auth/auth/contract`
|
|
52
|
-
* inlined with no other file changes there).
|
|
53
|
-
*/
|
|
54
|
-
const INLINE_EVERYTHING_EXCEPT_RUNTIME_BUILTINS = [/^(?!bun$)(?!bun:)(?!node:).+/];
|
|
55
|
-
/**
|
|
56
|
-
* The registry route for one service's build: descriptor by
|
|
57
|
-
* `build.extension`, control by `build.type`, kind must be "build". The CLI's
|
|
58
|
-
* coverage validation reports the same misses earlier with the config fix;
|
|
59
|
-
* these errors are the backstop for programmatic callers.
|
|
24
|
+
* The registry route for one service's build: extension by
|
|
25
|
+
* `build.extension`, node descriptor by `build.type`, kind must be "build".
|
|
26
|
+
* The CLI's coverage validation reports the same misses earlier with the
|
|
27
|
+
* config fix; these errors are the backstop for programmatic callers.
|
|
60
28
|
*/
|
|
61
|
-
function
|
|
29
|
+
function buildDescriptorAssemble(config, node, address, cwd) {
|
|
62
30
|
const { extension, type } = node.build;
|
|
63
|
-
const
|
|
64
|
-
if (
|
|
65
|
-
const
|
|
66
|
-
if (
|
|
67
|
-
if (
|
|
68
|
-
return
|
|
31
|
+
const extensionDescriptor = config.extensions.find((candidate) => candidate.id === extension);
|
|
32
|
+
if (extensionDescriptor === void 0) throw new AssembleError(`No extension "${extension}" is configured (needed by service "${node.name}"'s build) — add it to prisma-composer.config.ts's \`extensions\`.`);
|
|
33
|
+
const nodeDescriptor = extensionDescriptor.nodes[type];
|
|
34
|
+
if (nodeDescriptor === void 0) throw new AssembleError(`Extension "${extension}" has no descriptor for build type "${type}" (known: ${Object.keys(extensionDescriptor.nodes).join(", ")}).`);
|
|
35
|
+
if (nodeDescriptor.kind !== "build") throw new AssembleError(`Extension "${extension}"'s descriptor for type "${type}" is a "${nodeDescriptor.kind}" descriptor — assembling a service build needs a "build" descriptor.`);
|
|
36
|
+
return nodeDescriptor.assemble({
|
|
69
37
|
build: node.build,
|
|
70
|
-
wrapperNoExternal: INLINE_EVERYTHING_EXCEPT_RUNTIME_BUILTINS,
|
|
71
38
|
address,
|
|
72
39
|
cwd
|
|
73
40
|
});
|
|
74
41
|
}
|
|
75
42
|
async function assembleServices(graph, config, cwd, run) {
|
|
76
|
-
const runAssembler = run ?? ((node, address, nodeCwd) =>
|
|
43
|
+
const runAssembler = run ?? ((node, address, nodeCwd) => buildDescriptorAssemble(config, node, address, nodeCwd));
|
|
77
44
|
const serviceNodes = graph.nodes.filter((n) => n.node.kind === "service");
|
|
78
45
|
if (serviceNodes.length === 0) throw new AssembleError("The loaded graph has no service to assemble.");
|
|
79
46
|
const bundles = {};
|
|
@@ -121,7 +88,7 @@ function blindCast(input) {
|
|
|
121
88
|
return input;
|
|
122
89
|
}
|
|
123
90
|
//#endregion
|
|
124
|
-
//#region ../../0-framework/1-core/core/dist/graph-
|
|
91
|
+
//#region ../../0-framework/1-core/core/dist/graph-CP16cJyH.mjs
|
|
125
92
|
/** Thrown by Load when the graph is malformed. */
|
|
126
93
|
var LoadError = class extends Error {
|
|
127
94
|
constructor(message) {
|
|
@@ -139,6 +106,20 @@ const SECRET_SOURCE = blindCast(Symbol.for("prisma:secret-source"));
|
|
|
139
106
|
function isSecretSource(value) {
|
|
140
107
|
return typeof value === "object" && value !== null && blindCast(value)[SECRET_SOURCE] === true;
|
|
141
108
|
}
|
|
109
|
+
const PARAM_SOURCE = blindCast(Symbol.for("prisma:param-source"));
|
|
110
|
+
/** True if `value` is a param source (an `envParam` result or a forwarded ctx.params ref). */
|
|
111
|
+
function isParamSource(value) {
|
|
112
|
+
return typeof value === "object" && value !== null && blindCast(value)[PARAM_SOURCE] === true;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Core's grammar for every name that becomes a config-key segment —
|
|
116
|
+
* addresses, input/param/secret names: ASCII letters and digits only.
|
|
117
|
+
* Conservative by design so any target medium — POSIX env-var keys
|
|
118
|
+
* included — can uppercase and "_"-join segments without escaping.
|
|
119
|
+
*/
|
|
120
|
+
function isConfigKeySegment(name) {
|
|
121
|
+
return /^[A-Za-z0-9]+$/.test(name);
|
|
122
|
+
}
|
|
142
123
|
/**
|
|
143
124
|
* True if `value` was constructed by this module's factories. Checks the
|
|
144
125
|
* brand only, never a prototype — a graph may mix nodes from a different
|
|
@@ -238,7 +219,8 @@ function loadService(root, rootId) {
|
|
|
238
219
|
root: rootGraphNode,
|
|
239
220
|
nodes: [...topoSort(nodes, edges), rootGraphNode],
|
|
240
221
|
edges,
|
|
241
|
-
secrets: []
|
|
222
|
+
secrets: [],
|
|
223
|
+
params: []
|
|
242
224
|
};
|
|
243
225
|
}
|
|
244
226
|
/**
|
|
@@ -285,6 +267,8 @@ function producerIdOf(ref) {
|
|
|
285
267
|
const MODULE_INPUT_KEY = Symbol("prisma:module-input-key");
|
|
286
268
|
/** Same per-key identity trick as MODULE_INPUT_KEY, for the parallel `ctx.secrets` forwarding channel. */
|
|
287
269
|
const MODULE_SECRET_KEY = Symbol("prisma:module-secret-key");
|
|
270
|
+
/** Same per-key identity trick as MODULE_INPUT_KEY, for the parallel `ctx.params` forwarding channel. */
|
|
271
|
+
const MODULE_PARAM_KEY = Symbol("prisma:module-param-key");
|
|
288
272
|
/** Whether `ref` carries a callable `satisfies` that accepts `required` truthily. */
|
|
289
273
|
function satisfiesRequired(ref, required) {
|
|
290
274
|
return typeof ref === "object" && ref !== null && "satisfies" in ref && typeof ref.satisfies === "function" && ref.satisfies(required);
|
|
@@ -344,6 +328,29 @@ function validateSecretBinding(child, id, secretWiring, enclosingModuleName) {
|
|
|
344
328
|
for (const slot of Object.keys(secretWiring)) if (!Object.hasOwn(child.secretSlots, slot)) throw new LoadError(`The secrets for "${id}" name "${slot}", which is not a secret slot of that ${kind} (module "${enclosingModuleName}").`);
|
|
345
329
|
}
|
|
346
330
|
/**
|
|
331
|
+
* Checks the params wired into a provisioned SERVICE: unlike a secret slot, a
|
|
332
|
+
* declared param is never required to be bound here — it may fall back to its
|
|
333
|
+
* own `default` (checked later, at `buildConfig`) — so this only rejects a
|
|
334
|
+
* wired key that names something other than a real declared param.
|
|
335
|
+
*/
|
|
336
|
+
function validateServiceParamBinding(child, id, paramWiring, enclosingModuleName) {
|
|
337
|
+
for (const slot of Object.keys(paramWiring)) if (!Object.hasOwn(child.params, slot)) throw new LoadError(`The params for "${id}" name "${slot}", which is not a param of that service (module "${enclosingModuleName}").`);
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Checks the params wired into a provisioned MODULE: every declared
|
|
341
|
+
* param-forwarding slot must be bound to a real `ParamSource` — a slot has no
|
|
342
|
+
* schema of its own, so (unlike a service param) it cannot fall back to a
|
|
343
|
+
* literal or a default; the param analog of `validateSecretBinding`.
|
|
344
|
+
*/
|
|
345
|
+
function validateParamNeedBinding(child, id, paramWiring, enclosingModuleName) {
|
|
346
|
+
for (const slot of Object.keys(child.paramSlots)) {
|
|
347
|
+
const bound = paramWiring[slot];
|
|
348
|
+
if (bound === void 0) throw new LoadError(`Param slot "${slot}" of provisioned module "${id}" is not bound (module "${enclosingModuleName}") — bind it with a param source (e.g. envParam('NAME')) or forward ctx.params.`);
|
|
349
|
+
if (!isParamSource(bound)) throw new LoadError(`Param slot "${slot}" of "${id}" (module "${enclosingModuleName}") was wired with a non-source value — a module param-forwarding slot carries no schema to validate a literal against; use envParam('NAME') or a forwarded ctx.params ref.`);
|
|
350
|
+
}
|
|
351
|
+
for (const slot of Object.keys(paramWiring)) if (!Object.hasOwn(child.paramSlots, slot)) throw new LoadError(`The params for "${id}" name "${slot}", which is not a param slot of that module (module "${enclosingModuleName}").`);
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
347
354
|
* Recursively flattens one module's body into the shared graph state and
|
|
348
355
|
* returns its resolved ModuleOutputs (one ref-port per expose key) for the
|
|
349
356
|
* caller (the enclosing provision() call, or Load itself for the root) to
|
|
@@ -357,10 +364,11 @@ function validateSecretBinding(child, id, secretWiring, enclosingModuleName) {
|
|
|
357
364
|
* forward in a producer provisioned by an ancestor scope, and it is the
|
|
358
365
|
* shared `byId` (keyed by full address) that lets that resolve.
|
|
359
366
|
*/
|
|
360
|
-
function flatten(moduleNode, address, wiring, secretWiring, nodes, edges, pending, secretBindings, byId) {
|
|
367
|
+
function flatten(moduleNode, address, wiring, secretWiring, paramWiring, nodes, edges, pending, secretBindings, paramBindings, byId) {
|
|
361
368
|
const localIds = /* @__PURE__ */ new Set();
|
|
362
369
|
const used = /* @__PURE__ */ new Set();
|
|
363
370
|
const usedSecrets = /* @__PURE__ */ new Set();
|
|
371
|
+
const usedParams = /* @__PURE__ */ new Set();
|
|
364
372
|
const ctxInputs = {};
|
|
365
373
|
for (const key of Object.keys(moduleNode.deps)) {
|
|
366
374
|
const wired = wiring[key];
|
|
@@ -383,12 +391,24 @@ function flatten(moduleNode, address, wiring, secretWiring, nodes, edges, pendin
|
|
|
383
391
|
const markSecretsUsed = (values) => {
|
|
384
392
|
for (const value of Object.values(values)) for (const key of Object.keys(ctxSecrets)) if (value === ctxSecrets[key]) usedSecrets.add(key);
|
|
385
393
|
};
|
|
394
|
+
const ctxParams = {};
|
|
395
|
+
for (const key of Object.keys(moduleNode.paramSlots)) {
|
|
396
|
+
const bound = paramWiring[key];
|
|
397
|
+
ctxParams[key] = typeof bound === "object" && bound !== null ? {
|
|
398
|
+
...bound,
|
|
399
|
+
[MODULE_PARAM_KEY]: key
|
|
400
|
+
} : bound;
|
|
401
|
+
}
|
|
402
|
+
const markParamsUsed = (values) => {
|
|
403
|
+
for (const value of Object.values(values)) for (const key of Object.keys(ctxParams)) if (value === ctxParams[key]) usedParams.add(key);
|
|
404
|
+
};
|
|
386
405
|
const provision = (child, opts) => {
|
|
387
406
|
const id = opts?.id ?? child.name;
|
|
388
407
|
const provisionWiring = opts?.deps;
|
|
389
408
|
const provisionSecrets = opts?.secrets;
|
|
409
|
+
const provisionParams = opts?.params;
|
|
390
410
|
if (typeof id !== "string" || id.length === 0) throw new LoadError(`provision() requires a non-empty id (module "${moduleNode.name}").`);
|
|
391
|
-
if (
|
|
411
|
+
if (!isConfigKeySegment(id)) throw new LoadError(`provision() id "${id}" (module "${moduleNode.name}") must contain only ASCII letters and digits ([A-Za-z0-9]) — the id becomes an address segment, and addresses derive deterministic config keys (segments uppercased and joined with "_"). "${id}" would put "${id.toUpperCase()}" inside the derived key. ("_" collides with the key separator and "." with the address separator.)`);
|
|
392
412
|
if (localIds.has(id)) throw new LoadError(`Duplicate provision id "${id}" in module "${moduleNode.name}".`);
|
|
393
413
|
const untrusted = child;
|
|
394
414
|
if (!isNode(untrusted) || untrusted.kind !== "service" && untrusted.kind !== "resource" && untrusted.kind !== "module") throw new LoadError(`provision("${id}") expects a branded service, resource, or module node (construct it with the service()/resource()/module() factories or a pack's own).`);
|
|
@@ -397,6 +417,7 @@ function flatten(moduleNode, address, wiring, secretWiring, nodes, edges, pendin
|
|
|
397
417
|
if (child.kind === "resource") {
|
|
398
418
|
if (provisionWiring !== void 0) throw new LoadError(`provision("${id}") received deps for a resource — a resource has no dependency slots to satisfy.`);
|
|
399
419
|
if (provisionSecrets !== void 0) throw new LoadError(`provision("${id}") received secrets for a resource — a resource has no secret slots to satisfy.`);
|
|
420
|
+
if (provisionParams !== void 0) throw new LoadError(`provision("${id}") received params for a resource — a resource has no params to satisfy.`);
|
|
400
421
|
if (child.type.length === 0) throw new LoadError(`provision("${id}") received a resource with an empty node type.`);
|
|
401
422
|
byId.set(fullAddress, child);
|
|
402
423
|
nodes.push({
|
|
@@ -410,6 +431,8 @@ function flatten(moduleNode, address, wiring, secretWiring, nodes, edges, pendin
|
|
|
410
431
|
const localSecrets = { ...provisionSecrets ?? {} };
|
|
411
432
|
markSecretsUsed(localSecrets);
|
|
412
433
|
validateSecretBinding(child, id, localSecrets, moduleNode.name);
|
|
434
|
+
const localParams = { ...provisionParams ?? {} };
|
|
435
|
+
markParamsUsed(localParams);
|
|
413
436
|
if (child.kind === "service") {
|
|
414
437
|
for (const slot of Object.keys(child.secretSlots)) {
|
|
415
438
|
const bound = localSecrets[slot];
|
|
@@ -419,6 +442,12 @@ function flatten(moduleNode, address, wiring, secretWiring, nodes, edges, pendin
|
|
|
419
442
|
source: bound
|
|
420
443
|
});
|
|
421
444
|
}
|
|
445
|
+
validateServiceParamBinding(child, id, localParams, moduleNode.name);
|
|
446
|
+
for (const [slot, binding] of Object.entries(localParams)) paramBindings.push({
|
|
447
|
+
serviceAddress: fullAddress,
|
|
448
|
+
slot,
|
|
449
|
+
binding
|
|
450
|
+
});
|
|
422
451
|
const inputs = serviceInputs(child, fullAddress);
|
|
423
452
|
nodes.push(...inputs.nodes, {
|
|
424
453
|
id: fullAddress,
|
|
@@ -435,6 +464,7 @@ function flatten(moduleNode, address, wiring, secretWiring, nodes, edges, pendin
|
|
|
435
464
|
byId.set(fullAddress, child);
|
|
436
465
|
return refFor(fullAddress, child);
|
|
437
466
|
}
|
|
467
|
+
validateParamNeedBinding(child, id, localParams, moduleNode.name);
|
|
438
468
|
edges.push(...wiringEdges(localWiring, fullAddress));
|
|
439
469
|
pending.push({
|
|
440
470
|
deps: child.deps,
|
|
@@ -443,7 +473,7 @@ function flatten(moduleNode, address, wiring, secretWiring, nodes, edges, pendin
|
|
|
443
473
|
targetKind: "module",
|
|
444
474
|
enclosingModuleName: moduleNode.name
|
|
445
475
|
});
|
|
446
|
-
const childOutputs = flatten(child, fullAddress, localWiring, localSecrets, nodes, edges, pending, secretBindings, byId);
|
|
476
|
+
const childOutputs = flatten(child, fullAddress, localWiring, localSecrets, localParams, nodes, edges, pending, secretBindings, paramBindings, byId);
|
|
447
477
|
nodes.push({
|
|
448
478
|
id: fullAddress,
|
|
449
479
|
node: child
|
|
@@ -457,12 +487,14 @@ function flatten(moduleNode, address, wiring, secretWiring, nodes, edges, pendin
|
|
|
457
487
|
const ctx = blindCast({
|
|
458
488
|
inputs: ctxInputs,
|
|
459
489
|
secrets: ctxSecrets,
|
|
490
|
+
params: ctxParams,
|
|
460
491
|
provision: blindCast(provision)
|
|
461
492
|
});
|
|
462
493
|
const outputs = blindCast(moduleNode.body(ctx) ?? {});
|
|
463
494
|
markUsed(outputs);
|
|
464
495
|
for (const key of Object.keys(moduleNode.deps)) if (!used.has(key)) throw new LoadError(`Module "${moduleNode.name}" declares input "${key}" but never forwards it into a provision nor returns it as an output.`);
|
|
465
496
|
for (const key of Object.keys(moduleNode.secretSlots)) if (!usedSecrets.has(key)) throw new LoadError(`Module "${moduleNode.name}" declares secret "${key}" but never forwards it into a provision.`);
|
|
497
|
+
for (const key of Object.keys(moduleNode.paramSlots)) if (!usedParams.has(key)) throw new LoadError(`Module "${moduleNode.name}" declares param "${key}" but never forwards it into a provision.`);
|
|
466
498
|
for (const [key, contract] of Object.entries(moduleNode.expose)) {
|
|
467
499
|
const port = outputs[key];
|
|
468
500
|
if (port === void 0) throw new LoadError(`Module "${moduleNode.name}" declares expose "${key}" but its body did not return a port for it.`);
|
|
@@ -482,12 +514,18 @@ function loadModule(root, opts) {
|
|
|
482
514
|
const names = rootSecretKeys.map((k) => `"${k}"`).join(", ");
|
|
483
515
|
throw new LoadError(`Module "${root.name}" declares secret${rootSecretKeys.length > 1 ? "s" : ""} ${names} but is being deployed as the root — a root has no enclosing scope to bind them; the root binds secrets with envSecret('NAME'), it does not declare secret slots of its own.`);
|
|
484
516
|
}
|
|
517
|
+
const rootParamKeys = Object.keys(root.paramSlots);
|
|
518
|
+
if (rootParamKeys.length > 0) {
|
|
519
|
+
const names = rootParamKeys.map((k) => `"${k}"`).join(", ");
|
|
520
|
+
throw new LoadError(`Module "${root.name}" declares param${rootParamKeys.length > 1 ? "s" : ""} ${names} but is being deployed as the root — a root has no enclosing scope to bind them; the root binds params directly on each provision() call, it does not declare param-forwarding slots of its own.`);
|
|
521
|
+
}
|
|
485
522
|
const nodes = [];
|
|
486
523
|
const edges = [];
|
|
487
524
|
const pending = [];
|
|
488
525
|
const secretBindings = [];
|
|
526
|
+
const paramBindings = [];
|
|
489
527
|
const byId = /* @__PURE__ */ new Map();
|
|
490
|
-
flatten(root, void 0, {}, {}, nodes, edges, pending, secretBindings, byId);
|
|
528
|
+
flatten(root, void 0, {}, {}, {}, nodes, edges, pending, secretBindings, paramBindings, byId);
|
|
491
529
|
for (const entry of pending) validateWiring(entry, byId);
|
|
492
530
|
assertDependencyDag(edges);
|
|
493
531
|
const rootGraphNode = {
|
|
@@ -498,7 +536,8 @@ function loadModule(root, opts) {
|
|
|
498
536
|
root: rootGraphNode,
|
|
499
537
|
nodes: [...topoSort(nodes, edges), rootGraphNode],
|
|
500
538
|
edges,
|
|
501
|
-
secrets: secretBindings
|
|
539
|
+
secrets: secretBindings,
|
|
540
|
+
params: paramBindings
|
|
502
541
|
};
|
|
503
542
|
}
|
|
504
543
|
/**
|
|
@@ -545,145 +584,34 @@ function Load(root, opts) {
|
|
|
545
584
|
throw new LoadError("Load expects a service or module root (received another node kind).");
|
|
546
585
|
}
|
|
547
586
|
//#endregion
|
|
548
|
-
//#region ../../
|
|
549
|
-
/**
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
}));
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
*/
|
|
563
|
-
|
|
564
|
-
const
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
try: f,
|
|
572
|
-
catch: (cause) => new PrismaApiError({
|
|
573
|
-
status: 0,
|
|
574
|
-
message: String(cause)
|
|
575
|
-
})
|
|
576
|
-
});
|
|
577
|
-
const fail = (r) => Effect.fail(new PrismaApiError({
|
|
578
|
-
status: r.response.status,
|
|
579
|
-
message: JSON.stringify(r.error)
|
|
580
|
-
}));
|
|
581
|
-
/** Unwrap `data`, failing on any API error. Preserves the SDK's response type. */
|
|
582
|
-
const call = (f) => attempt(f).pipe(Effect.flatMap((r) => r.error !== void 0 || r.data === void 0 ? fail(r) : Effect.succeed(r.data)));
|
|
583
|
-
/** Fire-and-forget a call, tolerating a 404 (already deleted). */
|
|
584
|
-
const callVoid = (f) => attempt(f).pipe(Effect.flatMap((r) => r.response.status === 404 || r.error === void 0 ? Effect.void : fail(r)));
|
|
585
|
-
Schedule.both(Schedule.exponential("2 seconds", 2), Schedule.during("5 minutes"));
|
|
586
|
-
Resource("Prisma.ComputeService");
|
|
587
|
-
Resource("Prisma.Deployment");
|
|
588
|
-
Resource("Prisma.EnvironmentVariable");
|
|
589
|
-
Resource("Prisma.Connection");
|
|
590
|
-
Resource("Prisma.Database");
|
|
591
|
-
Resource("Prisma.Project");
|
|
592
|
-
//#endregion
|
|
593
|
-
//#region ../../1-prisma-cloud/0-lowering/lowering/dist/index.mjs
|
|
594
|
-
/** Raised with `ensure: false` when the app's Project (or a named stage's Branch) doesn't exist. */
|
|
595
|
-
var ContainerNotFoundError = class extends Data.TaggedError("ContainerNotFoundError") {};
|
|
596
|
-
const listAllProjects = (client) => Effect.gen(function* () {
|
|
597
|
-
const projects = [];
|
|
598
|
-
let cursor;
|
|
599
|
-
for (;;) {
|
|
600
|
-
const query = cursor === void 0 ? {} : { cursor };
|
|
601
|
-
const page = yield* call(() => client.GET("/v1/projects", { params: { query } }));
|
|
602
|
-
projects.push(...page.data);
|
|
603
|
-
if (!page.pagination.hasMore || page.pagination.nextCursor === null) break;
|
|
604
|
-
cursor = page.pagination.nextCursor;
|
|
587
|
+
//#region ../../0-framework/1-core/core/dist/container-transport-DKmKg5JQ.mjs
|
|
588
|
+
/** '@prisma/composer-prisma-cloud' → 'PRISMA_COMPOSER_CONTAINER_PRISMA_COMPOSER_PRISMA_CLOUD' */
|
|
589
|
+
function containerEnvVarName(extensionId) {
|
|
590
|
+
return `PRISMA_COMPOSER_CONTAINER_${extensionId.toUpperCase().replace(/[^A-Z0-9]+/g, "_").replace(/^_+|_+$/g, "")}`;
|
|
591
|
+
}
|
|
592
|
+
function collisionError(a, b, varName) {
|
|
593
|
+
return /* @__PURE__ */ new Error(`Extension ids "${a}" and "${b}" both mangle to the container transport variable "${varName}" — rename one of the extensions.`);
|
|
594
|
+
}
|
|
595
|
+
function emptySerializeError(extensionId) {
|
|
596
|
+
return /* @__PURE__ */ new Error(`Extension "${extensionId}"'s container instance serialized to an empty string — ContainerInstance.serialize() must return a non-empty string.`);
|
|
597
|
+
}
|
|
598
|
+
/** The env entries the CLI sets on the alchemy process: `{ [containerEnvVarName(id)]: instance.serialize() }` for every resolved instance. */
|
|
599
|
+
function containerEnv(instances) {
|
|
600
|
+
const env = {};
|
|
601
|
+
const ownerByVarName = /* @__PURE__ */ new Map();
|
|
602
|
+
for (const [extensionId, instance] of instances) {
|
|
603
|
+
const varName = containerEnvVarName(extensionId);
|
|
604
|
+
const owner = ownerByVarName.get(varName);
|
|
605
|
+
if (owner !== void 0) throw collisionError(owner, extensionId, varName);
|
|
606
|
+
ownerByVarName.set(varName, extensionId);
|
|
607
|
+
const serialized = instance.serialize();
|
|
608
|
+
if (serialized.length === 0) throw emptySerializeError(extensionId);
|
|
609
|
+
env[varName] = serialized;
|
|
605
610
|
}
|
|
606
|
-
return
|
|
607
|
-
}
|
|
608
|
-
/**
|
|
609
|
-
* Workspace ids circulate in two shapes: `wksp_`-prefixed and bare. Compare
|
|
610
|
-
* bare-to-bare so a `wksp_`-prefixed API id still matches a bare configured
|
|
611
|
-
* one (the same normalization `state/bootstrap.ts` applies to the same
|
|
612
|
-
* `/v1/projects` listing).
|
|
613
|
-
*/
|
|
614
|
-
const bareWorkspaceId = (id) => id.startsWith("wksp_") ? id.slice(5) : id;
|
|
615
|
-
/**
|
|
616
|
-
* Finds the app's Project by name in the workspace — PDP allows duplicate
|
|
617
|
-
* project names, so more than one can match; the oldest wins. Creates one
|
|
618
|
-
* if none match, unless `ensure` is `false` (find-only — `destroy`), in
|
|
619
|
-
* which case an absent Project fails with `ContainerNotFoundError`. No
|
|
620
|
-
* ownership marker and no `--project` override (both deferred — see
|
|
621
|
-
* ADR-0019).
|
|
622
|
-
*/
|
|
623
|
-
const resolveProject = (client, workspaceId, appName, ensure) => Effect.gen(function* () {
|
|
624
|
-
const oldest = (yield* listAllProjects(client)).filter((p) => bareWorkspaceId(p.workspace.id) === bareWorkspaceId(workspaceId) && p.name === appName).sort((a, b) => a.createdAt.localeCompare(b.createdAt))[0];
|
|
625
|
-
if (oldest !== void 0) return oldest.id;
|
|
626
|
-
if (!ensure) return yield* Effect.fail(new ContainerNotFoundError({ appName }));
|
|
627
|
-
return (yield* call(() => client.POST("/v1/projects", { body: {
|
|
628
|
-
name: appName,
|
|
629
|
-
workspaceId
|
|
630
|
-
} }))).data.id;
|
|
631
|
-
});
|
|
632
|
-
const findBranchId = (client, projectId, gitName) => call(() => client.GET("/v1/projects/{projectId}/branches", { params: {
|
|
633
|
-
path: { projectId },
|
|
634
|
-
query: { gitName }
|
|
635
|
-
} })).pipe(Effect.map((page) => page.data[0]?.id));
|
|
636
|
-
/**
|
|
637
|
-
* Finds the stage's Branch by its exact `gitName`, creating it if absent
|
|
638
|
-
* unless `ensure` is `false` (find-only — `destroy`), in which case an
|
|
639
|
-
* absent Branch fails with `ContainerNotFoundError`. The Management API has
|
|
640
|
-
* no server-side "create-or-return" idempotency (`POST
|
|
641
|
-
* /v1/projects/:id/branches` 409s on a duplicate `gitName`, with no request
|
|
642
|
-
* field to make that a no-op), so idempotency is client-side: observe
|
|
643
|
-
* first, and on a racing 409 from create, re-observe rather than fail.
|
|
644
|
-
*/
|
|
645
|
-
const resolveBranch = (client, projectId, gitName, appName, ensure) => Effect.gen(function* () {
|
|
646
|
-
const existing = yield* findBranchId(client, projectId, gitName);
|
|
647
|
-
if (existing !== void 0) return existing;
|
|
648
|
-
if (!ensure) return yield* Effect.fail(new ContainerNotFoundError({
|
|
649
|
-
appName,
|
|
650
|
-
stage: gitName
|
|
651
|
-
}));
|
|
652
|
-
return yield* call(() => client.POST("/v1/projects/{projectId}/branches", {
|
|
653
|
-
params: { path: { projectId } },
|
|
654
|
-
body: { gitName }
|
|
655
|
-
})).pipe(Effect.map((r) => r.data.id), Effect.catch((err) => err.status === 409 ? findBranchId(client, projectId, gitName).pipe(Effect.flatMap((id) => id === void 0 ? Effect.fail(err) : Effect.succeed(id))) : Effect.fail(err)));
|
|
656
|
-
});
|
|
657
|
-
/**
|
|
658
|
-
* Resolves the two containers a stage's deploy runs into (ADR-0019): the
|
|
659
|
-
* app's **Project**, found-or-created by name, and — for a named stage
|
|
660
|
-
* only — its **Branch**, found-or-created by `gitName`. The default stage
|
|
661
|
-
* (no `stage`) creates no Branch; `branchId` is omitted. With `ensure:
|
|
662
|
-
* false` (`destroy`), nothing is created — an absent Project or Branch
|
|
663
|
-
* fails with `ContainerNotFoundError` instead.
|
|
664
|
-
*/
|
|
665
|
-
const resolveContainer = (opts) => Effect.gen(function* () {
|
|
666
|
-
const client = yield* ManagementClient;
|
|
667
|
-
const ensure = opts.ensure ?? true;
|
|
668
|
-
const projectId = yield* resolveProject(client, opts.workspaceId, opts.appName, ensure);
|
|
669
|
-
if (opts.stage === void 0) return { projectId };
|
|
670
|
-
return {
|
|
671
|
-
projectId,
|
|
672
|
-
branchId: yield* resolveBranch(client, projectId, opts.stage, opts.appName, ensure)
|
|
673
|
-
};
|
|
674
|
-
});
|
|
675
|
-
/**
|
|
676
|
-
* Soft-deletes a Branch. Tolerates a 404 (already gone). The API refuses if
|
|
677
|
-
* the Branch still has live members or is the production/default Branch —
|
|
678
|
-
* that surfaces as a `PrismaApiError`.
|
|
679
|
-
*/
|
|
680
|
-
const deleteBranch = (branchId) => Effect.gen(function* () {
|
|
681
|
-
const client = yield* ManagementClient;
|
|
682
|
-
yield* callVoid(() => client.DELETE("/v1/branches/{branchId}", { params: { path: { branchId } } }));
|
|
683
|
-
});
|
|
684
|
-
Provider.ProviderCollection()("Prisma");
|
|
611
|
+
return env;
|
|
612
|
+
}
|
|
685
613
|
//#endregion
|
|
686
|
-
//#region ../../0-framework/3-tooling/cli/dist/cli-
|
|
614
|
+
//#region ../../0-framework/3-tooling/cli/dist/cli-riJGhp7G.mjs
|
|
687
615
|
/**
|
|
688
616
|
* A user-facing failure with a message that already names the fix (deploy-cli.md
|
|
689
617
|
* § Error surface). `bin.ts` catches this — and any other Error, including
|
|
@@ -695,59 +623,6 @@ var CliError = class extends Error {
|
|
|
695
623
|
this.name = "CliError";
|
|
696
624
|
}
|
|
697
625
|
};
|
|
698
|
-
/**
|
|
699
|
-
* Pipeline pre-stack step: resolves the app's Project + (named stage) Branch
|
|
700
|
-
* via `@internal/lowering`'s `resolveContainer`, before the generated stack file
|
|
701
|
-
* runs — `deploy` creates-if-absent, `destroy` finds only.
|
|
702
|
-
*/
|
|
703
|
-
/** Validates `stage` as a git ref name via `git check-ref-format` — no silent normalization. */
|
|
704
|
-
function validateStageName(stage) {
|
|
705
|
-
const result = spawnSync("git", ["check-ref-format", `refs/heads/${stage}`], { stdio: "ignore" });
|
|
706
|
-
if (result.error) throw new CliError(`git is required to validate --stage "${stage}" (git check-ref-format): ${result.error.message}.`);
|
|
707
|
-
if (result.status !== 0) throw new CliError(`Invalid --stage "${stage}": must be a valid git ref name (git check-ref-format rejected "refs/heads/${stage}").`);
|
|
708
|
-
}
|
|
709
|
-
async function ensureContainers(input, deps) {
|
|
710
|
-
const env = input.env ?? process.env;
|
|
711
|
-
const workspaceId = env["PRISMA_WORKSPACE_ID"];
|
|
712
|
-
if (workspaceId === void 0 || workspaceId.length === 0) throw new CliError("environment variable PRISMA_WORKSPACE_ID is required.");
|
|
713
|
-
if (deps?.client === void 0 && (env["PRISMA_SERVICE_TOKEN"] ?? "").length === 0) throw new CliError("environment variable PRISMA_SERVICE_TOKEN is required.");
|
|
714
|
-
if (input.stage !== void 0) validateStageName(input.stage);
|
|
715
|
-
const program = resolveContainer({
|
|
716
|
-
workspaceId,
|
|
717
|
-
appName: input.appName,
|
|
718
|
-
...input.stage !== void 0 ? { stage: input.stage } : {},
|
|
719
|
-
ensure: input.command === "deploy"
|
|
720
|
-
}).pipe(Effect.map((c) => ({
|
|
721
|
-
ok: true,
|
|
722
|
-
container: c
|
|
723
|
-
})), Effect.catchTag("ContainerNotFoundError", (e) => Effect.succeed({
|
|
724
|
-
ok: false,
|
|
725
|
-
message: `Nothing deployed for ${e.appName}${e.stage ? `/${e.stage}` : ""} — deploy it first.`
|
|
726
|
-
})), Effect.catchTag("PrismaApiError", (e) => Effect.succeed({
|
|
727
|
-
ok: false,
|
|
728
|
-
message: `Prisma Management API error resolving containers: ${e.message}.`
|
|
729
|
-
})));
|
|
730
|
-
const provided = deps?.client !== void 0 ? program.pipe(Effect.provideService(ManagementClient, deps.client)) : program.pipe(Effect.provide(layer().pipe(Layer.provide(fromEnv()))));
|
|
731
|
-
const outcome = await Effect.runPromise(provided);
|
|
732
|
-
if (!outcome.ok) throw new CliError(outcome.message);
|
|
733
|
-
return outcome.container;
|
|
734
|
-
}
|
|
735
|
-
/**
|
|
736
|
-
* Soft-deletes a named stage's Branch after a successful `alchemy destroy`
|
|
737
|
-
* has removed its members (spec §10) — the Management API refuses to delete
|
|
738
|
-
* a Branch that still has live members.
|
|
739
|
-
*/
|
|
740
|
-
async function deleteStageBranch(input, deps) {
|
|
741
|
-
const env = input.env ?? process.env;
|
|
742
|
-
if (deps?.client === void 0 && (env["PRISMA_SERVICE_TOKEN"] ?? "").length === 0) throw new CliError("environment variable PRISMA_SERVICE_TOKEN is required.");
|
|
743
|
-
const program = deleteBranch(input.branchId).pipe(Effect.map(() => ({ ok: true })), Effect.catchTag("PrismaApiError", (e) => Effect.succeed({
|
|
744
|
-
ok: false,
|
|
745
|
-
message: `Failed to delete the stage Branch: ${e.message}.`
|
|
746
|
-
})));
|
|
747
|
-
const provided = deps?.client !== void 0 ? program.pipe(Effect.provideService(ManagementClient, deps.client)) : program.pipe(Effect.provide(layer().pipe(Layer.provide(fromEnv()))));
|
|
748
|
-
const outcome = await Effect.runPromise(provided);
|
|
749
|
-
if (!outcome.ok) throw new CliError(outcome.message);
|
|
750
|
-
}
|
|
751
626
|
/** Pipeline step 6: writes a regenerated-every-run, independently runnable stack module at `.prisma-composer/alchemy.run.ts`. */
|
|
752
627
|
const GENERATED_DIR = ".prisma-composer";
|
|
753
628
|
const GENERATED_FILE = "alchemy.run.ts";
|
|
@@ -768,6 +643,7 @@ function renderOptions(input) {
|
|
|
768
643
|
lines.push(" bundles: {");
|
|
769
644
|
for (const [id, bundle] of Object.entries(input.assembled.bundles)) lines.push(` ${quote(id)}: ${renderBundle(bundle)},`);
|
|
770
645
|
lines.push(" },");
|
|
646
|
+
lines.push(" report: deploymentReport,");
|
|
771
647
|
return lines.join("\n");
|
|
772
648
|
}
|
|
773
649
|
/** Renders the stack module's source (tests assert on it without touching disk) — uses `//` headers, not a block comment, since a cwd path with a star-slash could close one early. */
|
|
@@ -782,6 +658,7 @@ function renderStackFile(input) {
|
|
|
782
658
|
//
|
|
783
659
|
// bisects a CLI bug from an Alchemy bug (deploy-cli.md § Implementation decisions).
|
|
784
660
|
import { lower } from '@prisma/composer/deploy';
|
|
661
|
+
import { deploymentReport } from '@prisma/composer/report';
|
|
785
662
|
import config from ${quote(configImport)};
|
|
786
663
|
import app from ${quote(appImport)};
|
|
787
664
|
|
|
@@ -848,7 +725,8 @@ function validateConfigShape(loaded, configPath) {
|
|
|
848
725
|
if (seen.has(id)) throw new CliError(`${CONFIG_FILENAME}: extension "${id}" is listed more than once in \`extensions\`.`);
|
|
849
726
|
seen.add(id);
|
|
850
727
|
}
|
|
851
|
-
|
|
728
|
+
const state = loaded["state"];
|
|
729
|
+
if (!isRecord(state) || typeof state["extension"] !== "string" || typeof state["create"] !== "function") throw fieldError("state", "must be a state descriptor (e.g. prismaState())");
|
|
852
730
|
return blindCast(loaded);
|
|
853
731
|
}
|
|
854
732
|
/**
|
|
@@ -873,6 +751,41 @@ async function loadAppConfig(configPath) {
|
|
|
873
751
|
};
|
|
874
752
|
}
|
|
875
753
|
/**
|
|
754
|
+
* `loadEntry` imports a user's topology with the host runtime's own module
|
|
755
|
+
* loader — under node (the CLI's shebang runtime), that loader strips
|
|
756
|
+
* TypeScript types but has no JSX transform (ADR-0005: the framework
|
|
757
|
+
* doesn't bundle or transform the app's code; deploy loading is no
|
|
758
|
+
* exception). A `.tsx`/`.jsx` file anywhere in the topology's import graph
|
|
759
|
+
* dies with node's raw `ERR_UNKNOWN_FILE_EXTENSION`, which names the file
|
|
760
|
+
* but not the cause or the fix. This turns that one case into a CliError
|
|
761
|
+
* message that does — every other load failure (syntax errors, missing
|
|
762
|
+
* files, non-JSX unknown extensions) is untouched, re-thrown as-is.
|
|
763
|
+
*/
|
|
764
|
+
const UNKNOWN_EXTENSION_CODE = "ERR_UNKNOWN_FILE_EXTENSION";
|
|
765
|
+
const JSX_EXTENSIONS = [".tsx", ".jsx"];
|
|
766
|
+
function errorCode(error) {
|
|
767
|
+
return "code" in error && typeof error.code === "string" ? error.code : void 0;
|
|
768
|
+
}
|
|
769
|
+
function unknownExtensionPath(error) {
|
|
770
|
+
if (!(error instanceof Error)) return void 0;
|
|
771
|
+
if (errorCode(error) !== UNKNOWN_EXTENSION_CODE) return void 0;
|
|
772
|
+
return /Unknown file extension "\.\w+" for (.+)$/.exec(error.message)?.[1];
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* Returns a CliError-ready message when `error` is node's
|
|
776
|
+
* `ERR_UNKNOWN_FILE_EXTENSION` for a `.tsx`/`.jsx` file — `undefined` for
|
|
777
|
+
* every other error (including unknown-extension failures for some other
|
|
778
|
+
* extension), so the caller can fall through to its normal rethrow.
|
|
779
|
+
*/
|
|
780
|
+
function explainJsxLoadError(error, entryPath) {
|
|
781
|
+
const offendingPath = unknownExtensionPath(error);
|
|
782
|
+
if (offendingPath === void 0) return void 0;
|
|
783
|
+
if (!JSX_EXTENSIONS.some((ext) => offendingPath.endsWith(ext))) return void 0;
|
|
784
|
+
return `"${offendingPath}" can't be loaded: deploy loads "${entryPath}"'s topology with node's own module loader, which strips TypeScript types but has no JSX transform — so a .tsx/.jsx file can't sit anywhere in that import graph.
|
|
785
|
+
|
|
786
|
+
Precompile that file in your own build (transform the JSX away, keep the real npm packages external) and import the compiled output instead of the raw .tsx/.jsx — see examples/email/scripts/build.ts in the prisma/composer repo for a worked example.`;
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
876
789
|
* Pipeline step 1 (deploy-cli.md § The pipeline): import the entry module
|
|
877
790
|
* (resolved against cwd) and require its default export to be a node — a
|
|
878
791
|
* service or module, branded by core's factories. Whatever this module exports
|
|
@@ -880,7 +793,15 @@ async function loadAppConfig(configPath) {
|
|
|
880
793
|
*/
|
|
881
794
|
async function loadEntry(entryArg, cwd) {
|
|
882
795
|
const resolvedPath = path.resolve(cwd, entryArg);
|
|
883
|
-
|
|
796
|
+
let mod;
|
|
797
|
+
try {
|
|
798
|
+
mod = await import(pathToFileURL(resolvedPath).href);
|
|
799
|
+
} catch (error) {
|
|
800
|
+
const explained = explainJsxLoadError(error, resolvedPath);
|
|
801
|
+
if (explained !== void 0) throw new CliError(explained);
|
|
802
|
+
throw error;
|
|
803
|
+
}
|
|
804
|
+
const root = blindCast(mod).default;
|
|
884
805
|
if (!isNode(root) || root.kind === "dependency" || root.kind === "resource") throw new CliError(`Entry module "${resolvedPath}" must default-export a node (a service or a module) — construct it with service() or module() from @prisma/composer.`);
|
|
885
806
|
return {
|
|
886
807
|
path: resolvedPath,
|
|
@@ -907,7 +828,7 @@ function resolveAlchemyBin(startDir) {
|
|
|
907
828
|
dir = parent;
|
|
908
829
|
}
|
|
909
830
|
}
|
|
910
|
-
/** Runs `alchemy deploy|destroy <stack file> --yes [--stage <stage>]`, inheriting stdio + env, plus
|
|
831
|
+
/** Runs `alchemy deploy|destroy <stack file> --yes [--stage <stage>]`, inheriting stdio + env, plus every extension's resolved container. */
|
|
911
832
|
function runAlchemy(input) {
|
|
912
833
|
const bin = resolveAlchemyBin(input.cwd);
|
|
913
834
|
const args = [
|
|
@@ -921,8 +842,7 @@ function runAlchemy(input) {
|
|
|
921
842
|
stdio: "inherit",
|
|
922
843
|
env: {
|
|
923
844
|
...input.env ?? process.env,
|
|
924
|
-
|
|
925
|
-
...input.branchId !== void 0 ? { PRISMA_BRANCH_ID: input.branchId } : {}
|
|
845
|
+
...input.containerEnv
|
|
926
846
|
}
|
|
927
847
|
});
|
|
928
848
|
if (result.error !== void 0) throw result.error;
|
|
@@ -948,6 +868,12 @@ function validateRegistryCoverage(graph, config) {
|
|
|
948
868
|
lookup(extensions, node.build.extension, node.build.type, "build", `service node "${id}"'s build descriptor`);
|
|
949
869
|
}
|
|
950
870
|
}
|
|
871
|
+
/** A stage name must be a valid git ref (deploy-cli.md) — checked via `git check-ref-format`, never silently normalized. Runs before anything platform-specific. */
|
|
872
|
+
function validateStageName(stage) {
|
|
873
|
+
const result = spawnSync("git", ["check-ref-format", `refs/heads/${stage}`], { stdio: "ignore" });
|
|
874
|
+
if (result.error) throw new CliError(`git is required to validate --stage "${stage}" (git check-ref-format): ${result.error.message}.`);
|
|
875
|
+
if (result.status !== 0) throw new CliError(`Invalid --stage "${stage}": must be a valid git ref name (git check-ref-format rejected "refs/heads/${stage}").`);
|
|
876
|
+
}
|
|
951
877
|
/**
|
|
952
878
|
* Argument parsing (clipanion — prisma-next's CLI idiom, see
|
|
953
879
|
* prisma-next/packages/1-framework/3-tooling/cli/src/migration-cli.ts) +
|
|
@@ -1043,6 +969,7 @@ async function run(argv, deps = {}) {
|
|
|
1043
969
|
throw error;
|
|
1044
970
|
}
|
|
1045
971
|
const stage = effectiveStage(args);
|
|
972
|
+
if (stage !== void 0) validateStageName(stage);
|
|
1046
973
|
const cwd = process.cwd();
|
|
1047
974
|
if (args.command === "destroy") warnIfNoLocalDeployState(cwd);
|
|
1048
975
|
const resolvedEntryPath = path.resolve(cwd, args.entry);
|
|
@@ -1062,18 +989,32 @@ async function run(argv, deps = {}) {
|
|
|
1062
989
|
if (args.command === "destroy" && error instanceof Error) throw new CliError(`${error.message}\n\ndestroy evaluates the same stack program as deploy, which packages the built artifacts — so the app must be built first. Run the build, then retry the destroy.`);
|
|
1063
990
|
throw error;
|
|
1064
991
|
}
|
|
1065
|
-
const
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
992
|
+
const containers = /* @__PURE__ */ new Map();
|
|
993
|
+
for (const extension of config.extensions) {
|
|
994
|
+
if (extension.container === void 0) continue;
|
|
995
|
+
try {
|
|
996
|
+
if (args.command === "deploy") containers.set(extension.id, await extension.container.ensure({
|
|
997
|
+
appName: name,
|
|
998
|
+
stage
|
|
999
|
+
}));
|
|
1000
|
+
else {
|
|
1001
|
+
const instance = await extension.container.locate({
|
|
1002
|
+
appName: name,
|
|
1003
|
+
stage
|
|
1004
|
+
});
|
|
1005
|
+
if (instance === void 0) throw new CliError(`Nothing deployed for ${name}${stage !== void 0 ? `/${stage}` : ""} — deploy it first.`);
|
|
1006
|
+
containers.set(extension.id, instance);
|
|
1007
|
+
}
|
|
1008
|
+
} catch (error) {
|
|
1009
|
+
throw error instanceof CliError ? error : new CliError(error instanceof Error ? error.message : String(error));
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1070
1012
|
if (args.command === "deploy") for (const extension of config.extensions) {
|
|
1071
1013
|
if (extension.preflight === void 0) continue;
|
|
1072
1014
|
try {
|
|
1073
1015
|
await extension.preflight({
|
|
1074
1016
|
graph,
|
|
1075
|
-
|
|
1076
|
-
branchId,
|
|
1017
|
+
container: containers.get(extension.id),
|
|
1077
1018
|
stage
|
|
1078
1019
|
});
|
|
1079
1020
|
} catch (error) {
|
|
@@ -1093,15 +1034,36 @@ async function run(argv, deps = {}) {
|
|
|
1093
1034
|
stackFileRelativePath: GENERATED_STACK_RELATIVE_PATH,
|
|
1094
1035
|
cwd,
|
|
1095
1036
|
stage,
|
|
1096
|
-
|
|
1097
|
-
...branchId !== void 0 ? { branchId } : {}
|
|
1037
|
+
containerEnv: containerEnv(containers)
|
|
1098
1038
|
});
|
|
1099
1039
|
if (status !== 0) {
|
|
1100
1040
|
console.error(`\nGenerated stack file: ${stackPath}`);
|
|
1101
1041
|
console.error(`Run \`alchemy ${args.command} ${GENERATED_STACK_RELATIVE_PATH} --yes\` from ${cwd} to reproduce this directly.`);
|
|
1102
1042
|
return status;
|
|
1103
1043
|
}
|
|
1104
|
-
if (args.command === "destroy"
|
|
1044
|
+
if (args.command === "destroy") {
|
|
1045
|
+
for (const extension of config.extensions) {
|
|
1046
|
+
if (extension.teardown === void 0) continue;
|
|
1047
|
+
try {
|
|
1048
|
+
await extension.teardown({
|
|
1049
|
+
container: containers.get(extension.id),
|
|
1050
|
+
stage
|
|
1051
|
+
});
|
|
1052
|
+
} catch (error) {
|
|
1053
|
+
throw error instanceof CliError ? error : new CliError(error instanceof Error ? error.message : String(error));
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
for (const extension of config.extensions) {
|
|
1057
|
+
if (extension.container === void 0) continue;
|
|
1058
|
+
const instance = containers.get(extension.id);
|
|
1059
|
+
if (instance === void 0) continue;
|
|
1060
|
+
try {
|
|
1061
|
+
await extension.container.remove(instance);
|
|
1062
|
+
} catch (error) {
|
|
1063
|
+
throw error instanceof CliError ? error : new CliError(error instanceof Error ? error.message : String(error));
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1105
1067
|
return status;
|
|
1106
1068
|
} catch (error) {
|
|
1107
1069
|
console.error(`\nGenerated stack file: ${stackPath}`);
|