@samebase/alchemy-convex 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -69,6 +69,28 @@ deployment if it has code, only with `--adopt`.
69
69
  it stays until Convex deletes it at expiry (14 days by default).
70
70
  [`examples/app/alchemy.run.ts`](examples/app/alchemy.run.ts) is the same stack.
71
71
 
72
+ ### The stack file at the repository root
73
+
74
+ `rootDir` is the app directory, relative to the directory where `alchemy deploy` runs, which is
75
+ the directory of the stack file. The default is `"."`. The push runs in `rootDir`, and `source` is
76
+ relative to it:
77
+
78
+ ```ts
79
+ Effect.gen(function* () {
80
+ const project = yield* Convex.Project("Project", { team: "my-team", slug: "my-app" });
81
+ const backend = yield* Convex.Deployment("Backend", {
82
+ project,
83
+ type: "prod",
84
+ rootDir: "apps/web", // the Convex app, with its package.json
85
+ source: "./convex", // apps/web/convex
86
+ });
87
+ return { convexUrl: backend.url };
88
+ });
89
+ ```
90
+
91
+ State always holds `rootDir`, `"."` included, so a reader of the state finds the app directory of
92
+ each deployment.
93
+
72
94
  ## Credentials
73
95
 
74
96
  Credentials are a layer, never props, so they do not go into Alchemy state. `Convex.providers()`
@@ -160,25 +182,29 @@ call.
160
182
  taken over with `--adopt`; it never creates or renames a project. Outputs `projectId`, `slug`,
161
183
  `name`, `teamId`, `prodDeploymentName`, `prodDeploymentUrl`.
162
184
  - `Convex.Deployment`: `{ project, type: "prod" }` or `{ project, type: "dev" | "preview", name }`,
163
- each with optional `source` and `env`. Outputs `name` (such as `happy-animal-123`), `url`,
164
- `siteUrl`, `type`, `projectId`, `reference`, `previewName`, `hash`, and `envNames`.
185
+ each with optional `rootDir`, `source`, and `env`. Outputs `name` (such as `happy-animal-123`),
186
+ `url`, `siteUrl`, `type`, `projectId`, `reference`, `previewName`, `hash`, and `envNames`.
165
187
  - `prod` is the project's default production deployment (with a deploy key: the key's
166
188
  production deployment). The resource never creates or deletes one. A production deployment
167
189
  with code that is not in state is taken over only with `--adopt`.
168
190
  - `dev` creates a dev deployment with `name` as its reference; `preview` creates a preview
169
191
  deployment with `name` as its preview name. An expired preview is created again on the next
170
192
  deploy.
171
- - `source` is the Convex functions directory: `convex/`, or the `functions` field of
172
- convex.json (`SourceMismatch`). The CLI runs in the nearest directory above it with a
173
- package.json, with the Node.js that runs Alchemy, and is never downloaded (`ConvexCliNotFound`).
174
- - The content identity is a hash of every file of that app directory that git does not ignore,
175
- the nearest lockfile, the deployment name, and `env`. The same hash is a no-op. Otherwise the
176
- update sets `env`, then pushes. A failed push sets the changed variables back, and deletes a
177
- dev or preview deployment that the same run created.
193
+ - `rootDir` is the app directory, relative to the directory where the deploy runs, `"."` by
194
+ default. It must be relative (`AbsoluteRootDir`) and have a package.json (`ConvexAppNotFound`).
195
+ There is no search in the directories above it. When convex.json puts the functions outside
196
+ `rootDir`, the hash covers them too.
197
+ - `source` is the Convex functions directory relative to `rootDir`: `convex/`, or the `functions`
198
+ field of convex.json (`SourceMismatch`). The CLI runs in `rootDir`, with the Node.js that runs
199
+ Alchemy, and is never downloaded (`ConvexCliNotFound`).
200
+ - The content identity is a hash of every file of `rootDir` that git does not ignore, the nearest
201
+ lockfile, the deployment name, and `env`. The same hash is a no-op. Otherwise the update sets
202
+ `env`, then pushes. A failed push sets the changed variables back, and deletes a dev or preview
203
+ deployment that the same run created.
178
204
  - `env` values are strings or secrets such as `Config.Redacted("AUTH_SECRET")`. A name that
179
205
  leaves `env` is removed from the deployment.
180
- - `source: { path, command?, include? }` replaces `convex deploy` with `command`, run in the app
181
- directory with the push key in `CONVEX_DEPLOY_KEY`, such as
206
+ - `source: { path, command?, include? }` replaces `convex deploy` with `command`, run in `rootDir`
207
+ with the push key in `CONVEX_DEPLOY_KEY`, such as
182
208
  `["node_modules/.bin/convex-hosting", "deploy", "--build", "node_modules/.bin/vp build", "--dist", "dist/client"]`.
183
209
  The command must push with `convex deploy` (convex-hosting does, through `--cmd`). `include`
184
210
  narrows the hash to `path`, package.json, convex.json, the lockfile, and the listed globs, such
@@ -215,8 +241,8 @@ call.
215
241
 
216
242
  - `env` values are props, so Alchemy state holds them. Keep `.alchemy/` git-ignored, and use an
217
243
  encrypted state store, such as `Cloudflare.state()`, in CI.
218
- - The hash does not see a change in a workspace package outside the app directory, or a push from
219
- outside Alchemy. Change a file of the app to push again.
244
+ - The hash does not see a change in a workspace package outside `rootDir`, or a push from outside
245
+ Alchemy. Change a file of the app to push again.
220
246
  - With a deploy key, the provider reaches the deployment at `https://<name>.convex.cloud`, so the
221
247
  deployment must answer there.
222
248
  - A run that stops between a deployment create and the state write leaves a deployment that state
package/lib/index.d.ts CHANGED
@@ -9,6 +9,7 @@ import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
9
9
  import { createDeploymentClient, createManagementClient } from "@convex-dev/platform";
10
10
  import "effect/Scope";
11
11
  import { State, StateStoreError } from "alchemy/State";
12
+ import { InputProps } from "alchemy/Input";
12
13
  import { components } from "@convex-dev/platform/managementApi";
13
14
  //#region src/Credentials.d.ts
14
15
  interface ResolvedCredentials {
@@ -117,11 +118,12 @@ export declare const providers: (credentials?: Layer.Layer<Credentials>) => Laye
117
118
  /** A value of the `env` prop: a string, or a secret such as `Config.Redacted("AUTH_SECRET")`. */
118
119
  type EnvValue = string | Redacted.Redacted<string>;
119
120
  /**
120
- * The `source` prop: the Convex functions directory, such as `./convex`, or
121
- * that directory with a push command and more files for the content identity.
121
+ * The `source` prop: the Convex functions directory relative to the app
122
+ * directory (`rootDir`), such as `./convex`, or that directory with a push
123
+ * command and more files for the content identity.
122
124
  */
123
125
  type Source = string | {
124
- /** The Convex functions directory, such as `./convex`. */
126
+ /** The Convex functions directory relative to the app directory, such as `./convex`. */
125
127
  readonly path: string;
126
128
  /**
127
129
  * Replaces `convex deploy`: a program and its arguments, run in the app
@@ -147,9 +149,10 @@ export declare class ConvexCliNotFound extends ConvexCliNotFound_base {
147
149
  get message(): string;
148
150
  }
149
151
  declare const ConvexAppNotFound_base: Schema.Class<ConvexAppNotFound, Schema.TaggedStruct<"ConvexAppNotFound", {
150
- readonly source: Schema.String;
152
+ /** The app directory, resolved against the working directory of the deploy. */
153
+ readonly rootDir: Schema.String;
151
154
  }>, import("effect/Cause").YieldableError>;
152
- /** No directory at or above the parent of `source` has a package.json. */
155
+ /** The app directory (`rootDir`) has no package.json. */
153
156
  export declare class ConvexAppNotFound extends ConvexAppNotFound_base {
154
157
  get message(): string;
155
158
  }
@@ -199,10 +202,16 @@ interface DeploymentInputs {
199
202
  readonly projectId: number;
200
203
  };
201
204
  /**
202
- * The Convex functions directory, such as `./convex`, or that directory
203
- * with a push command and more hashed files. The push runs in the nearest
204
- * directory above it with a package.json. Without `source`, the resource
205
- * pushes nothing.
205
+ * The app directory, relative to the directory where the deploy runs (the
206
+ * directory of the stack file), such as `apps/web`. Default ".". It must
207
+ * have a package.json. The push runs in it, and `source` is relative to
208
+ * it. State from 0.7 has no `rootDir`.
209
+ */
210
+ readonly rootDir?: string;
211
+ /**
212
+ * The Convex functions directory relative to `rootDir`, such as
213
+ * `./convex`, or that directory with a push command and more hashed
214
+ * files. Without `source`, the resource pushes nothing.
206
215
  */
207
216
  readonly source?: Source;
208
217
  /** Environment variables to set on the deployment before the push. */
@@ -242,7 +251,36 @@ interface DeploymentAttributes extends DeploymentTarget {
242
251
  readonly envNames: readonly string[];
243
252
  }
244
253
  export type Deployment = Resource<"Convex.Deployment", DeploymentProps, DeploymentAttributes, never, Providers>;
245
- export declare const Deployment: import("alchemy").ResourceClass<Deployment>;
254
+ /** The props of a `Convex.Deployment` call: Alchemy inputs, with `rootDir` a plain string. */
255
+ type DeploymentInput = DeploymentProps extends (infer P) ? P extends DeploymentProps ? InputProps<P, "rootDir"> : never : never;
256
+ declare const AbsoluteRootDir_base: Schema.Class<AbsoluteRootDir, Schema.TaggedStruct<"AbsoluteRootDir", {
257
+ readonly rootDir: Schema.String;
258
+ }>, import("effect/Cause").YieldableError>;
259
+ /** `rootDir` is an absolute path. The declaration stops with it as a defect, before the plan. */
260
+ export declare class AbsoluteRootDir extends AbsoluteRootDir_base {
261
+ get message(): string;
262
+ }
263
+ /**
264
+ * `Convex.Deployment("id", props)`. The props go to state with `rootDir` in
265
+ * POSIX form, and with "." when it is not set, so that every row names the
266
+ * app directory of its deployment. An absolute `rootDir` stops the
267
+ * declaration, so it never reaches the plan or state. The check dies,
268
+ * because the program of `Alchemy.Stack` can fail only with a ConfigError.
269
+ * `win32.isAbsolute` also answers true for a POSIX absolute path. The other
270
+ * fields keep it usable where Alchemy takes a resource class, such as
271
+ * `Provider.collection`.
272
+ */
273
+ export declare const Deployment: ((id: string, props: DeploymentInput) => Effect.Effect<Deployment, never, Providers>) & {
274
+ Type: "Convex.Deployment";
275
+ Props: DeploymentProps;
276
+ Self: import("alchemy/Self").Self<Deployment>;
277
+ Provider: Provider.Provider<Deployment>;
278
+ Aliases: readonly string[] | undefined;
279
+ ref: (id: string, options?: {
280
+ stage?: string;
281
+ stack?: string;
282
+ }) => Effect.Effect<Deployment, never, never>;
283
+ };
246
284
  declare const DeploymentIdentityChange_base: Schema.Class<DeploymentIdentityChange, Schema.TaggedStruct<"DeploymentIdentityChange", {
247
285
  /** Name of the deployment in state, such as `happy-animal-123`. */
248
286
  readonly deployment: Schema.String;
@@ -410,5 +448,5 @@ export declare const state: (options?: {
410
448
  readonly team?: string | number;
411
449
  }) => Layer.Layer<State, never, ChildProcessSpawner.ChildProcessSpawner>;
412
450
  //#endregion
413
- export type { DeploymentAttributes, DeploymentProps, DeploymentRegion, EnvValue, ProjectAttributes, ProjectProps, RunRecord, Source };
451
+ export type { DeploymentAttributes, DeploymentInput, DeploymentProps, DeploymentRegion, EnvValue, ProjectAttributes, ProjectProps, RunRecord, Source };
414
452
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/Credentials.ts","../src/ManagementApi.ts","../src/Providers.ts","../src/Push.ts","../src/Deployment.ts","../src/Project.ts","../src/RunRecord.ts","../src/State.ts"],"mappings":";;;;;;;;;;;;;UA6BiB;;WAEN,aAAa,SAAS;;WAEtB;;UAGM;;WAEN,aAAa,OAAO,OAAO;;WAE3B,gBAAgB,OAAO;;WAEvB,WAAW,OAAO,OAAO,SAAS;;;qBAGhC,oBAAoB;;qBAKpB;;qBAGA;;;;qBAOA,yBAAyB;;;;;;;;;qBAiCzB,kBAAiB,MAAM,MAAM;;qBA0B7B,aAAc;WAChB,cAAc,SAAS;WACvB,YAAY,SAAS;MAC5B,MAAM,MAAM;;;KC3GJ,mBAAmB,kBAAkB;KACrC,mBAAmB,kBAAkB;;UAGhC,YAAY;WAClB,OAAO;WACP;WACA,UAAU;;;;;;;;;;;;;qBAQR,uBAAuB;UAuBnB;;WAEN,UAAU,GACjB,mBACA,MAAM,QAAQ,qBAAqB,QAAQ,YAAY,QACpD,OAAO,OAAO,GAAG;;WAEb,cACP,mBACA,MAAM,QAAQ,qBAAqB,QAAQ,0BACxC,OAAO,aAAa;;WAEhB,oBAAoB,GAC3B,mBACA,6BACA,WAAW,SAAS,kBACpB,MAAM,QAAQ,qBAAqB,QAAQ,YAAY,QACpD,OAAO,OAAO,GAAG;;;;;;;;WAQb,gBACP,sCACG,OAAO,eAAe;;;;;;;;WAQlB,oBAAoB,mBAAmB,OAAO;;;;;WAK9C,YAAY,OAAO,SAAS,qBAAqB;;;qBAM/C,sBAAsB;;;;qBC1FtB,kBAAkB;;;;;;qBAOlB,YAAa,cAAa,MAAM,MAAM,iBAA2B,MAAA,MAAA,8CAAA,mCAAA,oDAAA;;;;KC+BlE,oBAAoB,SAAS;;;;;KAM7B;;WAIG;;;;;;;;WAQA;;;;;;;WAOA;;;;;;qBAkCF,0BAA0B;MAIxB;;;;;;qBAMF,0BAA0B;MAIxB;;;;;;;;;qBAMF,uBAAuB;MAMrB;;;;;;;qBAQF,2BAA2B;MAIzB;;;;;;;qBAMF,oCAAoC;MAIlC;;;;;;qBAMF,kCAAkC;MAIhC;;;qBAuCF,oBACX;EACG;EAAa;;;;UChIR;;WAEC;aAAoB;;;;;;;;WAOpB,SAAS;;WAET,MAAM,SAAS,eAAe;;KAG7B,mBACP;;WAEU;MAEV;;WAEU;;;;;WAKA;;;UAIL;WACC;WACA,MAAM;;WAEN;;WAEA;;WAEA;;WAEA;;WAEA;;UAGM,6BAA6B;;WAEnC;;WAEA;;YAGC,aAAa,8BAEvB,iBACA,6BAEA;qBAEW,8BAAU,cAAA;;;;;;;;;qBAKV,iCAAiC;MAU/B;;;;;;qBAMF,qCAAqC;MAInC;;;;;;;;;qBAMF,4BAA4B;MAS1B;;;;;KC5IH,mBAAmB;KAEnB;;WAGG;;WAEA;;WAEA,mBAAmB;;WAEnB;;;WAIA;;WAEA;;UAGE;WACN;WACA;WACA;WACA;WACA;WACA;;YAGC,UAAU,2BAA2B,cAAc,0BAA0B;qBAC5E,2BAAO,cAAA;;;;;;;qBAGP,0BAA0B;MAIxB;;;;;;;;;;;;qBASF,gCAAgC;MAS9B;;;;;;;;qBAOF,yBAAyB;MAKvB;;;;;;qBAMF,+BAA+B;MAI7B;;;;;;;qBAMF,wBAAwB;MAItB;;;;;qBC5GF;;UAMI;WACN;;WAEA;;WAEA;;WAEA;;WAEA;;WAEA;;WAEA;WACA;;WAEA;;;;;qBAGE,uBAAuB;MAGrB;;;UAME;WACN;WACA,KAAK,SAAS;;;;;;;;qBAuIZ,YAAa,OAAO,mBAAiB,OAAO,OAAO,WAAW;;;;;;;qBAgB9D,gBAAiB,iBAAiB,QAAQ;;;;qBCxH1C;;;;;;;;;qBAwWA,QACX;WAAoB;MACnB,MAAM,MAAM,cAAc,oBAAoB"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/Credentials.ts","../src/ManagementApi.ts","../src/Providers.ts","../src/Push.ts","../src/Deployment.ts","../src/Project.ts","../src/RunRecord.ts","../src/State.ts"],"mappings":"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"}
package/lib/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { access, mkdtemp, readFile, realpath, rm, writeFile } from "node:fs/promises";
3
3
  import { homedir, tmpdir } from "node:os";
4
- import { dirname, join, relative, resolve, sep } from "node:path";
4
+ import { dirname, join, posix, relative, resolve, sep, win32 } from "node:path";
5
5
  import * as Config from "effect/Config";
6
6
  import * as Context from "effect/Context";
7
7
  import * as Effect from "effect/Effect";
@@ -282,13 +282,15 @@ const EnvironmentVariableList = Schema.Struct({ environmentVariables: Schema.Rec
282
282
  /** No `convex` package resolves from the app directory. */
283
283
  var ConvexCliNotFound = class extends Schema.TaggedError()("ConvexCliNotFound", { cwd: Schema.String }) {
284
284
  get message() {
285
- return `Convex.Deployment found no installed convex package from ${this.cwd}, and it does not download one. Install convex in the app (npm install convex), or set source to the functions directory of the app.`;
285
+ return `Convex.Deployment found no installed convex package from ${this.cwd}, and it does not download one. Install convex in the app (npm install convex), or set rootDir to the directory of the app.`;
286
286
  }
287
287
  };
288
- /** No directory at or above the parent of `source` has a package.json. */
289
- var ConvexAppNotFound = class extends Schema.TaggedError()("ConvexAppNotFound", { source: Schema.String }) {
288
+ /** The app directory (`rootDir`) has no package.json. */
289
+ var ConvexAppNotFound = class extends Schema.TaggedError()("ConvexAppNotFound", {
290
+ /** The app directory, resolved against the working directory of the deploy. */
291
+ rootDir: Schema.String }) {
290
292
  get message() {
291
- return `Convex.Deployment found no package.json at or above the parent of ${this.source}. Set source to the functions directory of a Convex app, such as "./convex".`;
293
+ return `Convex.Deployment found no package.json in ${this.rootDir}. Set rootDir to the directory of the Convex app, relative to the directory where the deploy runs, such as "apps/web".`;
292
294
  }
293
295
  };
294
296
  /** The Convex CLI failed, or exited 0 without the deployment URL. The output tail never contains the key. */
@@ -372,28 +374,26 @@ const parseDeployOutput = (text) => {
372
374
  /** The last non-empty lines of `text`, without escape codes and with every secret replaced. */
373
375
  const outputTail = (text, secrets) => secrets.reduce((redacted, secret) => redacted.split(secret).join("[REDACTED]"), stripVTControlCharacters(text)).split(/\r?\n|\r/).filter((line) => line.trim() !== "").slice(-20).join("\n");
374
376
  const valueOf$1 = (value) => Redacted.isRedacted(value) ? Redacted.value(value) : value;
375
- /** The app directory: the nearest directory at or above the parent of `source` with a package.json. */
376
- const appDirectoryOf = (source) => Effect.tryPromise({
377
- try: async () => {
378
- let directory = dirname(resolve(source));
379
- while (true) try {
380
- await access(join(directory, "package.json"));
381
- return directory;
382
- } catch {
383
- const parent = dirname(directory);
384
- if (parent === directory) throw new Error("no package.json");
385
- directory = parent;
386
- }
387
- },
388
- catch: () => new ConvexAppNotFound({ source })
389
- });
390
- /** `source` is not the functions directory that the Convex CLI pushes from this app. */
377
+ /**
378
+ * The app directory: `rootDir` resolved against the working directory of the
379
+ * deploy. It must have a package.json. There is no search above it.
380
+ */
381
+ const appDirectoryOf = (rootDir) => {
382
+ const directory = resolve(rootDir);
383
+ return Effect.tryPromise({
384
+ try: () => access(join(directory, "package.json")).then(() => directory),
385
+ catch: () => new ConvexAppNotFound({ rootDir: directory })
386
+ });
387
+ };
388
+ /** `source` is not the functions directory that the Convex CLI pushes from the app directory. */
391
389
  var SourceMismatch = class extends Schema.TaggedError()("SourceMismatch", {
390
+ /** The app directory, resolved. */
391
+ rootDir: Schema.String,
392
392
  source: Schema.String,
393
393
  functions: Schema.String
394
394
  }) {
395
395
  get message() {
396
- return `Convex.Deployment source is ${this.source}, but the Convex CLI pushes ${this.functions} in this app (the "functions" field of convex.json, or convex/ by default). Set source to ${this.functions}.`;
396
+ return `Convex.Deployment source is ${this.source}, but the Convex CLI pushes ${this.functions} from the app directory ${this.rootDir} (the "functions" field of convex.json, or convex/ by default). Set rootDir to the directory of the Convex app, and source to its functions directory relative to rootDir, such as "./convex".`;
397
397
  }
398
398
  };
399
399
  const ConvexJson = Schema.Struct({ functions: Schema.optionalKey(Schema.String) });
@@ -402,8 +402,9 @@ const checkFunctionsDirectory = (cwd, path) => Effect.gen(function* () {
402
402
  const text = yield* Effect.promise(() => readFile(join(cwd, "convex.json"), "utf8").catch(() => void 0));
403
403
  const configured = text === void 0 ? void 0 : Schema.decodeUnknownSync(Schema.fromJsonString(ConvexJson))(text).functions;
404
404
  const functions = resolve(cwd, configured ?? "convex");
405
- if (functions !== resolve(path)) return yield* new SourceMismatch({
406
- source: resolve(path),
405
+ if (functions !== path) return yield* new SourceMismatch({
406
+ rootDir: cwd,
407
+ source: path,
407
408
  functions
408
409
  });
409
410
  });
@@ -427,18 +428,24 @@ const installedCli = (cwd) => Effect.tryPromise({
427
428
  */
428
429
  const pushHash = (options) => Effect.gen(function* () {
429
430
  const source = options.source === void 0 ? void 0 : sourceOf(options.source);
430
- const files = source === void 0 ? void 0 : yield* appDirectoryOf(source.path).pipe(Effect.flatMap((cwd) => hashDirectory({
431
- cwd,
432
- memo: source.include === void 0 ? {} : {
433
- include: [
434
- `${relative(cwd, resolve(source.path)).split("\\").join("/")}/**`,
435
- "package.json",
436
- "convex.json",
437
- ...source.include
438
- ],
439
- lockfile: true
440
- }
441
- })));
431
+ const files = source === void 0 ? void 0 : yield* appDirectoryOf(options.rootDir).pipe(Effect.flatMap((cwd) => {
432
+ const functions = relative(cwd, resolve(cwd, source.path)).split("\\").join("/");
433
+ return hashDirectory({
434
+ cwd,
435
+ memo: source.include !== void 0 ? {
436
+ include: [
437
+ `${functions}/**`,
438
+ "package.json",
439
+ "convex.json",
440
+ ...source.include
441
+ ],
442
+ lockfile: true
443
+ } : functions === ".." || functions.startsWith("../") ? {
444
+ include: ["**/*", `${functions}/**`],
445
+ lockfile: true
446
+ } : {}
447
+ });
448
+ }));
442
449
  const env = Object.entries(options.env).map(([name, value]) => [name, valueOf$1(value)]).sort(([a], [b]) => a < b ? -1 : 1);
443
450
  return createHash("sha256").update(JSON.stringify({
444
451
  files,
@@ -510,13 +517,13 @@ const writeVariables = (url, key, changes) => settleVoid("update environment var
510
517
  })) } }));
511
518
  /**
512
519
  * The app directory and the program of a push. Fails before any change when
513
- * `source` is not the functions directory of the app, or when the app has no
514
- * installed Convex CLI and no push command.
520
+ * `rootDir` has no package.json, when `source` is not the functions directory
521
+ * of the app, or when the app has no installed Convex CLI and no push command.
515
522
  */
516
- const checkSource = (input) => Effect.gen(function* () {
523
+ const checkSource = (rootDir, input) => Effect.gen(function* () {
517
524
  const source = sourceOf(input);
518
- const cwd = yield* appDirectoryOf(source.path);
519
- yield* checkFunctionsDirectory(cwd, source.path);
525
+ const cwd = yield* appDirectoryOf(rootDir);
526
+ yield* checkFunctionsDirectory(cwd, resolve(cwd, source.path));
520
527
  return {
521
528
  cwd,
522
529
  program: source.command ?? (yield* installedCli(cwd))
@@ -530,7 +537,7 @@ const checkSource = (input) => Effect.gen(function* () {
530
537
  */
531
538
  const push = (spawner, key, options) => Effect.gen(function* () {
532
539
  const source = options.source === void 0 ? void 0 : sourceOf(options.source);
533
- const resolved = options.source === void 0 ? void 0 : yield* checkSource(options.source);
540
+ const resolved = options.source === void 0 ? void 0 : yield* checkSource(options.rootDir, options.source);
534
541
  const cwd = resolved?.cwd;
535
542
  const program = resolved?.program;
536
543
  const env = Object.entries(options.env).sort(([a], [b]) => a < b ? -1 : 1);
@@ -628,7 +635,43 @@ const runPush = (spawner, key, options) => Effect.gen(function* () {
628
635
  });
629
636
  //#endregion
630
637
  //#region src/Deployment.ts
631
- const Deployment = Resource("Convex.Deployment", { defaultRemovalPolicy: "retain" });
638
+ /** The resource class. Stacks call {@link Deployment}, which writes `rootDir` into the props. */
639
+ const DeploymentResource = Resource("Convex.Deployment", { defaultRemovalPolicy: "retain" });
640
+ /** `rootDir` in POSIX form, without "./" and a trailing slash, such as `apps/web` or ".". */
641
+ const normalizeRootDir = (rootDir) => {
642
+ const normalized = posix.normalize(rootDir.replaceAll("\\", "/"));
643
+ return normalized.length > 1 && normalized.endsWith("/") ? normalized.slice(0, -1) : normalized;
644
+ };
645
+ /** `rootDir` is an absolute path. The declaration stops with it as a defect, before the plan. */
646
+ var AbsoluteRootDir = class extends Schema.TaggedError()("AbsoluteRootDir", { rootDir: Schema.String }) {
647
+ get message() {
648
+ return `Convex.Deployment rootDir ${this.rootDir} is an absolute path. State keeps rootDir for every machine that runs the stack, so set it relative to the directory where the deploy runs, such as "apps/web".`;
649
+ }
650
+ };
651
+ /**
652
+ * `Convex.Deployment("id", props)`. The props go to state with `rootDir` in
653
+ * POSIX form, and with "." when it is not set, so that every row names the
654
+ * app directory of its deployment. An absolute `rootDir` stops the
655
+ * declaration, so it never reaches the plan or state. The check dies,
656
+ * because the program of `Alchemy.Stack` can fail only with a ConfigError.
657
+ * `win32.isAbsolute` also answers true for a POSIX absolute path. The other
658
+ * fields keep it usable where Alchemy takes a resource class, such as
659
+ * `Provider.collection`.
660
+ */
661
+ const Deployment = Object.assign((id, props) => {
662
+ const rootDir = props.rootDir ?? ".";
663
+ return win32.isAbsolute(rootDir) ? Effect.die(new AbsoluteRootDir({ rootDir })) : DeploymentResource(id, {
664
+ ...props,
665
+ rootDir: normalizeRootDir(rootDir)
666
+ });
667
+ }, {
668
+ Type: DeploymentResource.Type,
669
+ Props: DeploymentResource.Props,
670
+ Self: DeploymentResource.Self,
671
+ Provider: DeploymentResource.Provider,
672
+ Aliases: DeploymentResource.Aliases,
673
+ ref: (id, options) => DeploymentResource.ref(id, options)
674
+ });
632
675
  /** The props name another deployment than the one in state. */
633
676
  var DeploymentIdentityChange = class extends Schema.TaggedError()("DeploymentIdentityChange", {
634
677
  /** Name of the deployment in state, such as `happy-animal-123`. */
@@ -750,7 +793,7 @@ const ProjectSlugs = Schema.Struct({
750
793
  });
751
794
  /** True for an answer that says this credential cannot read the object. */
752
795
  const isRefused = (error) => error.status === 400 || error.status === 401 || error.status === 403 || error.status === 404;
753
- const DeploymentProvider = () => Provider.effect(Deployment, Effect.gen(function* () {
796
+ const DeploymentProvider = () => Provider.effect(DeploymentResource, Effect.gen(function* () {
754
797
  const api = yield* ManagementApi;
755
798
  const credentials = yield* Credentials;
756
799
  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
@@ -843,7 +886,8 @@ const DeploymentProvider = () => Provider.effect(Deployment, Effect.gen(function
843
886
  return row === void 0 ? void 0 : targetOf(row, output.type, output.previewName);
844
887
  });
845
888
  /** The content identity of `source` and `env` on a deployment, or undefined when there is nothing to push. */
846
- const wantedHash = (news, deployment) => news.source === void 0 && Object.keys(news.env ?? {}).length === 0 ? Effect.succeed(void 0) : pushHash({
889
+ const wantedHash = (news, rootDir, deployment) => news.source === void 0 && Object.keys(news.env ?? {}).length === 0 ? Effect.succeed(void 0) : pushHash({
890
+ rootDir,
847
891
  source: news.source,
848
892
  deployment,
849
893
  env: news.env ?? {}
@@ -854,12 +898,15 @@ const DeploymentProvider = () => Provider.effect(Deployment, Effect.gen(function
854
898
  "type",
855
899
  "previewName"
856
900
  ],
857
- diff: Effect.fn(function* ({ news, output }) {
901
+ diff: Effect.fn(function* ({ news, olds, output }) {
858
902
  if (!isResolved(news) || output === void 0) return void 0;
859
903
  yield* checkIdentity(output, news);
904
+ const rootDir = news.rootDir ?? ".";
905
+ if (news.source !== void 0) yield* checkSource(rootDir, news.source);
906
+ if (olds.rootDir !== rootDir) return { action: "update" };
860
907
  const access = yield* accessFor(addressOf(news), changesEnv(news, output));
861
908
  if ((yield* live(access, output))?.name !== output.name) return { action: "update" };
862
- return (yield* wantedHash(news, output.name)) === output.hash ? { action: "noop" } : { action: "update" };
909
+ return (yield* wantedHash(news, rootDir, output.name)) === output.hash ? { action: "noop" } : { action: "update" };
863
910
  }),
864
911
  read: Effect.fn(function* ({ olds, output }) {
865
912
  if (output !== void 0) {
@@ -897,7 +944,8 @@ const DeploymentProvider = () => Provider.effect(Deployment, Effect.gen(function
897
944
  }),
898
945
  reconcile: Effect.fn(function* ({ fqn, news, output }) {
899
946
  if (output !== void 0) yield* checkIdentity(output, news);
900
- if (news.source !== void 0) yield* checkSource(news.source);
947
+ const rootDir = news.rootDir ?? ".";
948
+ if (news.source !== void 0) yield* checkSource(rootDir, news.source);
901
949
  const address = addressOf(news);
902
950
  const access = yield* accessFor(address, changesEnv(news, output));
903
951
  const env = news.env ?? {};
@@ -905,7 +953,7 @@ const DeploymentProvider = () => Provider.effect(Deployment, Effect.gen(function
905
953
  const adopt = yield* shouldAdopt(fqn);
906
954
  const refuse = (name) => new OwnedBySomeoneElse({
907
955
  message: `Project ${address.projectId} already has ${news.type} deployment ${name}${news.type === "prod" ? " with code" : ` named "${news.name}"`}. Re-run with --adopt to take it over${news.type === "prod" ? "" : ", or use another name"}.`,
908
- resourceType: Deployment.Type,
956
+ resourceType: DeploymentResource.Type,
909
957
  physicalName: name
910
958
  });
911
959
  const { target, created } = yield* Effect.gen(function* () {
@@ -963,7 +1011,7 @@ const DeploymentProvider = () => Provider.effect(Deployment, Effect.gen(function
963
1011
  };
964
1012
  });
965
1013
  const previousEnvNames = output?.name === target.name ? envNamesOf(output) : [];
966
- const before = yield* wantedHash(news, target.name);
1014
+ const before = yield* wantedHash(news, rootDir, target.name);
967
1015
  if (before === void 0 && previousEnvNames.length === 0) return {
968
1016
  ...target,
969
1017
  hash: void 0,
@@ -977,6 +1025,7 @@ const DeploymentProvider = () => Provider.effect(Deployment, Effect.gen(function
977
1025
  const options = {
978
1026
  deployment: target.name,
979
1027
  url: target.url,
1028
+ rootDir,
980
1029
  source: news.source,
981
1030
  env,
982
1031
  previousEnvNames,
@@ -994,7 +1043,7 @@ const DeploymentProvider = () => Provider.effect(Deployment, Effect.gen(function
994
1043
  }))).pipe(Effect.onError(() => created && access.kind !== "deployKey" ? absentAsUndefined(retryIdempotentWrite(access.api.requestVoid("delete deployment", (client) => client.POST("/deployments/{deployment_name}/delete", { params: { path: { deployment_name: target.name } } })))).pipe(Effect.catch((error) => Effect.logWarning(`Convex.Deployment could not delete ${target.name} after the failed push (${error.message}). The next run takes it over only with --adopt.`))) : Effect.void));
995
1044
  return {
996
1045
  ...target,
997
- hash: yield* wantedHash(news, target.name),
1046
+ hash: yield* wantedHash(news, rootDir, target.name),
998
1047
  envNames: Object.keys(env).sort()
999
1048
  };
1000
1049
  }),
@@ -1328,7 +1377,7 @@ var Providers = class extends Provider.ProviderCollection()("Convex") {};
1328
1377
  * default lookup (`CONVEX_ACCESS_TOKEN`, then the Convex CLI login, and
1329
1378
  * `CONVEX_DEPLOY_KEY`).
1330
1379
  */
1331
- const providers = (credentials = fromConfig()) => Layer.effect(Providers, Provider.collection([Project, Deployment])).pipe(Layer.provide(Layer.mergeAll(ProjectProvider(), DeploymentProvider())), Layer.provide(ManagementApiLive()), Layer.provide(credentials), Layer.orDie);
1380
+ const providers = (credentials = fromConfig()) => Layer.effect(Providers, Provider.collection([Project, DeploymentResource])).pipe(Layer.provide(Layer.mergeAll(ProjectProvider(), DeploymentProvider())), Layer.provide(ManagementApiLive()), Layer.provide(credentials), Layer.orDie);
1332
1381
  //#endregion
1333
1382
  //#region src/RunRecord.ts
1334
1383
  /** The key of the record in the stage output document. */
@@ -1573,6 +1622,7 @@ const pushStore = (options, deployment) => Effect.gen(function* () {
1573
1622
  yield* withPushKey(options.api, deployment.name, PUSH_KEY_ACTIONS, (key) => push(options.spawner, key, {
1574
1623
  deployment: deployment.name,
1575
1624
  url: deployment.deploymentUrl,
1625
+ rootDir: join(options.store, ".."),
1576
1626
  source: {
1577
1627
  path: options.store,
1578
1628
  command: [
@@ -1709,6 +1759,6 @@ const state = (options = {}) => Layer.effect(State, Effect.gen(function* () {
1709
1759
  }).pipe(Effect.provideContext(context), Effect.orDie));
1710
1760
  }));
1711
1761
  //#endregion
1712
- export { ACCESS_TOKEN_ENV, AmbiguousDeployment, AmbiguousProject, CodePushFailed, ConvexApiError, ConvexAppNotFound, ConvexCliNotFound, CreatedProjectNotFound, Credentials, CredentialsError, DEPLOY_KEY_ENV, Deployment, DeploymentIdentityChange, EnvironmentNeedsDeployKey, ManagementApi, ProductionDeploymentNotFound, Project, ProjectDeploymentChange, ProjectNotFound, ProjectTeamChange, Providers, PushTargetMismatch, RUN_RECORD_KEY, RunRecordError, STORE_PROJECT_NAME, VariablesOwnedBySomeoneElse, fromConfig, fromValues, parseDeployOutput, providers, runRecord, state, withRunRecord };
1762
+ export { ACCESS_TOKEN_ENV, AbsoluteRootDir, AmbiguousDeployment, AmbiguousProject, CodePushFailed, ConvexApiError, ConvexAppNotFound, ConvexCliNotFound, CreatedProjectNotFound, Credentials, CredentialsError, DEPLOY_KEY_ENV, Deployment, DeploymentIdentityChange, EnvironmentNeedsDeployKey, ManagementApi, ProductionDeploymentNotFound, Project, ProjectDeploymentChange, ProjectNotFound, ProjectTeamChange, Providers, PushTargetMismatch, RUN_RECORD_KEY, RunRecordError, STORE_PROJECT_NAME, VariablesOwnedBySomeoneElse, fromConfig, fromValues, parseDeployOutput, providers, runRecord, state, withRunRecord };
1713
1763
 
1714
1764
  //# sourceMappingURL=index.js.map