@micacms/runtime 0.4.1 → 0.5.0-rc.2

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.
Files changed (112) hide show
  1. package/README.md +2 -2
  2. package/dist/app.d.ts +8 -37
  3. package/dist/app.d.ts.map +1 -1
  4. package/dist/app.js +7 -42
  5. package/dist/app.js.map +1 -1
  6. package/dist/config.d.ts +12 -82
  7. package/dist/config.d.ts.map +1 -1
  8. package/dist/config.js +14 -82
  9. package/dist/config.js.map +1 -1
  10. package/dist/context.d.ts +37 -66
  11. package/dist/context.d.ts.map +1 -1
  12. package/dist/context.js +24 -75
  13. package/dist/context.js.map +1 -1
  14. package/dist/http.d.ts +8 -16
  15. package/dist/http.d.ts.map +1 -1
  16. package/dist/http.js +6 -13
  17. package/dist/http.js.map +1 -1
  18. package/dist/index.d.ts +13 -14
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +12 -14
  21. package/dist/index.js.map +1 -1
  22. package/dist/lib/errors.js.map +1 -1
  23. package/dist/lib/file-type.js.map +1 -1
  24. package/dist/lib/id.js.map +1 -1
  25. package/dist/lib/logger.js.map +1 -1
  26. package/dist/lib/ports.js.map +1 -1
  27. package/dist/lib/rate-limit.js.map +1 -1
  28. package/dist/lib/schema-stats.js.map +1 -1
  29. package/dist/lib/version.js.map +1 -1
  30. package/dist/middleware/index.js.map +1 -1
  31. package/dist/routes/admin-content.d.ts +4 -17
  32. package/dist/routes/admin-content.d.ts.map +1 -1
  33. package/dist/routes/admin-content.js +11 -51
  34. package/dist/routes/admin-content.js.map +1 -1
  35. package/dist/routes/admin-media.d.ts +3 -14
  36. package/dist/routes/admin-media.d.ts.map +1 -1
  37. package/dist/routes/admin-media.js +7 -29
  38. package/dist/routes/admin-media.js.map +1 -1
  39. package/dist/routes/admin-spa.js.map +1 -1
  40. package/dist/routes/admin-staging.d.ts +4 -9
  41. package/dist/routes/admin-staging.d.ts.map +1 -1
  42. package/dist/routes/admin-staging.js +31 -18
  43. package/dist/routes/admin-staging.js.map +1 -1
  44. package/dist/routes/admin-system.d.ts +2 -8
  45. package/dist/routes/admin-system.d.ts.map +1 -1
  46. package/dist/routes/admin-system.js +9 -44
  47. package/dist/routes/admin-system.js.map +1 -1
  48. package/dist/routes/health.d.ts +3 -10
  49. package/dist/routes/health.d.ts.map +1 -1
  50. package/dist/routes/health.js +9 -26
  51. package/dist/routes/health.js.map +1 -1
  52. package/dist/schema.d.ts +10 -38
  53. package/dist/schema.d.ts.map +1 -1
  54. package/dist/schema.js +14 -65
  55. package/dist/schema.js.map +1 -1
  56. package/dist/server.d.ts +2 -10
  57. package/dist/server.d.ts.map +1 -1
  58. package/dist/server.js +3 -9
  59. package/dist/server.js.map +1 -1
  60. package/dist/services/changes.d.ts +150 -0
  61. package/dist/services/changes.d.ts.map +1 -0
  62. package/dist/services/changes.js +339 -0
  63. package/dist/services/changes.js.map +1 -0
  64. package/dist/services/content-store.d.ts +61 -0
  65. package/dist/services/content-store.d.ts.map +1 -0
  66. package/dist/services/content-store.js +330 -0
  67. package/dist/services/content-store.js.map +1 -0
  68. package/dist/services/media.d.ts +9 -29
  69. package/dist/services/media.d.ts.map +1 -1
  70. package/dist/services/media.js +17 -57
  71. package/dist/services/media.js.map +1 -1
  72. package/dist/services/staging.d.ts +35 -167
  73. package/dist/services/staging.d.ts.map +1 -1
  74. package/dist/services/staging.js +195 -458
  75. package/dist/services/staging.js.map +1 -1
  76. package/dist/workspace/developer.d.ts +14 -0
  77. package/dist/workspace/developer.d.ts.map +1 -0
  78. package/dist/workspace/developer.js +31 -0
  79. package/dist/workspace/developer.js.map +1 -0
  80. package/dist/workspace/files.d.ts +70 -0
  81. package/dist/workspace/files.d.ts.map +1 -0
  82. package/dist/workspace/files.js +176 -0
  83. package/dist/workspace/files.js.map +1 -0
  84. package/dist/workspace/git.d.ts +45 -0
  85. package/dist/workspace/git.d.ts.map +1 -0
  86. package/dist/workspace/git.js +68 -0
  87. package/dist/workspace/git.js.map +1 -0
  88. package/dist/workspace/locks.d.ts +33 -0
  89. package/dist/workspace/locks.d.ts.map +1 -0
  90. package/dist/workspace/locks.js +76 -0
  91. package/dist/workspace/locks.js.map +1 -0
  92. package/dist/workspace/policy.d.ts +92 -0
  93. package/dist/workspace/policy.d.ts.map +1 -0
  94. package/dist/workspace/policy.js +7 -0
  95. package/dist/workspace/policy.js.map +1 -0
  96. package/dist/workspace/tree.d.ts +73 -0
  97. package/dist/workspace/tree.d.ts.map +1 -0
  98. package/dist/workspace/tree.js +229 -0
  99. package/dist/workspace/tree.js.map +1 -0
  100. package/package.json +2 -3
  101. package/dist/db/driver.d.ts +0 -35
  102. package/dist/db/driver.d.ts.map +0 -1
  103. package/dist/db/driver.js +0 -95
  104. package/dist/db/driver.js.map +0 -1
  105. package/dist/lib/git.d.ts +0 -29
  106. package/dist/lib/git.d.ts.map +0 -1
  107. package/dist/lib/git.js +0 -62
  108. package/dist/lib/git.js.map +0 -1
  109. package/dist/services/staging-local.d.ts +0 -58
  110. package/dist/services/staging-local.d.ts.map +0 -1
  111. package/dist/services/staging-local.js +0 -391
  112. package/dist/services/staging-local.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"schema.js","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Reads the content model from `.mica/schema.json` in the content repository.\n * The Astro project writes it. The manifest is untrusted input (another core\n * version, hand-edited or truncated), so it is validated again here.\n */\n\nimport {\n checkCompatibility,\n defineSite,\n isFieldKind,\n isProtocolManifest,\n MANIFEST_PATH,\n PROTOCOL_VERSION,\n type Collection,\n type ContentSource,\n type FieldMap,\n type Singleton,\n type Site,\n} from \"@micacms/core\";\n\nexport class SchemaError extends Error {\n override name = \"SchemaError\";\n}\n\n/** Where a model came from, for logs and the system screen. */\nexport function schemaOrigin(source: ContentSource): string {\n return `${source.origin}/${MANIFEST_PATH}`;\n}\n\n/**\n * Reads the model from the content repository. A missing file gets a long\n * message because a first deployment hits it.\n */\nexport async function loadSchema(source: ContentSource): Promise<Site> {\n const origin = schemaOrigin(source);\n\n let text: string | null;\n try {\n text = await source.read(MANIFEST_PATH);\n } catch (error) {\n // An unreachable repository is not a bad model; the fixes are different.\n throw new SchemaError(\n `Could not read the content model from ${source.origin}.\\n` +\n ` ${(error as Error).message}\\n` +\n ` The repository has to be reachable before Mica can start: the model\\n` +\n ` lives in it, at ${MANIFEST_PATH}.`,\n );\n }\n\n if (text === null) throw new SchemaError(describeMissing(origin));\n\n let raw: unknown;\n try {\n raw = JSON.parse(text);\n } catch (error) {\n throw new SchemaError(`${origin} is not valid JSON: ${(error as Error).message}`);\n }\n\n return checkSchema(raw, origin);\n}\n\nfunction describeMissing(origin: string): string {\n return (\n `No content model at ${origin}.\\n` +\n ` Mica ships without one — the Astro project defines it and writes it here.\\n` +\n ` In the project: npx astro build (or start the dev server once)\\n` +\n ` then commit ${MANIFEST_PATH} and push it to the branch Mica is reading.`\n );\n}\n\n/**\n * Runs the `defineSite` checks, and also refuses an unknown field `kind`, which\n * `defineSite` does not check. This turns a core version mismatch into a\n * startup error, not a later failure on an editor's screen.\n */\nexport function checkSchema(raw: unknown, origin = \"the schema\"): Site {\n if (raw === null || typeof raw !== \"object\") {\n throw new SchemaError(`${origin} is ${raw === null ? \"null\" : typeof raw}, expected an object.`);\n }\n\n // A bare `{ collections }` without the protocol envelope is also accepted.\n if (isProtocolManifest(raw)) {\n const compatibility = checkCompatibility(raw);\n if (!compatibility.ok && compatibility.reason === \"newer\") {\n throw new SchemaError(\n `${origin} follows protocol version ${compatibility.version}; this build of Mica ` +\n `understands version ${PROTOCOL_VERSION}.\\n` +\n ` The project was built against a newer @micacms/astro. Upgrade Mica.`,\n );\n }\n }\n\n const candidate = raw as { collections?: unknown; singletons?: unknown };\n\n if (candidate.collections === null || typeof candidate.collections !== \"object\") {\n throw new SchemaError(`${origin} has no \\`collections\\`. Expected defineSite({ collections: { ... } }).`);\n }\n\n const rawSingletons = candidate.singletons;\n if (rawSingletons !== undefined && (rawSingletons === null || typeof rawSingletons !== \"object\")) {\n throw new SchemaError(`${origin} has a \\`singletons\\` that is not an object.`);\n }\n\n const collections = candidate.collections as Record<string, Collection>;\n const singletons = (rawSingletons ?? {}) as Record<string, Singleton>;\n\n for (const [name, def] of Object.entries(collections)) {\n assertFieldMap(def?.fields, `collections.${name}`, origin);\n }\n for (const [name, def] of Object.entries(singletons)) {\n assertFieldMap(def?.fields, `singletons.${name}`, origin);\n }\n\n // Drop, do not refuse, a default sort on an undeclared field: only the list order is affected.\n for (const def of Object.values(collections)) {\n const field = def?.defaultSort?.field;\n if (field && field !== \"position\" && !def.fields?.[field]) delete def.defaultSort;\n }\n\n try {\n return defineSite({ collections, singletons });\n } catch (error) {\n throw new SchemaError(`${origin} is not a valid content model.\\n ${(error as Error).message}`);\n }\n}\n\nfunction assertFieldMap(fields: unknown, at: string, origin: string): void {\n if (fields === null || typeof fields !== \"object\") {\n throw new SchemaError(`${origin}: \"${at}\" has no \\`fields\\` object.`);\n }\n\n for (const [key, field] of Object.entries(fields as FieldMap)) {\n const here = `${at}.${key}`;\n\n if (field === null || typeof field !== \"object\") {\n throw new SchemaError(`${origin}: field \"${here}\" is not an object.`);\n }\n if (!isFieldKind(field.kind)) {\n throw new SchemaError(\n `${origin}: field \"${here}\" has unknown kind \"${String(field.kind)}\".\\n` +\n ` This build of Mica does not support it — the project may have been built\\n` +\n ` against a newer @micacms/astro than the server is running.`,\n );\n }\n if (field.kind === \"object\" || field.kind === \"repeater\") {\n assertFieldMap(field.fields, here, origin);\n }\n }\n}\n\n/**\n * A short digest of a model, to show drift. Field order counts, because it is\n * the order of the admin form.\n */\nexport async function schemaFingerprint(site: Site): Promise<string> {\n const { createHash } = await import(\"node:crypto\");\n return createHash(\"sha256\").update(JSON.stringify(site)).digest(\"hex\").slice(0, 12);\n}\n"]}
package/dist/server.d.ts CHANGED
@@ -1,11 +1,6 @@
1
1
  /**
2
- * Listening, and stopping.
3
- *
4
- * The shared half of every app's entry point: bind the Hono app to a socket,
5
- * shut down cleanly on SIGTERM/SIGINT, and turn the one most likely first-run
6
- * failure — a busy port — into a message instead of a stack trace. What the
7
- * apps keep for themselves is what to *say* when the server is up, because
8
- * that is where a laptop and a container genuinely differ.
2
+ * Binds the app to a socket and shuts down cleanly on SIGTERM and SIGINT. Each
3
+ * app keeps its own startup message.
9
4
  */
10
5
  import type { Hono } from "hono";
11
6
  import type { Env } from "./http.js";
@@ -15,13 +10,10 @@ export interface StartServerOptions {
15
10
  host: string;
16
11
  port: number;
17
12
  logger: Logger;
18
- /** Called once the socket is listening — the banner or the log line. */
19
13
  onListening(info: {
20
14
  port: number;
21
15
  }): void;
22
- /** The message for a busy port. Defaults to a plain sentence. */
23
16
  portInUseMessage?(): string;
24
- /** Releases what the composition holds — timers, the database. */
25
17
  shutdown(): Promise<void>;
26
18
  }
27
19
  export declare function startServer(options: StartServerOptions): void;
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAEjC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AACrC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAE9C,MAAM,WAAW,kBAAkB;IACjC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,wEAAwE;IACxE,WAAW,CAAC,IAAI,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC1C,iEAAiE;IACjE,gBAAgB,CAAC,IAAI,MAAM,CAAC;IAC5B,kEAAkE;IAClE,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3B;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,kBAAkB,GAAG,IAAI,CA+C7D"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAEjC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AACrC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAE9C,MAAM,WAAW,kBAAkB;IACjC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,IAAI,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC1C,gBAAgB,CAAC,IAAI,MAAM,CAAC;IAC5B,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3B;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,kBAAkB,GAAG,IAAI,CA8C7D"}
package/dist/server.js CHANGED
@@ -1,11 +1,6 @@
1
1
  /**
2
- * Listening, and stopping.
3
- *
4
- * The shared half of every app's entry point: bind the Hono app to a socket,
5
- * shut down cleanly on SIGTERM/SIGINT, and turn the one most likely first-run
6
- * failure — a busy port — into a message instead of a stack trace. What the
7
- * apps keep for themselves is what to *say* when the server is up, because
8
- * that is where a laptop and a container genuinely differ.
2
+ * Binds the app to a socket and shuts down cleanly on SIGTERM and SIGINT. Each
3
+ * app keeps its own startup message.
9
4
  */
10
5
  import { serve } from "@hono/node-server";
11
6
  export function startServer(options) {
@@ -25,14 +20,13 @@ export function startServer(options) {
25
20
  return;
26
21
  shuttingDown = true;
27
22
  logger.info("Shutting down", { signal });
28
- // Stop accepting connections, then release whatever the app holds.
29
23
  server.close(() => {
30
24
  void options.shutdown().then(() => {
31
25
  logger.info("Shutdown complete");
32
26
  process.exit(0);
33
27
  });
34
28
  });
35
- // Do not let a hung connection block a container restart indefinitely.
29
+ // A hung connection must not block a container restart.
36
30
  setTimeout(() => {
37
31
  logger.warn("Forcing exit after shutdown timeout");
38
32
  process.exit(1);
@@ -1 +1 @@
1
- {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAmB1C,MAAM,UAAU,WAAW,CAAC,OAA2B;IACrD,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC;IAE3B,MAAM,MAAM,GAAG,KAAK,CAClB,EAAE,KAAK,EAAE,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,QAAQ,EAAE,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,EACxE,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CACnD,CAAC;IAEF,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,KAA4B,EAAE,EAAE;QAClD,IAAI,KAAK,CAAC,IAAI,KAAK,YAAY;YAAE,MAAM,KAAK,CAAC;QAE7C,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,OAAO,CAAC,gBAAgB,EAAE,EAAE;YAC1B,QAAQ,OAAO,CAAC,IAAI,OAAO,OAAO,CAAC,IAAI,qCAAqC;gBAC1E,yBAAyB,CAC9B,CAAC;QACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC,CAAC;IAEH,IAAI,YAAY,GAAG,KAAK,CAAC;IAEzB,MAAM,QAAQ,GAAG,CAAC,MAAc,EAAQ,EAAE;QACxC,IAAI,YAAY;YAAE,OAAO;QACzB,YAAY,GAAG,IAAI,CAAC;QACpB,MAAM,CAAC,IAAI,CAAC,eAAe,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;QAEzC,mEAAmE;QACnE,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE;YAChB,KAAK,OAAO,CAAC,QAAQ,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE;gBAChC,MAAM,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAC;gBACjC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAClB,CAAC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;QAEH,uEAAuE;QACvE,UAAU,CAAC,GAAG,EAAE;YACd,MAAM,CAAC,IAAI,CAAC,qCAAqC,CAAC,CAAC;YACnD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC,EAAE,MAAM,CAAC,CAAC,KAAK,EAAE,CAAC;IACrB,CAAC,CAAC;IAEF,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC;IACjD,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC;IAE/C,OAAO,CAAC,EAAE,CAAC,oBAAoB,EAAE,CAAC,MAAM,EAAE,EAAE;QAC1C,MAAM,CAAC,KAAK,CAAC,6BAA6B,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IAC1E,CAAC,CAAC,CAAC;AACL,CAAC"}
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAgB1C,MAAM,UAAU,WAAW,CAAC,OAA2B;IACrD,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC;IAE3B,MAAM,MAAM,GAAG,KAAK,CAClB,EAAE,KAAK,EAAE,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,QAAQ,EAAE,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,EACxE,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CACnD,CAAC;IAEF,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,KAA4B,EAAE,EAAE;QAClD,IAAI,KAAK,CAAC,IAAI,KAAK,YAAY;YAAE,MAAM,KAAK,CAAC;QAE7C,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,OAAO,CAAC,gBAAgB,EAAE,EAAE;YAC1B,QAAQ,OAAO,CAAC,IAAI,OAAO,OAAO,CAAC,IAAI,qCAAqC;gBAC1E,yBAAyB,CAC9B,CAAC;QACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC,CAAC;IAEH,IAAI,YAAY,GAAG,KAAK,CAAC;IAEzB,MAAM,QAAQ,GAAG,CAAC,MAAc,EAAQ,EAAE;QACxC,IAAI,YAAY;YAAE,OAAO;QACzB,YAAY,GAAG,IAAI,CAAC;QACpB,MAAM,CAAC,IAAI,CAAC,eAAe,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;QAEzC,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE;YAChB,KAAK,OAAO,CAAC,QAAQ,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE;gBAChC,MAAM,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAC;gBACjC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAClB,CAAC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;QAEH,wDAAwD;QACxD,UAAU,CAAC,GAAG,EAAE;YACd,MAAM,CAAC,IAAI,CAAC,qCAAqC,CAAC,CAAC;YACnD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC,EAAE,MAAM,CAAC,CAAC,KAAK,EAAE,CAAC;IACrB,CAAC,CAAC;IAEF,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC;IACjD,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC;IAE/C,OAAO,CAAC,EAAE,CAAC,oBAAoB,EAAE,CAAC,MAAM,EAAE,EAAE;QAC1C,MAAM,CAAC,KAAK,CAAC,6BAA6B,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IAC1E,CAAC,CAAC,CAAC;AACL,CAAC","sourcesContent":["/**\n * Binds the app to a socket and shuts down cleanly on SIGTERM and SIGINT. Each\n * app keeps its own startup message.\n */\n\nimport { serve } from \"@hono/node-server\";\nimport type { Hono } from \"hono\";\n\nimport type { Env } from \"./http.js\";\nimport type { Logger } from \"./lib/logger.js\";\n\nexport interface StartServerOptions {\n app: Hono<Env>;\n host: string;\n port: number;\n logger: Logger;\n onListening(info: { port: number }): void;\n portInUseMessage?(): string;\n shutdown(): Promise<void>;\n}\n\nexport function startServer(options: StartServerOptions): void {\n const { logger } = options;\n\n const server = serve(\n { fetch: options.app.fetch, hostname: options.host, port: options.port },\n (info) => options.onListening({ port: info.port }),\n );\n\n server.on(\"error\", (error: NodeJS.ErrnoException) => {\n if (error.code !== \"EADDRINUSE\") throw error;\n\n process.stderr.write(\n options.portInUseMessage?.() ??\n `Port ${options.port} on ${options.host} is already in use. Stop the other ` +\n `process, or set PORT.\\n`,\n );\n process.exit(1);\n });\n\n let shuttingDown = false;\n\n const shutdown = (signal: string): void => {\n if (shuttingDown) return;\n shuttingDown = true;\n logger.info(\"Shutting down\", { signal });\n\n server.close(() => {\n void options.shutdown().then(() => {\n logger.info(\"Shutdown complete\");\n process.exit(0);\n });\n });\n\n // A hung connection must not block a container restart.\n setTimeout(() => {\n logger.warn(\"Forcing exit after shutdown timeout\");\n process.exit(1);\n }, 15_000).unref();\n };\n\n process.on(\"SIGTERM\", () => shutdown(\"SIGTERM\"));\n process.on(\"SIGINT\", () => shutdown(\"SIGINT\"));\n\n process.on(\"unhandledRejection\", (reason) => {\n logger.error(\"Unhandled promise rejection\", { reason: String(reason) });\n });\n}\n"]}
@@ -0,0 +1,150 @@
1
+ /** The staging API types, the before/after comparison and the analysis of a pending change set. */
2
+ import type { Author } from "../workspace/files.js";
3
+ import { type ContentChange, type ContentSource, type FieldIssue, type PublishResult, type Site } from "@micacms/core";
4
+ import type { SyncStatus } from "../workspace/policy.js";
5
+ /** Where the change happened, in words an editor recognises. */
6
+ export type ChangeOwner = {
7
+ kind: "entry";
8
+ collection: string;
9
+ slug: string;
10
+ label: string;
11
+ /** The slug before a pending rename. Set on the files of both slugs. */
12
+ renamedFrom?: string;
13
+ } | {
14
+ kind: "singleton";
15
+ name: string;
16
+ label: string;
17
+ } | {
18
+ kind: "order";
19
+ collection: string;
20
+ label: string;
21
+ } | {
22
+ kind: "media";
23
+ label: string;
24
+ } | {
25
+ kind: "other";
26
+ label: string;
27
+ };
28
+ export interface AnnotatedChange extends ContentChange {
29
+ owner: ChangeOwner;
30
+ /**
31
+ * The file's revision, null for a deletion. Publish sends it back, so a file
32
+ * saved after the review is not published unseen.
33
+ */
34
+ revision?: string | null;
35
+ }
36
+ /** One thing that would make the staged result unpublishable. */
37
+ export interface StagingProblem {
38
+ /** The file the problem is about, relative to the content root. */
39
+ path: string;
40
+ label: string;
41
+ issues: FieldIssue[];
42
+ }
43
+ export interface StagingValidation {
44
+ ok: boolean;
45
+ problems: StagingProblem[];
46
+ }
47
+ export interface StagingStatus {
48
+ /** Whose working tree it is. The UI wording changes with it. */
49
+ owner: "developer" | "managed";
50
+ publish: {
51
+ enabled: boolean;
52
+ reason?: string;
53
+ };
54
+ /** The commit the pending changes sit on. */
55
+ head: string | null;
56
+ /** The branch a publication lands on, where the workspace knows it. */
57
+ branch: string | null;
58
+ /** Commits created and not yet pushed. */
59
+ ahead: number | null;
60
+ changes: AnnotatedChange[];
61
+ validation: StagingValidation;
62
+ /** Where a clone that Mica keeps stands against its remote. Null for the developer's checkout. */
63
+ sync: SyncStatus | null;
64
+ }
65
+ export interface PublishInput {
66
+ /** Subject line of the commit. */
67
+ message: string;
68
+ /**
69
+ * The reviewed paths, with the revision each had in the review. Only these
70
+ * are published; a file saved again since, or another file of the same entry,
71
+ * makes Publish refuse.
72
+ */
73
+ paths: Record<string, string | null>;
74
+ author?: Author;
75
+ }
76
+ /** What the System page shows about the repository. */
77
+ export interface RepositoryInfo {
78
+ owner: "developer" | "managed";
79
+ branch: string | null;
80
+ head: string | null;
81
+ /** The remote, for a clone that Mica keeps. */
82
+ url: string | null;
83
+ cloneBytes: number | null;
84
+ sync: SyncStatus | null;
85
+ }
86
+ export interface ContentStagingService {
87
+ status(): Promise<StagingStatus>;
88
+ publish(input: PublishInput): Promise<PublishResult>;
89
+ discard(paths: string[]): Promise<{
90
+ discarded: string[];
91
+ }>;
92
+ /** Retries the push after `committed-pending-push`. */
93
+ push(): Promise<PublishResult>;
94
+ /**
95
+ * A document before and after a pending change. `origin` compares against the
96
+ * website's version, not the last commit.
97
+ */
98
+ compare(target: CompareTarget, against?: "head" | "origin"): Promise<Comparison>;
99
+ /** Brings a clone that Mica keeps up to date now. Refused for the developer's checkout. */
100
+ sync(): Promise<void>;
101
+ resolve(group: string, choice: "mine" | "website"): Promise<void>;
102
+ repository(): Promise<RepositoryInfo>;
103
+ }
104
+ /** What to compare on the Publish page. */
105
+ export type CompareTarget =
106
+ /** `from` names the published slug of a renamed entry. */
107
+ {
108
+ kind: "entry";
109
+ collection: string;
110
+ slug: string;
111
+ from?: string;
112
+ } | {
113
+ kind: "singleton";
114
+ name: string;
115
+ } | {
116
+ kind: "order";
117
+ collection: string;
118
+ };
119
+ /**
120
+ * Raw field data on both sides of a pending change. The editor renders the
121
+ * comparison from the model.
122
+ */
123
+ export interface Comparison {
124
+ /** As the repository has it — null when the change creates the document. */
125
+ before: Record<string, unknown> | null;
126
+ /** As staged for publication — null when the change deletes it. */
127
+ after: Record<string, unknown> | null;
128
+ }
129
+ /**
130
+ * Reads one document from `published` and from `staged`. An order target
131
+ * becomes `{ order: [{ slug, title }] }` on each side, in position order.
132
+ */
133
+ export declare function compareSources(model: Site, published: ContentSource, staged: ContentSource, target: CompareTarget): Promise<Comparison>;
134
+ /**
135
+ * Annotates a change set with owners and validates the entries it touches, in
136
+ * one read of the affected collections. Only changed documents are validated: a
137
+ * broken entry committed earlier must not block an unrelated publication.
138
+ */
139
+ export declare function analyseChanges(model: Site, source: ContentSource, changes: ContentChange[],
140
+ /** The repository's side. When given, renamed entries are detected. */
141
+ published?: ContentSource): Promise<{
142
+ changes: AnnotatedChange[];
143
+ validation: StagingValidation;
144
+ }>;
145
+ /** Which piece of content a file belongs to, read from the model's path layout. */
146
+ export declare function classifyPath(model: Site, path: string): ChangeOwner;
147
+ export declare function describeProblems(problems: StagingProblem[]): string;
148
+ /** Field issues with the file path folded in, for the flat API envelope. */
149
+ export declare function flattenProblems(problems: StagingProblem[]): FieldIssue[];
150
+ //# sourceMappingURL=changes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"changes.d.ts","sourceRoot":"","sources":["../../src/services/changes.ts"],"names":[],"mappings":"AAAA,mGAAmG;AAEnG,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,EAcL,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,KAAK,UAAU,EACf,KAAK,aAAa,EAClB,KAAK,IAAI,EACV,MAAM,eAAe,CAAC;AAGvB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAEzD,gEAAgE;AAChE,MAAM,MAAM,WAAW,GACnB;IACE,IAAI,EAAE,OAAO,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,wEAAwE;IACxE,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,GACD;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAClD;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACpD;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAChC;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAErC,MAAM,WAAW,eAAgB,SAAQ,aAAa;IACpD,KAAK,EAAE,WAAW,CAAC;IACnB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,iEAAiE;AACjE,MAAM,WAAW,cAAc;IAC7B,mEAAmE;IACnE,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,UAAU,EAAE,CAAC;CACtB;AAED,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,OAAO,CAAC;IACZ,QAAQ,EAAE,cAAc,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,aAAa;IAC5B,gEAAgE;IAChE,KAAK,EAAE,WAAW,GAAG,SAAS,CAAC;IAC/B,OAAO,EAAE;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC/C,6CAA6C;IAC7C,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,uEAAuE;IACvE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,0CAA0C;IAC1C,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,UAAU,EAAE,iBAAiB,CAAC;IAC9B,kGAAkG;IAClG,IAAI,EAAE,UAAU,GAAG,IAAI,CAAC;CACzB;AAED,MAAM,WAAW,YAAY;IAC3B,kCAAkC;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC;IACrC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,uDAAuD;AACvD,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,WAAW,GAAG,SAAS,CAAC;IAC/B,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,+CAA+C;IAC/C,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,IAAI,EAAE,UAAU,GAAG,IAAI,CAAC;CACzB;AAED,MAAM,WAAW,qBAAqB;IACpC,MAAM,IAAI,OAAO,CAAC,aAAa,CAAC,CAAC;IACjC,OAAO,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;IACrD,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC;QAAE,SAAS,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC;IAC3D,uDAAuD;IACvD,IAAI,IAAI,OAAO,CAAC,aAAa,CAAC,CAAC;IAC/B;;;OAGG;IACH,OAAO,CAAC,MAAM,EAAE,aAAa,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IACjF,2FAA2F;IAC3F,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClE,UAAU,IAAI,OAAO,CAAC,cAAc,CAAC,CAAC;CACvC;AAED,2CAA2C;AAC3C,MAAM,MAAM,aAAa;AACvB,0DAA0D;AACxD;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAClE;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACnC;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC;AAE1C;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB,4EAA4E;IAC5E,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACvC,mEAAmE;IACnE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CACvC;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAClC,KAAK,EAAE,IAAI,EACX,SAAS,EAAE,aAAa,EACxB,MAAM,EAAE,aAAa,EACrB,MAAM,EAAE,aAAa,GACpB,OAAO,CAAC,UAAU,CAAC,CAuCrB;AAED;;;;GAIG;AACH,wBAAsB,cAAc,CAClC,KAAK,EAAE,IAAI,EACX,MAAM,EAAE,aAAa,EACrB,OAAO,EAAE,aAAa,EAAE;AACxB,uEAAuE;AACvE,SAAS,CAAC,EAAE,aAAa,GACxB,OAAO,CAAC;IAAE,OAAO,EAAE,eAAe,EAAE,CAAC;IAAC,UAAU,EAAE,iBAAiB,CAAA;CAAE,CAAC,CAiGxE;AAED,mFAAmF;AACnF,wBAAgB,YAAY,CAAC,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,GAAG,WAAW,CAoCnE;AAwKD,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,cAAc,EAAE,GAAG,MAAM,CAInE;AAED,4EAA4E;AAC5E,wBAAgB,eAAe,CAAC,QAAQ,EAAE,cAAc,EAAE,GAAG,UAAU,EAAE,CAOxE"}
@@ -0,0 +1,339 @@
1
+ /** The staging API types, the before/after comparison and the analysis of a pending change set. */
2
+ import { ORDER_FILE, collectionEntryPath, entryLayout, entryTitle, isValidSlug, readCollection, readCollectionEntry, readSingleton, relativizeMedia, resolveSlugField, singletonPath, validateFields, } from "@micacms/core";
3
+ import { badRequest } from "../lib/errors.js";
4
+ /**
5
+ * Reads one document from `published` and from `staged`. An order target
6
+ * becomes `{ order: [{ slug, title }] }` on each side, in position order.
7
+ */
8
+ export async function compareSources(model, published, staged, target) {
9
+ if (target.kind === "singleton") {
10
+ const def = model.singletons[target.name];
11
+ if (!def)
12
+ throw badRequest(`There is no singleton called "${target.name}".`);
13
+ return {
14
+ before: await readSingleton(published, def, target.name),
15
+ after: await readSingleton(staged, def, target.name),
16
+ };
17
+ }
18
+ const def = model.collections[target.collection];
19
+ if (!def)
20
+ throw badRequest(`There is no collection called "${target.collection}".`);
21
+ if (target.kind === "entry") {
22
+ const from = target.from ?? target.slug;
23
+ for (const slug of [target.slug, from]) {
24
+ if (!isValidSlug(slug))
25
+ throw badRequest(`"${slug}" is not a slug.`);
26
+ }
27
+ const before = await readCollectionEntry(published, def, target.collection, from);
28
+ const after = await readCollectionEntry(staged, def, target.collection, target.slug);
29
+ if (from === target.slug)
30
+ return { before, after };
31
+ // A rename: compare the on-disk form, where colocated media paths do not
32
+ // name the entry's folder.
33
+ return {
34
+ before: before && onDisk(def, target.collection, from, before),
35
+ after: after && onDisk(def, target.collection, target.slug, after),
36
+ };
37
+ }
38
+ const ordering = async (source) => {
39
+ const entries = await readCollection(source, def, target.collection);
40
+ if (entries.length === 0)
41
+ return null;
42
+ return {
43
+ order: entries.map((entry) => ({ slug: entry.slug, title: entryTitle(def, entry.data) })),
44
+ };
45
+ };
46
+ return { before: await ordering(published), after: await ordering(staged) };
47
+ }
48
+ /**
49
+ * Annotates a change set with owners and validates the entries it touches, in
50
+ * one read of the affected collections. Only changed documents are validated: a
51
+ * broken entry committed earlier must not block an unrelated publication.
52
+ */
53
+ export async function analyseChanges(model, source, changes,
54
+ /** The repository's side. When given, renamed entries are detected. */
55
+ published) {
56
+ const owners = changes.map((change) => classifyPath(model, change.path));
57
+ const collectionsToRead = new Set();
58
+ for (const owner of owners) {
59
+ if (owner.kind === "entry" || owner.kind === "order")
60
+ collectionsToRead.add(owner.collection);
61
+ }
62
+ const problems = [];
63
+ const titles = new Map();
64
+ const entriesByCollection = new Map();
65
+ for (const name of collectionsToRead) {
66
+ const def = model.collections[name];
67
+ try {
68
+ const entries = await readCollection(source, def, name);
69
+ const bySlug = new Map(entries.map((entry) => [entry.slug, entry.data]));
70
+ entriesByCollection.set(name, bySlug);
71
+ for (const [slug, data] of bySlug)
72
+ titles.set(`${name}/${slug}`, entryTitle(def, data));
73
+ }
74
+ catch (error) {
75
+ // An unreadable collection, possibly from an untouched file, is a problem
76
+ // at the path it names.
77
+ const path = error.path ?? name;
78
+ problems.push({
79
+ path,
80
+ label: def.label,
81
+ issues: [{ path: "", message: error.message }],
82
+ });
83
+ }
84
+ }
85
+ if (published) {
86
+ await detectRenames(model, published, source, changes, owners, entriesByCollection);
87
+ }
88
+ const validated = new Set();
89
+ for (const owner of owners) {
90
+ if (owner.kind === "entry") {
91
+ const key = `${owner.collection}/${owner.slug}`;
92
+ if (validated.has(key))
93
+ continue;
94
+ validated.add(key);
95
+ const def = model.collections[owner.collection];
96
+ // Absent means the change set deletes this entry.
97
+ const data = entriesByCollection.get(owner.collection)?.get(owner.slug);
98
+ if (!data)
99
+ continue;
100
+ const result = validateFields(def.fields, data, "storage");
101
+ if (!result.ok) {
102
+ problems.push({
103
+ path: entryDataPath(def, owner.collection, owner.slug),
104
+ label: labelEntry(def, owner.slug, titles.get(key)),
105
+ issues: result.issues,
106
+ });
107
+ }
108
+ }
109
+ if (owner.kind === "singleton" && !validated.has(`singleton:${owner.name}`)) {
110
+ validated.add(`singleton:${owner.name}`);
111
+ const def = model.singletons[owner.name];
112
+ const dataPath = singletonPath(def.fields, owner.name);
113
+ try {
114
+ const data = await readSingleton(source, def, owner.name);
115
+ if (data === null)
116
+ continue;
117
+ const result = validateFields(def.fields, data, "storage");
118
+ if (!result.ok) {
119
+ problems.push({ path: dataPath, label: def.label, issues: result.issues });
120
+ }
121
+ }
122
+ catch (error) {
123
+ problems.push({
124
+ path: error.path ?? dataPath,
125
+ label: def.label,
126
+ issues: [{ path: "", message: error.message }],
127
+ });
128
+ }
129
+ }
130
+ }
131
+ const annotated = changes.map((change, index) => {
132
+ const owner = owners[index];
133
+ if (owner.kind === "entry") {
134
+ const def = model.collections[owner.collection];
135
+ return {
136
+ ...change,
137
+ owner: { ...owner, label: labelEntry(def, owner.slug, titles.get(`${owner.collection}/${owner.slug}`)) },
138
+ };
139
+ }
140
+ return { ...change, owner };
141
+ });
142
+ return {
143
+ changes: annotated,
144
+ validation: { ok: problems.length === 0, problems },
145
+ };
146
+ }
147
+ /** Which piece of content a file belongs to, read from the model's path layout. */
148
+ export function classifyPath(model, path) {
149
+ const segments = path.split("/");
150
+ const head = segments[0] ?? "";
151
+ const collection = model.collections[head];
152
+ if (collection && segments.length > 1) {
153
+ if (segments.length === 2 && segments[1] === ORDER_FILE) {
154
+ return { kind: "order", collection: head, label: `${collection.label}: order` };
155
+ }
156
+ if (entryLayout(collection.fields) === "directory") {
157
+ const slug = segments[1];
158
+ if (segments.length >= 3 && isValidSlug(slug)) {
159
+ return { kind: "entry", collection: head, slug, label: slug };
160
+ }
161
+ }
162
+ else if (segments.length === 2 && segments[1].endsWith(".json")) {
163
+ const slug = segments[1].slice(0, -".json".length);
164
+ if (isValidSlug(slug))
165
+ return { kind: "entry", collection: head, slug, label: slug };
166
+ }
167
+ }
168
+ const singletonDir = model.singletons[head];
169
+ if (singletonDir && segments.length > 1) {
170
+ return { kind: "singleton", name: head, label: singletonDir.label };
171
+ }
172
+ if (head.endsWith(".json")) {
173
+ const name = head.slice(0, -".json".length);
174
+ const singleton = model.singletons[name];
175
+ if (singleton && segments.length === 1) {
176
+ return { kind: "singleton", name, label: singleton.label };
177
+ }
178
+ }
179
+ if (head === "media")
180
+ return { kind: "media", label: "Shared media" };
181
+ return { kind: "other", label: path };
182
+ }
183
+ /** Content share that two versions of an entry need to count as one moved entry, as in Git. */
184
+ const RENAME_SIMILARITY = 0.5;
185
+ /**
186
+ * Marks renamed entries in place in `owners`. An entry has no identity but its
187
+ * slug, so deleted and created entries are paired by content, like Git's rename
188
+ * detection. An order file whose only change is the renamed slug joins the
189
+ * entry, so a discard cannot leave the order naming a slug that does not exist.
190
+ */
191
+ async function detectRenames(model, published, source, changes, owners, staged) {
192
+ const kinds = new Map();
193
+ owners.forEach((owner, index) => {
194
+ if (owner.kind !== "entry")
195
+ return;
196
+ const bySlug = kinds.get(owner.collection) ?? new Map();
197
+ kinds.set(owner.collection, bySlug);
198
+ const set = bySlug.get(owner.slug) ?? new Set();
199
+ bySlug.set(owner.slug, set);
200
+ set.add(changes[index].kind);
201
+ });
202
+ for (const [name, bySlug] of kinds) {
203
+ const after = staged.get(name);
204
+ if (!after)
205
+ continue;
206
+ const only = (kind) => [...bySlug].filter(([, set]) => set.size === 1 && set.has(kind)).map(([slug]) => slug);
207
+ const deleted = only("delete").filter((slug) => !after.has(slug));
208
+ const created = only("create").filter((slug) => after.has(slug));
209
+ if (deleted.length === 0 || created.length === 0)
210
+ continue;
211
+ const def = model.collections[name];
212
+ let before;
213
+ try {
214
+ const entries = await readCollection(published, def, name);
215
+ before = new Map(entries.map((entry) => [entry.slug, entry.data]));
216
+ }
217
+ catch {
218
+ continue;
219
+ }
220
+ const candidates = [];
221
+ for (const from of deleted) {
222
+ const old = before.get(from);
223
+ if (!old)
224
+ continue;
225
+ for (const to of created) {
226
+ if (before.has(to))
227
+ continue;
228
+ const score = similarity(def, onDisk(def, name, from, old), onDisk(def, name, to, after.get(to)));
229
+ if (score >= RENAME_SIMILARITY)
230
+ candidates.push({ from, to, score });
231
+ }
232
+ }
233
+ const renamedTo = new Map();
234
+ const paired = new Set();
235
+ for (const { from, to } of candidates.sort((a, b) => b.score - a.score)) {
236
+ if (renamedTo.has(from) || paired.has(to))
237
+ continue;
238
+ renamedTo.set(from, to);
239
+ paired.add(to);
240
+ }
241
+ if (renamedTo.size === 0)
242
+ continue;
243
+ const renamedFrom = new Map([...renamedTo].map(([from, to]) => [to, from]));
244
+ const orderMovesOnlySlugs = await onlyRenamedInOrder(published, source, name, renamedTo);
245
+ const [firstFrom, firstTo] = [...renamedTo][0];
246
+ owners.forEach((owner, index) => {
247
+ if (owner.kind === "order" && owner.collection === name && orderMovesOnlySlugs) {
248
+ owners[index] = {
249
+ kind: "entry",
250
+ collection: name,
251
+ slug: firstTo,
252
+ label: firstTo,
253
+ renamedFrom: firstFrom,
254
+ };
255
+ }
256
+ if (owner.kind !== "entry" || owner.collection !== name)
257
+ return;
258
+ const to = renamedTo.get(owner.slug);
259
+ if (to)
260
+ owners[index] = { ...owner, slug: to, label: to, renamedFrom: owner.slug };
261
+ const from = renamedFrom.get(owner.slug);
262
+ if (from)
263
+ owners[index] = { ...owner, renamedFrom: from };
264
+ });
265
+ }
266
+ }
267
+ /** True when a collection's staged order is the published one with the renamed slugs replaced. */
268
+ async function onlyRenamedInOrder(published, staged, name, renamedTo) {
269
+ const path = `${name}/${ORDER_FILE}`;
270
+ const parse = (raw) => {
271
+ try {
272
+ return raw === null ? null : JSON.parse(raw);
273
+ }
274
+ catch {
275
+ return null;
276
+ }
277
+ };
278
+ const before = parse(await published.read(path));
279
+ const after = parse(await staged.read(path));
280
+ if (!Array.isArray(before) || !Array.isArray(after))
281
+ return false;
282
+ const expected = before.map((slug) => typeof slug === "string" ? (renamedTo.get(slug) ?? slug) : slug);
283
+ return canonical(expected) === canonical(after);
284
+ }
285
+ /** The share of non-empty fields, the slug aside, that two versions of an entry agree on. */
286
+ function similarity(def, a, b) {
287
+ const slugKey = resolveSlugField(def);
288
+ let compared = 0;
289
+ let equal = 0;
290
+ for (const key of Object.keys(def.fields)) {
291
+ if (key === slugKey || (isEmpty(a[key]) && isEmpty(b[key])))
292
+ continue;
293
+ compared += 1;
294
+ if (canonical(a[key]) === canonical(b[key]))
295
+ equal += 1;
296
+ }
297
+ return compared === 0 ? 0 : equal / compared;
298
+ }
299
+ /**
300
+ * Entry data with media paths as the files store them: relative to the entry,
301
+ * where it has a folder.
302
+ */
303
+ function onDisk(def, collection, slug, data) {
304
+ return entryLayout(def.fields) === "directory"
305
+ ? relativizeMedia(def.fields, data, collectionEntryPath(def.fields, collection, slug))
306
+ : data;
307
+ }
308
+ function isEmpty(value) {
309
+ return value === undefined || value === null || value === "" || (Array.isArray(value) && value.length === 0);
310
+ }
311
+ /** JSON with sorted keys, so two equal objects compare equal. */
312
+ function canonical(value) {
313
+ return JSON.stringify(value ?? null, (_key, entry) => {
314
+ if (entry === null || typeof entry !== "object" || Array.isArray(entry))
315
+ return entry;
316
+ const record = entry;
317
+ return Object.fromEntries(Object.keys(record).sort().map((key) => [key, record[key]]));
318
+ });
319
+ }
320
+ function labelEntry(def, slug, title) {
321
+ return `${def.labelSingular ?? def.label}: ${title ?? slug}`;
322
+ }
323
+ function entryDataPath(def, collection, slug) {
324
+ const base = collectionEntryPath(def.fields, collection, slug);
325
+ return entryLayout(def.fields) === "directory" ? `${base}/index.json` : base;
326
+ }
327
+ export function describeProblems(problems) {
328
+ const names = problems.map((problem) => problem.label);
329
+ const listed = names.slice(0, 3).join(", ") + (names.length > 3 ? ", …" : "");
330
+ return `The pending changes cannot be published until they are valid: ${listed}.`;
331
+ }
332
+ /** Field issues with the file path folded in, for the flat API envelope. */
333
+ export function flattenProblems(problems) {
334
+ return problems.flatMap((problem) => problem.issues.map((issue) => ({
335
+ path: issue.path ? `${problem.path}: ${issue.path}` : problem.path,
336
+ message: issue.message,
337
+ })));
338
+ }
339
+ //# sourceMappingURL=changes.js.map