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

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,9 +24,10 @@ 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
+ import { encodeBytes } from "primitives-ts";
30
31
  import { watch } from "chokidar";
31
32
  //#region package.json
32
33
  var version = "0.87.0-pre.0";
@@ -242,7 +243,7 @@ const ARTIFACT_PROTOCOL = {
242
243
  /** Inside the build directory: the published static public assets. */
243
244
  publicDirName: "public",
244
245
  /** Inside the build directory: the lock that a running build or development session holds. */
245
- lockFileName: "build.lock",
246
+ lockDirName: "build.lock",
246
247
  /** Inside the build directory: the directory in which a running build stages its output. */
247
248
  stagingDirName: "staging",
248
249
  publicOutputFileNamePrefix: PUBLIC_OUTPUT_FILE_NAME_PREFIX,
@@ -515,14 +516,14 @@ const Owner = Schema.fromJsonString(Schema.Struct({
515
516
  dir: Schema.String,
516
517
  /**
517
518
  * 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.
519
+ * that it took. A flat lock file is readable for diagnostics but cannot
520
+ * be removed atomically by owner.
520
521
  */
521
522
  token: Schema.optionalKey(Schema.NonEmptyString)
522
523
  }));
523
524
  function getRemovalCmd(lockPath) {
524
- if (platform === "win32") return `PowerShell: Remove-Item -LiteralPath '${lockPath.replaceAll("'", "''")}'`;
525
- return `rm -- '${lockPath.replaceAll("'", "'\"'\"'")}'`;
525
+ if (platform === "win32") return `PowerShell: Remove-Item -Recurse -LiteralPath '${lockPath.replaceAll("'", "''")}'`;
526
+ return `rm -r -- '${lockPath.replaceAll("'", "'\"'\"'")}'`;
526
527
  }
527
528
  /** Whether a process with this id exists; a process of another user exists but refuses the signal. */
528
529
  function testProcessExists(processId) {
@@ -533,61 +534,35 @@ function testProcessExists(processId) {
533
534
  return error.code === "EPERM";
534
535
  }
535
536
  }
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
- }
537
+ /** Reads complete directory records and flat lock files used for diagnostics. */
538
+ function readLockRecord(input) {
539
+ const { fileSystem, path, lockPath } = input;
549
540
  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
- }));
541
+ if ((yield* fileSystem.stat(lockPath)).type !== "Directory") return {
542
+ owner: yield* fileSystem.readFileString(lockPath).pipe(Effect.flatMap(Schema.decodeUnknownEffect(Owner))),
543
+ recordPath: void 0
544
+ };
545
+ const entries = yield* fileSystem.readDirectory(lockPath);
546
+ if (entries.length !== 1) return;
547
+ const recordPath = path.join(lockPath, entries[0]);
548
+ const owner = yield* fileSystem.readFileString(recordPath).pipe(Effect.flatMap(Schema.decodeUnknownEffect(Owner)));
549
+ return owner.token !== void 0 && entries[0] === getOwnerFileName(owner.token) ? {
550
+ owner,
551
+ recordPath
552
+ } : void 0;
553
+ }).pipe(Effect.catch(() => Effect.succeed(void 0)));
563
554
  }
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)));
555
+ function getOwnerFileName(token) {
556
+ return `owner-${token}.json`;
576
557
  }
577
558
  /**
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.
559
+ * Only the process that removes the unique owner record may remove its directory.
560
+ * A delayed cleanup cannot remove a later owner's record, so it cannot expose
561
+ * the later build to another writer. The directory stays locked until removal
562
+ * completes. A flat lock file has no such conditional-removal operation.
581
563
  */
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
- }));
564
+ function removeLockRecord(input) {
565
+ 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
566
  }
592
567
  /** Resolves aliases even when the output directory has not been created. */
593
568
  function canonicalizeOutputPath(filePath) {
@@ -629,10 +604,10 @@ function checkOutputPath(input) {
629
604
  });
630
605
  }
631
606
  /** The entries that Vorma writes in a build directory. */
632
- const VORMA_DIST_ENTRY_NAMES = /* @__PURE__ */ new Set([
607
+ const VORMA_BUILD_DIR_ENTRY_NAMES = /* @__PURE__ */ new Set([
633
608
  ARTIFACT_PROTOCOL.buildMetadataFileName,
634
609
  ARTIFACT_PROTOCOL.publicDirName,
635
- ARTIFACT_PROTOCOL.lockFileName,
610
+ ARTIFACT_PROTOCOL.lockDirName,
636
611
  ARTIFACT_PROTOCOL.stagingDirName
637
612
  ]);
638
613
  /**
@@ -646,7 +621,7 @@ function checkBuildDirContents(input) {
646
621
  const entries = yield* input.fileSystem.readDirectory(input.dir).pipe(Effect.catchReason("PlatformError", "NotFound", () => {
647
622
  return Effect.succeed([]);
648
623
  }));
649
- if (entries.includes(ARTIFACT_PROTOCOL.buildMetadataFileName) || entries.every((entry) => VORMA_DIST_ENTRY_NAMES.has(entry))) return;
624
+ if (entries.includes(ARTIFACT_PROTOCOL.buildMetadataFileName) || entries.every((entry) => VORMA_BUILD_DIR_ENTRY_NAMES.has(entry))) return;
650
625
  return yield* Effect.fail({
651
626
  _tag: "OutputOwnershipIssue",
652
627
  dir: input.dir,
@@ -782,7 +757,7 @@ function createStagedBuildDir(input) {
782
757
  discard: true
783
758
  });
784
759
  yield* Effect.forEach(yield* fileSystem.readDirectory(buildDir), (entry) => {
785
- return VORMA_DIST_ENTRY_NAMES.has(entry) ? Effect.void : fileSystem.remove(path.join(buildDir, entry), {
760
+ return VORMA_BUILD_DIR_ENTRY_NAMES.has(entry) ? Effect.void : fileSystem.remove(path.join(buildDir, entry), {
786
761
  recursive: true,
787
762
  force: true
788
763
  });
@@ -816,31 +791,33 @@ function acquireOutputOwnership(input) {
816
791
  dir
817
792
  });
818
793
  yield* fileSystem.makeDirectory(dir, { recursive: true });
819
- const lockPath = path.join(dir, ARTIFACT_PROTOCOL.lockFileName);
794
+ const lockPath = path.join(dir, ARTIFACT_PROTOCOL.lockDirName);
820
795
  const owner = {
821
796
  pid,
822
797
  kind: input.kind,
823
798
  dir,
824
799
  token: randomUUID()
825
800
  };
826
- const createLock = Effect.scoped(fileSystem.open(lockPath, {
827
- flag: "wx",
828
- mode: 384
829
- })).pipe(Effect.asVoid, Effect.catch((cause) => {
801
+ const createLock = fileSystem.makeDirectory(lockPath, { mode: 448 }).pipe(Effect.catch((cause) => {
830
802
  return Effect.gen(function* () {
831
803
  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({
804
+ const record = yield* readLockRecord({
805
+ fileSystem,
806
+ path,
807
+ lockPath
808
+ });
809
+ const recorded = record?.owner;
810
+ if (record?.recordPath !== void 0 && !testProcessExists(record.owner.pid) && (yield* removeLockRecord({
834
811
  fileSystem,
835
812
  lockPath,
836
- pid: recorded.pid
837
- }))) return yield* createLock;
813
+ recordPath: record.recordPath
814
+ }).pipe(Effect.catch(() => Effect.succeed(false))))) return yield* createLock;
838
815
  return yield* Effect.fail({
839
816
  _tag: "OutputOwnershipIssue",
840
817
  dir,
841
818
  msg: [
842
819
  `Cannot start ${input.kind}: the build directory is locked.`,
843
- `Lockfile: ${lockPath}`,
820
+ `Lock: ${lockPath}`,
844
821
  `Recorded owner PID: ${recorded?.pid ?? "unavailable"}`,
845
822
  `Recorded operation: ${recorded?.kind ?? "unavailable"}`,
846
823
  "Wait for the owning process to finish. If it has stopped, remove the leftover lock and retry:",
@@ -849,14 +826,18 @@ function acquireOutputOwnership(input) {
849
826
  });
850
827
  });
851
828
  }));
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
- }));
829
+ const recordPath = path.join(lockPath, getOwnerFileName(owner.token));
830
+ yield* Effect.acquireRelease(createLock.pipe(Effect.andThen(fileSystem.writeFileString(recordPath, JSON.stringify(owner)).pipe(Effect.tapError(() => {
831
+ return fileSystem.remove(lockPath, { recursive: true }).pipe(Effect.ignore);
832
+ })))), () => {
833
+ return removeLockRecord({
834
+ fileSystem,
835
+ lockPath,
836
+ recordPath
837
+ }).pipe(Effect.catch((cause) => {
838
+ return Effect.logError(`Could not remove Vorma lock ${lockPath}. If the owner has stopped, run ${getRemovalCmd(lockPath)}`, cause);
839
+ }));
840
+ });
860
841
  }).pipe(Effect.mapError((cause) => {
861
842
  return cause._tag === "OutputOwnershipIssue" ? cause : {
862
843
  _tag: "OutputOwnershipIssue",
@@ -2784,10 +2765,9 @@ function writeOutputFile(input) {
2784
2765
  }
2785
2766
  //#endregion
2786
2767
  //#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. */
2768
+ /** Builds each unique target's outer-to-inner chain by looking up its compiled path prefixes. */
2788
2769
  function createNestedRouteChains(input) {
2789
2770
  const branches = /* @__PURE__ */ new Map();
2790
- const exact = new Map(input.nodes.map((node) => [node.compiledPattern.pattern, node]));
2791
2771
  for (const node of input.nodes) {
2792
2772
  const terminal = node.compiledPattern.segments.at(-1)?.kind;
2793
2773
  if (terminal !== "index" && terminal !== "splat") branches.set(getNestedRoutePathKey(node.compiledPattern.segments), node);
@@ -2800,10 +2780,7 @@ function createNestedRouteChains(input) {
2800
2780
  if (branch !== void 0) chain.push(branch);
2801
2781
  }
2802
2782
  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
- }
2783
+ if (terminal === "index" || terminal === "splat") chain.push(target);
2807
2784
  return {
2808
2785
  target,
2809
2786
  chain
@@ -3062,7 +3039,11 @@ function compileApplicationModel(inspection) {
3062
3039
  const childPatterns = /* @__PURE__ */ new Map();
3063
3040
  for (const { target, chain } of chains) {
3064
3041
  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]);
3042
+ if (parent !== void 0) {
3043
+ const siblings = childPatterns.get(parent.pattern);
3044
+ if (siblings === void 0) childPatterns.set(parent.pattern, [target.pattern]);
3045
+ else siblings.push(target.pattern);
3046
+ }
3066
3047
  }
3067
3048
  const linkedViews = chains.map(({ target, chain }) => {
3068
3049
  return {
@@ -3376,7 +3357,7 @@ const PROCESS_ENV_PROTOCOL = {
3376
3357
  * reads its build from the build directory of `vorma dev` in it.
3377
3358
  * `vorma dev` sets the variable for the server that it starts. A server in
3378
3359
  * any other process ignores it and reads the application's
3379
- * `prod_dist_dir`.
3360
+ * `dist_dir`.
3380
3361
  */
3381
3362
  distDirEnvVar: "VORMA_DIST_DIR" };
3382
3363
  /** Private development coordination between the launcher and the application process. */
@@ -4127,7 +4108,7 @@ function startDevClient(input) {
4127
4108
  finishInspection = void 0;
4128
4109
  }
4129
4110
  /** Admits the live critical CSS update under a strict `style-src`; the application's own `html.cspNonce` replaces it. */
4130
- const sessionCspNonce = randomBytes(16).toString("base64");
4111
+ const sessionCspNonce = encodeBytes(randomBytes(16), { encoding: "base64" });
4131
4112
  const devPlugin = {
4132
4113
  name: "vorma:development",
4133
4114
  enforce: "pre",
@@ -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.9",
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.1",
29
29
  "rolldown": "^1.2.6",
30
30
  "typescript": "7.0.2"
31
31
  },