@sjcx/vorma 0.0.0-pre.7 → 0.0.0-pre.8

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/build/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { i as readConfigFilePath, r as checkVormaPlugin } from "../plugin-api-Btb0teGk.js";
3
3
  import { t as VormaConfigSchema } from "../config-FbabCxBO.js";
4
4
  import { NodeChildProcessSpawner, NodeCrypto, NodeFileSystem, NodePath, NodeRuntime, NodeSocketServer, NodeStdio, NodeTerminal } from "@effect/platform-node-shared";
5
- import { Cause, Console, Deferred, Effect, Equal, Exit, Fiber, FileSystem, Layer, Option, Order, Path, Predicate, Queue, Record, Result, Schedule, Schema, Scope, ScopedRef, Stream } from "effect";
5
+ import { Cause, Console, Deferred, Effect, Equal, Exit, Fiber, FileSystem, Layer, Order, Path, Predicate, Queue, Record, Result, Schedule, Schema, Scope, ScopedRef, Stream } from "effect";
6
6
  import { Argument, Command } from "effect/cli";
7
7
  import { stripVTControlCharacters } from "node:util";
8
8
  import * as FileSystem$1 from "effect/FileSystem";
@@ -24,7 +24,7 @@ import { Writable } from "node:stream";
24
24
  import { ChildProcess as ChildProcess$1 } from "effect/process";
25
25
  import * as NativeHttpMethod from "effect/http/HttpMethod";
26
26
  import { DEV_CONTROLS_PROTOCOL, DOCUMENT_PROTOCOL, GENERATED_TYPES_PROTOCOL, HTTP_PROTOCOL, PATTERN_PROTOCOL, encodeRoutePattern, formatPathWireError } from "@sjcx/vorma-client/unstable";
27
- import { compileNestedPattern, formatPatternCompileError } from "@sjcx/vorma-primitives/matcher";
27
+ import { compileNestedPattern, formatPatternCompileError } from "primitives-ts/matcher";
28
28
  import { createServer as createServer$1, request } from "node:http";
29
29
  import { createConnection } from "node:net";
30
30
  import { watch } from "chokidar";
@@ -242,7 +242,7 @@ const ARTIFACT_PROTOCOL = {
242
242
  /** Inside the build directory: the published static public assets. */
243
243
  publicDirName: "public",
244
244
  /** Inside the build directory: the lock that a running build or development session holds. */
245
- lockFileName: "build.lock",
245
+ lockDirName: "build.lock",
246
246
  /** Inside the build directory: the directory in which a running build stages its output. */
247
247
  stagingDirName: "staging",
248
248
  publicOutputFileNamePrefix: PUBLIC_OUTPUT_FILE_NAME_PREFIX,
@@ -515,14 +515,14 @@ const Owner = Schema.fromJsonString(Schema.Struct({
515
515
  dir: Schema.String,
516
516
  /**
517
517
  * Identifies one acquisition, so that a process releases only the lock
518
- * that it took. A lock written without it is still recognized as stale
519
- * when its process no longer exists, and no process releases it.
518
+ * that it took. A flat lock file is readable for diagnostics but cannot
519
+ * be removed atomically by owner.
520
520
  */
521
521
  token: Schema.optionalKey(Schema.NonEmptyString)
522
522
  }));
523
523
  function getRemovalCmd(lockPath) {
524
- if (platform === "win32") return `PowerShell: Remove-Item -LiteralPath '${lockPath.replaceAll("'", "''")}'`;
525
- return `rm -- '${lockPath.replaceAll("'", "'\"'\"'")}'`;
524
+ if (platform === "win32") return `PowerShell: Remove-Item -Recurse -LiteralPath '${lockPath.replaceAll("'", "''")}'`;
525
+ return `rm -r -- '${lockPath.replaceAll("'", "'\"'\"'")}'`;
526
526
  }
527
527
  /** Whether a process with this id exists; a process of another user exists but refuses the signal. */
528
528
  function testProcessExists(processId) {
@@ -533,61 +533,35 @@ function testProcessExists(processId) {
533
533
  return error.code === "EPERM";
534
534
  }
535
535
  }
536
- /**
537
- * Removes the lock at `lockPath` when `testOwnerRemovable` accepts the owner
538
- * that it records, and reports whether it removed the lock. A missing lock, a
539
- * lock that records no owner, and a lock whose owner `testOwnerRemovable`
540
- * rejects stay as they are. The lock moves to `claimedPath` first, so that of
541
- * two contenders only one removes it, and the moved file is read again: a lock
542
- * that another process created in the meantime is moved back and kept.
543
- */
544
- function removeLockIfRemovable(input) {
545
- const { fileSystem, lockPath, claimedPath, testOwnerRemovable } = input;
546
- function readOwner(path) {
547
- return fileSystem.readFileString(path).pipe(Effect.flatMap(Schema.decodeUnknownEffect(Owner)), Effect.option);
548
- }
536
+ /** Reads complete directory records and flat lock files used for diagnostics. */
537
+ function readLockRecord(input) {
538
+ const { fileSystem, path, lockPath } = input;
549
539
  return Effect.gen(function* () {
550
- const recorded = yield* readOwner(lockPath);
551
- if (Option.isNone(recorded) || !testOwnerRemovable(recorded.value)) return false;
552
- yield* fileSystem.rename(lockPath, claimedPath);
553
- const claimed = yield* readOwner(claimedPath);
554
- if (Option.isNone(claimed) || !testOwnerRemovable(claimed.value)) {
555
- yield* fileSystem.rename(claimedPath, lockPath);
556
- return false;
557
- }
558
- yield* fileSystem.remove(claimedPath, { force: true });
559
- return true;
560
- }).pipe(Effect.catchReason("PlatformError", "NotFound", () => {
561
- return Effect.succeed(false);
562
- }));
540
+ if ((yield* fileSystem.stat(lockPath)).type !== "Directory") return {
541
+ owner: yield* fileSystem.readFileString(lockPath).pipe(Effect.flatMap(Schema.decodeUnknownEffect(Owner))),
542
+ recordPath: void 0
543
+ };
544
+ const entries = yield* fileSystem.readDirectory(lockPath);
545
+ if (entries.length !== 1) return;
546
+ const recordPath = path.join(lockPath, entries[0]);
547
+ const owner = yield* fileSystem.readFileString(recordPath).pipe(Effect.flatMap(Schema.decodeUnknownEffect(Owner)));
548
+ return owner.token !== void 0 && entries[0] === getOwnerFileName(owner.token) ? {
549
+ owner,
550
+ recordPath
551
+ } : void 0;
552
+ }).pipe(Effect.catch(() => Effect.succeed(void 0)));
563
553
  }
564
- /**
565
- * Removes a lock whose recorded process no longer exists. The check assumes
566
- * that the lock's process ran on this machine, as a build directory is local to
567
- * the machine that builds it.
568
- */
569
- function removeStaleLock(input) {
570
- return removeLockIfRemovable({
571
- fileSystem: input.fileSystem,
572
- lockPath: input.lockPath,
573
- claimedPath: `${input.lockPath}.stale-${pid}`,
574
- testOwnerRemovable: (owner) => owner.pid === input.pid
575
- }).pipe(Effect.catch(() => Effect.succeed(false)));
554
+ function getOwnerFileName(token) {
555
+ return `owner-${token}.json`;
576
556
  }
577
557
  /**
578
- * Removes the lock that `owner` took, and only that lock. A build's
579
- * publication moves the build directory, and its lock with it, so that the
580
- * path can hold the lock of a later build by the time that this one releases.
558
+ * Only the process that removes the unique owner record may remove its directory.
559
+ * A delayed cleanup cannot remove a later owner's record, so it cannot expose
560
+ * the later build to another writer. The directory stays locked until removal
561
+ * completes. A flat lock file has no such conditional-removal operation.
581
562
  */
582
- function removeOwnedLock(input) {
583
- return removeLockIfRemovable({
584
- fileSystem: input.fileSystem,
585
- lockPath: input.lockPath,
586
- claimedPath: `${input.lockPath}.release-${input.owner.token}`,
587
- testOwnerRemovable: (recorded) => recorded.token === input.owner.token
588
- }).pipe(Effect.catch((cause) => {
589
- return Effect.logError(`Could not remove Vorma lock ${input.lockPath}. If the owner has stopped, run ${getRemovalCmd(input.lockPath)}`, cause);
590
- }));
563
+ function removeLockRecord(input) {
564
+ return input.fileSystem.remove(input.recordPath).pipe(Effect.andThen(input.fileSystem.remove(input.lockPath, { recursive: true })), Effect.as(true), Effect.catchReason("PlatformError", "NotFound", () => Effect.succeed(false)));
591
565
  }
592
566
  /** Resolves aliases even when the output directory has not been created. */
593
567
  function canonicalizeOutputPath(filePath) {
@@ -629,10 +603,10 @@ function checkOutputPath(input) {
629
603
  });
630
604
  }
631
605
  /** The entries that Vorma writes in a build directory. */
632
- const VORMA_DIST_ENTRY_NAMES = /* @__PURE__ */ new Set([
606
+ const VORMA_BUILD_DIR_ENTRY_NAMES = /* @__PURE__ */ new Set([
633
607
  ARTIFACT_PROTOCOL.buildMetadataFileName,
634
608
  ARTIFACT_PROTOCOL.publicDirName,
635
- ARTIFACT_PROTOCOL.lockFileName,
609
+ ARTIFACT_PROTOCOL.lockDirName,
636
610
  ARTIFACT_PROTOCOL.stagingDirName
637
611
  ]);
638
612
  /**
@@ -646,7 +620,7 @@ function checkBuildDirContents(input) {
646
620
  const entries = yield* input.fileSystem.readDirectory(input.dir).pipe(Effect.catchReason("PlatformError", "NotFound", () => {
647
621
  return Effect.succeed([]);
648
622
  }));
649
- if (entries.includes(ARTIFACT_PROTOCOL.buildMetadataFileName) || entries.every((entry) => VORMA_DIST_ENTRY_NAMES.has(entry))) return;
623
+ if (entries.includes(ARTIFACT_PROTOCOL.buildMetadataFileName) || entries.every((entry) => VORMA_BUILD_DIR_ENTRY_NAMES.has(entry))) return;
650
624
  return yield* Effect.fail({
651
625
  _tag: "OutputOwnershipIssue",
652
626
  dir: input.dir,
@@ -782,7 +756,7 @@ function createStagedBuildDir(input) {
782
756
  discard: true
783
757
  });
784
758
  yield* Effect.forEach(yield* fileSystem.readDirectory(buildDir), (entry) => {
785
- return VORMA_DIST_ENTRY_NAMES.has(entry) ? Effect.void : fileSystem.remove(path.join(buildDir, entry), {
759
+ return VORMA_BUILD_DIR_ENTRY_NAMES.has(entry) ? Effect.void : fileSystem.remove(path.join(buildDir, entry), {
786
760
  recursive: true,
787
761
  force: true
788
762
  });
@@ -816,31 +790,33 @@ function acquireOutputOwnership(input) {
816
790
  dir
817
791
  });
818
792
  yield* fileSystem.makeDirectory(dir, { recursive: true });
819
- const lockPath = path.join(dir, ARTIFACT_PROTOCOL.lockFileName);
793
+ const lockPath = path.join(dir, ARTIFACT_PROTOCOL.lockDirName);
820
794
  const owner = {
821
795
  pid,
822
796
  kind: input.kind,
823
797
  dir,
824
798
  token: randomUUID()
825
799
  };
826
- const createLock = Effect.scoped(fileSystem.open(lockPath, {
827
- flag: "wx",
828
- mode: 384
829
- })).pipe(Effect.asVoid, Effect.catch((cause) => {
800
+ const createLock = fileSystem.makeDirectory(lockPath, { mode: 448 }).pipe(Effect.catch((cause) => {
830
801
  return Effect.gen(function* () {
831
802
  if (cause.reason._tag !== "AlreadyExists") return yield* Effect.fail(cause);
832
- const recorded = Option.getOrUndefined(yield* fileSystem.readFileString(lockPath).pipe(Effect.flatMap(Schema.decodeUnknownEffect(Owner)), Effect.option));
833
- if (recorded !== void 0 && !testProcessExists(recorded.pid) && (yield* removeStaleLock({
803
+ const record = yield* readLockRecord({
804
+ fileSystem,
805
+ path,
806
+ lockPath
807
+ });
808
+ const recorded = record?.owner;
809
+ if (record?.recordPath !== void 0 && !testProcessExists(record.owner.pid) && (yield* removeLockRecord({
834
810
  fileSystem,
835
811
  lockPath,
836
- pid: recorded.pid
837
- }))) return yield* createLock;
812
+ recordPath: record.recordPath
813
+ }).pipe(Effect.catch(() => Effect.succeed(false))))) return yield* createLock;
838
814
  return yield* Effect.fail({
839
815
  _tag: "OutputOwnershipIssue",
840
816
  dir,
841
817
  msg: [
842
818
  `Cannot start ${input.kind}: the build directory is locked.`,
843
- `Lockfile: ${lockPath}`,
819
+ `Lock: ${lockPath}`,
844
820
  `Recorded owner PID: ${recorded?.pid ?? "unavailable"}`,
845
821
  `Recorded operation: ${recorded?.kind ?? "unavailable"}`,
846
822
  "Wait for the owning process to finish. If it has stopped, remove the leftover lock and retry:",
@@ -849,14 +825,18 @@ function acquireOutputOwnership(input) {
849
825
  });
850
826
  });
851
827
  }));
852
- yield* Effect.acquireRelease(createLock, () => removeOwnedLock({
853
- fileSystem,
854
- lockPath,
855
- owner
856
- }));
857
- yield* fileSystem.writeFileString(lockPath, JSON.stringify(owner)).pipe(Effect.tapError(() => {
858
- return fileSystem.remove(lockPath, { force: true }).pipe(Effect.ignore);
859
- }));
828
+ const recordPath = path.join(lockPath, getOwnerFileName(owner.token));
829
+ yield* Effect.acquireRelease(createLock.pipe(Effect.andThen(fileSystem.writeFileString(recordPath, JSON.stringify(owner)).pipe(Effect.tapError(() => {
830
+ return fileSystem.remove(lockPath, { recursive: true }).pipe(Effect.ignore);
831
+ })))), () => {
832
+ return removeLockRecord({
833
+ fileSystem,
834
+ lockPath,
835
+ recordPath
836
+ }).pipe(Effect.catch((cause) => {
837
+ return Effect.logError(`Could not remove Vorma lock ${lockPath}. If the owner has stopped, run ${getRemovalCmd(lockPath)}`, cause);
838
+ }));
839
+ });
860
840
  }).pipe(Effect.mapError((cause) => {
861
841
  return cause._tag === "OutputOwnershipIssue" ? cause : {
862
842
  _tag: "OutputOwnershipIssue",
@@ -2784,10 +2764,9 @@ function writeOutputFile(input) {
2784
2764
  }
2785
2765
  //#endregion
2786
2766
  //#region src/build/patterns/nested-route-chains.ts
2787
- /** Materializes each target's compiledPattern outer-to-inner route chain in time proportional to route depth. */
2767
+ /** Builds each unique target's outer-to-inner chain by looking up its compiled path prefixes. */
2788
2768
  function createNestedRouteChains(input) {
2789
2769
  const branches = /* @__PURE__ */ new Map();
2790
- const exact = new Map(input.nodes.map((node) => [node.compiledPattern.pattern, node]));
2791
2770
  for (const node of input.nodes) {
2792
2771
  const terminal = node.compiledPattern.segments.at(-1)?.kind;
2793
2772
  if (terminal !== "index" && terminal !== "splat") branches.set(getNestedRoutePathKey(node.compiledPattern.segments), node);
@@ -2800,10 +2779,7 @@ function createNestedRouteChains(input) {
2800
2779
  if (branch !== void 0) chain.push(branch);
2801
2780
  }
2802
2781
  const terminal = target.compiledPattern.segments.at(-1)?.kind;
2803
- if (terminal === "index" || terminal === "splat") {
2804
- const leaf = exact.get(target.compiledPattern.pattern);
2805
- if (leaf !== void 0) chain.push(leaf);
2806
- }
2782
+ if (terminal === "index" || terminal === "splat") chain.push(target);
2807
2783
  return {
2808
2784
  target,
2809
2785
  chain
@@ -3062,7 +3038,11 @@ function compileApplicationModel(inspection) {
3062
3038
  const childPatterns = /* @__PURE__ */ new Map();
3063
3039
  for (const { target, chain } of chains) {
3064
3040
  const parent = chain.at(-1) === target ? chain.at(-2) : void 0;
3065
- if (parent !== void 0) childPatterns.set(parent.pattern, [...childPatterns.get(parent.pattern) ?? [], target.pattern]);
3041
+ if (parent !== void 0) {
3042
+ const siblings = childPatterns.get(parent.pattern);
3043
+ if (siblings === void 0) childPatterns.set(parent.pattern, [target.pattern]);
3044
+ else siblings.push(target.pattern);
3045
+ }
3066
3046
  }
3067
3047
  const linkedViews = chains.map(({ target, chain }) => {
3068
3048
  return {
@@ -3376,7 +3356,7 @@ const PROCESS_ENV_PROTOCOL = {
3376
3356
  * reads its build from the build directory of `vorma dev` in it.
3377
3357
  * `vorma dev` sets the variable for the server that it starts. A server in
3378
3358
  * any other process ignores it and reads the application's
3379
- * `prod_dist_dir`.
3359
+ * `dist_dir`.
3380
3360
  */
3381
3361
  distDirEnvVar: "VORMA_DIST_DIR" };
3382
3362
  /** Private development coordination between the launcher and the application process. */
@@ -5,7 +5,7 @@ export declare const PROCESS_ENV_PROTOCOL: {
5
5
  * reads its build from the build directory of `vorma dev` in it.
6
6
  * `vorma dev` sets the variable for the server that it starts. A server in
7
7
  * any other process ignores it and reads the application's
8
- * `prod_dist_dir`.
8
+ * `dist_dir`.
9
9
  */
10
10
  readonly distDirEnvVar: "VORMA_DIST_DIR";
11
11
  };
@@ -1,5 +1,5 @@
1
- import type { CompiledNestedPattern } from "@sjcx/vorma-primitives/matcher";
2
1
  import { Result } from "effect";
2
+ import type { CompiledNestedPattern } from "primitives-ts/matcher";
3
3
  import type { InspectedClientView, InspectedEndpoint, InspectedProjects, InspectedViewLoader } from "../inspection/inspect-projects.ts";
4
4
  export type ApplicationView = {
5
5
  /** The authored pattern, as the loader or the view wrote it. */
@@ -10,7 +10,7 @@ export declare const ARTIFACT_PROTOCOL: {
10
10
  /** Inside the build directory: the published static public assets. */
11
11
  readonly publicDirName: "public";
12
12
  /** Inside the build directory: the lock that a running build or development session holds. */
13
- readonly lockFileName: "build.lock";
13
+ readonly lockDirName: "build.lock";
14
14
  /** Inside the build directory: the directory in which a running build stages its output. */
15
15
  readonly stagingDirName: "staging";
16
16
  readonly publicOutputFileNamePrefix: "vorma_out_";
@@ -1,8 +1,8 @@
1
- import type { CompiledNestedPattern } from "@sjcx/vorma-primitives/matcher";
1
+ import type { CompiledNestedPattern } from "primitives-ts/matcher";
2
2
  type NestedRouteNode = {
3
3
  readonly compiledPattern: CompiledNestedPattern;
4
4
  };
5
- /** Materializes each target's compiledPattern outer-to-inner route chain in time proportional to route depth. */
5
+ /** Builds each unique target's outer-to-inner chain by looking up its compiled path prefixes. */
6
6
  export declare function createNestedRouteChains<Node extends NestedRouteNode>(input: {
7
7
  readonly nodes: ReadonlyArray<Node>;
8
8
  }): ReadonlyArray<{
@@ -1,3 +1,3 @@
1
- import type * as Matcher from "@sjcx/vorma-primitives/matcher";
1
+ import type * as Matcher from "primitives-ts/matcher";
2
2
  /** The view pattern language, with the markers that the client and the server use. */
3
3
  export declare const NESTED_PATTERN_LANGUAGE: Matcher.NestedPatternLanguage;
@@ -1,6 +1,6 @@
1
1
  import { type PathWireError } from "@sjcx/vorma-client/unstable";
2
- import { type CompiledNestedPattern, type PatternCompileError } from "@sjcx/vorma-primitives/matcher";
3
2
  import { Result } from "effect";
3
+ import { type CompiledNestedPattern, type PatternCompileError } from "primitives-ts/matcher";
4
4
  export type RoutePatternError = PathWireError | PatternCompileError;
5
5
  /**
6
6
  * Compiles an authored view route pattern: wire encoding first, then the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjcx/vorma",
3
- "version": "0.0.0-pre.7",
3
+ "version": "0.0.0-pre.8",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "license": "(MIT OR Apache-2.0)",
@@ -22,10 +22,10 @@
22
22
  "@effect/platform-node-shared": "^4.0.0",
23
23
  "@noble/hashes": "^2.4.0",
24
24
  "@scure/base": "^2.4.0",
25
- "@sjcx/vorma-primitives": "0.0.0-pre.7",
26
25
  "chokidar": "^5.0.0",
27
26
  "effect": "^4.0.0",
28
27
  "lightningcss": "^1.33.0",
28
+ "primitives-ts": "0.0.1-pre.0",
29
29
  "rolldown": "^1.2.6",
30
30
  "typescript": "7.0.2"
31
31
  },