@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,17 +1,7 @@
1
1
  /**
2
- * Media uploads.
3
- *
4
- * Every uploaded byte passes through here, so this is where the file is judged:
5
- * size, and the real content type read from the bytes rather than from the
6
- * browser's claim.
7
- *
8
- * An upload is a *file in the content repository*. There is no media table, no
9
- * generated key and no object store — a media value is the path to a file a
10
- * reviewer can see in the diff.
11
- *
12
- * The filename therefore matters. It is derived from what the editor uploaded,
13
- * slugified so it is safe as a path segment, and made unique inside its own
14
- * directory rather than globally.
2
+ * Media uploads. Every upload is checked here for size and for its real content
3
+ * type, read from the bytes. An upload is a file in the content repository; its
4
+ * name is slugified and unique in its own directory.
15
5
  */
16
6
  import path from "node:path";
17
7
  import { contentPath, ContentPathError, MEDIA_DIR, slugify } from "@micacms/core";
@@ -21,17 +11,10 @@ import { detectFileType, isAccepted, isImageType, SVG_TYPE } from "../lib/file-t
21
11
  /** Where uploads go when they are not attached to a particular entry. */
22
12
  export const SHARED_MEDIA_DIR = "media";
23
13
  export function createMediaService(deps) {
24
- const { connector, maxBytes } = deps;
25
- /**
26
- * A name that is unique in `directory`.
27
- *
28
- * Scoped to the directory rather than to the whole repository, because that
29
- * is where a collision would actually overwrite something — and because
30
- * `haus-a/media/exterior.jpg` and `haus-b/media/exterior.jpg` are two
31
- * perfectly reasonable files with the same name.
32
- */
14
+ const { files, maxBytes } = deps;
15
+ /** A name that is unique in `directory`. Only there can a collision overwrite a file. */
33
16
  const uniqueName = async (directory, filename) => {
34
- const taken = new Set((await connector.list(directory)).filter((item) => item.type === "file").map((item) => item.name));
17
+ const taken = new Set((await files.list(directory)).filter((item) => item.type === "file").map((item) => item.name));
35
18
  const extension = path.extname(filename);
36
19
  const stem = filename.slice(0, filename.length - extension.length);
37
20
  let candidate = filename;
@@ -40,14 +23,7 @@ export function createMediaService(deps) {
40
23
  return candidate;
41
24
  };
42
25
  return {
43
- /**
44
- * Reads and checks an upload, without writing it.
45
- *
46
- * Separate from the commit so that an entry and the media it references go
47
- * into the repository in *one* change set. Uploading first and saving
48
- * second would leave an orphaned file behind whenever the editor changed
49
- * their mind.
50
- */
26
+ /** Reads and checks an upload, without writing it. */
51
27
  async prepare(input) {
52
28
  const { file } = input;
53
29
  const directory = uploadDirectory(input.directory ?? SHARED_MEDIA_DIR);
@@ -57,7 +33,7 @@ export function createMediaService(deps) {
57
33
  throw new ApiError("payload_too_large", `The file is ${formatBytes(file.size)}. The limit is ${formatBytes(maxBytes)}.`);
58
34
  }
59
35
  const contents = new Uint8Array(await file.arrayBuffer());
60
- // From the bytes, never from the browser's claim about them.
36
+ // From the bytes, never from the browser's claim.
61
37
  const detected = detectFileType(contents);
62
38
  if (!detected) {
63
39
  throw new ApiError("unsupported_media_type", "This file type is not supported.");
@@ -80,16 +56,13 @@ export function createMediaService(deps) {
80
56
  };
81
57
  },
82
58
  /**
83
- * Every media file in the repository, for the editor's picker.
84
- *
85
- * A walk of the content tree rather than a table scan. Content trees are
86
- * small, and the alternative — an index Mica maintains — would be a second
87
- * source of truth that could disagree with the repository.
59
+ * Every media file in the repository. A tree walk is sufficient because
60
+ * content trees are small.
88
61
  */
89
62
  async list() {
90
63
  const found = [];
91
64
  const walk = async (directory) => {
92
- for (const item of await connector.list(directory)) {
65
+ for (const item of await files.list(directory)) {
93
66
  const child = directory === "" ? item.name : `${directory}/${item.name}`;
94
67
  if (item.type === "directory") {
95
68
  await walk(child);
@@ -97,7 +70,7 @@ export function createMediaService(deps) {
97
70
  }
98
71
  if (!isMediaName(item.name))
99
72
  continue;
100
- const bytes = await connector.readBinary(child);
73
+ const bytes = await files.readBinary(child);
101
74
  if (!bytes)
102
75
  continue;
103
76
  const mimeType = typeOf(bytes, item.name);
@@ -116,7 +89,7 @@ export function createMediaService(deps) {
116
89
  return found.sort((a, b) => a.path.localeCompare(b.path));
117
90
  },
118
91
  async read(mediaPath) {
119
- const bytes = await connector.readBinary(contentPath(mediaPath));
92
+ const bytes = await files.readBinary(contentPath(mediaPath));
120
93
  if (!bytes)
121
94
  return null;
122
95
  const mimeType = typeOf(bytes, mediaPath);
@@ -142,24 +115,12 @@ function uploadDirectory(directory) {
142
115
  }
143
116
  return clean;
144
117
  }
145
- /* ------------------------------------------------------------------ */
146
- /**
147
- * A filename that is safe as a path segment and still recognisable.
148
- *
149
- * Slugified rather than sanitised: the result is part of a URL in the built
150
- * site and part of a path in a Git repository, and both are better off without
151
- * spaces, accents or case.
152
- */
118
+ /** A slugified filename: safe as a path segment and as part of a URL. */
153
119
  function safeName(original, extension) {
154
120
  const stem = slugify(path.basename(original).replace(/\.[^.]*$/, "")).slice(0, 80);
155
121
  return `${stem || "upload"}.${extension}`;
156
122
  }
157
- /**
158
- * Extension to media type, for the formats byte-sniffing does not identify.
159
- *
160
- * A fallback, never the first answer: a file's own bytes are the only thing
161
- * that cannot be renamed to lie.
162
- */
123
+ /** Extension to media type, only for formats that byte-sniffing does not identify. */
163
124
  const BY_EXTENSION = {
164
125
  svg: "image/svg+xml",
165
126
  pdf: "application/pdf",
@@ -171,9 +132,8 @@ const BY_EXTENSION = {
171
132
  xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
172
133
  };
173
134
  /**
174
- * The bytes are authoritative; the extension is the fallback for formats the
175
- * sniffer does not cover. An SVG name counts even when its bytes do not look
176
- * like one, so that a leading comment cannot slip one past the refusal.
135
+ * The bytes win over the extension, except that an SVG name always counts, so a
136
+ * leading comment cannot hide an SVG.
177
137
  */
178
138
  function typeOf(bytes, name) {
179
139
  const byName = mimeFromName(name);
@@ -1 +1 @@
1
- {"version":3,"file":"media.js","sourceRoot":"","sources":["../../src/services/media.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAElF,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEvC,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACxD,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAExF,yEAAyE;AACzE,MAAM,CAAC,MAAM,gBAAgB,GAAG,OAAO,CAAC;AAiBxC,MAAM,UAAU,kBAAkB,CAAC,IAAsB;IACvD,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAC;IAErC;;;;;;;OAOG;IACH,MAAM,UAAU,GAAG,KAAK,EAAE,SAAiB,EAAE,QAAgB,EAAmB,EAAE;QAChF,MAAM,KAAK,GAAG,IAAI,GAAG,CACnB,CAAC,MAAM,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAClG,CAAC;QAEF,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QACzC,MAAM,IAAI,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,MAAM,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC;QAEnE,IAAI,SAAS,GAAG,QAAQ,CAAC;QACzB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC,IAAI,CAAC;YAAE,SAAS,GAAG,GAAG,IAAI,IAAI,CAAC,GAAG,SAAS,EAAE,CAAC;QACrF,OAAO,SAAS,CAAC;IACnB,CAAC,CAAC;IAEF,OAAO;QACL;;;;;;;WAOG;QACH,KAAK,CAAC,OAAO,CAAC,KAKb;YACC,MAAM,EAAE,IAAI,EAAE,GAAG,KAAK,CAAC;YACvB,MAAM,SAAS,GAAG,eAAe,CAAC,KAAK,CAAC,SAAS,IAAI,gBAAgB,CAAC,CAAC;YAEvE,IAAI,IAAI,CAAC,IAAI,KAAK,CAAC;gBAAE,MAAM,UAAU,CAAC,oBAAoB,CAAC,CAAC;YAC5D,IAAI,IAAI,CAAC,IAAI,GAAG,QAAQ,EAAE,CAAC;gBACzB,MAAM,IAAI,QAAQ,CAChB,mBAAmB,EACnB,eAAe,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,kBAAkB,WAAW,CAAC,QAAQ,CAAC,GAAG,CAChF,CAAC;YACJ,CAAC;YAED,MAAM,QAAQ,GAAG,IAAI,UAAU,CAAC,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC;YAC1D,6DAA6D;YAC7D,MAAM,QAAQ,GAAG,cAAc,CAAC,QAAQ,CAAC,CAAC;YAE1C,IAAI,CAAC,QAAQ,EAAE,CAAC;gBACd,MAAM,IAAI,QAAQ,CAAC,wBAAwB,EAAE,kCAAkC,CAAC,CAAC;YACnF,CAAC;YACD,IAAI,QAAQ,CAAC,QAAQ,KAAK,QAAQ;gBAAE,MAAM,SAAS,EAAE,CAAC;YACtD,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,EAAE,CAAC;gBAC1D,MAAM,IAAI,QAAQ,CAChB,wBAAwB,EACxB,KAAK,KAAK,CAAC,MAAM,gCAAgC,QAAQ,CAAC,QAAQ,GAAG,CACtE,CAAC;YACJ,CAAC;YAED,MAAM,QAAQ,GAAG,MAAM,UAAU,CAAC,SAAS,EAAE,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC;YAEtF,OAAO;gBACL,QAAQ;gBACR,IAAI,EAAE;oBACJ,IAAI,EAAE,GAAG,SAAS,IAAI,QAAQ,EAAE;oBAChC,QAAQ;oBACR,QAAQ,EAAE,QAAQ,CAAC,QAAQ;oBAC3B,IAAI,EAAE,QAAQ,CAAC,UAAU;oBACzB,GAAG,UAAU,CAAC,QAAQ,EAAE,QAAQ,CAAC,QAAQ,CAAC;iBAC3C;aACF,CAAC;QACJ,CAAC;QAED;;;;;;WAMG;QACH,KAAK,CAAC,IAAI;YACR,MAAM,KAAK,GAAgB,EAAE,CAAC;YAE9B,MAAM,IAAI,GAAG,KAAK,EAAE,SAAiB,EAAiB,EAAE;gBACtD,KAAK,MAAM,IAAI,IAAI,MAAM,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC;oBACnD,MAAM,KAAK,GAAG,SAAS,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,SAAS,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;oBAEzE,IAAI,IAAI,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;wBAC9B,MAAM,IAAI,CAAC,KAAK,CAAC,CAAC;wBAClB,SAAS;oBACX,CAAC;oBAED,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC;wBAAE,SAAS;oBAEtC,MAAM,KAAK,GAAG,MAAM,SAAS,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;oBAChD,IAAI,CAAC,KAAK;wBAAE,SAAS;oBAErB,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;oBAC1C,IAAI,QAAQ,KAAK,QAAQ;wBAAE,SAAS;oBAEpC,KAAK,CAAC,IAAI,CAAC;wBACT,IAAI,EAAE,KAAK;wBACX,QAAQ,EAAE,IAAI,CAAC,IAAI;wBACnB,QAAQ;wBACR,IAAI,EAAE,KAAK,CAAC,UAAU;wBACtB,GAAG,UAAU,CAAC,KAAK,EAAE,QAAQ,CAAC;qBAC/B,CAAC,CAAC;gBACL,CAAC;YACH,CAAC,CAAC;YAEF,MAAM,IAAI,CAAC,EAAE,CAAC,CAAC;YACf,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;QAC5D,CAAC;QAED,KAAK,CAAC,IAAI,CAAC,SAAiB;YAC1B,MAAM,KAAK,GAAG,MAAM,SAAS,CAAC,UAAU,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC,CAAC;YACjE,IAAI,CAAC,KAAK;gBAAE,OAAO,IAAI,CAAC;YAExB,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;YAC1C,IAAI,QAAQ,KAAK,QAAQ;gBAAE,MAAM,SAAS,EAAE,CAAC;YAC7C,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC;QAC7B,CAAC;KACF,CAAC;AACJ,CAAC;AAID,uFAAuF;AACvF,SAAS,eAAe,CAAC,SAAiB;IACxC,IAAI,KAAa,CAAC;IAClB,IAAI,CAAC;QACH,KAAK,GAAG,WAAW,CAAC,SAAS,CAAC,CAAC;IACjC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,KAAK,YAAY,gBAAgB;YAAE,MAAM,UAAU,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QACvE,MAAM,KAAK,CAAC;IACd,CAAC;IACD,IAAI,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,SAAS,EAAE,CAAC;QAC1C,MAAM,UAAU,CAAC,sBAAsB,SAAS,kBAAkB,SAAS,IAAI,CAAC,CAAC;IACnF,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,wEAAwE;AAExE;;;;;;GAMG;AACH,SAAS,QAAQ,CAAC,QAAgB,EAAE,SAAiB;IACnD,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACnF,OAAO,GAAG,IAAI,IAAI,QAAQ,IAAI,SAAS,EAAE,CAAC;AAC5C,CAAC;AAED;;;;;GAKG;AACH,MAAM,YAAY,GAA2B;IAC3C,GAAG,EAAE,eAAe;IACpB,GAAG,EAAE,iBAAiB;IACtB,GAAG,EAAE,YAAY;IACjB,GAAG,EAAE,UAAU;IACf,GAAG,EAAE,oBAAoB;IACzB,IAAI,EAAE,yEAAyE;IAC/E,GAAG,EAAE,0BAA0B;IAC/B,IAAI,EAAE,mEAAmE;CAC1E,CAAC;AAEF;;;;GAIG;AACH,SAAS,MAAM,CAAC,KAAiB,EAAE,IAAY;IAC7C,MAAM,MAAM,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;IAClC,IAAI,MAAM,KAAK,QAAQ;QAAE,OAAO,QAAQ,CAAC;IACzC,OAAO,cAAc,CAAC,KAAK,CAAC,EAAE,QAAQ,IAAI,MAAM,CAAC;AACnD,CAAC;AAED,MAAM,SAAS,GAAG,GAAG,EAAE,CACrB,IAAI,QAAQ,CACV,wBAAwB,EACxB,4FAA4F,CAC7F,CAAC;AAEJ,SAAS,YAAY,CAAC,IAAY;IAChC,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;IAC5D,OAAO,YAAY,CAAC,SAAS,CAAC,IAAI,0BAA0B,CAAC;AAC/D,CAAC;AAED,wDAAwD;AACxD,SAAS,WAAW,CAAC,IAAY;IAC/B,OAAO,6DAA6D,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAClF,CAAC;AAED,SAAS,UAAU,CACjB,KAAiB,EACjB,QAAgB;IAEhB,IAAI,KAAK,CAAC,UAAU,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,EAAE,CAAC;QACrD,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;IACvC,CAAC;IACD,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;QAC3C,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,IAAI,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,IAAI,IAAI,EAAE,CAAC;IACpE,CAAC;IAAC,MAAM,CAAC;QACP,2DAA2D;QAC3D,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;IACvC,CAAC;AACH,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,KAAa;IACvC,IAAI,KAAK,GAAG,IAAI;QAAE,OAAO,GAAG,KAAK,IAAI,CAAC;IACtC,IAAI,KAAK,GAAG,IAAI,GAAG,IAAI;QAAE,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;IACjE,OAAO,GAAG,CAAC,KAAK,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC;AACpD,CAAC"}
1
+ {"version":3,"file":"media.js","sourceRoot":"","sources":["../../src/services/media.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Media uploads. Every upload is checked here for size and for its real content\n * type, read from the bytes. An upload is a file in the content repository; its\n * name is slugified and unique in its own directory.\n */\n\nimport path from \"node:path\";\n\nimport { contentPath, ContentPathError, MEDIA_DIR, slugify } from \"@micacms/core\";\nimport type { ContentFiles } from \"../workspace/files.js\";\nimport { imageSize } from \"image-size\";\n\nimport { ApiError, badRequest } from \"../lib/errors.js\";\nimport { detectFileType, isAccepted, isImageType, SVG_TYPE } from \"../lib/file-type.js\";\n\n/** Where uploads go when they are not attached to a particular entry. */\nexport const SHARED_MEDIA_DIR = \"media\";\n\nexport interface MediaServiceDeps {\n files: ContentFiles;\n maxBytes: number;\n}\n\nexport interface MediaFile {\n /** Path relative to the content root: the value a `media()` field holds. */\n path: string;\n filename: string;\n mimeType: string;\n size: number;\n width: number | null;\n height: number | null;\n}\n\nexport function createMediaService(deps: MediaServiceDeps) {\n const { files, maxBytes } = deps;\n\n /** A name that is unique in `directory`. Only there can a collision overwrite a file. */\n const uniqueName = async (directory: string, filename: string): Promise<string> => {\n const taken = new Set(\n (await files.list(directory)).filter((item) => item.type === \"file\").map((item) => item.name),\n );\n\n const extension = path.extname(filename);\n const stem = filename.slice(0, filename.length - extension.length);\n\n let candidate = filename;\n for (let n = 2; taken.has(candidate); n += 1) candidate = `${stem}-${n}${extension}`;\n return candidate;\n };\n\n return {\n /** Reads and checks an upload, without writing it. */\n async prepare(input: {\n file: File;\n /** Directory to place the file in, relative to the content root. */\n directory?: string;\n accept?: \"image\" | \"document\" | \"any\";\n }): Promise<{ file: MediaFile; contents: Uint8Array }> {\n const { file } = input;\n const directory = uploadDirectory(input.directory ?? SHARED_MEDIA_DIR);\n\n if (file.size === 0) throw badRequest(\"The file is empty.\");\n if (file.size > maxBytes) {\n throw new ApiError(\n \"payload_too_large\",\n `The file is ${formatBytes(file.size)}. The limit is ${formatBytes(maxBytes)}.`,\n );\n }\n\n const contents = new Uint8Array(await file.arrayBuffer());\n // From the bytes, never from the browser's claim.\n const detected = detectFileType(contents);\n\n if (!detected) {\n throw new ApiError(\"unsupported_media_type\", \"This file type is not supported.\");\n }\n if (detected.mimeType === SVG_TYPE) throw refuseSvg();\n if (!isAccepted(detected.mimeType, input.accept ?? \"any\")) {\n throw new ApiError(\n \"unsupported_media_type\",\n `A ${input.accept} was expected, but this is a ${detected.mimeType}.`,\n );\n }\n\n const filename = await uniqueName(directory, safeName(file.name, detected.extension));\n\n return {\n contents,\n file: {\n path: `${directory}/${filename}`,\n filename,\n mimeType: detected.mimeType,\n size: contents.byteLength,\n ...dimensions(contents, detected.mimeType),\n },\n };\n },\n\n /**\n * Every media file in the repository. A tree walk is sufficient because\n * content trees are small.\n */\n async list(): Promise<MediaFile[]> {\n const found: MediaFile[] = [];\n\n const walk = async (directory: string): Promise<void> => {\n for (const item of await files.list(directory)) {\n const child = directory === \"\" ? item.name : `${directory}/${item.name}`;\n\n if (item.type === \"directory\") {\n await walk(child);\n continue;\n }\n\n if (!isMediaName(item.name)) continue;\n\n const bytes = await files.readBinary(child);\n if (!bytes) continue;\n\n const mimeType = typeOf(bytes, item.name);\n if (mimeType === SVG_TYPE) continue;\n\n found.push({\n path: child,\n filename: item.name,\n mimeType,\n size: bytes.byteLength,\n ...dimensions(bytes, mimeType),\n });\n }\n };\n\n await walk(\"\");\n return found.sort((a, b) => a.path.localeCompare(b.path));\n },\n\n async read(mediaPath: string): Promise<{ bytes: Uint8Array; mimeType: string } | null> {\n const bytes = await files.readBinary(contentPath(mediaPath));\n if (!bytes) return null;\n\n const mimeType = typeOf(bytes, mediaPath);\n if (mimeType === SVG_TYPE) throw refuseSvg();\n return { bytes, mimeType };\n },\n };\n}\n\nexport type MediaService = ReturnType<typeof createMediaService>;\n\n/** An upload goes into a `media/` folder inside the content root, and nowhere else. */\nfunction uploadDirectory(directory: string): string {\n let clean: string;\n try {\n clean = contentPath(directory);\n } catch (error) {\n if (error instanceof ContentPathError) throw badRequest(error.message);\n throw error;\n }\n if (clean.split(\"/\").at(-1) !== MEDIA_DIR) {\n throw badRequest(`Uploads go into a \"${MEDIA_DIR}\" folder, not \"${directory}\".`);\n }\n return clean;\n}\n\n/** A slugified filename: safe as a path segment and as part of a URL. */\nfunction safeName(original: string, extension: string): string {\n const stem = slugify(path.basename(original).replace(/\\.[^.]*$/, \"\")).slice(0, 80);\n return `${stem || \"upload\"}.${extension}`;\n}\n\n/** Extension to media type, only for formats that byte-sniffing does not identify. */\nconst BY_EXTENSION: Record<string, string> = {\n svg: \"image/svg+xml\",\n pdf: \"application/pdf\",\n txt: \"text/plain\",\n csv: \"text/csv\",\n doc: \"application/msword\",\n docx: \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\",\n xls: \"application/vnd.ms-excel\",\n xlsx: \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\n};\n\n/**\n * The bytes win over the extension, except that an SVG name always counts, so a\n * leading comment cannot hide an SVG.\n */\nfunction typeOf(bytes: Uint8Array, name: string): string {\n const byName = mimeFromName(name);\n if (byName === SVG_TYPE) return SVG_TYPE;\n return detectFileType(bytes)?.mimeType ?? byName;\n}\n\nconst refuseSvg = () =>\n new ApiError(\n \"unsupported_media_type\",\n \"Mica does not accept SVG files, because they can contain scripts. Use PNG or WebP instead.\",\n );\n\nfunction mimeFromName(name: string): string {\n const extension = path.extname(name).slice(1).toLowerCase();\n return BY_EXTENSION[extension] ?? \"application/octet-stream\";\n}\n\n/** Files a `media()` field could plausibly point at. */\nfunction isMediaName(name: string): boolean {\n return /\\.(jpe?g|png|webp|avif|gif|tiff?|pdf|docx?|xlsx?|txt|csv)$/i.test(name);\n}\n\nfunction dimensions(\n bytes: Uint8Array,\n mimeType: string,\n): { width: number | null; height: number | null } {\n if (bytes.byteLength === 0 || !isImageType(mimeType)) {\n return { width: null, height: null };\n }\n try {\n const size = imageSize(Buffer.from(bytes));\n return { width: size.width ?? null, height: size.height ?? null };\n } catch {\n // A format image-size cannot read is still a valid upload.\n return { width: null, height: null };\n }\n}\n\nexport function formatBytes(bytes: number): string {\n if (bytes < 1024) return `${bytes} B`;\n if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} kB`;\n return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n}\n"]}
@@ -1,174 +1,42 @@
1
1
  /**
2
- * Content staging: the orchestration between "saved" and "published".
3
- *
4
- * Two implementations of one service, because the two deployment shapes have
5
- * genuinely different draft workspaces:
6
- *
7
- * - working tree (`staging-local.ts`): the checkout is the draft. Autosave
8
- * writes files the Astro dev server can hot-reload, which is the whole
9
- * value of local mode; Recent changes is `git status`, and Publish is a
10
- * real commit and push through the developer's own Git.
11
- * - draft store (this file): the repository is remote and writing through
12
- * its API would already be a commit, so saves accumulate in SQLite and
13
- * Publish turns the whole set into one provider-side commit.
14
- *
15
- * The editor sees one API either way. What differs is only what Saved means —
16
- * and the two must stay visibly different words in the UI, because in neither
17
- * mode does Saved imply that a change is in Git.
2
+ * The staging service on a Git working tree. Autosave has already written the
3
+ * files; Publish commits the reviewed paths.
4
+ *
5
+ * - Every Git operation is limited to an explicit path set, so other work in
6
+ * the same checkout is never swept in.
7
+ * - Only the reviewed files are published, as the editor saw them. Drafts of
8
+ * other entries stay drafts.
9
+ * - When the push fails, the commit stays as `committed-pending-push`; a
10
+ * retry pushes and never commits again.
18
11
  */
19
- import type { Author, RepositoryConnector } from "@micacms/connectors";
20
- import { type ContentChange, type ContentSource, type DraftStore, type FieldIssue, type PublishResult, type Site } from "@micacms/core";
21
- /** Where the change happened, in words an editor recognises. */
22
- export type ChangeOwner = {
23
- kind: "entry";
24
- collection: string;
25
- slug: string;
26
- label: string;
27
- /** The slug before a pending rename. Set on the files of both slugs. */
28
- renamedFrom?: string;
29
- } | {
30
- kind: "singleton";
31
- name: string;
32
- label: string;
33
- } | {
34
- kind: "order";
35
- collection: string;
36
- label: string;
37
- } | {
38
- kind: "media";
39
- label: string;
40
- } | {
41
- kind: "other";
42
- label: string;
43
- };
44
- export interface AnnotatedChange extends ContentChange {
45
- owner: ChangeOwner;
46
- }
47
- /** One thing that would make the staged result unpublishable. */
48
- export interface StagingProblem {
49
- /** The file the problem is about, relative to the content root. */
50
- path: string;
51
- label: string;
52
- issues: FieldIssue[];
53
- }
54
- export interface StagingValidation {
55
- ok: boolean;
56
- problems: StagingProblem[];
57
- }
58
- export interface StagingStatus {
59
- /** How this deployment stages. The UI words differ with it. */
60
- workspace: "working-tree" | "draft-store";
61
- publish: {
62
- enabled: boolean;
63
- reason?: string;
64
- };
12
+ import { type Site } from "@micacms/core";
13
+ import type { Logger } from "../lib/logger.js";
14
+ import type { ContentFiles } from "../workspace/files.js";
15
+ import { type WorkspaceLocks } from "../workspace/locks.js";
16
+ import type { OwnerPolicy } from "../workspace/policy.js";
17
+ import { type GitTree } from "../workspace/tree.js";
18
+ import { type ContentStagingService } from "./changes.js";
19
+ export interface WorkingTreeStagingDeps {
20
+ model: Site;
65
21
  /**
66
- * What the pending changes are based on: HEAD for a working tree, the
67
- * branch head for a draft store. Sent back with Publish as `expectedBase`.
22
+ * The working tree's files without the lock wrapper: this service holds the
23
+ * locks around whole operations.
68
24
  */
69
- baseRevision: string | null;
70
- /** The branch a publication lands on, where the workspace knows it. */
71
- branch: string | null;
72
- /** Commits created and not yet pushed. Working tree only. */
73
- ahead: number | null;
74
- changes: AnnotatedChange[];
75
- validation: StagingValidation;
76
- /** When the pending set last moved. Null when nothing is pending. */
77
- updatedAt: string | null;
78
- }
79
- export interface PublishInput {
80
- /** Subject line of the commit. Written by the editor, for `git log`. */
81
- message: string;
82
- /** The base the editor reviewed. A repository that moved past it refuses. */
83
- expectedBase: string | null;
84
- author?: Author;
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`. Working tree only. */
93
- push(): Promise<PublishResult>;
94
- /** The document a pending change touches, before and after that change. */
95
- compare(target: CompareTarget): Promise<Comparison>;
96
- }
97
- /** One thing an editor recognises on the Publish page, named for comparison. */
98
- export type CompareTarget =
99
- /** `from` names the published slug of a renamed entry. */
100
- {
101
- kind: "entry";
102
- collection: string;
103
- slug: string;
104
- from?: string;
105
- } | {
106
- kind: "singleton";
107
- name: string;
108
- } | {
109
- kind: "order";
110
- collection: string;
111
- };
112
- /**
113
- * A document on both sides of the pending change set.
114
- *
115
- * Raw field data, deliberately: the editor holds the model and can name,
116
- * order and render every field, so the friendly comparison is its job. What
117
- * only the server can do is read the same document from two places.
118
- */
119
- export interface Comparison {
120
- /** As the repository has it — null when the change creates the document. */
121
- before: Record<string, unknown> | null;
122
- /** As staged for publication — null when the change deletes it. */
123
- after: Record<string, unknown> | null;
124
- }
125
- /**
126
- * Reads one document from the repository (`published`) and from the pending
127
- * workspace (`staged`) — the whole of a before/after comparison, shared by
128
- * both staging implementations since a `ContentSource` is all it needs.
129
- *
130
- * An order target compares the collection's ordering rather than a document:
131
- * each side becomes `{ order: [{ slug, title }] }`, 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.
136
- *
137
- * One pass on purpose: naming a change and validating it both need the same
138
- * read of the same entries, and content trees are small enough to read the
139
- * affected collections whole.
140
- *
141
- * Validation is scoped to *changed* documents. Content that was already in
142
- * the repository is not this publication's problem — refusing
143
- * to publish a fixed typo because somebody once hand-committed a broken entry
144
- * elsewhere would make the editor hostage to history it cannot touch.
145
- */
146
- export declare function analyseChanges(model: Site, source: ContentSource, changes: ContentChange[],
147
- /** The repository's side. When given, renamed entries are detected. */
148
- published?: ContentSource): Promise<{
149
- changes: AnnotatedChange[];
150
- validation: StagingValidation;
151
- }>;
152
- /**
153
- * Which piece of content a file belongs to.
154
- *
155
- * The path layout is the model's own (`layout.ts`), so this is a reading of
156
- * it, not a second convention: `collection/slug/…` or `collection/slug.json`,
157
- * `singleton.json` or `singleton/…`, and the shared `media/` directory.
158
- */
159
- export declare function classifyPath(model: Site, path: string): ChangeOwner;
160
- export interface DraftStagingDeps {
161
- model: Site;
162
- drafts: DraftStore;
163
- /** The real repository. The one thing here that writes to it. */
164
- repository: RepositoryConnector;
165
- /** The staged overlay the rest of the application reads through. */
166
- overlay: RepositoryConnector;
167
- /** Revision of `.mica/schema.json` when the model was loaded. */
25
+ files: ContentFiles;
26
+ contentRoot: string;
168
27
  schemaRevision: string | null;
28
+ /** `MICA_LOCAL_PUBLISH`, for the default developer policy. */
29
+ publishEnabled: boolean;
30
+ logger: Logger;
31
+ /** Defaults to the developer. */
32
+ policy?: OwnerPolicy;
33
+ /** Shared with the services that write content. */
34
+ locks?: WorkspaceLocks;
35
+ /**
36
+ * Set by a managed clone, whose Git runs in an isolated environment. Without
37
+ * it, the developer's Git finds the tree.
38
+ */
39
+ tree?: GitTree;
169
40
  }
170
- export declare function createDraftStagingService(deps: DraftStagingDeps): ContentStagingService;
171
- export declare function describeProblems(problems: StagingProblem[]): string;
172
- /** Field issues with the file path folded in, for the flat API envelope. */
173
- export declare function flattenProblems(problems: StagingProblem[]): FieldIssue[];
41
+ export declare function createWorkingTreeStagingService(deps: WorkingTreeStagingDeps): ContentStagingService;
174
42
  //# sourceMappingURL=staging.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"staging.d.ts","sourceRoot":"","sources":["../../src/services/staging.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"staging.d.ts","sourceRoot":"","sources":["../../src/services/staging.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAmD,KAAK,IAAI,EAAE,MAAM,eAAe,CAAC;AAG3F,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAE/C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAE1D,OAAO,EAAwB,KAAK,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAClF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EASL,KAAK,OAAO,EAEb,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAQL,KAAK,qBAAqB,EAK3B,MAAM,cAAc,CAAC;AAEtB,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,IAAI,CAAC;IACZ;;;OAGG;IACH,KAAK,EAAE,YAAY,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,8DAA8D;IAC9D,cAAc,EAAE,OAAO,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,iCAAiC;IACjC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,mDAAmD;IACnD,KAAK,CAAC,EAAE,cAAc,CAAC;IACvB;;;OAGG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,wBAAgB,+BAA+B,CAC7C,IAAI,EAAE,sBAAsB,GAC3B,qBAAqB,CA8OvB"}