@phtngyn/cms 0.2.0-beta.2 → 0.2.0-beta.4
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 -0
- package/package.json +6 -6
- package/src/Content.js +41 -18
- package/src/Content.js.map +1 -1
- package/src/{S3MediaObjects-D5Ns4FM6.js → S3MediaObjects-CR3p1WTg.js} +8 -2
- package/src/S3MediaObjects-CR3p1WTg.js.map +1 -0
- package/src/cli.js +2 -2
- package/src/connection-D5S9Iph-.js.map +1 -1
- package/src/{dev-lifecycle-CZ7iCzG9.js → dev-lifecycle-D3f9-upa.js} +2 -2
- package/src/dev-lifecycle-D3f9-upa.js.map +1 -0
- package/src/{installation-BNuoRDOz.js → installation-fZtNxtoW.js} +27 -12
- package/src/installation-fZtNxtoW.js.map +1 -0
- package/src/internal/configuration.js +1 -1
- package/src/internal/dev-lifecycle.js +1 -1
- package/src/internal/studio-app.js +8 -6
- package/src/internal/studio-app.js.map +1 -1
- package/src/runtime/delivery/document-page.js +1 -5
- package/src/runtime/delivery/document-page.js.map +1 -1
- package/src/runtime/delivery/plugin.js +34 -5
- package/src/runtime/delivery/plugin.js.map +1 -1
- package/src/runtime/delivery/useCmsDocumentSeo.d.ts +1 -0
- package/src/runtime/delivery/useCmsDocumentSeo.js +5 -4
- package/src/runtime/delivery/useCmsDocumentSeo.js.map +1 -1
- package/src/runtime/forms/useCmsForm.js +53 -30
- package/src/runtime/forms/useCmsForm.js.map +1 -1
- package/src/runtime/i18n/useCmsI18n.js.map +1 -1
- package/src/runtime/installation.js +1 -1
- package/src/runtime/plugin.js +1 -1
- package/src/runtime/preview/plugin.client.js +1 -1
- package/src/runtime/preview/plugin.client.js.map +1 -1
- package/src/runtime/preview/store.js +1 -1
- package/src/{store-B49J21NA.js → store-CCsjHaZm.js} +6 -6
- package/src/store-CCsjHaZm.js.map +1 -0
- package/studio/host.js +2562 -2556
- package/src/S3MediaObjects-D5Ns4FM6.js.map +0 -1
- package/src/dev-lifecycle-CZ7iCzG9.js.map +0 -1
- package/src/installation-BNuoRDOz.js.map +0 -1
- package/src/store-B49J21NA.js.map +0 -1
package/README.md
CHANGED
|
@@ -106,6 +106,8 @@ CMS_URL=https://target.example.com CMS_EMAIL=admin@example.com cms content apply
|
|
|
106
106
|
|
|
107
107
|
Export writes each authored source at its CMS path and a `.cms-content.json` index with content hashes and the exact-revision publication state. Commit both to Git to review changes. Export refuses to replace locally edited Markdown or index entries. It does not delete files that disappeared from the CMS; remove stale files and index entries yourself after review. `diff` compares local Markdown and publication state with the current target and reports target-only documents. `plan` validates the whole incoming set and lists creates, updates, publications, and conflicts without writing. `apply` plans again, then commits the batch atomically against the generation it just inspected. Run `plan` and `diff` before applying, especially when targeting another environment.
|
|
108
108
|
|
|
109
|
+
The export index and every indexed Markdown path must stay inside the selected content directory without symlinked path components. Import and export reject symlinks at those boundaries.
|
|
110
|
+
|
|
109
111
|
For a folder without `.cms-content.json`, the CLI reads nested `.md` and `.mdc` files and uses folder-relative document paths. These sources are eligible for publication with `--publish`. Imports create drafts by default. Existing content with different Markdown is a conflict until you pass `--overwrite` to `plan` and `apply`. For an exported folder, `--publish` publishes only index entries marked `published: true`; without it, they remain drafts on the target. A newly imported draft does not unpublish an older target revision. The CLI does not delete target documents or move a target-only document. A concurrent change after planning causes `apply` to fail; review and retry.
|
|
110
112
|
|
|
111
113
|
The command tree is intentionally shared: future CMS operations can be added beside `content` without separate executables.
|
package/package.json
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"directory": "packages/nuxt"
|
|
18
18
|
},
|
|
19
19
|
"name": "@phtngyn/cms",
|
|
20
|
-
"version": "0.2.0-beta.
|
|
20
|
+
"version": "0.2.0-beta.4",
|
|
21
21
|
"type": "module",
|
|
22
22
|
"files": [
|
|
23
23
|
"bin",
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
"@hocuspocus/server": "^4.7.0",
|
|
71
71
|
"@nuxt/kit": "^4.5.2",
|
|
72
72
|
"@nuxt/schema": "^4.5.2",
|
|
73
|
-
"@nuxtjs/robots": "^6.2.
|
|
73
|
+
"@nuxtjs/robots": "^6.2.3",
|
|
74
74
|
"@nuxtjs/sitemap": "^8.5.1",
|
|
75
75
|
"@standard-schema/spec": "^1.1.0",
|
|
76
76
|
"@tiptap/core": "^3.31.3",
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
"@tiptap/y-tiptap": "^3.0.9",
|
|
79
79
|
"@vue/compiler-sfc": "^3.6.0-rc.8",
|
|
80
80
|
"better-auth": "^1.7.4",
|
|
81
|
-
"c12": "^
|
|
81
|
+
"c12": "^4.0.0-rc.1",
|
|
82
82
|
"comark": "^0.7.0",
|
|
83
83
|
"diff": "^9.0.0",
|
|
84
84
|
"drizzle-orm": "^1.0.0-rc.5-169397b",
|
|
@@ -86,8 +86,8 @@
|
|
|
86
86
|
"h3": "^1.15.11",
|
|
87
87
|
"jiti": "^2.7.0",
|
|
88
88
|
"nitropack": "^2.13.4",
|
|
89
|
-
"nuxt-schema-org": "^6.3.
|
|
90
|
-
"nuxt-seo-utils": "^8.5.
|
|
89
|
+
"nuxt-schema-org": "^6.3.2",
|
|
90
|
+
"nuxt-seo-utils": "^8.5.1",
|
|
91
91
|
"nuxt-site-config": "^4.2.3",
|
|
92
92
|
"pg": "^8.23.0",
|
|
93
93
|
"typescript": "npm:typescript-native-bridge@6.0.3-bridge.16.tsgo.7.0.2",
|
|
@@ -100,4 +100,4 @@
|
|
|
100
100
|
"nuxt": ">=4.5.0 <5.0.0",
|
|
101
101
|
"vue": ">=3.6.0-rc.6 <4.0.0"
|
|
102
102
|
}
|
|
103
|
-
}
|
|
103
|
+
}
|
package/src/Content.js
CHANGED
|
@@ -2,7 +2,7 @@ import { a as ContentSource, i as ContentSnapshot, n as ContentApplyResult, r as
|
|
|
2
2
|
import { t as CMS_AUTHENTICATION_PATH, u as STUDIO_API_PATH } from "./HttpPaths-c9nLZ9H2.js";
|
|
3
3
|
import { Result, Schema } from "effect";
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
5
|
-
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
5
|
+
import { lstat, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
6
6
|
import { dirname, extname, relative, resolve, sep } from "node:path";
|
|
7
7
|
|
|
8
8
|
//#region src/Content.ts
|
|
@@ -69,13 +69,18 @@ function createCmsContentClient(options) {
|
|
|
69
69
|
const snapshot = async () => contentRequest("snapshot", ContentSnapshot, "content snapshot");
|
|
70
70
|
return {
|
|
71
71
|
plan: async (source, authoredOptions = {}) => {
|
|
72
|
-
const
|
|
73
|
-
|
|
72
|
+
const sources = Object.freeze(source.sources.map((entry) => Object.freeze(ContentSource.make({
|
|
73
|
+
sourcePath: entry.sourcePath,
|
|
74
|
+
markdown: entry.markdown,
|
|
75
|
+
published: entry.published
|
|
76
|
+
}))));
|
|
77
|
+
const input = Object.freeze({
|
|
78
|
+
sources,
|
|
74
79
|
overwrite: authoredOptions.overwrite ?? false,
|
|
75
80
|
publish: authoredOptions.publish ?? false
|
|
76
|
-
};
|
|
81
|
+
});
|
|
77
82
|
const [protocol, current] = await Promise.all([contentRequest("plan", ContentPlan, "content plan", input), snapshot()]);
|
|
78
|
-
const sourcePaths = new Set(
|
|
83
|
+
const sourcePaths = new Set(sources.map((entry) => entry.sourcePath));
|
|
79
84
|
const entries = (action) => protocol.entries.filter((entry) => entry.action === action);
|
|
80
85
|
const plan = Object.freeze({
|
|
81
86
|
generation: protocol.generation,
|
|
@@ -125,7 +130,7 @@ function createCmsContentClient(options) {
|
|
|
125
130
|
async function readCmsContentDirectory(directory) {
|
|
126
131
|
try {
|
|
127
132
|
const root = resolve(directory);
|
|
128
|
-
const indexPath =
|
|
133
|
+
const indexPath = await safeContentFile(root, ".cms-content.json");
|
|
129
134
|
let index;
|
|
130
135
|
try {
|
|
131
136
|
index = parseIndex(await readFile(indexPath, "utf8"));
|
|
@@ -134,7 +139,7 @@ async function readCmsContentDirectory(directory) {
|
|
|
134
139
|
}
|
|
135
140
|
if (index) return { sources: await Promise.all(index.sources.map(async (entry) => decode(ContentSource, {
|
|
136
141
|
sourcePath: entry.sourcePath,
|
|
137
|
-
markdown: await readFile(
|
|
142
|
+
markdown: await readFile(await safeContentFile(root, entry.sourcePath), "utf8"),
|
|
138
143
|
published: entry.published
|
|
139
144
|
}, "content source"))) };
|
|
140
145
|
const files = await markdownFiles(root);
|
|
@@ -166,34 +171,37 @@ const ContentIndex = Schema.Struct({
|
|
|
166
171
|
async function writeContentDirectory(directory, sources) {
|
|
167
172
|
try {
|
|
168
173
|
const root = resolve(directory);
|
|
174
|
+
const indexPath = await safeContentFile(root, ".cms-content.json");
|
|
175
|
+
await Promise.all(sources.map((source) => safeContentFile(root, source.sourcePath)));
|
|
169
176
|
let previous;
|
|
170
177
|
try {
|
|
171
|
-
previous = parseIndex(await readFile(
|
|
178
|
+
previous = parseIndex(await readFile(indexPath, "utf8"));
|
|
172
179
|
} catch (cause) {
|
|
173
180
|
if (!isMissingFile(cause)) throw cause;
|
|
174
181
|
}
|
|
182
|
+
const previousByPath = new Map(previous?.sources.map((entry) => [entry.sourcePath, entry]));
|
|
175
183
|
if (previous) {
|
|
176
184
|
const metadata = {
|
|
177
185
|
version: previous.version,
|
|
178
186
|
sources: previous.sources
|
|
179
187
|
};
|
|
180
188
|
if (sha256(JSON.stringify(metadata)) !== previous.exportSha256) throw new CmsContentError("filesystem-error", "The local .cms-content.json file has been edited.");
|
|
181
|
-
const previousByPath = new Map(previous.sources.map((entry) => [entry.sourcePath, entry]));
|
|
182
189
|
const incomingPaths = new Set(sources.map((source) => source.sourcePath));
|
|
183
190
|
for (const entry of previous.sources) if (!incomingPaths.has(entry.sourcePath)) throw new CmsContentError("filesystem-error", `CMS document ${entry.sourcePath} no longer exists; resolve the local file before exporting again.`);
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
+
}
|
|
192
|
+
for (const source of sources) try {
|
|
193
|
+
const local = await readFile(await safeContentFile(root, source.sourcePath), "utf8");
|
|
194
|
+
const before = previousByPath.get(source.sourcePath);
|
|
195
|
+
if (before ? sha256(local) !== before.sha256 : local !== source.markdown) throw new CmsContentError("filesystem-error", `Local edits in ${source.sourcePath}; export was stopped.`);
|
|
196
|
+
} catch (cause) {
|
|
197
|
+
if (!isMissingFile(cause)) throw cause;
|
|
191
198
|
}
|
|
192
199
|
await mkdir(root, { recursive: true });
|
|
193
200
|
const entries = [];
|
|
194
201
|
for (const source of sources) {
|
|
195
|
-
const file =
|
|
202
|
+
const file = await safeContentFile(root, source.sourcePath);
|
|
196
203
|
await mkdir(dirname(file), { recursive: true });
|
|
204
|
+
await safeContentFile(root, source.sourcePath);
|
|
197
205
|
await writeFile(file, source.markdown);
|
|
198
206
|
entries.push({
|
|
199
207
|
sourcePath: source.sourcePath,
|
|
@@ -209,7 +217,8 @@ async function writeContentDirectory(directory, sources) {
|
|
|
209
217
|
...metadata,
|
|
210
218
|
exportSha256: sha256(JSON.stringify(metadata))
|
|
211
219
|
};
|
|
212
|
-
await
|
|
220
|
+
await safeContentFile(root, ".cms-content.json");
|
|
221
|
+
await writeFile(indexPath, `${JSON.stringify(index, void 0, 2)}\n`);
|
|
213
222
|
} catch (cause) {
|
|
214
223
|
throw new CmsContentError("filesystem-error", `Could not export CMS content to ${directory}.`, { cause });
|
|
215
224
|
}
|
|
@@ -246,6 +255,20 @@ function safeContentPath(root, sourcePath) {
|
|
|
246
255
|
if (!path.startsWith(`${root}${sep}`)) throw new CmsContentError("filesystem-error", `Unsafe content path: ${sourcePath}`);
|
|
247
256
|
return path;
|
|
248
257
|
}
|
|
258
|
+
async function safeContentFile(root, sourcePath) {
|
|
259
|
+
const path = safeContentPath(root, sourcePath);
|
|
260
|
+
const segments = relative(root, path).split(sep);
|
|
261
|
+
let current = root;
|
|
262
|
+
for (const segment of ["", ...segments]) {
|
|
263
|
+
current = segment ? resolve(current, segment) : current;
|
|
264
|
+
try {
|
|
265
|
+
if ((await lstat(current)).isSymbolicLink()) throw new CmsContentError("filesystem-error", `Symlinks are not allowed in CMS content paths: ${sourcePath}`);
|
|
266
|
+
} catch (cause) {
|
|
267
|
+
if (!isMissingFile(cause)) throw cause;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return path;
|
|
271
|
+
}
|
|
249
272
|
function contentBaseUrl(value) {
|
|
250
273
|
let url;
|
|
251
274
|
try {
|
package/src/Content.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Content.js","names":["ContentSnapshotSchema","ProtocolContentPlanSchema","ContentApplyResultSchema","ContentSourceSchema"],"sources":["../../../src/Content.ts"],"sourcesContent":["/* oxlint-disable effecttsgo/async-function effecttsgo/extends-native-error effecttsgo/node-builtin-import -- This public Node-only API deliberately exposes Promises, native filesystem paths, and an Error subclass to ordinary deployment scripts. */\nimport type {\n ContentApplyResult,\n ContentPlan as ProtocolContentPlan,\n ContentPlanEntry,\n ContentSnapshotSource,\n ContentSource,\n ContentSnapshot,\n} from '@internal/protocol/ContentTransfer'\nimport {\n ContentApplyResult as ContentApplyResultSchema,\n ContentPlan as ProtocolContentPlanSchema,\n ContentSource as ContentSourceSchema,\n ContentSnapshot as ContentSnapshotSchema,\n} from '@internal/protocol/ContentTransfer'\nimport type { DocumentIssue } from '@internal/protocol/Documents'\nimport { CMS_AUTHENTICATION_PATH, STUDIO_API_PATH } from '@internal/protocol/HttpPaths'\nimport { Result, Schema } from 'effect'\nimport { createHash } from 'node:crypto'\nimport { readdir, readFile, mkdir, writeFile } from 'node:fs/promises'\nimport { dirname, extname, relative, resolve, sep } from 'node:path'\n\nexport type CmsContentErrorCode =\n | 'authentication-required'\n | 'content-conflict'\n | 'filesystem-error'\n | 'generation-changed'\n | 'invalid-document'\n | 'document-not-found'\n | 'invalid-response'\n | 'network-error'\n\nexport class CmsContentError extends Error {\n readonly code: CmsContentErrorCode\n readonly conflicts?: ReadonlyArray<CmsContentPlanEntry>\n readonly issues?: ReadonlyArray<DocumentIssue>\n\n constructor(\n code: CmsContentErrorCode,\n message: string,\n options: {\n readonly cause?: unknown\n readonly conflicts?: ReadonlyArray<CmsContentPlanEntry>\n readonly issues?: ReadonlyArray<DocumentIssue>\n } = {},\n ) {\n super(message, options.cause === undefined ? undefined : { cause: options.cause })\n this.name = 'CmsContentError'\n this.code = code\n this.conflicts = options.conflicts\n this.issues = options.issues\n }\n}\n\nexport interface CmsContentCredentials {\n readonly email: string\n readonly password: string\n}\n\nexport interface CmsContentClientOptions {\n readonly baseUrl: string\n readonly credentials: CmsContentCredentials\n readonly fetch?: typeof globalThis.fetch\n}\n\nexport interface CmsContentSourceSet {\n readonly sources: ReadonlyArray<ContentSource>\n}\n\nexport interface CmsContentDocument extends ContentSnapshotSource {}\n\nexport interface CmsContentPlanEntry extends ContentPlanEntry {}\n\nexport interface CmsContentPlan {\n readonly generation: string\n readonly creates: ReadonlyArray<CmsContentPlanEntry>\n readonly updates: ReadonlyArray<CmsContentPlanEntry>\n readonly publications: ReadonlyArray<CmsContentPlanEntry>\n readonly conflicts: ReadonlyArray<CmsContentPlanEntry>\n readonly unchanged: ReadonlyArray<CmsContentPlanEntry>\n readonly issues: ReadonlyArray<DocumentIssue>\n readonly targetOnly: ReadonlyArray<ContentSource>\n readonly valid: boolean\n}\n\nexport interface CmsContentImportOptions {\n readonly overwrite?: boolean\n readonly publish?: boolean\n}\n\nexport interface CmsContentDiff {\n readonly changes: ReadonlyArray<{\n readonly sourcePath: string\n readonly local?: ContentSource\n readonly target?: ContentSource\n readonly markdownChanged: boolean\n readonly publicationChanged: boolean\n }>\n readonly targetOnly: ReadonlyArray<ContentSource>\n}\n\nexport interface CmsContentClient {\n readonly plan: (source: CmsContentSourceSet, options?: CmsContentImportOptions) => Promise<CmsContentPlan>\n readonly apply: (plan: CmsContentPlan) => Promise<ContentApplyResult>\n readonly diff: (source: CmsContentSourceSet) => Promise<CmsContentDiff>\n readonly export: (options: { readonly directory: string }) => Promise<CmsContentSourceSet>\n readonly readDocument: (documentId: string) => Promise<CmsContentDocument>\n}\n\ninterface PlanState {\n readonly client: object\n readonly input: {\n readonly sources: ReadonlyArray<ContentSource>\n readonly overwrite: boolean\n readonly publish: boolean\n }\n readonly protocol: ProtocolContentPlan\n}\n\ntype CmsContentRequestBody =\n | PlanState['input']\n | (PlanState['input'] & { readonly expectedGeneration: ProtocolContentPlan['generation'] })\n\nconst plans = new WeakMap<CmsContentPlan, PlanState>()\n\nexport function createCmsContentClient(options: CmsContentClientOptions): CmsContentClient {\n const baseUrl = contentBaseUrl(options.baseUrl)\n const requestFetch = options.fetch ?? globalThis.fetch\n const identity = {}\n let cookie: Promise<string> | undefined\n\n const authenticate = async (): Promise<string> => {\n if (cookie) return cookie\n\n cookie = request(requestFetch, new URL(`${CMS_AUTHENTICATION_PATH}/sign-in/email`, baseUrl), {\n method: 'POST',\n headers: { accept: 'application/json', 'content-type': 'application/json', origin: baseUrl.origin },\n body: JSON.stringify(options.credentials),\n }).then(async (response) => {\n if (!response.ok) throw new CmsContentError('authentication-required', `CMS sign-in failed (${response.status}).`)\n const value = response.headers.get('set-cookie')?.split(';', 1)[0]\n\n if (!value) throw new CmsContentError('authentication-required', 'CMS sign-in did not return a session cookie.')\n\n return value\n })\n\n return cookie\n }\n\n const contentRequest = async <SchemaType extends Schema.ConstraintDecoder<unknown>>(\n path: string,\n schema: SchemaType,\n label: string,\n body?: CmsContentRequestBody,\n ): Promise<SchemaType['Type']> => {\n const session = await authenticate()\n\n const response = await request(requestFetch, new URL(`${STUDIO_API_PATH}/content/${path}`, baseUrl), {\n method: body === undefined ? 'GET' : 'POST',\n headers: {\n accept: 'application/json',\n cookie: session,\n ...(body === undefined ? undefined : { 'content-type': 'application/json' }),\n },\n ...(body === undefined ? undefined : { body: JSON.stringify(body) }),\n })\n\n if (!response.ok) {\n if (response.status === 401 || response.status === 403)\n throw new CmsContentError('authentication-required', `CMS content request was rejected (${response.status}).`)\n\n if (response.status === 409)\n throw new CmsContentError('generation-changed', 'The CMS changed after this content plan was created.')\n\n throw new CmsContentError('network-error', `CMS content request failed (${response.status}).`)\n }\n\n let payload: unknown\n\n try {\n payload = await response.json()\n } catch (cause) {\n throw new CmsContentError('invalid-response', 'CMS returned an invalid JSON response.', { cause })\n }\n\n return decode(schema, payload, label)\n }\n\n const snapshot = async (): Promise<ContentSnapshot> =>\n contentRequest('snapshot', ContentSnapshotSchema, 'content snapshot')\n\n const client: CmsContentClient = {\n plan: async (source, authoredOptions = {}) => {\n const input = {\n sources: source.sources,\n overwrite: authoredOptions.overwrite ?? false,\n publish: authoredOptions.publish ?? false,\n }\n\n const [protocol, current] = await Promise.all([\n contentRequest('plan', ProtocolContentPlanSchema, 'content plan', input),\n snapshot(),\n ])\n\n const sourcePaths = new Set(source.sources.map((entry) => entry.sourcePath))\n\n const entries = (action: ContentPlanEntry['action']) =>\n protocol.entries.filter((entry) => entry.action === action)\n\n const plan: CmsContentPlan = Object.freeze({\n generation: protocol.generation,\n creates: entries('create'),\n updates: entries('update'),\n publications: entries('publish'),\n conflicts: entries('conflict'),\n unchanged: entries('unchanged'),\n issues: protocol.issues,\n targetOnly: current.sources.filter((entry) => !sourcePaths.has(entry.sourcePath)),\n valid: protocol.valid,\n })\n\n plans.set(plan, { client: identity, input, protocol })\n\n return plan\n },\n apply: async (plan) => {\n const state = plans.get(plan)\n\n if (!state || state.client !== identity)\n throw new CmsContentError('content-conflict', 'This content plan was not created by this client.')\n\n if (!plan.valid)\n throw new CmsContentError('content-conflict', 'The content plan is not valid.', {\n conflicts: plan.conflicts,\n issues: plan.issues,\n })\n\n if (plan.conflicts.length)\n throw new CmsContentError('content-conflict', 'The content plan contains conflicts.', {\n conflicts: plan.conflicts,\n })\n\n if (plan.issues.length)\n throw new CmsContentError('invalid-document', 'The content plan contains invalid documents.', {\n issues: plan.issues,\n })\n\n return contentRequest('apply', ContentApplyResultSchema, 'content apply result', {\n ...state.input,\n expectedGeneration: state.protocol.generation,\n })\n },\n diff: async (source) => diffSources(source.sources, (await snapshot()).sources),\n export: async ({ directory }) => {\n const value = await snapshot()\n await writeContentDirectory(directory, value.sources)\n\n return { sources: value.sources }\n },\n readDocument: async (documentId) => {\n const document = (await snapshot()).sources.find((source) => source.documentId === documentId)\n\n if (!document) throw new CmsContentError('document-not-found', `CMS document ${documentId} was not found.`)\n\n return document\n },\n }\n\n return client\n}\n\nexport async function readCmsContentDirectory(directory: string): Promise<CmsContentSourceSet> {\n try {\n const root = resolve(directory)\n const indexPath = resolve(root, '.cms-content.json')\n let index: ContentIndex | undefined\n\n try {\n index = parseIndex(await readFile(indexPath, 'utf8'))\n } catch (cause) {\n if (!isMissingFile(cause)) throw cause\n }\n\n if (index) {\n const sources = await Promise.all(\n index.sources.map(async (entry) =>\n decode(\n ContentSourceSchema,\n {\n sourcePath: entry.sourcePath,\n markdown: await readFile(safeContentPath(root, entry.sourcePath), 'utf8'),\n published: entry.published,\n },\n 'content source',\n ),\n ),\n )\n\n return { sources }\n }\n\n const files = await markdownFiles(root)\n\n if (!files.length) throw new CmsContentError('filesystem-error', `No Markdown files found in ${root}.`)\n\n return {\n sources: await Promise.all(\n files.map(async (file) =>\n decode(\n ContentSourceSchema,\n {\n sourcePath: relative(root, file).replaceAll('\\\\', '/'),\n markdown: await readFile(file, 'utf8'),\n published: true,\n },\n 'content source',\n ),\n ),\n ),\n }\n } catch (cause) {\n if (cause instanceof CmsContentError) throw cause\n throw new CmsContentError('filesystem-error', `Could not read CMS content from ${directory}.`, { cause })\n }\n}\n\nexport function diffCmsContentSources(\n local: ReadonlyArray<ContentSource>,\n target: ReadonlyArray<ContentSource>,\n): CmsContentDiff {\n return diffSources(local, target)\n}\n\nconst ContentHash = Schema.String.check(Schema.isPattern(/^[a-f0-9]{64}$/u))\n\nconst ContentIndexEntry = Schema.Struct({\n sourcePath: ContentSourceSchema.fields.sourcePath,\n published: Schema.Boolean,\n sha256: ContentHash,\n})\n\nconst ContentIndex = Schema.Struct({\n version: Schema.Literal(1),\n sources: Schema.Array(ContentIndexEntry),\n exportSha256: ContentHash,\n})\n\ntype ContentIndex = typeof ContentIndex.Type\n\ntype ContentIndexEntry = typeof ContentIndexEntry.Type\n\nasync function writeContentDirectory(directory: string, sources: ReadonlyArray<ContentSource>): Promise<void> {\n try {\n const root = resolve(directory)\n let previous: ContentIndex | undefined\n\n try {\n previous = parseIndex(await readFile(resolve(root, '.cms-content.json'), 'utf8'))\n } catch (cause) {\n if (!isMissingFile(cause)) throw cause\n }\n\n if (previous) {\n const metadata = { version: previous.version, sources: previous.sources }\n\n if (sha256(JSON.stringify(metadata)) !== previous.exportSha256)\n throw new CmsContentError('filesystem-error', 'The local .cms-content.json file has been edited.')\n\n const previousByPath = new Map(previous.sources.map((entry) => [entry.sourcePath, entry]))\n const incomingPaths = new Set(sources.map((source) => source.sourcePath))\n\n for (const entry of previous.sources)\n if (!incomingPaths.has(entry.sourcePath))\n throw new CmsContentError(\n 'filesystem-error',\n `CMS document ${entry.sourcePath} no longer exists; resolve the local file before exporting again.`,\n )\n\n for (const source of sources) {\n try {\n const local = await readFile(safeContentPath(root, source.sourcePath), 'utf8')\n const before = previousByPath.get(source.sourcePath)\n\n if (before ? sha256(local) !== before.sha256 : local !== source.markdown)\n throw new CmsContentError('filesystem-error', `Local edits in ${source.sourcePath}; export was stopped.`)\n } catch (cause) {\n if (!isMissingFile(cause)) throw cause\n }\n }\n }\n\n await mkdir(root, { recursive: true })\n const entries: ContentIndexEntry[] = []\n\n for (const source of sources) {\n const file = safeContentPath(root, source.sourcePath)\n await mkdir(dirname(file), { recursive: true })\n await writeFile(file, source.markdown)\n entries.push({ sourcePath: source.sourcePath, published: source.published, sha256: sha256(source.markdown) })\n }\n\n const metadata = { version: 1 as const, sources: entries }\n const index: ContentIndex = { ...metadata, exportSha256: sha256(JSON.stringify(metadata)) }\n await writeFile(resolve(root, '.cms-content.json'), `${JSON.stringify(index, undefined, 2)}\\n`)\n } catch (cause) {\n throw new CmsContentError('filesystem-error', `Could not export CMS content to ${directory}.`, { cause })\n }\n}\n\nfunction diffSources(local: ReadonlyArray<ContentSource>, target: ReadonlyArray<ContentSource>): CmsContentDiff {\n const targetByPath = new Map(target.map((source) => [source.sourcePath, source]))\n const localPaths = new Set(local.map((source) => source.sourcePath))\n\n return {\n changes: local.flatMap((source) => {\n const current = targetByPath.get(source.sourcePath)\n const markdownChanged = current?.markdown !== source.markdown\n const publicationChanged = current !== undefined && current.published !== source.published\n\n return markdownChanged || publicationChanged\n ? [{ sourcePath: source.sourcePath, local: source, target: current, markdownChanged, publicationChanged }]\n : []\n }),\n targetOnly: target.filter((source) => !localPaths.has(source.sourcePath)),\n }\n}\n\nasync function markdownFiles(directory: string): Promise<ReadonlyArray<string>> {\n const entries = await readdir(directory, { withFileTypes: true })\n\n const nested = await Promise.all(\n entries.map(async (entry) => {\n const path = resolve(directory, entry.name)\n\n if (entry.isDirectory()) return markdownFiles(path)\n\n return entry.isFile() && ['.md', '.mdc'].includes(extname(path)) ? [path] : []\n }),\n )\n\n return nested.flat().sort((left, right) => left.localeCompare(right))\n}\n\nfunction safeContentPath(root: string, sourcePath: string): string {\n const path = resolve(root, sourcePath)\n\n if (!path.startsWith(`${root}${sep}`))\n throw new CmsContentError('filesystem-error', `Unsafe content path: ${sourcePath}`)\n\n return path\n}\n\nfunction contentBaseUrl(value: string): URL {\n let url: URL\n\n try {\n url = new URL(value)\n } catch (cause) {\n throw new CmsContentError('network-error', 'CMS baseUrl is invalid.', { cause })\n }\n\n if (url.username || url.password || url.search || url.hash)\n throw new CmsContentError('network-error', 'CMS baseUrl must be an origin without credentials, query, or fragment.')\n\n if (\n url.protocol !== 'https:' &&\n !(url.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname))\n )\n throw new CmsContentError('network-error', 'Use HTTPS for CMS credentials; HTTP is allowed only on localhost.')\n\n return new URL(url.origin)\n}\n\nasync function request(fetch: typeof globalThis.fetch, input: URL, init: RequestInit): Promise<Response> {\n try {\n return await fetch(input, init)\n } catch (cause) {\n throw new CmsContentError('network-error', `Could not reach ${input.origin}.`, { cause })\n }\n}\n\n/* oxlint-disable anti-slop/no-unknown-parameters -- Untrusted JSON and filesystem values enter their explicit Schema decoder here. */\nfunction decode<SchemaType extends Schema.ConstraintDecoder<unknown>>(\n schema: SchemaType,\n value: unknown,\n label: string,\n): SchemaType['Type'] {\n const decoded = Schema.decodeUnknownResult(schema)(value)\n\n if (Result.isFailure(decoded))\n throw new CmsContentError('invalid-response', `CMS returned an invalid ${label}.`, { cause: decoded.failure })\n\n return decoded.success\n}\n/* oxlint-enable anti-slop/no-unknown-parameters */\n\nfunction sha256(value: string): string {\n return createHash('sha256').update(value).digest('hex')\n}\n\nfunction parseIndex(value: string): ContentIndex {\n const decoded = Schema.decodeUnknownResult(ContentIndex)(JSON.parse(value))\n\n if (Result.isFailure(decoded))\n throw new CmsContentError('filesystem-error', 'The .cms-content.json index is invalid.')\n\n const sourcePaths = new Set(decoded.success.sources.map((entry) => entry.sourcePath))\n\n if (sourcePaths.size !== decoded.success.sources.length)\n throw new CmsContentError('filesystem-error', 'The .cms-content.json index contains duplicate source paths.')\n\n return decoded.success\n}\n\nfunction isMissingFile(cause: unknown): boolean {\n return cause instanceof Error && 'code' in cause && cause.code === 'ENOENT'\n}\n\nexport type { ContentApplyResult, ContentSource }\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Content.js","names":["ContentSnapshotSchema","ContentSourceSchema","ProtocolContentPlanSchema","ContentApplyResultSchema"],"sources":["../../../src/Content.ts"],"sourcesContent":["/* oxlint-disable effecttsgo/async-function effecttsgo/extends-native-error effecttsgo/node-builtin-import -- This public Node-only API deliberately exposes Promises, native filesystem paths, and an Error subclass to ordinary deployment scripts. */\nimport type {\n ContentApplyResult,\n ContentPlan as ProtocolContentPlan,\n ContentPlanEntry,\n ContentSnapshotSource,\n ContentSource,\n ContentSnapshot,\n} from '@internal/protocol/ContentTransfer'\nimport {\n ContentApplyResult as ContentApplyResultSchema,\n ContentPlan as ProtocolContentPlanSchema,\n ContentSource as ContentSourceSchema,\n ContentSnapshot as ContentSnapshotSchema,\n} from '@internal/protocol/ContentTransfer'\nimport type { DocumentIssue } from '@internal/protocol/Documents'\nimport { CMS_AUTHENTICATION_PATH, STUDIO_API_PATH } from '@internal/protocol/HttpPaths'\nimport { Result, Schema } from 'effect'\nimport { createHash } from 'node:crypto'\nimport { lstat, readdir, readFile, mkdir, writeFile } from 'node:fs/promises'\nimport { dirname, extname, relative, resolve, sep } from 'node:path'\n\nexport type CmsContentErrorCode =\n | 'authentication-required'\n | 'content-conflict'\n | 'filesystem-error'\n | 'generation-changed'\n | 'invalid-document'\n | 'document-not-found'\n | 'invalid-response'\n | 'network-error'\n\nexport class CmsContentError extends Error {\n readonly code: CmsContentErrorCode\n readonly conflicts?: ReadonlyArray<CmsContentPlanEntry>\n readonly issues?: ReadonlyArray<DocumentIssue>\n\n constructor(\n code: CmsContentErrorCode,\n message: string,\n options: {\n readonly cause?: unknown\n readonly conflicts?: ReadonlyArray<CmsContentPlanEntry>\n readonly issues?: ReadonlyArray<DocumentIssue>\n } = {},\n ) {\n super(message, options.cause === undefined ? undefined : { cause: options.cause })\n this.name = 'CmsContentError'\n this.code = code\n this.conflicts = options.conflicts\n this.issues = options.issues\n }\n}\n\nexport interface CmsContentCredentials {\n readonly email: string\n readonly password: string\n}\n\nexport interface CmsContentClientOptions {\n readonly baseUrl: string\n readonly credentials: CmsContentCredentials\n readonly fetch?: typeof globalThis.fetch\n}\n\nexport interface CmsContentSourceSet {\n readonly sources: ReadonlyArray<ContentSource>\n}\n\nexport interface CmsContentDocument extends ContentSnapshotSource {}\n\nexport interface CmsContentPlanEntry extends ContentPlanEntry {}\n\nexport interface CmsContentPlan {\n readonly generation: string\n readonly creates: ReadonlyArray<CmsContentPlanEntry>\n readonly updates: ReadonlyArray<CmsContentPlanEntry>\n readonly publications: ReadonlyArray<CmsContentPlanEntry>\n readonly conflicts: ReadonlyArray<CmsContentPlanEntry>\n readonly unchanged: ReadonlyArray<CmsContentPlanEntry>\n readonly issues: ReadonlyArray<DocumentIssue>\n readonly targetOnly: ReadonlyArray<ContentSource>\n readonly valid: boolean\n}\n\nexport interface CmsContentImportOptions {\n readonly overwrite?: boolean\n readonly publish?: boolean\n}\n\nexport interface CmsContentDiff {\n readonly changes: ReadonlyArray<{\n readonly sourcePath: string\n readonly local?: ContentSource\n readonly target?: ContentSource\n readonly markdownChanged: boolean\n readonly publicationChanged: boolean\n }>\n readonly targetOnly: ReadonlyArray<ContentSource>\n}\n\nexport interface CmsContentClient {\n readonly plan: (source: CmsContentSourceSet, options?: CmsContentImportOptions) => Promise<CmsContentPlan>\n readonly apply: (plan: CmsContentPlan) => Promise<ContentApplyResult>\n readonly diff: (source: CmsContentSourceSet) => Promise<CmsContentDiff>\n readonly export: (options: { readonly directory: string }) => Promise<CmsContentSourceSet>\n readonly readDocument: (documentId: string) => Promise<CmsContentDocument>\n}\n\ninterface PlanState {\n readonly client: object\n readonly input: {\n readonly sources: ReadonlyArray<ContentSource>\n readonly overwrite: boolean\n readonly publish: boolean\n }\n readonly protocol: ProtocolContentPlan\n}\n\ntype CmsContentRequestBody =\n | PlanState['input']\n | (PlanState['input'] & { readonly expectedGeneration: ProtocolContentPlan['generation'] })\n\nconst plans = new WeakMap<CmsContentPlan, PlanState>()\n\nexport function createCmsContentClient(options: CmsContentClientOptions): CmsContentClient {\n const baseUrl = contentBaseUrl(options.baseUrl)\n const requestFetch = options.fetch ?? globalThis.fetch\n const identity = {}\n let cookie: Promise<string> | undefined\n\n const authenticate = async (): Promise<string> => {\n if (cookie) return cookie\n\n cookie = request(requestFetch, new URL(`${CMS_AUTHENTICATION_PATH}/sign-in/email`, baseUrl), {\n method: 'POST',\n headers: { accept: 'application/json', 'content-type': 'application/json', origin: baseUrl.origin },\n body: JSON.stringify(options.credentials),\n }).then(async (response) => {\n if (!response.ok) throw new CmsContentError('authentication-required', `CMS sign-in failed (${response.status}).`)\n const value = response.headers.get('set-cookie')?.split(';', 1)[0]\n\n if (!value) throw new CmsContentError('authentication-required', 'CMS sign-in did not return a session cookie.')\n\n return value\n })\n\n return cookie\n }\n\n const contentRequest = async <SchemaType extends Schema.ConstraintDecoder<unknown>>(\n path: string,\n schema: SchemaType,\n label: string,\n body?: CmsContentRequestBody,\n ): Promise<SchemaType['Type']> => {\n const session = await authenticate()\n\n const response = await request(requestFetch, new URL(`${STUDIO_API_PATH}/content/${path}`, baseUrl), {\n method: body === undefined ? 'GET' : 'POST',\n headers: {\n accept: 'application/json',\n cookie: session,\n ...(body === undefined ? undefined : { 'content-type': 'application/json' }),\n },\n ...(body === undefined ? undefined : { body: JSON.stringify(body) }),\n })\n\n if (!response.ok) {\n if (response.status === 401 || response.status === 403)\n throw new CmsContentError('authentication-required', `CMS content request was rejected (${response.status}).`)\n\n if (response.status === 409)\n throw new CmsContentError('generation-changed', 'The CMS changed after this content plan was created.')\n\n throw new CmsContentError('network-error', `CMS content request failed (${response.status}).`)\n }\n\n let payload: unknown\n\n try {\n payload = await response.json()\n } catch (cause) {\n throw new CmsContentError('invalid-response', 'CMS returned an invalid JSON response.', { cause })\n }\n\n return decode(schema, payload, label)\n }\n\n const snapshot = async (): Promise<ContentSnapshot> =>\n contentRequest('snapshot', ContentSnapshotSchema, 'content snapshot')\n\n const client: CmsContentClient = {\n plan: async (source, authoredOptions = {}) => {\n const sources = Object.freeze(\n source.sources.map((entry) =>\n Object.freeze(\n ContentSourceSchema.make({\n sourcePath: entry.sourcePath,\n markdown: entry.markdown,\n published: entry.published,\n }),\n ),\n ),\n )\n\n const input = Object.freeze({\n sources,\n overwrite: authoredOptions.overwrite ?? false,\n publish: authoredOptions.publish ?? false,\n })\n\n const [protocol, current] = await Promise.all([\n contentRequest('plan', ProtocolContentPlanSchema, 'content plan', input),\n snapshot(),\n ])\n\n const sourcePaths = new Set(sources.map((entry) => entry.sourcePath))\n\n const entries = (action: ContentPlanEntry['action']) =>\n protocol.entries.filter((entry) => entry.action === action)\n\n const plan: CmsContentPlan = Object.freeze({\n generation: protocol.generation,\n creates: entries('create'),\n updates: entries('update'),\n publications: entries('publish'),\n conflicts: entries('conflict'),\n unchanged: entries('unchanged'),\n issues: protocol.issues,\n targetOnly: current.sources.filter((entry) => !sourcePaths.has(entry.sourcePath)),\n valid: protocol.valid,\n })\n\n plans.set(plan, { client: identity, input, protocol })\n\n return plan\n },\n apply: async (plan) => {\n const state = plans.get(plan)\n\n if (!state || state.client !== identity)\n throw new CmsContentError('content-conflict', 'This content plan was not created by this client.')\n\n if (!plan.valid)\n throw new CmsContentError('content-conflict', 'The content plan is not valid.', {\n conflicts: plan.conflicts,\n issues: plan.issues,\n })\n\n if (plan.conflicts.length)\n throw new CmsContentError('content-conflict', 'The content plan contains conflicts.', {\n conflicts: plan.conflicts,\n })\n\n if (plan.issues.length)\n throw new CmsContentError('invalid-document', 'The content plan contains invalid documents.', {\n issues: plan.issues,\n })\n\n return contentRequest('apply', ContentApplyResultSchema, 'content apply result', {\n ...state.input,\n expectedGeneration: state.protocol.generation,\n })\n },\n diff: async (source) => diffSources(source.sources, (await snapshot()).sources),\n export: async ({ directory }) => {\n const value = await snapshot()\n await writeContentDirectory(directory, value.sources)\n\n return { sources: value.sources }\n },\n readDocument: async (documentId) => {\n const document = (await snapshot()).sources.find((source) => source.documentId === documentId)\n\n if (!document) throw new CmsContentError('document-not-found', `CMS document ${documentId} was not found.`)\n\n return document\n },\n }\n\n return client\n}\n\nexport async function readCmsContentDirectory(directory: string): Promise<CmsContentSourceSet> {\n try {\n const root = resolve(directory)\n const indexPath = await safeContentFile(root, '.cms-content.json')\n let index: ContentIndex | undefined\n\n try {\n index = parseIndex(await readFile(indexPath, 'utf8'))\n } catch (cause) {\n if (!isMissingFile(cause)) throw cause\n }\n\n if (index) {\n const sources = await Promise.all(\n index.sources.map(async (entry) =>\n decode(\n ContentSourceSchema,\n {\n sourcePath: entry.sourcePath,\n markdown: await readFile(await safeContentFile(root, entry.sourcePath), 'utf8'),\n published: entry.published,\n },\n 'content source',\n ),\n ),\n )\n\n return { sources }\n }\n\n const files = await markdownFiles(root)\n\n if (!files.length) throw new CmsContentError('filesystem-error', `No Markdown files found in ${root}.`)\n\n return {\n sources: await Promise.all(\n files.map(async (file) =>\n decode(\n ContentSourceSchema,\n {\n sourcePath: relative(root, file).replaceAll('\\\\', '/'),\n markdown: await readFile(file, 'utf8'),\n published: true,\n },\n 'content source',\n ),\n ),\n ),\n }\n } catch (cause) {\n if (cause instanceof CmsContentError) throw cause\n throw new CmsContentError('filesystem-error', `Could not read CMS content from ${directory}.`, { cause })\n }\n}\n\nexport function diffCmsContentSources(\n local: ReadonlyArray<ContentSource>,\n target: ReadonlyArray<ContentSource>,\n): CmsContentDiff {\n return diffSources(local, target)\n}\n\nconst ContentHash = Schema.String.check(Schema.isPattern(/^[a-f0-9]{64}$/u))\n\nconst ContentIndexEntry = Schema.Struct({\n sourcePath: ContentSourceSchema.fields.sourcePath,\n published: Schema.Boolean,\n sha256: ContentHash,\n})\n\nconst ContentIndex = Schema.Struct({\n version: Schema.Literal(1),\n sources: Schema.Array(ContentIndexEntry),\n exportSha256: ContentHash,\n})\n\ntype ContentIndex = typeof ContentIndex.Type\n\ntype ContentIndexEntry = typeof ContentIndexEntry.Type\n\nasync function writeContentDirectory(directory: string, sources: ReadonlyArray<ContentSource>): Promise<void> {\n try {\n const root = resolve(directory)\n const indexPath = await safeContentFile(root, '.cms-content.json')\n await Promise.all(sources.map((source) => safeContentFile(root, source.sourcePath)))\n let previous: ContentIndex | undefined\n\n try {\n previous = parseIndex(await readFile(indexPath, 'utf8'))\n } catch (cause) {\n if (!isMissingFile(cause)) throw cause\n }\n\n const previousByPath = new Map(previous?.sources.map((entry) => [entry.sourcePath, entry]))\n\n if (previous) {\n const metadata = { version: previous.version, sources: previous.sources }\n\n if (sha256(JSON.stringify(metadata)) !== previous.exportSha256)\n throw new CmsContentError('filesystem-error', 'The local .cms-content.json file has been edited.')\n\n const incomingPaths = new Set(sources.map((source) => source.sourcePath))\n\n for (const entry of previous.sources)\n if (!incomingPaths.has(entry.sourcePath))\n throw new CmsContentError(\n 'filesystem-error',\n `CMS document ${entry.sourcePath} no longer exists; resolve the local file before exporting again.`,\n )\n }\n\n for (const source of sources) {\n try {\n const local = await readFile(await safeContentFile(root, source.sourcePath), 'utf8')\n const before = previousByPath.get(source.sourcePath)\n\n if (before ? sha256(local) !== before.sha256 : local !== source.markdown)\n throw new CmsContentError('filesystem-error', `Local edits in ${source.sourcePath}; export was stopped.`)\n } catch (cause) {\n if (!isMissingFile(cause)) throw cause\n }\n }\n\n await mkdir(root, { recursive: true })\n const entries: ContentIndexEntry[] = []\n\n for (const source of sources) {\n const file = await safeContentFile(root, source.sourcePath)\n await mkdir(dirname(file), { recursive: true })\n await safeContentFile(root, source.sourcePath)\n await writeFile(file, source.markdown)\n entries.push({ sourcePath: source.sourcePath, published: source.published, sha256: sha256(source.markdown) })\n }\n\n const metadata = { version: 1 as const, sources: entries }\n const index: ContentIndex = { ...metadata, exportSha256: sha256(JSON.stringify(metadata)) }\n await safeContentFile(root, '.cms-content.json')\n await writeFile(indexPath, `${JSON.stringify(index, undefined, 2)}\\n`)\n } catch (cause) {\n throw new CmsContentError('filesystem-error', `Could not export CMS content to ${directory}.`, { cause })\n }\n}\n\nfunction diffSources(local: ReadonlyArray<ContentSource>, target: ReadonlyArray<ContentSource>): CmsContentDiff {\n const targetByPath = new Map(target.map((source) => [source.sourcePath, source]))\n const localPaths = new Set(local.map((source) => source.sourcePath))\n\n return {\n changes: local.flatMap((source) => {\n const current = targetByPath.get(source.sourcePath)\n const markdownChanged = current?.markdown !== source.markdown\n const publicationChanged = current !== undefined && current.published !== source.published\n\n return markdownChanged || publicationChanged\n ? [{ sourcePath: source.sourcePath, local: source, target: current, markdownChanged, publicationChanged }]\n : []\n }),\n targetOnly: target.filter((source) => !localPaths.has(source.sourcePath)),\n }\n}\n\nasync function markdownFiles(directory: string): Promise<ReadonlyArray<string>> {\n const entries = await readdir(directory, { withFileTypes: true })\n\n const nested = await Promise.all(\n entries.map(async (entry) => {\n const path = resolve(directory, entry.name)\n\n if (entry.isDirectory()) return markdownFiles(path)\n\n return entry.isFile() && ['.md', '.mdc'].includes(extname(path)) ? [path] : []\n }),\n )\n\n return nested.flat().sort((left, right) => left.localeCompare(right))\n}\n\nfunction safeContentPath(root: string, sourcePath: string): string {\n const path = resolve(root, sourcePath)\n\n if (!path.startsWith(`${root}${sep}`))\n throw new CmsContentError('filesystem-error', `Unsafe content path: ${sourcePath}`)\n\n return path\n}\n\nasync function safeContentFile(root: string, sourcePath: string): Promise<string> {\n const path = safeContentPath(root, sourcePath)\n const segments = relative(root, path).split(sep)\n let current = root\n\n for (const segment of ['', ...segments]) {\n current = segment ? resolve(current, segment) : current\n\n try {\n if ((await lstat(current)).isSymbolicLink())\n throw new CmsContentError('filesystem-error', `Symlinks are not allowed in CMS content paths: ${sourcePath}`)\n } catch (cause) {\n if (!isMissingFile(cause)) throw cause\n }\n }\n\n return path\n}\n\nfunction contentBaseUrl(value: string): URL {\n let url: URL\n\n try {\n url = new URL(value)\n } catch (cause) {\n throw new CmsContentError('network-error', 'CMS baseUrl is invalid.', { cause })\n }\n\n if (url.username || url.password || url.search || url.hash)\n throw new CmsContentError('network-error', 'CMS baseUrl must be an origin without credentials, query, or fragment.')\n\n if (\n url.protocol !== 'https:' &&\n !(url.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname))\n )\n throw new CmsContentError('network-error', 'Use HTTPS for CMS credentials; HTTP is allowed only on localhost.')\n\n return new URL(url.origin)\n}\n\nasync function request(fetch: typeof globalThis.fetch, input: URL, init: RequestInit): Promise<Response> {\n try {\n return await fetch(input, init)\n } catch (cause) {\n throw new CmsContentError('network-error', `Could not reach ${input.origin}.`, { cause })\n }\n}\n\n/* oxlint-disable anti-slop/no-unknown-parameters -- Untrusted JSON and filesystem values enter their explicit Schema decoder here. */\nfunction decode<SchemaType extends Schema.ConstraintDecoder<unknown>>(\n schema: SchemaType,\n value: unknown,\n label: string,\n): SchemaType['Type'] {\n const decoded = Schema.decodeUnknownResult(schema)(value)\n\n if (Result.isFailure(decoded))\n throw new CmsContentError('invalid-response', `CMS returned an invalid ${label}.`, { cause: decoded.failure })\n\n return decoded.success\n}\n/* oxlint-enable anti-slop/no-unknown-parameters */\n\nfunction sha256(value: string): string {\n return createHash('sha256').update(value).digest('hex')\n}\n\nfunction parseIndex(value: string): ContentIndex {\n const decoded = Schema.decodeUnknownResult(ContentIndex)(JSON.parse(value))\n\n if (Result.isFailure(decoded))\n throw new CmsContentError('filesystem-error', 'The .cms-content.json index is invalid.')\n\n const sourcePaths = new Set(decoded.success.sources.map((entry) => entry.sourcePath))\n\n if (sourcePaths.size !== decoded.success.sources.length)\n throw new CmsContentError('filesystem-error', 'The .cms-content.json index contains duplicate source paths.')\n\n return decoded.success\n}\n\nfunction isMissingFile(cause: unknown): boolean {\n return cause instanceof Error && 'code' in cause && cause.code === 'ENOENT'\n}\n\nexport type { ContentApplyResult, ContentSource }\n"],"mappings":"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"}
|
|
@@ -1068,11 +1068,17 @@ function makeDocumentState(authored, published, snapshot) {
|
|
|
1068
1068
|
authoredById.set(document.id, document);
|
|
1069
1069
|
}
|
|
1070
1070
|
for (const documentId of activeById.keys()) if (!authoredById.has(documentId)) throw new Error(`Compiled document "${documentId}" is missing from the repository snapshot.`);
|
|
1071
|
+
const publishedById = /* @__PURE__ */ new Map();
|
|
1072
|
+
for (const document of published.documents) {
|
|
1073
|
+
if (publishedById.has(document.id)) throw new Error(`Compiled document "${document.id}" appears more than once in the published catalog.`);
|
|
1074
|
+
publishedById.set(document.id, document);
|
|
1075
|
+
}
|
|
1071
1076
|
return {
|
|
1072
1077
|
activeById,
|
|
1073
1078
|
authored,
|
|
1074
1079
|
authoredById,
|
|
1075
1080
|
published,
|
|
1081
|
+
publishedById,
|
|
1076
1082
|
snapshot
|
|
1077
1083
|
};
|
|
1078
1084
|
}
|
|
@@ -1178,7 +1184,7 @@ function errorMessage(cause) {
|
|
|
1178
1184
|
return cause instanceof Error && cause.message ? cause.message : "Markdown could not be parsed.";
|
|
1179
1185
|
}
|
|
1180
1186
|
function browseMetadata(state, source) {
|
|
1181
|
-
const live = state.
|
|
1187
|
+
const live = state.publishedById.get(source.documentId);
|
|
1182
1188
|
const metadata = {};
|
|
1183
1189
|
if (source.createdBy) metadata.createdBy = source.createdBy;
|
|
1184
1190
|
if (source.editedBy) metadata.editedBy = source.editedBy;
|
|
@@ -2795,4 +2801,4 @@ const maxS3SingleCopyBytes = 5e9;
|
|
|
2795
2801
|
|
|
2796
2802
|
//#endregion
|
|
2797
2803
|
export { DocumentRevision as $, StudioBadRequest as A, User as B, DocumentStore as C, CurrentStudioRequest as D, defaultAccessControlLayer as E, studioInternalError as F, UserSelfRemovalDenied as G, UserAlreadyExists as H, InitializationAlreadyCompleted as I, CollaborationServerMessage as J, UserSelfRoleChangeDenied as K, InitializationProofInvalid as L, StudioRequestConflict as M, StudioSchemaError as N, StudioApi as O, StudioInternalError as P, DocumentHistoryInvalidCursor as Q, InitializationState as R, DocumentPersistenceError as S, AccessControl as T, UserNotFound as U, UserAdministrationDenied as V, UserPage as W, StateVector as X, CollaborationTicket as Y, DocumentHistoryComparisonTooLarge as Z, Documents as _, mediaObjectPrefix as a, ActivityEntry as at, cursorCodec as b, Seo as c, ActivityPage as ct, seoDeliveryLayer as d, DocumentMediaUnavailable as dt, ACTIVITY_ACTIONS as et, seoLayer as f, DocumentNotFound as ft, DocumentPreviews as g, PublishedDocumentNotFound as gt, DocumentMaintenance as h, DocumentVersionConflict as ht, MediaObjectStore as i, ActivityCursorInvalid as it, StudioBootstrap as j, StudioAuthentication as k, SeoDelivery as l, ActivityTarget as lt, SeoStore as m, DocumentValidationFailed as mt, s3MediaObjectStoreLayer as n, ActivityContext as nt, PreviewSeo as o, ActivityEntryDraft as ot, SeoPersistenceError as p, DocumentPathConflict as pt, CollaborationClientMessage as q, MediaPersistenceError as r, ActivityCursor as rt, PublishedSeo as s, ActivityId as st, maxS3SingleCopyBytes as t, ActivityAction as tt, makeDefaultSeoOptions as u, ActivityTargetKind as ut, PublishedDocuments as v, renderEditorMarkdown as w, DocumentGenerationConflict as x, documentsLayer as y, CreateUserInput as z };
|
|
2798
|
-
//# sourceMappingURL=S3MediaObjects-
|
|
2804
|
+
//# sourceMappingURL=S3MediaObjects-CR3p1WTg.js.map
|