@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.
- package/README.md +2 -2
- package/dist/app.d.ts +8 -37
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +7 -42
- package/dist/app.js.map +1 -1
- package/dist/config.d.ts +12 -82
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +14 -82
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts +37 -66
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +24 -75
- package/dist/context.js.map +1 -1
- package/dist/http.d.ts +8 -16
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +6 -13
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +13 -14
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -14
- package/dist/index.js.map +1 -1
- package/dist/lib/errors.js.map +1 -1
- package/dist/lib/file-type.js.map +1 -1
- package/dist/lib/id.js.map +1 -1
- package/dist/lib/logger.js.map +1 -1
- package/dist/lib/ports.js.map +1 -1
- package/dist/lib/rate-limit.js.map +1 -1
- package/dist/lib/schema-stats.js.map +1 -1
- package/dist/lib/version.js.map +1 -1
- package/dist/middleware/index.js.map +1 -1
- package/dist/routes/admin-content.d.ts +4 -17
- package/dist/routes/admin-content.d.ts.map +1 -1
- package/dist/routes/admin-content.js +11 -51
- package/dist/routes/admin-content.js.map +1 -1
- package/dist/routes/admin-media.d.ts +3 -14
- package/dist/routes/admin-media.d.ts.map +1 -1
- package/dist/routes/admin-media.js +7 -29
- package/dist/routes/admin-media.js.map +1 -1
- package/dist/routes/admin-spa.js.map +1 -1
- package/dist/routes/admin-staging.d.ts +4 -9
- package/dist/routes/admin-staging.d.ts.map +1 -1
- package/dist/routes/admin-staging.js +31 -18
- package/dist/routes/admin-staging.js.map +1 -1
- package/dist/routes/admin-system.d.ts +2 -8
- package/dist/routes/admin-system.d.ts.map +1 -1
- package/dist/routes/admin-system.js +9 -44
- package/dist/routes/admin-system.js.map +1 -1
- package/dist/routes/health.d.ts +3 -10
- package/dist/routes/health.d.ts.map +1 -1
- package/dist/routes/health.js +9 -26
- package/dist/routes/health.js.map +1 -1
- package/dist/schema.d.ts +10 -38
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +14 -65
- package/dist/schema.js.map +1 -1
- package/dist/server.d.ts +2 -10
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +3 -9
- package/dist/server.js.map +1 -1
- package/dist/services/changes.d.ts +150 -0
- package/dist/services/changes.d.ts.map +1 -0
- package/dist/services/changes.js +339 -0
- package/dist/services/changes.js.map +1 -0
- package/dist/services/content-store.d.ts +61 -0
- package/dist/services/content-store.d.ts.map +1 -0
- package/dist/services/content-store.js +330 -0
- package/dist/services/content-store.js.map +1 -0
- package/dist/services/media.d.ts +9 -29
- package/dist/services/media.d.ts.map +1 -1
- package/dist/services/media.js +17 -57
- package/dist/services/media.js.map +1 -1
- package/dist/services/staging.d.ts +35 -167
- package/dist/services/staging.d.ts.map +1 -1
- package/dist/services/staging.js +195 -458
- package/dist/services/staging.js.map +1 -1
- package/dist/workspace/developer.d.ts +14 -0
- package/dist/workspace/developer.d.ts.map +1 -0
- package/dist/workspace/developer.js +31 -0
- package/dist/workspace/developer.js.map +1 -0
- package/dist/workspace/files.d.ts +70 -0
- package/dist/workspace/files.d.ts.map +1 -0
- package/dist/workspace/files.js +176 -0
- package/dist/workspace/files.js.map +1 -0
- package/dist/workspace/git.d.ts +45 -0
- package/dist/workspace/git.d.ts.map +1 -0
- package/dist/workspace/git.js +68 -0
- package/dist/workspace/git.js.map +1 -0
- package/dist/workspace/locks.d.ts +33 -0
- package/dist/workspace/locks.d.ts.map +1 -0
- package/dist/workspace/locks.js +76 -0
- package/dist/workspace/locks.js.map +1 -0
- package/dist/workspace/policy.d.ts +92 -0
- package/dist/workspace/policy.d.ts.map +1 -0
- package/dist/workspace/policy.js +7 -0
- package/dist/workspace/policy.js.map +1 -0
- package/dist/workspace/tree.d.ts +73 -0
- package/dist/workspace/tree.d.ts.map +1 -0
- package/dist/workspace/tree.js +229 -0
- package/dist/workspace/tree.js.map +1 -0
- package/package.json +2 -3
- package/dist/db/driver.d.ts +0 -35
- package/dist/db/driver.d.ts.map +0 -1
- package/dist/db/driver.js +0 -95
- package/dist/db/driver.js.map +0 -1
- package/dist/lib/git.d.ts +0 -29
- package/dist/lib/git.d.ts.map +0 -1
- package/dist/lib/git.js +0 -62
- package/dist/lib/git.js.map +0 -1
- package/dist/services/staging-local.d.ts +0 -58
- package/dist/services/staging-local.d.ts.map +0 -1
- package/dist/services/staging-local.js +0 -391
- package/dist/services/staging-local.js.map +0 -1
package/dist/services/media.js
CHANGED
|
@@ -1,17 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Media uploads.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
175
|
-
*
|
|
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":"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"}
|
|
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
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* -
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
|
20
|
-
import
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
*
|
|
67
|
-
*
|
|
22
|
+
* The working tree's files without the lock wrapper: this service holds the
|
|
23
|
+
* locks around whole operations.
|
|
68
24
|
*/
|
|
69
|
-
|
|
70
|
-
|
|
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
|
|
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":"AAAA
|
|
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"}
|