@micacms/runtime 0.2.0
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 +16 -0
- package/dist/app.d.ts +62 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +107 -0
- package/dist/app.js.map +1 -0
- package/dist/config.d.ts +187 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +154 -0
- package/dist/config.js.map +1 -0
- package/dist/context.d.ts +121 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +117 -0
- package/dist/context.js.map +1 -0
- package/dist/db/driver.d.ts +35 -0
- package/dist/db/driver.d.ts.map +1 -0
- package/dist/db/driver.js +95 -0
- package/dist/db/driver.js.map +1 -0
- package/dist/http.d.ts +40 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +38 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/errors.d.ts +39 -0
- package/dist/lib/errors.d.ts.map +1 -0
- package/dist/lib/errors.js +52 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/file-type.d.ts +26 -0
- package/dist/lib/file-type.d.ts.map +1 -0
- package/dist/lib/file-type.js +89 -0
- package/dist/lib/file-type.js.map +1 -0
- package/dist/lib/git.d.ts +29 -0
- package/dist/lib/git.d.ts.map +1 -0
- package/dist/lib/git.js +62 -0
- package/dist/lib/git.js.map +1 -0
- package/dist/lib/id.d.ts +13 -0
- package/dist/lib/id.d.ts.map +1 -0
- package/dist/lib/id.js +56 -0
- package/dist/lib/id.js.map +1 -0
- package/dist/lib/logger.d.ts +20 -0
- package/dist/lib/logger.d.ts.map +1 -0
- package/dist/lib/logger.js +65 -0
- package/dist/lib/logger.js.map +1 -0
- package/dist/lib/ports.d.ts +17 -0
- package/dist/lib/ports.d.ts.map +1 -0
- package/dist/lib/ports.js +17 -0
- package/dist/lib/ports.js.map +1 -0
- package/dist/lib/rate-limit.d.ts +29 -0
- package/dist/lib/rate-limit.d.ts.map +1 -0
- package/dist/lib/rate-limit.js +47 -0
- package/dist/lib/rate-limit.js.map +1 -0
- package/dist/lib/schema-stats.d.ts +64 -0
- package/dist/lib/schema-stats.d.ts.map +1 -0
- package/dist/lib/schema-stats.js +103 -0
- package/dist/lib/schema-stats.js.map +1 -0
- package/dist/lib/version.d.ts +14 -0
- package/dist/lib/version.d.ts.map +1 -0
- package/dist/lib/version.js +25 -0
- package/dist/lib/version.js.map +1 -0
- package/dist/middleware/index.d.ts +41 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +142 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/routes/admin-content.d.ts +22 -0
- package/dist/routes/admin-content.d.ts.map +1 -0
- package/dist/routes/admin-content.js +194 -0
- package/dist/routes/admin-content.js.map +1 -0
- package/dist/routes/admin-media.d.ts +19 -0
- package/dist/routes/admin-media.d.ts.map +1 -0
- package/dist/routes/admin-media.js +81 -0
- package/dist/routes/admin-media.js.map +1 -0
- package/dist/routes/admin-spa.d.ts +13 -0
- package/dist/routes/admin-spa.d.ts.map +1 -0
- package/dist/routes/admin-spa.js +72 -0
- package/dist/routes/admin-spa.js.map +1 -0
- package/dist/routes/admin-staging.d.ts +15 -0
- package/dist/routes/admin-staging.d.ts.map +1 -0
- package/dist/routes/admin-staging.js +90 -0
- package/dist/routes/admin-staging.js.map +1 -0
- package/dist/routes/admin-system.d.ts +14 -0
- package/dist/routes/admin-system.d.ts.map +1 -0
- package/dist/routes/admin-system.js +153 -0
- package/dist/routes/admin-system.js.map +1 -0
- package/dist/routes/health.d.ts +12 -0
- package/dist/routes/health.d.ts.map +1 -0
- package/dist/routes/health.js +44 -0
- package/dist/routes/health.js.map +1 -0
- package/dist/schema.d.ts +56 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +173 -0
- package/dist/schema.js.map +1 -0
- package/dist/server.d.ts +28 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +47 -0
- package/dist/server.js.map +1 -0
- package/dist/services/media.d.ts +65 -0
- package/dist/services/media.d.ts.map +1 -0
- package/dist/services/media.js +180 -0
- package/dist/services/media.js.map +1 -0
- package/dist/services/staging-local.d.ts +58 -0
- package/dist/services/staging-local.d.ts.map +1 -0
- package/dist/services/staging-local.js +391 -0
- package/dist/services/staging-local.js.map +1 -0
- package/dist/services/staging.d.ts +167 -0
- package/dist/services/staging.d.ts.map +1 -0
- package/dist/services/staging.js +333 -0
- package/dist/services/staging.js.map +1 -0
- package/package.json +49 -0
package/dist/schema.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Acquiring the content model at runtime.
|
|
3
|
+
*
|
|
4
|
+
* The server ships with no model of its own. It reads one out of the content
|
|
5
|
+
* repository, at `.mica/schema.json`, through the same connector it reads
|
|
6
|
+
* entries with. The Astro project writes that file and the developer commits
|
|
7
|
+
* it, so the model reaches the server the way content does — no artifact to
|
|
8
|
+
* mount, no second channel to keep in step, and a model change that is
|
|
9
|
+
* reviewable in a diff.
|
|
10
|
+
*
|
|
11
|
+
* That is what lets one published image serve any project: the model is plain
|
|
12
|
+
* data (field descriptors hold no functions, so `JSON.stringify(site)`
|
|
13
|
+
* round-trips), and every consumer downstream — form generation, payload
|
|
14
|
+
* validation, the admin — already reads it out of the request context.
|
|
15
|
+
*
|
|
16
|
+
* Whatever arrives is re-validated as if it had just been written by hand. A
|
|
17
|
+
* manifest from a repository is untrusted input: it may have been produced by a
|
|
18
|
+
* different version of `@micacms/core`, hand-edited, or truncated.
|
|
19
|
+
*/
|
|
20
|
+
import { type ContentSource, type Site } from "@micacms/core";
|
|
21
|
+
export declare class SchemaError extends Error {
|
|
22
|
+
name: string;
|
|
23
|
+
}
|
|
24
|
+
/** Where a model came from, for logs and the system screen. */
|
|
25
|
+
export declare function schemaOrigin(source: ContentSource): string;
|
|
26
|
+
/**
|
|
27
|
+
* Reads the model the content repository publishes.
|
|
28
|
+
*
|
|
29
|
+
* A missing file is the interesting failure and gets the long message: it is
|
|
30
|
+
* what a first deployment hits, and the fix — build the Astro project, commit
|
|
31
|
+
* the result — is not guessable from "not found".
|
|
32
|
+
*/
|
|
33
|
+
export declare function loadSchema(source: ContentSource): Promise<Site>;
|
|
34
|
+
/**
|
|
35
|
+
* Re-runs the checks `defineSite` performs on a hand-written model, plus the
|
|
36
|
+
* one it does not: that every `kind` is a kind this build knows about.
|
|
37
|
+
*
|
|
38
|
+
* `defineSite` assumes its input came from the typed builders, so it never
|
|
39
|
+
* questions a `kind`. Field kinds reach a `switch` with no default when a form
|
|
40
|
+
* is rendered and when a payload is validated, so an unknown one surfaces as an
|
|
41
|
+
* incoherent failure much later, on an editor's screen. Catching it here turns
|
|
42
|
+
* a version mismatch between the project's `@micacms/core` and the image's
|
|
43
|
+
* into a startup error that names the field.
|
|
44
|
+
*/
|
|
45
|
+
export declare function checkSchema(raw: unknown, origin?: string): Site;
|
|
46
|
+
/**
|
|
47
|
+
* A stable short digest of a model, for spotting drift.
|
|
48
|
+
*
|
|
49
|
+
* Logged at startup and returned by the schema endpoint so that a website built
|
|
50
|
+
* against one model and a CMS serving another is a visible mismatch rather than
|
|
51
|
+
* a puzzling absence of fields. Key order follows the model's own declaration
|
|
52
|
+
* order, which is meaningful here — reordering fields reorders the admin form,
|
|
53
|
+
* so it is a real change and should produce a different digest.
|
|
54
|
+
*/
|
|
55
|
+
export declare function schemaFingerprint(site: Site): Promise<string>;
|
|
56
|
+
//# sourceMappingURL=schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAQL,KAAK,aAAa,EAGlB,KAAK,IAAI,EACV,MAAM,eAAe,CAAC;AAEvB,qBAAa,WAAY,SAAQ,KAAK;IAC3B,IAAI,SAAiB;CAC/B;AAED,+DAA+D;AAC/D,wBAAgB,YAAY,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,CAE1D;AAED;;;;;;GAMG;AACH,wBAAsB,UAAU,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CA2BrE;AAWD;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,SAAe,GAAG,IAAI,CAuErE;AA0BD;;;;;;;;GAQG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,CAGnE"}
|
package/dist/schema.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Acquiring the content model at runtime.
|
|
3
|
+
*
|
|
4
|
+
* The server ships with no model of its own. It reads one out of the content
|
|
5
|
+
* repository, at `.mica/schema.json`, through the same connector it reads
|
|
6
|
+
* entries with. The Astro project writes that file and the developer commits
|
|
7
|
+
* it, so the model reaches the server the way content does — no artifact to
|
|
8
|
+
* mount, no second channel to keep in step, and a model change that is
|
|
9
|
+
* reviewable in a diff.
|
|
10
|
+
*
|
|
11
|
+
* That is what lets one published image serve any project: the model is plain
|
|
12
|
+
* data (field descriptors hold no functions, so `JSON.stringify(site)`
|
|
13
|
+
* round-trips), and every consumer downstream — form generation, payload
|
|
14
|
+
* validation, the admin — already reads it out of the request context.
|
|
15
|
+
*
|
|
16
|
+
* Whatever arrives is re-validated as if it had just been written by hand. A
|
|
17
|
+
* manifest from a repository is untrusted input: it may have been produced by a
|
|
18
|
+
* different version of `@micacms/core`, hand-edited, or truncated.
|
|
19
|
+
*/
|
|
20
|
+
import { checkCompatibility, defineSite, isFieldKind, isProtocolManifest, MANIFEST_PATH, PROTOCOL_VERSION, } from "@micacms/core";
|
|
21
|
+
export class SchemaError extends Error {
|
|
22
|
+
name = "SchemaError";
|
|
23
|
+
}
|
|
24
|
+
/** Where a model came from, for logs and the system screen. */
|
|
25
|
+
export function schemaOrigin(source) {
|
|
26
|
+
return `${source.origin}/${MANIFEST_PATH}`;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Reads the model the content repository publishes.
|
|
30
|
+
*
|
|
31
|
+
* A missing file is the interesting failure and gets the long message: it is
|
|
32
|
+
* what a first deployment hits, and the fix — build the Astro project, commit
|
|
33
|
+
* the result — is not guessable from "not found".
|
|
34
|
+
*/
|
|
35
|
+
export async function loadSchema(source) {
|
|
36
|
+
const origin = schemaOrigin(source);
|
|
37
|
+
let text;
|
|
38
|
+
try {
|
|
39
|
+
text = await source.read(MANIFEST_PATH);
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
// A repository that cannot be reached is not a bad model. Say which it is,
|
|
43
|
+
// because the two have completely different fixes.
|
|
44
|
+
throw new SchemaError(`Could not read the content model from ${source.origin}.\n` +
|
|
45
|
+
` ${error.message}\n` +
|
|
46
|
+
` The repository has to be reachable before Mica can start: the model\n` +
|
|
47
|
+
` lives in it, at ${MANIFEST_PATH}.`);
|
|
48
|
+
}
|
|
49
|
+
if (text === null)
|
|
50
|
+
throw new SchemaError(describeMissing(origin));
|
|
51
|
+
let raw;
|
|
52
|
+
try {
|
|
53
|
+
raw = JSON.parse(text);
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
throw new SchemaError(`${origin} is not valid JSON: ${error.message}`);
|
|
57
|
+
}
|
|
58
|
+
return checkSchema(raw, origin);
|
|
59
|
+
}
|
|
60
|
+
function describeMissing(origin) {
|
|
61
|
+
return (`No content model at ${origin}.\n` +
|
|
62
|
+
` Mica ships without one — the Astro project defines it and writes it here.\n` +
|
|
63
|
+
` In the project: npx astro build (or start the dev server once)\n` +
|
|
64
|
+
` then commit ${MANIFEST_PATH} and push it to the branch Mica is reading.`);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Re-runs the checks `defineSite` performs on a hand-written model, plus the
|
|
68
|
+
* one it does not: that every `kind` is a kind this build knows about.
|
|
69
|
+
*
|
|
70
|
+
* `defineSite` assumes its input came from the typed builders, so it never
|
|
71
|
+
* questions a `kind`. Field kinds reach a `switch` with no default when a form
|
|
72
|
+
* is rendered and when a payload is validated, so an unknown one surfaces as an
|
|
73
|
+
* incoherent failure much later, on an editor's screen. Catching it here turns
|
|
74
|
+
* a version mismatch between the project's `@micacms/core` and the image's
|
|
75
|
+
* into a startup error that names the field.
|
|
76
|
+
*/
|
|
77
|
+
export function checkSchema(raw, origin = "the schema") {
|
|
78
|
+
if (raw === null || typeof raw !== "object") {
|
|
79
|
+
throw new SchemaError(`${origin} is ${raw === null ? "null" : typeof raw}, expected an object.`);
|
|
80
|
+
}
|
|
81
|
+
// Only when the envelope says so. A bare `{ collections }` is still accepted:
|
|
82
|
+
// the protocol version is a backstop for changes that cannot be detected by
|
|
83
|
+
// looking, not an admission ticket.
|
|
84
|
+
if (isProtocolManifest(raw)) {
|
|
85
|
+
const compatibility = checkCompatibility(raw);
|
|
86
|
+
if (!compatibility.ok && compatibility.reason === "newer") {
|
|
87
|
+
throw new SchemaError(`${origin} follows protocol version ${compatibility.version}; this build of Mica ` +
|
|
88
|
+
`understands version ${PROTOCOL_VERSION}.\n` +
|
|
89
|
+
` The project was built against a newer @micacms/astro. Upgrade Mica.`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const candidate = raw;
|
|
93
|
+
if (candidate.collections === null || typeof candidate.collections !== "object") {
|
|
94
|
+
throw new SchemaError(`${origin} has no \`collections\`. Expected defineSite({ collections: { ... } }).`);
|
|
95
|
+
}
|
|
96
|
+
// `globals` was the term before the model settled on `singletons`. A manifest
|
|
97
|
+
// outlives the release that produced it, so both spellings are read here;
|
|
98
|
+
// only the new one is written anywhere.
|
|
99
|
+
const rawSingletons = candidate.singletons ?? candidate.globals;
|
|
100
|
+
if (rawSingletons !== undefined && (rawSingletons === null || typeof rawSingletons !== "object")) {
|
|
101
|
+
throw new SchemaError(`${origin} has a \`singletons\` that is not an object.`);
|
|
102
|
+
}
|
|
103
|
+
const collections = candidate.collections;
|
|
104
|
+
const singletons = (rawSingletons ?? {});
|
|
105
|
+
for (const [name, def] of Object.entries(collections)) {
|
|
106
|
+
assertFieldMap(def?.fields, `collections.${name}`, origin);
|
|
107
|
+
}
|
|
108
|
+
for (const [name, def] of Object.entries(singletons)) {
|
|
109
|
+
assertFieldMap(def?.fields, `singletons.${name}`, origin);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* A default sort naming a field the collection does not declare is dropped
|
|
113
|
+
* here rather than refused.
|
|
114
|
+
*
|
|
115
|
+
* `defineSite` rejects it, which is right when somebody is writing the model
|
|
116
|
+
* and can fix the line they are looking at. By the time it reaches Mica the
|
|
117
|
+
* model is committed and a site is starting against it: the entries are
|
|
118
|
+
* intact, the fields are intact, and the only casualty is the order of a
|
|
119
|
+
* list. Refusing to serve the site over that would be out of proportion —
|
|
120
|
+
* and models written before the check existed contain exactly this, because
|
|
121
|
+
* the old fixed list of sort fields offered names no collection had.
|
|
122
|
+
*
|
|
123
|
+
* So the option is dropped, the list falls back to the order editors
|
|
124
|
+
* arranged by hand, and the project's next build reports it properly.
|
|
125
|
+
*/
|
|
126
|
+
for (const def of Object.values(collections)) {
|
|
127
|
+
const field = def?.defaultSort?.field;
|
|
128
|
+
if (field && field !== "position" && !def.fields?.[field])
|
|
129
|
+
delete def.defaultSort;
|
|
130
|
+
}
|
|
131
|
+
// Name injection and every cross-reference check — unknown relation targets,
|
|
132
|
+
// slugs derived from absent siblings, reserved collection names — come from
|
|
133
|
+
// the same function a hand-written model goes through.
|
|
134
|
+
try {
|
|
135
|
+
return defineSite({ collections, singletons });
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
throw new SchemaError(`${origin} is not a valid content model.\n ${error.message}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function assertFieldMap(fields, at, origin) {
|
|
142
|
+
if (fields === null || typeof fields !== "object") {
|
|
143
|
+
throw new SchemaError(`${origin}: "${at}" has no \`fields\` object.`);
|
|
144
|
+
}
|
|
145
|
+
for (const [key, field] of Object.entries(fields)) {
|
|
146
|
+
const here = `${at}.${key}`;
|
|
147
|
+
if (field === null || typeof field !== "object") {
|
|
148
|
+
throw new SchemaError(`${origin}: field "${here}" is not an object.`);
|
|
149
|
+
}
|
|
150
|
+
if (!isFieldKind(field.kind)) {
|
|
151
|
+
throw new SchemaError(`${origin}: field "${here}" has unknown kind "${String(field.kind)}".\n` +
|
|
152
|
+
` This build of Mica does not support it — the project may have been built\n` +
|
|
153
|
+
` against a newer @micacms/astro than the server is running.`);
|
|
154
|
+
}
|
|
155
|
+
if (field.kind === "object" || field.kind === "repeater") {
|
|
156
|
+
assertFieldMap(field.fields, here, origin);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* A stable short digest of a model, for spotting drift.
|
|
162
|
+
*
|
|
163
|
+
* Logged at startup and returned by the schema endpoint so that a website built
|
|
164
|
+
* against one model and a CMS serving another is a visible mismatch rather than
|
|
165
|
+
* a puzzling absence of fields. Key order follows the model's own declaration
|
|
166
|
+
* order, which is meaningful here — reordering fields reorders the admin form,
|
|
167
|
+
* so it is a real change and should produce a different digest.
|
|
168
|
+
*/
|
|
169
|
+
export async function schemaFingerprint(site) {
|
|
170
|
+
const { createHash } = await import("node:crypto");
|
|
171
|
+
return createHash("sha256").update(JSON.stringify(site)).digest("hex").slice(0, 12);
|
|
172
|
+
}
|
|
173
|
+
//# sourceMappingURL=schema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"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"}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
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.
|
|
9
|
+
*/
|
|
10
|
+
import type { Hono } from "hono";
|
|
11
|
+
import type { Env } from "./http.js";
|
|
12
|
+
import type { Logger } from "./lib/logger.js";
|
|
13
|
+
export interface StartServerOptions {
|
|
14
|
+
app: Hono<Env>;
|
|
15
|
+
host: string;
|
|
16
|
+
port: number;
|
|
17
|
+
logger: Logger;
|
|
18
|
+
/** Called once the socket is listening — the banner or the log line. */
|
|
19
|
+
onListening(info: {
|
|
20
|
+
port: number;
|
|
21
|
+
}): void;
|
|
22
|
+
/** The message for a busy port. Defaults to a plain sentence. */
|
|
23
|
+
portInUseMessage?(): string;
|
|
24
|
+
/** Releases what the composition holds — timers, the database. */
|
|
25
|
+
shutdown(): Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
export declare function startServer(options: StartServerOptions): void;
|
|
28
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +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"}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
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.
|
|
9
|
+
*/
|
|
10
|
+
import { serve } from "@hono/node-server";
|
|
11
|
+
export function startServer(options) {
|
|
12
|
+
const { logger } = options;
|
|
13
|
+
const server = serve({ fetch: options.app.fetch, hostname: options.host, port: options.port }, (info) => options.onListening({ port: info.port }));
|
|
14
|
+
server.on("error", (error) => {
|
|
15
|
+
if (error.code !== "EADDRINUSE")
|
|
16
|
+
throw error;
|
|
17
|
+
process.stderr.write(options.portInUseMessage?.() ??
|
|
18
|
+
`Port ${options.port} on ${options.host} is already in use. Stop the other ` +
|
|
19
|
+
`process, or set PORT.\n`);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
});
|
|
22
|
+
let shuttingDown = false;
|
|
23
|
+
const shutdown = (signal) => {
|
|
24
|
+
if (shuttingDown)
|
|
25
|
+
return;
|
|
26
|
+
shuttingDown = true;
|
|
27
|
+
logger.info("Shutting down", { signal });
|
|
28
|
+
// Stop accepting connections, then release whatever the app holds.
|
|
29
|
+
server.close(() => {
|
|
30
|
+
void options.shutdown().then(() => {
|
|
31
|
+
logger.info("Shutdown complete");
|
|
32
|
+
process.exit(0);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
// Do not let a hung connection block a container restart indefinitely.
|
|
36
|
+
setTimeout(() => {
|
|
37
|
+
logger.warn("Forcing exit after shutdown timeout");
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}, 15_000).unref();
|
|
40
|
+
};
|
|
41
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
42
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
43
|
+
process.on("unhandledRejection", (reason) => {
|
|
44
|
+
logger.error("Unhandled promise rejection", { reason: String(reason) });
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,65 @@
|
|
|
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.
|
|
15
|
+
*/
|
|
16
|
+
import type { RepositoryConnector } from "@micacms/connectors";
|
|
17
|
+
/** Where uploads go when they are not attached to a particular entry. */
|
|
18
|
+
export declare const SHARED_MEDIA_DIR = "media";
|
|
19
|
+
export interface MediaServiceDeps {
|
|
20
|
+
connector: RepositoryConnector;
|
|
21
|
+
maxBytes: number;
|
|
22
|
+
}
|
|
23
|
+
export interface MediaFile {
|
|
24
|
+
/** Path relative to the content root — the value a `media()` field holds. */
|
|
25
|
+
path: string;
|
|
26
|
+
filename: string;
|
|
27
|
+
mimeType: string;
|
|
28
|
+
size: number;
|
|
29
|
+
width: number | null;
|
|
30
|
+
height: number | null;
|
|
31
|
+
}
|
|
32
|
+
export declare function createMediaService(deps: MediaServiceDeps): {
|
|
33
|
+
/**
|
|
34
|
+
* Reads and checks an upload, without writing it.
|
|
35
|
+
*
|
|
36
|
+
* Separate from the commit so that an entry and the media it references go
|
|
37
|
+
* into the repository in *one* change set. Uploading first and saving
|
|
38
|
+
* second would leave an orphaned file behind whenever the editor changed
|
|
39
|
+
* their mind.
|
|
40
|
+
*/
|
|
41
|
+
prepare(input: {
|
|
42
|
+
file: File;
|
|
43
|
+
/** Directory to place the file in, relative to the content root. */
|
|
44
|
+
directory?: string;
|
|
45
|
+
accept?: "image" | "document" | "any";
|
|
46
|
+
}): Promise<{
|
|
47
|
+
file: MediaFile;
|
|
48
|
+
contents: Uint8Array;
|
|
49
|
+
}>;
|
|
50
|
+
/**
|
|
51
|
+
* Every media file in the repository, for the editor's picker.
|
|
52
|
+
*
|
|
53
|
+
* A walk of the content tree rather than a table scan. Content trees are
|
|
54
|
+
* small, and the alternative — an index Mica maintains — would be a second
|
|
55
|
+
* source of truth that could disagree with the repository.
|
|
56
|
+
*/
|
|
57
|
+
list(): Promise<MediaFile[]>;
|
|
58
|
+
read(mediaPath: string): Promise<{
|
|
59
|
+
bytes: Uint8Array;
|
|
60
|
+
mimeType: string;
|
|
61
|
+
} | null>;
|
|
62
|
+
};
|
|
63
|
+
export type MediaService = ReturnType<typeof createMediaService>;
|
|
64
|
+
export declare function formatBytes(bytes: number): string;
|
|
65
|
+
//# sourceMappingURL=media.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"media.d.ts","sourceRoot":"","sources":["../../src/services/media.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAKH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAM/D,yEAAyE;AACzE,eAAO,MAAM,gBAAgB,UAAU,CAAC;AAExC,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,mBAAmB,CAAC;IAC/B,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,SAAS;IACxB,6EAA6E;IAC7E,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,gBAAgB;IAyBrD;;;;;;;OAOG;mBACkB;QACnB,IAAI,EAAE,IAAI,CAAC;QACX,oEAAoE;QACpE,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,MAAM,CAAC,EAAE,OAAO,GAAG,UAAU,GAAG,KAAK,CAAC;KACvC,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,SAAS,CAAC;QAAC,QAAQ,EAAE,UAAU,CAAA;KAAE,CAAC;IAwCtD;;;;;;OAMG;YACW,OAAO,CAAC,SAAS,EAAE,CAAC;oBAmCZ,MAAM,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,UAAU,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;EAOzF;AAED,MAAM,MAAM,YAAY,GAAG,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC;AA2DjE,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAIjD"}
|
|
@@ -0,0 +1,180 @@
|
|
|
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.
|
|
15
|
+
*/
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import { slugify } from "@micacms/core";
|
|
18
|
+
import { imageSize } from "image-size";
|
|
19
|
+
import { ApiError, badRequest } from "../lib/errors.js";
|
|
20
|
+
import { detectFileType, isAccepted, isImageType } from "../lib/file-type.js";
|
|
21
|
+
/** Where uploads go when they are not attached to a particular entry. */
|
|
22
|
+
export const SHARED_MEDIA_DIR = "media";
|
|
23
|
+
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
|
+
*/
|
|
33
|
+
const uniqueName = async (directory, filename) => {
|
|
34
|
+
const taken = new Set((await connector.list(directory)).filter((item) => item.type === "file").map((item) => item.name));
|
|
35
|
+
const extension = path.extname(filename);
|
|
36
|
+
const stem = filename.slice(0, filename.length - extension.length);
|
|
37
|
+
let candidate = filename;
|
|
38
|
+
for (let n = 2; taken.has(candidate); n += 1)
|
|
39
|
+
candidate = `${stem}-${n}${extension}`;
|
|
40
|
+
return candidate;
|
|
41
|
+
};
|
|
42
|
+
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
|
+
*/
|
|
51
|
+
async prepare(input) {
|
|
52
|
+
const { file } = input;
|
|
53
|
+
const directory = input.directory ?? SHARED_MEDIA_DIR;
|
|
54
|
+
if (file.size === 0)
|
|
55
|
+
throw badRequest("The file is empty.");
|
|
56
|
+
if (file.size > maxBytes) {
|
|
57
|
+
throw new ApiError("payload_too_large", `The file is ${formatBytes(file.size)}. The limit is ${formatBytes(maxBytes)}.`);
|
|
58
|
+
}
|
|
59
|
+
const contents = new Uint8Array(await file.arrayBuffer());
|
|
60
|
+
// From the bytes, never from the browser's claim about them.
|
|
61
|
+
const detected = detectFileType(contents);
|
|
62
|
+
if (!detected) {
|
|
63
|
+
throw new ApiError("unsupported_media_type", "This file type is not supported.");
|
|
64
|
+
}
|
|
65
|
+
if (!isAccepted(detected.mimeType, input.accept ?? "any")) {
|
|
66
|
+
throw new ApiError("unsupported_media_type", `A ${input.accept} was expected, but this is a ${detected.mimeType}.`);
|
|
67
|
+
}
|
|
68
|
+
const filename = await uniqueName(directory, safeName(file.name, detected.extension));
|
|
69
|
+
return {
|
|
70
|
+
contents,
|
|
71
|
+
file: {
|
|
72
|
+
path: `${directory}/${filename}`,
|
|
73
|
+
filename,
|
|
74
|
+
mimeType: detected.mimeType,
|
|
75
|
+
size: contents.byteLength,
|
|
76
|
+
...dimensions(contents, detected.mimeType),
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
/**
|
|
81
|
+
* Every media file in the repository, for the editor's picker.
|
|
82
|
+
*
|
|
83
|
+
* A walk of the content tree rather than a table scan. Content trees are
|
|
84
|
+
* small, and the alternative — an index Mica maintains — would be a second
|
|
85
|
+
* source of truth that could disagree with the repository.
|
|
86
|
+
*/
|
|
87
|
+
async list() {
|
|
88
|
+
const found = [];
|
|
89
|
+
const walk = async (directory) => {
|
|
90
|
+
for (const item of await connector.list(directory)) {
|
|
91
|
+
const child = directory === "" ? item.name : `${directory}/${item.name}`;
|
|
92
|
+
if (item.type === "directory") {
|
|
93
|
+
await walk(child);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (!isMediaName(item.name))
|
|
97
|
+
continue;
|
|
98
|
+
const bytes = await connector.readBinary(child);
|
|
99
|
+
if (!bytes)
|
|
100
|
+
continue;
|
|
101
|
+
// The bytes are authoritative; the extension is the fallback for the
|
|
102
|
+
// formats the sniffer does not cover, such as SVG.
|
|
103
|
+
const mimeType = detectFileType(bytes)?.mimeType ?? mimeFromName(item.name);
|
|
104
|
+
found.push({
|
|
105
|
+
path: child,
|
|
106
|
+
filename: item.name,
|
|
107
|
+
mimeType,
|
|
108
|
+
size: bytes.byteLength,
|
|
109
|
+
...dimensions(bytes, mimeType),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
await walk("");
|
|
114
|
+
return found.sort((a, b) => a.path.localeCompare(b.path));
|
|
115
|
+
},
|
|
116
|
+
async read(mediaPath) {
|
|
117
|
+
const bytes = await connector.readBinary(mediaPath);
|
|
118
|
+
if (!bytes)
|
|
119
|
+
return null;
|
|
120
|
+
return { bytes, mimeType: detectFileType(bytes)?.mimeType ?? mimeFromName(mediaPath) };
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/* ------------------------------------------------------------------ */
|
|
125
|
+
/**
|
|
126
|
+
* A filename that is safe as a path segment and still recognisable.
|
|
127
|
+
*
|
|
128
|
+
* Slugified rather than sanitised: the result is part of a URL in the built
|
|
129
|
+
* site and part of a path in a Git repository, and both are better off without
|
|
130
|
+
* spaces, accents or case.
|
|
131
|
+
*/
|
|
132
|
+
function safeName(original, extension) {
|
|
133
|
+
const stem = slugify(path.basename(original).replace(/\.[^.]*$/, "")).slice(0, 80);
|
|
134
|
+
return `${stem || "upload"}.${extension}`;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Extension to media type, for the formats byte-sniffing does not identify.
|
|
138
|
+
*
|
|
139
|
+
* A fallback, never the first answer: a file's own bytes are the only thing
|
|
140
|
+
* that cannot be renamed to lie.
|
|
141
|
+
*/
|
|
142
|
+
const BY_EXTENSION = {
|
|
143
|
+
svg: "image/svg+xml",
|
|
144
|
+
pdf: "application/pdf",
|
|
145
|
+
txt: "text/plain",
|
|
146
|
+
csv: "text/csv",
|
|
147
|
+
doc: "application/msword",
|
|
148
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
149
|
+
xls: "application/vnd.ms-excel",
|
|
150
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
151
|
+
};
|
|
152
|
+
function mimeFromName(name) {
|
|
153
|
+
const extension = path.extname(name).slice(1).toLowerCase();
|
|
154
|
+
return BY_EXTENSION[extension] ?? "application/octet-stream";
|
|
155
|
+
}
|
|
156
|
+
/** Files a `media()` field could plausibly point at. */
|
|
157
|
+
function isMediaName(name) {
|
|
158
|
+
return /\.(jpe?g|png|webp|avif|gif|tiff?|svg|pdf|docx?|xlsx?|txt|csv)$/i.test(name);
|
|
159
|
+
}
|
|
160
|
+
function dimensions(bytes, mimeType) {
|
|
161
|
+
if (bytes.byteLength === 0 || !isImageType(mimeType) || mimeType === "image/svg+xml") {
|
|
162
|
+
return { width: null, height: null };
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
const size = imageSize(Buffer.from(bytes));
|
|
166
|
+
return { width: size.width ?? null, height: size.height ?? null };
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
// A format image-size cannot read is still a valid upload.
|
|
170
|
+
return { width: null, height: null };
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
export function formatBytes(bytes) {
|
|
174
|
+
if (bytes < 1024)
|
|
175
|
+
return `${bytes} B`;
|
|
176
|
+
if (bytes < 1024 * 1024)
|
|
177
|
+
return `${Math.round(bytes / 1024)} kB`;
|
|
178
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
179
|
+
}
|
|
180
|
+
//# sourceMappingURL=media.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"media.js","sourceRoot":"","sources":["../../src/services/media.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Working-tree staging: local mode's half of the staging service.
|
|
3
|
+
*
|
|
4
|
+
* The checked-out working tree *is* the draft workspace. Autosave has already
|
|
5
|
+
* written the files — that is what lets the Astro dev server hot-reload them,
|
|
6
|
+
* which is the whole value of local mode — so this service's job is reading
|
|
7
|
+
* what Git thinks of the tree and, on Publish, turning exactly the reviewed
|
|
8
|
+
* paths into one commit and one push.
|
|
9
|
+
*
|
|
10
|
+
* Three rules keep it safe in a repository that belongs to a developer:
|
|
11
|
+
*
|
|
12
|
+
* - Only Mica-managed paths. Every Git operation is limited to an explicit
|
|
13
|
+
* path set derived from the change list — never `git add -A` over the
|
|
14
|
+
* repository, so unrelated work in the same checkout is never swept in.
|
|
15
|
+
* - HEAD is verified against the base the editor reviewed. A repository
|
|
16
|
+
* that moved refuses, like a provider branch that moved.
|
|
17
|
+
* - A commit and a push are two operations. When the push fails the commit
|
|
18
|
+
* is retained and reported as `committed-pending-push`; a retry pushes
|
|
19
|
+
* again and never creates a second commit.
|
|
20
|
+
*/
|
|
21
|
+
import type { RepositoryConnector } from "@micacms/connectors";
|
|
22
|
+
import { type ContentChange, type Site } from "@micacms/core";
|
|
23
|
+
import type { Logger } from "../lib/logger.js";
|
|
24
|
+
import { type ContentStagingService } from "./staging.js";
|
|
25
|
+
export interface WorkingTreeStagingDeps {
|
|
26
|
+
model: Site;
|
|
27
|
+
/** Reads the working tree — the same local connector autosave writes through. */
|
|
28
|
+
connector: RepositoryConnector;
|
|
29
|
+
/** Absolute path of the content root. */
|
|
30
|
+
contentRoot: string;
|
|
31
|
+
/** Revision of `.mica/schema.json` when the model was loaded. */
|
|
32
|
+
schemaRevision: string | null;
|
|
33
|
+
/** `MICA_LOCAL_PUBLISH`. False reduces this to a read-only changes view. */
|
|
34
|
+
publishEnabled: boolean;
|
|
35
|
+
logger: Logger;
|
|
36
|
+
}
|
|
37
|
+
/** A changed file plus what only Git status knows about it. */
|
|
38
|
+
export interface TreeChange extends ContentChange {
|
|
39
|
+
/** Not yet known to Git — discarding means deleting, not restoring. */
|
|
40
|
+
untracked: boolean;
|
|
41
|
+
/** Mid-merge conflict markers. Unpublishable until resolved with Git tools. */
|
|
42
|
+
conflicted: boolean;
|
|
43
|
+
}
|
|
44
|
+
export declare function createWorkingTreeStagingService(deps: WorkingTreeStagingDeps): ContentStagingService;
|
|
45
|
+
/**
|
|
46
|
+
* Parses `git status --porcelain=v1 -z` into content-root-relative changes.
|
|
47
|
+
*
|
|
48
|
+
* The `-z` format is the one that never munges a path: entries are
|
|
49
|
+
* NUL-terminated, and a rename is two fields — the new path, then the one it
|
|
50
|
+
* came from. A rename becomes a create and a delete, which is how the rest of
|
|
51
|
+
* Mica already thinks about moves.
|
|
52
|
+
*
|
|
53
|
+
* `.mica/` is excluded. The manifest there is build output a developer
|
|
54
|
+
* commits with their own tools; the editor neither shows nor publishes it,
|
|
55
|
+
* and model changes are guarded separately, by the drift check.
|
|
56
|
+
*/
|
|
57
|
+
export declare function parseStatus(raw: string, prefix: string): TreeChange[];
|
|
58
|
+
//# sourceMappingURL=staging-local.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"staging-local.d.ts","sourceRoot":"","sources":["../../src/services/staging-local.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAKH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC/D,OAAO,EAGL,KAAK,aAAa,EAIlB,KAAK,IAAI,EACV,MAAM,eAAe,CAAC;AAKvB,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAOL,KAAK,qBAAqB,EAI3B,MAAM,cAAc,CAAC;AAEtB,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,IAAI,CAAC;IACZ,iFAAiF;IACjF,SAAS,EAAE,mBAAmB,CAAC;IAC/B,yCAAyC;IACzC,WAAW,EAAE,MAAM,CAAC;IACpB,iEAAiE;IACjE,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,4EAA4E;IAC5E,cAAc,EAAE,OAAO,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,+DAA+D;AAC/D,MAAM,WAAW,UAAW,SAAQ,aAAa;IAC/C,uEAAuE;IACvE,SAAS,EAAE,OAAO,CAAC;IACnB,+EAA+E;IAC/E,UAAU,EAAE,OAAO,CAAC;CACrB;AAQD,wBAAgB,+BAA+B,CAC7C,IAAI,EAAE,sBAAsB,GAC3B,qBAAqB,CA2QvB;AAmED;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,UAAU,EAAE,CAmDrE"}
|