@micacms/editor 0.5.0 → 0.6.0-rc.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  # Mica Editor
2
2
 
3
- > **Experimental.** Pre-1.0: the CLI, the API and the content format can still
4
- > change without notice. Use it at your own risk.
3
+ > **Public.** From 1.0, semver covers the `mica` CLI (`init`, `check` and the
4
+ > editor's flags). Pre-1.0: it can still change in a minor release.
5
5
 
6
6
  The local Mica editor: `@micacms/runtime` and `@micacms/editor-ui` composed for
7
7
  a developer's own machine. No authentication, no user, no database — the
@@ -44,10 +44,12 @@ keeps Publish out of the editor entirely).
44
44
  ```bash
45
45
  npx @micacms/editor init # add Mica to the Astro project in this directory
46
46
  npx @micacms/editor check # check every entry against the model
47
+ npx @micacms/editor migrate-layout # move entries after a layout change
47
48
  ```
48
49
 
49
50
  `init` writes a starter `src/mica.ts` and `src/content.config.ts`, and never
50
- overwrites a file. `check` reads every entry the way Mica and the
51
+ overwrites a file. It also names the `mica` and `check` scripts that
52
+ `package.json` does not have yet. `check` reads every entry the way Mica and the
51
53
  website read it, and reports what they would skip or refuse: fields that do
52
54
  not validate, missing media, relations to entries that do not exist, slugs
53
55
  that do not match their file names, and files that are not read at all. It
@@ -60,14 +62,12 @@ are also `npx mica init` and `npx mica check`.
60
62
  rush update
61
63
  rush build --to tag:packages
62
64
 
63
- cd apps/local-editor
64
- cp .env.example .env
65
-
66
65
  rush dev # API on :6422, admin on :5173
67
66
  ```
68
67
 
69
- `CONTENT_ROOT=../../examples/muster-consulting/content` is the default, so the
70
- server edits the example project's content. There is nothing to seed: the
68
+ `rush dev` reads `dev.env`, which sets
69
+ `MICA_CONTENT_ROOT=../../examples/muster-consulting/content`, so the server edits
70
+ the example project's content. There is nothing to seed: the
71
71
  content is already there, in Git.
72
72
 
73
73
  The server has no content model of its own. It reads one from the content
@@ -79,8 +79,10 @@ no avatar and no account menu, because there is nothing behind them.
79
79
 
80
80
  `rush dev` runs this app and the Vite dev server together. Vite
81
81
  proxies `/api` to the API; the API still needs
82
- `ADMIN_ORIGINS=http://localhost:5173` for its Origin check, which
83
- `.env.example` already sets.
82
+ `MICA_ADMIN_ORIGINS=http://localhost:5173` for its Origin check, which
83
+ `dev.env` sets. The published editor reads no `.env` file: the project's `.env`
84
+ belongs to the site. Every variable is at
85
+ <https://micacms.com/docs/configuration/>.
84
86
 
85
87
  ## What lives where
86
88
 
@@ -1 +1 @@
1
- {"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../src/check.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AA2BH,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,OAAO,GAAG,SAAS,CAAC;IAC3B,8CAA8C;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,2EAA2E;IAC3E,KAAK,EAAE,OAAO,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,uFAAuF;AACvF,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAwErE;AAoJD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAexD;AAiED,mDAAmD;AACnD,wBAAgB,YAAY,CAAC,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAkBtE"}
1
+ {"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../src/check.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AA4BH,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,OAAO,GAAG,SAAS,CAAC;IAC3B,8CAA8C;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,2EAA2E;IAC3E,KAAK,EAAE,OAAO,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,uFAAuF;AACvF,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CA0ErE;AAsJD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAexD;AAuED,mDAAmD;AACnD,wBAAgB,YAAY,CAAC,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAkBtE"}
package/dist/check.js CHANGED
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import { existsSync } from "node:fs";
7
7
  import { relative } from "node:path";
8
- import { DOCUMENT_EXTENSION, documentPath, entryLayout, INDEX_FILE, isValidSlug, MANIFEST_PATH, ORDER_FILE, readCollectionEntry, readSingleton, resolveSlugField, singletonPath, validateFields, } from "@micacms/core";
8
+ import { DOCUMENT_EXTENSION, documentPath, entryLayout, INDEX_FILE, isValidSlug, MANIFEST_PATH, misplacedFiles, ORDER_FILE, readCollectionEntry, readSingleton, resolveSlugField, singletonPath, validateFields, } from "@micacms/core";
9
9
  import { within } from "@micacms/core/node";
10
10
  import { contentFiles, loadSchema, readOnlyParts } from "@micacms/runtime";
11
11
  /** Checks the content under `root` against the model in `<root>/.mica/schema.json`. */
@@ -64,9 +64,12 @@ export async function checkContent(root) {
64
64
  const directory = entryLayout(def.fields) === "directory";
65
65
  const base = singletonPath(def.fields, name);
66
66
  const dataPath = directory ? `${base}/${INDEX_FILE}` : base;
67
- const otherLayout = directory ? `${name}.json` : `${name}/${INDEX_FILE}`;
68
- if ((await readText(files, otherLayout)) !== null) {
69
- error(otherLayout, `Not read: "${def.label}" is stored as ${dataPath}, because ${directory ? "it has" : "it has no"} rich text or media.`);
67
+ const [otherLayout] = await misplacedFiles(files, def.fields, name, "singleton");
68
+ if (otherLayout) {
69
+ error(otherLayout, `Not read: "${def.label}" is stored as ${dataPath}, because ${directory ? "it has" : "it has no"} rich text or media. Run \`npx mica migrate-layout\`.`);
70
+ // Without a file in the model's layout, the reader stops at the other one: reported above.
71
+ if ((await readText(files, dataPath)) === null)
72
+ continue;
70
73
  }
71
74
  const data = await readOrReport(() => readSingleton(files, def, name), dataPath, error);
72
75
  if (data === undefined)
@@ -84,6 +87,8 @@ async function entrySlugs(files, fields, name, error, warning) {
84
87
  const directory = entryLayout(fields) === "directory";
85
88
  const shape = directory ? `a folder with ${INDEX_FILE}` : "a <slug>.json file";
86
89
  const found = [];
90
+ // The reader's own rule, so that `check` and `migrate-layout` agree.
91
+ const misplaced = new Set(await misplacedFiles(files, fields, name, "collection"));
87
92
  for (const item of await files.list(name)) {
88
93
  const path = `${name}/${item.name}`;
89
94
  if (item.name.startsWith("."))
@@ -93,12 +98,12 @@ async function entrySlugs(files, fields, name, error, warning) {
93
98
  continue;
94
99
  }
95
100
  // An entry in the other layout is lost content, not a stray file: the model gained or lost rich text or media.
96
- if (directory && item.type === "file" && item.name.endsWith(".json")) {
97
- error(path, `Not read: entries of "${name}" are folders with ${INDEX_FILE}, because it has rich text or media.`);
101
+ if (directory && misplaced.has(path)) {
102
+ error(path, `Not read: entries of "${name}" are folders with ${INDEX_FILE}, because it has rich text or media. Run \`npx mica migrate-layout\`.`);
98
103
  continue;
99
104
  }
100
- if (!directory && item.type === "directory" && (await readText(files, `${path}/${INDEX_FILE}`)) !== null) {
101
- error(path, `Not read: entries of "${name}" are <slug>.json files, because it has no rich text or media.`);
105
+ if (!directory && misplaced.has(`${path}/${INDEX_FILE}`)) {
106
+ error(path, `Not read: entries of "${name}" are <slug>.json files, because it has no rich text or media. Run \`npx mica migrate-layout\`.`);
102
107
  continue;
103
108
  }
104
109
  const slug = directory ? item.name : item.name.replace(/\.json$/, "");
@@ -156,7 +161,7 @@ async function readOrReport(read, path, error) {
156
161
  }
157
162
  async function checkEntry(context, fields, data, dataPath, base) {
158
163
  const { root, files, slugs, error, warning } = context;
159
- const result = validateFields(fields, data, "storage");
164
+ const result = validateFields(fields, data);
160
165
  if (!result.ok) {
161
166
  for (const issue of result.issues)
162
167
  error(dataPath, issue.path ? `${issue.path}: ${issue.message}` : issue.message);
@@ -167,7 +172,7 @@ async function checkEntry(context, fields, data, dataPath, base) {
167
172
  visit(fields, data, [], (field, value, path) => {
168
173
  const key = path.join(".");
169
174
  if (field.kind === "media") {
170
- for (const file of list(value)) {
175
+ for (const file of list(mediaPaths(value))) {
171
176
  let exists = false;
172
177
  try {
173
178
  exists = existsSync(within(root, file));
@@ -220,6 +225,11 @@ export function markdocWarnings(source) {
220
225
  messages.push("Footnotes ([^1]) are not Markdoc: they render as plain text.");
221
226
  return messages;
222
227
  }
228
+ /** The paths of a media value: a path, `{ path, alt }`, or a list of either. */
229
+ function mediaPaths(value) {
230
+ const values = Array.isArray(value) ? value : [value];
231
+ return values.map((item) => (item !== null && typeof item === "object" ? item.path : item));
232
+ }
223
233
  function list(value) {
224
234
  const values = Array.isArray(value) ? value : [value];
225
235
  return values.filter((item) => typeof item === "string" && item !== "");
package/dist/check.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"check.js","sourceRoot":"","sources":["../src/check.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAErC,OAAO,EACL,kBAAkB,EAClB,YAAY,EACZ,WAAW,EACX,UAAU,EACV,WAAW,EACX,aAAa,EACb,UAAU,EACV,mBAAmB,EACnB,aAAa,EACb,gBAAgB,EAChB,aAAa,EACb,cAAc,GAMf,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAC5C,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAkB3E,uFAAuF;AACvF,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,IAAY;IAC7C,MAAM,KAAK,GAAG,YAAY,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC;IACrC,MAAM,MAAM,GAAgB,EAAE,QAAQ,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,CAAC;IACtG,MAAM,KAAK,GAAG,CAAC,IAAY,EAAE,OAAe,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;IACzG,MAAM,OAAO,GAAG,CAAC,IAAY,EAAE,OAAe,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;IAE7G,MAAM,QAAQ,GAAG,MAAM,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC,CAAC;IACtD,IAAI,QAAQ,KAAK,IAAI,IAAI,QAAQ,YAAY,KAAK,EAAE,CAAC;QACnD,KAAK,CAAC,aAAa,EAAE,QAAQ,EAAE,OAAO,IAAI,mFAAmF,CAAC,CAAC;QAC/H,OAAO,MAAM,CAAC;IAChB,CAAC;IACD,IAAI,IAAU,CAAC;IACf,IAAI,CAAC;QACH,IAAI,GAAG,MAAM,UAAU,CAAC,KAAK,CAAC,CAAC;IACjC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,KAAK,CAAC,aAAa,EAAG,KAAe,CAAC,OAAO,CAAC,CAAC;QAC/C,OAAO,MAAM,CAAC;IAChB,CAAC;IACD,MAAM,CAAC,KAAK,GAAG,IAAI,CAAC;IAEpB,8EAA8E;IAC9E,MAAM,QAAQ,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC;IACrC,KAAK,MAAM,MAAM,IAAI,QAAQ,CAAC,MAAM,EAAE;QAAE,KAAK,CAAC,aAAa,EAAE,gBAAgB,MAAM,EAAE,CAAC,CAAC;IAEvF,MAAM,KAAK,GAAG,IAAI,GAAG,EAAuB,CAAC;IAC7C,KAAK,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC;QAC3D,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,GAAG,CAAC,MAAM,UAAU,CAAC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IACtF,CAAC;IAED,MAAM,OAAO,GAAiB,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC;IAE3E,KAAK,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC;QAC3D,MAAM,CAAC,WAAW,EAAE,CAAC;QACrB,IAAI,QAAQ,CAAC,GAAG,CAAC,eAAe,IAAI,EAAE,CAAC;YAAE,SAAS;QAClD,MAAM,SAAS,GAAG,gBAAgB,CAAC,GAAG,CAAC,CAAC;QACxC,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,GAAG,CAAC,IAAI,CAAE,EAAE,CAAC;YACpC,MAAM,CAAC,OAAO,EAAE,CAAC;YACjB,MAAM,SAAS,GAAG,WAAW,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,WAAW,CAAC;YAC1D,MAAM,IAAI,GAAG,SAAS,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;YAClD,MAAM,QAAQ,GAAG,SAAS,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,UAAU,EAAE,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,IAAI,OAAO,CAAC;YAE9E,MAAM,IAAI,GAAG,MAAM,YAAY,CAAC,GAAG,EAAE,CAAC,mBAAmB,CAAC,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC;YACpG,IAAI,CAAC,IAAI;gBAAE,SAAS;YACpB,MAAM,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;YACvD,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,EAAE,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;gBACnE,OAAO,CAAC,QAAQ,EAAE,GAAG,SAAS,QAAQ,MAAM,8BAA8B,IAAI,mDAAmD,CAAC,CAAC;YACrI,CAAC;YACD,MAAM,UAAU,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAC;QAC9D,CAAC;IACH,CAAC;IAED,KAAK,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;QAC1D,MAAM,CAAC,UAAU,EAAE,CAAC;QACpB,IAAI,QAAQ,CAAC,GAAG,CAAC,cAAc,IAAI,EAAE,CAAC;YAAE,SAAS;QACjD,MAAM,SAAS,GAAG,WAAW,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,WAAW,CAAC;QAC1D,MAAM,IAAI,GAAG,aAAa,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QAC7C,MAAM,QAAQ,GAAG,SAAS,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,UAAU,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;QAC5D,MAAM,WAAW,GAAG,SAAS,CAAC,CAAC,CAAC,GAAG,IAAI,OAAO,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,UAAU,EAAE,CAAC;QACzE,IAAI,CAAC,MAAM,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;YAClD,KAAK,CAAC,WAAW,EAAE,cAAc,GAAG,CAAC,KAAK,kBAAkB,QAAQ,aAAa,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,WAAW,sBAAsB,CAAC,CAAC;QAC7I,CAAC;QAED,MAAM,IAAI,GAAG,MAAM,YAAY,CAAC,GAAG,EAAE,CAAC,aAAa,CAAC,KAAK,EAAE,GAAG,EAAE,IAAI,CAAC,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC;QACxF,IAAI,IAAI,KAAK,SAAS;YAAE,SAAS;QACjC,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;YAClB,OAAO,CAAC,QAAQ,EAAE,mBAAmB,GAAG,CAAC,KAAK,QAAQ,CAAC,CAAC;YACxD,SAAS;QACX,CAAC;QACD,MAAM,UAAU,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IACjF,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAWD,0EAA0E;AAC1E,KAAK,UAAU,UAAU,CACvB,KAAoB,EACpB,MAAgB,EAChB,IAAY,EACZ,KAA8C,EAC9C,OAAgD;IAEhD,MAAM,SAAS,GAAG,WAAW,CAAC,MAAM,CAAC,KAAK,WAAW,CAAC;IACtD,MAAM,KAAK,GAAG,SAAS,CAAC,CAAC,CAAC,iBAAiB,UAAU,EAAE,CAAC,CAAC,CAAC,oBAAoB,CAAC;IAC/E,MAAM,KAAK,GAAa,EAAE,CAAC;IAE3B,KAAK,MAAM,IAAI,IAAI,MAAM,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QAC1C,MAAM,IAAI,GAAG,GAAG,IAAI,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;QACpC,IAAI,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,SAAS;QACxC,IAAI,IAAI,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;YAC7B,UAAU,CAAC,MAAM,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;YACrD,SAAS;QACX,CAAC;QAED,+GAA+G;QAC/G,IAAI,SAAS,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM,IAAI,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;YACrE,KAAK,CAAC,IAAI,EAAE,yBAAyB,IAAI,sBAAsB,UAAU,sCAAsC,CAAC,CAAC;YACjH,SAAS;QACX,CAAC;QACD,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,IAAI,KAAK,WAAW,IAAI,CAAC,MAAM,QAAQ,CAAC,KAAK,EAAE,GAAG,IAAI,IAAI,UAAU,EAAE,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;YACzG,KAAK,CAAC,IAAI,EAAE,yBAAyB,IAAI,gEAAgE,CAAC,CAAC;YAC3G,SAAS;QACX,CAAC;QAED,MAAM,IAAI,GAAG,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;QACtE,MAAM,cAAc,GAAG,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,KAAK,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,KAAK,MAAM,IAAI,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;QACnH,IAAI,CAAC,cAAc,EAAE,CAAC;YACpB,OAAO,CAAC,IAAI,EAAE,4BAA4B,IAAI,QAAQ,KAAK,GAAG,CAAC,CAAC;YAChE,SAAS;QACX,CAAC;QACD,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC;YACvB,KAAK,CAAC,IAAI,EAAE,cAAc,IAAI,oEAAoE,CAAC,CAAC;YACpG,SAAS;QACX,CAAC;QACD,MAAM,KAAK,GAAG,SAAS,CAAC,CAAC,CAAC,MAAM,QAAQ,CAAC,KAAK,EAAE,GAAG,IAAI,IAAI,UAAU,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC9E,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,YAAY,KAAK,EAAE,CAAC;YAC7C,KAAK,CAAC,IAAI,EAAE,aAAa,KAAK,EAAE,OAAO,IAAI,qBAAqB,UAAU,GAAG,EAAE,CAAC,CAAC;YACjF,SAAS;QACX,CAAC;QACD,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACnB,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,qFAAqF;AACrF,KAAK,UAAU,QAAQ,CAAC,KAAoB,EAAE,IAAY;IACxD,IAAI,CAAC;QACH,OAAO,MAAM,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAChC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,KAAc,CAAC;IACxB,CAAC;AACH,CAAC;AAED,SAAS,UAAU,CAAC,IAA2B,EAAE,IAAY,EAAE,KAA8C;IAC3G,IAAI,IAAI,KAAK,IAAI;QAAE,OAAO;IAC1B,IAAI,IAAI,YAAY,KAAK;QAAE,OAAO,KAAK,CAAC,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;IAC5D,IAAI,KAAc,CAAC;IACnB,IAAI,CAAC;QACH,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC3B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC,IAAI,EAAE,wCAAwC,CAAC,CAAC;IAC/D,CAAC;IACD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QAC1B,KAAK,CAAC,IAAI,EAAE,oFAAoF,CAAC,CAAC;IACpG,CAAC;AACH,CAAC;AAED,KAAK,UAAU,YAAY,CACzB,IAAsB,EACtB,IAAY,EACZ,KAA8C;IAE9C,IAAI,CAAC;QACH,OAAO,MAAM,IAAI,EAAE,CAAC;IACtB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,KAAK,CAAC,IAAI,EAAG,KAAe,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC;QAC/D,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED,KAAK,UAAU,UAAU,CACvB,OAAqB,EACrB,MAAgB,EAChB,IAA6B,EAC7B,QAAgB,EAChB,IAAmB;IAEnB,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC;IAEvD,MAAM,MAAM,GAAG,cAAc,CAAC,MAAM,EAAE,IAAI,EAAE,SAAS,CAAC,CAAC;IACvD,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC;QACf,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,MAAM;YAAE,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,IAAI,KAAK,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IACrH,CAAC;IAED,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAE,CAA4B,CAAC;IACjF,KAAK,MAAM,OAAO,IAAI,WAAW,CAAC,MAAM,EAAE,GAAG,EAAE,EAAE,CAAC;QAAE,OAAO,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IAE/E,KAAK,CAAC,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE;QAC7C,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC3B,IAAI,KAAK,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;YAC3B,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;gBAC/B,IAAI,MAAM,GAAG,KAAK,CAAC;gBACnB,IAAI,CAAC;oBACH,MAAM,GAAG,UAAU,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;gBAC1C,CAAC;gBAAC,MAAM,CAAC;oBACP,2DAA2D;gBAC7D,CAAC;gBACD,IAAI,CAAC,MAAM;oBAAE,KAAK,CAAC,QAAQ,EAAE,GAAG,GAAG,eAAe,IAAI,0CAA0C,CAAC,CAAC;YACpG,CAAC;QACH,CAAC;QACD,IAAI,KAAK,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;YAC9B,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;gBAC/B,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,UAAU,CAAC,EAAE,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;oBAC5C,KAAK,CAAC,QAAQ,EAAE,GAAG,GAAG,kBAAkB,IAAI,SAAS,KAAK,CAAC,UAAU,IAAI,CAAC,CAAC;gBAC7E,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,IAAI,IAAI,KAAK,IAAI;QAAE,OAAO;IAC1B,KAAK,MAAM,IAAI,IAAI,MAAM,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QAC1C,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,kBAAkB,CAAC;YAAE,SAAS;QAC9E,MAAM,IAAI,GAAG,GAAG,IAAI,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;QACpC,IAAI,CAAC,aAAa,CAAC,MAAM,EAAE,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;YACpD,OAAO,CAAC,IAAI,EAAE,6CAA6C,CAAC,CAAC;YAC7D,SAAS;QACX,CAAC;QACD,KAAK,MAAM,OAAO,IAAI,eAAe,CAAC,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC;YAAE,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IAChG,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,MAAc;IAC5C,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,4CAA4C,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC;IACzG,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,MAAM,IAAI,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,gDAAgD,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,CAAC;IACzI,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACpB,QAAQ,CAAC,IAAI,CAAC,aAAa,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,GAAG,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,wCAAwC,CAAC,CAAC;IAC/G,CAAC;IACD,MAAM,IAAI,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,yBAAyB,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,CAAC,CAAC,CAAC;IACpG,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACpB,QAAQ,CAAC,IAAI,CACX,iBAAiB,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,mFAAmF,CACpH,CAAC;IACJ,CAAC;IACD,IAAI,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,QAAQ,CAAC,IAAI,CAAC,8DAA8D,CAAC,CAAC;IAC9G,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,SAAS,IAAI,CAAC,KAAc;IAC1B,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;IACtD,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,EAAkB,EAAE,CAAC,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,EAAE,CAAC,CAAC;AAC1F,CAAC;AAED,4EAA4E;AAC5E,SAAS,KAAK,CACZ,MAAgB,EAChB,IAA6B,EAC7B,IAAmB,EACnB,OAAuE;IAEvE,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAClD,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC;QACxB,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI;YAAE,SAAS;QACpD,OAAO,CAAC,KAAK,EAAE,KAAK,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC;QACtC,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ,IAAI,QAAQ,CAAC,KAAK,CAAC;YAAE,KAAK,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,CAAC,EAAE,OAAO,CAAC,CAAC;QACpG,IAAI,KAAK,CAAC,IAAI,KAAK,UAAU,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YACtD,KAAK,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE;gBAC3B,IAAI,QAAQ,CAAC,GAAG,CAAC;oBAAE,KAAK,CAAC,KAAK,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,EAAE,OAAO,CAAC,CAAC;YAC9E,CAAC,CAAC,CAAC;QACL,CAAC;IACH,CAAC;AACH,CAAC;AAED,8FAA8F;AAC9F,SAAS,WAAW,CAAC,MAAgB,EAAE,GAA4B,EAAE,IAAmB;IACtF,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;QAC/C,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,EAAE,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACpC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;QAC1B,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,QAAQ,CAAC,IAAI,CAAC,GAAG,EAAE,gEAAgE,CAAC,CAAC;QACvF,CAAC;aAAM,IAAI,KAAK,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;YACrC,QAAQ,CAAC,IAAI,CAAC,GAAG,EAAE,0BAA0B,CAAC,GAAG,IAAI,EAAE,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,kBAAkB,oBAAoB,CAAC,CAAC;QAClH,CAAC;aAAM,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ,IAAI,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;YACtD,QAAQ,CAAC,IAAI,CAAC,GAAG,WAAW,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC;QACrE,CAAC;aAAM,IAAI,KAAK,CAAC,IAAI,KAAK,UAAU,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YAC7D,KAAK,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE;gBAC3B,IAAI,QAAQ,CAAC,GAAG,CAAC;oBAAE,QAAQ,CAAC,IAAI,CAAC,GAAG,WAAW,CAAC,KAAK,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;YAC7F,CAAC,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,kEAAkE;AAClE,SAAS,aAAa,CAAC,MAAgB,EAAE,IAAmB;IAC1D,IAAI,GAAG,GAAyB,MAAM,CAAC;IACvC,KAAK,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,CAAC;QAC9C,IAAI,OAAO,OAAO,KAAK,QAAQ;YAAE,SAAS;QAC1C,MAAM,KAAK,GAAyB,GAAG,EAAE,CAAC,OAAO,CAAC,CAAC;QACnD,IAAI,CAAC,KAAK;YAAE,OAAO,KAAK,CAAC;QACzB,IAAI,KAAK,KAAK,IAAI,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,KAAK,CAAC,IAAI,KAAK,UAAU,CAAC;QAChE,GAAG,GAAG,KAAK,CAAC,IAAI,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,KAAK,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;IACxF,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC;AAED,mDAAmD;AACnD,MAAM,UAAU,YAAY,CAAC,MAAmB,EAAE,IAAY;IAC5D,MAAM,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,KAAK,OAAO,CAAC,CAAC,MAAM,CAAC;IACrF,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,GAAG,MAAM,CAAC;IACjD,MAAM,KAAK,GAAG;QACZ,MAAM,CAAC,KAAK;YACV,CAAC,CAAC,WAAW,MAAM,CAAC,OAAO,IAAI,MAAM,CAAC,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,OAAO,MAAM,CAAC,WAAW,GAAG;gBACnG,GAAG,MAAM,CAAC,WAAW,KAAK,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,aAAa,QAAQ,MAAM,CAAC,UAAU,GAAG;gBACtF,GAAG,MAAM,CAAC,UAAU,KAAK,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,YAAY,OAAO,QAAQ,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,IAAI,CAAC,IAAI,GAAG,GAAG;YACvG,CAAC,CAAC,uCAAuC,QAAQ,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,IAAI,CAAC,IAAI,GAAG,4BAA4B;QAC3G,EAAE;KACH,CAAC;IACF,KAAK,MAAM,OAAO,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;QACtC,KAAK,CAAC,IAAI,CAAC,KAAK,OAAO,CAAC,KAAK,KAAK,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,KAAK,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;QACtF,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC;YAAE,KAAK,CAAC,IAAI,CAAC,cAAc,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;IAC1F,CAAC;IACD,IAAI,MAAM,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC;QAAE,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC/C,KAAK,CAAC,IAAI,CAAC,GAAG,MAAM,IAAI,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,KAAK,QAAQ,IAAI,QAAQ,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,UAAU,GAAG,CAAC,CAAC;IACtH,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC;AACjC,CAAC","sourcesContent":["/**\n * `mica check` — reads every entry the way Mica and the website read it, and\n * reports what they would skip, refuse or render as a gap. It never opens a\n * browser and exits with 1 when it finds an error, for CI and coding agents.\n */\n\nimport { existsSync } from \"node:fs\";\nimport { relative } from \"node:path\";\n\nimport {\n DOCUMENT_EXTENSION,\n documentPath,\n entryLayout,\n INDEX_FILE,\n isValidSlug,\n MANIFEST_PATH,\n ORDER_FILE,\n readCollectionEntry,\n readSingleton,\n resolveSlugField,\n singletonPath,\n validateFields,\n type AnyField,\n type ContentSource,\n type FieldMap,\n type PathSegment,\n type Site,\n} from \"@micacms/core\";\nimport { within } from \"@micacms/core/node\";\nimport { contentFiles, loadSchema, readOnlyParts } from \"@micacms/runtime\";\n\nexport interface CheckProblem {\n level: \"error\" | \"warning\";\n /** The file, relative to the content root. */\n path: string;\n message: string;\n}\n\nexport interface CheckReport {\n problems: CheckProblem[];\n /** False when the model could not be read, so nothing else was checked. */\n model: boolean;\n collections: number;\n entries: number;\n singletons: number;\n}\n\n/** Checks the content under `root` against the model in `<root>/.mica/schema.json`. */\nexport async function checkContent(root: string): Promise<CheckReport> {\n const files = contentFiles({ root });\n const report: CheckReport = { problems: [], model: false, collections: 0, entries: 0, singletons: 0 };\n const error = (path: string, message: string) => report.problems.push({ level: \"error\", path, message });\n const warning = (path: string, message: string) => report.problems.push({ level: \"warning\", path, message });\n\n const manifest = await readText(files, MANIFEST_PATH);\n if (manifest === null || manifest instanceof Error) {\n error(MANIFEST_PATH, manifest?.message ?? \"There is no content model. Run `npx astro sync` in the Astro project to write it.\");\n return report;\n }\n let site: Site;\n try {\n site = await loadSchema(files);\n } catch (cause) {\n error(MANIFEST_PATH, (cause as Error).message);\n return report;\n }\n report.model = true;\n\n // A model from a newer @micacms/astro: this check cannot judge those entries.\n const readOnly = readOnlyParts(site);\n for (const reason of readOnly.values()) error(MANIFEST_PATH, `Not checked. ${reason}`);\n\n const slugs = new Map<string, Set<string>>();\n for (const [name, def] of Object.entries(site.collections)) {\n slugs.set(name, new Set(await entrySlugs(files, def.fields, name, error, warning)));\n }\n\n const context: EntryContext = { root, files, site, slugs, error, warning };\n\n for (const [name, def] of Object.entries(site.collections)) {\n report.collections++;\n if (readOnly.has(`collections.${name}`)) continue;\n const slugField = resolveSlugField(def);\n for (const slug of slugs.get(name)!) {\n report.entries++;\n const directory = entryLayout(def.fields) === \"directory\";\n const base = directory ? `${name}/${slug}` : null;\n const dataPath = directory ? `${base}/${INDEX_FILE}` : `${name}/${slug}.json`;\n\n const data = await readOrReport(() => readCollectionEntry(files, def, name, slug), dataPath, error);\n if (!data) continue;\n const stored = slugField ? data[slugField] : undefined;\n if (typeof stored === \"string\" && stored !== \"\" && stored !== slug) {\n warning(dataPath, `${slugField} is \"${stored}\", but the entry is named \"${slug}\". The next save in the editor renames the entry.`);\n }\n await checkEntry(context, def.fields, data, dataPath, base);\n }\n }\n\n for (const [name, def] of Object.entries(site.singletons)) {\n report.singletons++;\n if (readOnly.has(`singletons.${name}`)) continue;\n const directory = entryLayout(def.fields) === \"directory\";\n const base = singletonPath(def.fields, name);\n const dataPath = directory ? `${base}/${INDEX_FILE}` : base;\n const otherLayout = directory ? `${name}.json` : `${name}/${INDEX_FILE}`;\n if ((await readText(files, otherLayout)) !== null) {\n error(otherLayout, `Not read: \"${def.label}\" is stored as ${dataPath}, because ${directory ? \"it has\" : \"it has no\"} rich text or media.`);\n }\n\n const data = await readOrReport(() => readSingleton(files, def, name), dataPath, error);\n if (data === undefined) continue;\n if (data === null) {\n warning(dataPath, `No content for \"${def.label}\" yet.`);\n continue;\n }\n await checkEntry(context, def.fields, data, dataPath, directory ? base : null);\n }\n\n return report;\n}\n\ninterface EntryContext {\n root: string;\n files: ContentSource;\n site: Site;\n slugs: Map<string, Set<string>>;\n error(path: string, message: string): void;\n warning(path: string, message: string): void;\n}\n\n/** The slugs Mica reads in a collection, reporting what it would skip. */\nasync function entrySlugs(\n files: ContentSource,\n fields: FieldMap,\n name: string,\n error: (path: string, message: string) => void,\n warning: (path: string, message: string) => void,\n): Promise<string[]> {\n const directory = entryLayout(fields) === \"directory\";\n const shape = directory ? `a folder with ${INDEX_FILE}` : \"a <slug>.json file\";\n const found: string[] = [];\n\n for (const item of await files.list(name)) {\n const path = `${name}/${item.name}`;\n if (item.name.startsWith(\".\")) continue;\n if (item.name === ORDER_FILE) {\n checkOrder(await readText(files, path), path, error);\n continue;\n }\n\n // An entry in the other layout is lost content, not a stray file: the model gained or lost rich text or media.\n if (directory && item.type === \"file\" && item.name.endsWith(\".json\")) {\n error(path, `Not read: entries of \"${name}\" are folders with ${INDEX_FILE}, because it has rich text or media.`);\n continue;\n }\n if (!directory && item.type === \"directory\" && (await readText(files, `${path}/${INDEX_FILE}`)) !== null) {\n error(path, `Not read: entries of \"${name}\" are <slug>.json files, because it has no rich text or media.`);\n continue;\n }\n\n const slug = directory ? item.name : item.name.replace(/\\.json$/, \"\");\n const looksLikeEntry = directory ? item.type === \"directory\" : item.type === \"file\" && item.name.endsWith(\".json\");\n if (!looksLikeEntry) {\n warning(path, `Not read. Each entry of \"${name}\" is ${shape}.`);\n continue;\n }\n if (!isValidSlug(slug)) {\n error(path, `Not read: \"${slug}\" is not a slug. Use lowercase letters, digits and single hyphens.`);\n continue;\n }\n const index = directory ? await readText(files, `${path}/${INDEX_FILE}`) : \"\";\n if (index === null || index instanceof Error) {\n error(path, `Not read: ${index?.message ?? `the folder has no ${INDEX_FILE}.`}`);\n continue;\n }\n found.push(slug);\n }\n return found;\n}\n\n/** The file's text, null when it is not there, or the error that reading it gave. */\nasync function readText(files: ContentSource, path: string): Promise<string | null | Error> {\n try {\n return await files.read(path);\n } catch (cause) {\n return cause as Error;\n }\n}\n\nfunction checkOrder(text: string | null | Error, path: string, error: (path: string, message: string) => void): void {\n if (text === null) return;\n if (text instanceof Error) return error(path, text.message);\n let order: unknown;\n try {\n order = JSON.parse(text);\n } catch {\n return error(path, \"Not valid JSON. The build fails on it.\");\n }\n if (!Array.isArray(order)) {\n error(path, 'Must be a JSON array of slugs, such as [\"second\", \"first\"]. The build fails on it.');\n }\n}\n\nasync function readOrReport<T>(\n read: () => Promise<T>,\n path: string,\n error: (path: string, message: string) => void,\n): Promise<T | undefined> {\n try {\n return await read();\n } catch (cause) {\n error(path, (cause as Error).message.replace(`${path}: `, \"\"));\n return undefined;\n }\n}\n\nasync function checkEntry(\n context: EntryContext,\n fields: FieldMap,\n data: Record<string, unknown>,\n dataPath: string,\n base: string | null,\n): Promise<void> {\n const { root, files, slugs, error, warning } = context;\n\n const result = validateFields(fields, data, \"storage\");\n if (!result.ok) {\n for (const issue of result.issues) error(dataPath, issue.path ? `${issue.path}: ${issue.message}` : issue.message);\n }\n\n const raw = JSON.parse((await files.read(dataPath))!) as Record<string, unknown>;\n for (const message of unknownKeys(fields, raw, [])) warning(dataPath, message);\n\n visit(fields, data, [], (field, value, path) => {\n const key = path.join(\".\");\n if (field.kind === \"media\") {\n for (const file of list(value)) {\n let exists = false;\n try {\n exists = existsSync(within(root, file));\n } catch {\n // A path outside the content root does not exist for Mica.\n }\n if (!exists) error(dataPath, `${key}: the file \"${file}\" does not exist under the content root.`);\n }\n }\n if (field.kind === \"relation\") {\n for (const slug of list(value)) {\n if (!slugs.get(field.collection)?.has(slug)) {\n error(dataPath, `${key}: there is no \"${slug}\" in \"${field.collection}\".`);\n }\n }\n }\n });\n\n if (base === null) return;\n for (const item of await files.list(base)) {\n if (item.type !== \"file\" || !item.name.endsWith(DOCUMENT_EXTENSION)) continue;\n const path = `${base}/${item.name}`;\n if (!namesRichText(fields, documentPath(item.name))) {\n warning(path, \"Not read: no rich-text field has this name.\");\n continue;\n }\n for (const message of markdocWarnings((await files.read(path)) ?? \"\")) warning(path, message);\n }\n}\n\n/**\n * What Markdoc renders differently from Markdown, found in the text outside\n * code. A scan, not a parse: it can warn about too much, never about too little.\n */\nexport function markdocWarnings(source: string): string[] {\n const prose = source.replace(/^(`{3,}|~{3,})[^\\n]*\\n[\\s\\S]*?^\\1[ \\t]*$/gm, \"\").replace(/`[^`\\n]*`/g, \"\");\n const messages: string[] = [];\n const html = [...new Set([...prose.matchAll(/<\\/?([a-zA-Z][a-zA-Z0-9-]*)(?=[\\s/>])[^>\\n]*>/g)].map((match) => match[1]!.toLowerCase()))];\n if (html.length > 0) {\n messages.push(`Raw HTML (${html.map((tag) => `<${tag}>`).join(\", \")}) is not rendered: Markdoc escapes it.`);\n }\n const tags = [...new Set([...prose.matchAll(/\\{%\\s*([a-zA-Z][\\w-]*)/g)].map((match) => match[1]!))];\n if (tags.length > 0) {\n messages.push(\n `Markdoc tags (${tags.join(\", \")}) render only if the website declares them in renderRichText(source, { config }).`,\n );\n }\n if (/\\[\\^[^\\]]+\\]/.test(prose)) messages.push(\"Footnotes ([^1]) are not Markdoc: they render as plain text.\");\n return messages;\n}\n\nfunction list(value: unknown): string[] {\n const values = Array.isArray(value) ? value : [value];\n return values.filter((item): item is string => typeof item === \"string\" && item !== \"\");\n}\n\n/** Calls `onField` for every field with a value, nested fields included. */\nfunction visit(\n fields: FieldMap,\n data: Record<string, unknown>,\n path: PathSegment[],\n onField: (field: AnyField, value: unknown, path: PathSegment[]) => void,\n): void {\n for (const [key, field] of Object.entries(fields)) {\n const value = data[key];\n if (value === undefined || value === null) continue;\n onField(field, value, [...path, key]);\n if (field.kind === \"object\" && isRecord(value)) visit(field.fields, value, [...path, key], onField);\n if (field.kind === \"repeater\" && Array.isArray(value)) {\n value.forEach((row, index) => {\n if (isRecord(row)) visit(field.fields, row, [...path, key, index], onField);\n });\n }\n }\n}\n\n/** Keys in the stored JSON that the model does not have; Mica drops them on the next save. */\nfunction unknownKeys(fields: FieldMap, raw: Record<string, unknown>, path: PathSegment[]): string[] {\n const messages: string[] = [];\n for (const [key, value] of Object.entries(raw)) {\n const at = [...path, key].join(\".\");\n const field = fields[key];\n if (!field) {\n messages.push(`${at}: the model has no such field. Mica drops it on the next save.`);\n } else if (field.kind === \"richText\") {\n messages.push(`${at}: rich text belongs in ${[...path, key].join(\".\")}${DOCUMENT_EXTENSION}, not in the JSON.`);\n } else if (field.kind === \"object\" && isRecord(value)) {\n messages.push(...unknownKeys(field.fields, value, [...path, key]));\n } else if (field.kind === \"repeater\" && Array.isArray(value)) {\n value.forEach((row, index) => {\n if (isRecord(row)) messages.push(...unknownKeys(field.fields, row, [...path, key, index]));\n });\n }\n }\n return messages;\n}\n\n/** Whether a sidecar's dotted path leads to a rich-text field. */\nfunction namesRichText(fields: FieldMap, path: PathSegment[]): boolean {\n let map: FieldMap | undefined = fields;\n for (const [index, segment] of path.entries()) {\n if (typeof segment === \"number\") continue;\n const field: AnyField | undefined = map?.[segment];\n if (!field) return false;\n if (index === path.length - 1) return field.kind === \"richText\";\n map = field.kind === \"object\" || field.kind === \"repeater\" ? field.fields : undefined;\n }\n return false;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === \"object\" && !Array.isArray(value);\n}\n\n/** The report as a person or an agent reads it. */\nexport function formatReport(report: CheckReport, root: string): string {\n const errors = report.problems.filter((problem) => problem.level === \"error\").length;\n const warnings = report.problems.length - errors;\n const lines = [\n report.model\n ? `Checked ${report.entries} ${report.entries === 1 ? \"entry\" : \"entries\"} in ${report.collections} ` +\n `${report.collections === 1 ? \"collection\" : \"collections\"} and ${report.singletons} ` +\n `${report.singletons === 1 ? \"singleton\" : \"singletons\"} in ${relative(process.cwd(), root) || \".\"}.`\n : `Could not read the content model in ${relative(process.cwd(), root) || \".\"}, so no entry was checked.`,\n \"\",\n ];\n for (const problem of report.problems) {\n lines.push(` ${problem.level === \"error\" ? \"error \" : \"warning\"} ${problem.path}`);\n for (const line of problem.message.split(\"\\n\")) lines.push(` ${line.trim()}`);\n }\n if (report.problems.length > 0) lines.push(\"\");\n lines.push(`${errors} ${errors === 1 ? \"error\" : \"errors\"}, ${warnings} ${warnings === 1 ? \"warning\" : \"warnings\"}.`);\n return `${lines.join(\"\\n\")}\\n`;\n}\n"]}
1
+ {"version":3,"file":"check.js","sourceRoot":"","sources":["../src/check.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `mica check` — reads every entry the way Mica and the website read it, and\n * reports what they would skip, refuse or render as a gap. It never opens a\n * browser and exits with 1 when it finds an error, for CI and coding agents.\n */\n\nimport { existsSync } from \"node:fs\";\nimport { relative } from \"node:path\";\n\nimport {\n DOCUMENT_EXTENSION,\n documentPath,\n entryLayout,\n INDEX_FILE,\n isValidSlug,\n MANIFEST_PATH,\n misplacedFiles,\n ORDER_FILE,\n readCollectionEntry,\n readSingleton,\n resolveSlugField,\n singletonPath,\n validateFields,\n type AnyField,\n type ContentSource,\n type FieldMap,\n type PathSegment,\n type Site,\n} from \"@micacms/core\";\nimport { within } from \"@micacms/core/node\";\nimport { contentFiles, loadSchema, readOnlyParts } from \"@micacms/runtime\";\n\nexport interface CheckProblem {\n level: \"error\" | \"warning\";\n /** The file, relative to the content root. */\n path: string;\n message: string;\n}\n\nexport interface CheckReport {\n problems: CheckProblem[];\n /** False when the model could not be read, so nothing else was checked. */\n model: boolean;\n collections: number;\n entries: number;\n singletons: number;\n}\n\n/** Checks the content under `root` against the model in `<root>/.mica/schema.json`. */\nexport async function checkContent(root: string): Promise<CheckReport> {\n const files = contentFiles({ root });\n const report: CheckReport = { problems: [], model: false, collections: 0, entries: 0, singletons: 0 };\n const error = (path: string, message: string) => report.problems.push({ level: \"error\", path, message });\n const warning = (path: string, message: string) => report.problems.push({ level: \"warning\", path, message });\n\n const manifest = await readText(files, MANIFEST_PATH);\n if (manifest === null || manifest instanceof Error) {\n error(MANIFEST_PATH, manifest?.message ?? \"There is no content model. Run `npx astro sync` in the Astro project to write it.\");\n return report;\n }\n let site: Site;\n try {\n site = await loadSchema(files);\n } catch (cause) {\n error(MANIFEST_PATH, (cause as Error).message);\n return report;\n }\n report.model = true;\n\n // A model from a newer @micacms/astro: this check cannot judge those entries.\n const readOnly = readOnlyParts(site);\n for (const reason of readOnly.values()) error(MANIFEST_PATH, `Not checked. ${reason}`);\n\n const slugs = new Map<string, Set<string>>();\n for (const [name, def] of Object.entries(site.collections)) {\n slugs.set(name, new Set(await entrySlugs(files, def.fields, name, error, warning)));\n }\n\n const context: EntryContext = { root, files, site, slugs, error, warning };\n\n for (const [name, def] of Object.entries(site.collections)) {\n report.collections++;\n if (readOnly.has(`collections.${name}`)) continue;\n const slugField = resolveSlugField(def);\n for (const slug of slugs.get(name)!) {\n report.entries++;\n const directory = entryLayout(def.fields) === \"directory\";\n const base = directory ? `${name}/${slug}` : null;\n const dataPath = directory ? `${base}/${INDEX_FILE}` : `${name}/${slug}.json`;\n\n const data = await readOrReport(() => readCollectionEntry(files, def, name, slug), dataPath, error);\n if (!data) continue;\n const stored = slugField ? data[slugField] : undefined;\n if (typeof stored === \"string\" && stored !== \"\" && stored !== slug) {\n warning(dataPath, `${slugField} is \"${stored}\", but the entry is named \"${slug}\". The next save in the editor renames the entry.`);\n }\n await checkEntry(context, def.fields, data, dataPath, base);\n }\n }\n\n for (const [name, def] of Object.entries(site.singletons)) {\n report.singletons++;\n if (readOnly.has(`singletons.${name}`)) continue;\n const directory = entryLayout(def.fields) === \"directory\";\n const base = singletonPath(def.fields, name);\n const dataPath = directory ? `${base}/${INDEX_FILE}` : base;\n const [otherLayout] = await misplacedFiles(files, def.fields, name, \"singleton\");\n if (otherLayout) {\n error(otherLayout, `Not read: \"${def.label}\" is stored as ${dataPath}, because ${directory ? \"it has\" : \"it has no\"} rich text or media. Run \\`npx mica migrate-layout\\`.`);\n // Without a file in the model's layout, the reader stops at the other one: reported above.\n if ((await readText(files, dataPath)) === null) continue;\n }\n\n const data = await readOrReport(() => readSingleton(files, def, name), dataPath, error);\n if (data === undefined) continue;\n if (data === null) {\n warning(dataPath, `No content for \"${def.label}\" yet.`);\n continue;\n }\n await checkEntry(context, def.fields, data, dataPath, directory ? base : null);\n }\n\n return report;\n}\n\ninterface EntryContext {\n root: string;\n files: ContentSource;\n site: Site;\n slugs: Map<string, Set<string>>;\n error(path: string, message: string): void;\n warning(path: string, message: string): void;\n}\n\n/** The slugs Mica reads in a collection, reporting what it would skip. */\nasync function entrySlugs(\n files: ContentSource,\n fields: FieldMap,\n name: string,\n error: (path: string, message: string) => void,\n warning: (path: string, message: string) => void,\n): Promise<string[]> {\n const directory = entryLayout(fields) === \"directory\";\n const shape = directory ? `a folder with ${INDEX_FILE}` : \"a <slug>.json file\";\n const found: string[] = [];\n // The reader's own rule, so that `check` and `migrate-layout` agree.\n const misplaced = new Set(await misplacedFiles(files, fields, name, \"collection\"));\n\n for (const item of await files.list(name)) {\n const path = `${name}/${item.name}`;\n if (item.name.startsWith(\".\")) continue;\n if (item.name === ORDER_FILE) {\n checkOrder(await readText(files, path), path, error);\n continue;\n }\n\n // An entry in the other layout is lost content, not a stray file: the model gained or lost rich text or media.\n if (directory && misplaced.has(path)) {\n error(path, `Not read: entries of \"${name}\" are folders with ${INDEX_FILE}, because it has rich text or media. Run \\`npx mica migrate-layout\\`.`);\n continue;\n }\n if (!directory && misplaced.has(`${path}/${INDEX_FILE}`)) {\n error(path, `Not read: entries of \"${name}\" are <slug>.json files, because it has no rich text or media. Run \\`npx mica migrate-layout\\`.`);\n continue;\n }\n\n const slug = directory ? item.name : item.name.replace(/\\.json$/, \"\");\n const looksLikeEntry = directory ? item.type === \"directory\" : item.type === \"file\" && item.name.endsWith(\".json\");\n if (!looksLikeEntry) {\n warning(path, `Not read. Each entry of \"${name}\" is ${shape}.`);\n continue;\n }\n if (!isValidSlug(slug)) {\n error(path, `Not read: \"${slug}\" is not a slug. Use lowercase letters, digits and single hyphens.`);\n continue;\n }\n const index = directory ? await readText(files, `${path}/${INDEX_FILE}`) : \"\";\n if (index === null || index instanceof Error) {\n error(path, `Not read: ${index?.message ?? `the folder has no ${INDEX_FILE}.`}`);\n continue;\n }\n found.push(slug);\n }\n return found;\n}\n\n/** The file's text, null when it is not there, or the error that reading it gave. */\nasync function readText(files: ContentSource, path: string): Promise<string | null | Error> {\n try {\n return await files.read(path);\n } catch (cause) {\n return cause as Error;\n }\n}\n\nfunction checkOrder(text: string | null | Error, path: string, error: (path: string, message: string) => void): void {\n if (text === null) return;\n if (text instanceof Error) return error(path, text.message);\n let order: unknown;\n try {\n order = JSON.parse(text);\n } catch {\n return error(path, \"Not valid JSON. The build fails on it.\");\n }\n if (!Array.isArray(order)) {\n error(path, 'Must be a JSON array of slugs, such as [\"second\", \"first\"]. The build fails on it.');\n }\n}\n\nasync function readOrReport<T>(\n read: () => Promise<T>,\n path: string,\n error: (path: string, message: string) => void,\n): Promise<T | undefined> {\n try {\n return await read();\n } catch (cause) {\n error(path, (cause as Error).message.replace(`${path}: `, \"\"));\n return undefined;\n }\n}\n\nasync function checkEntry(\n context: EntryContext,\n fields: FieldMap,\n data: Record<string, unknown>,\n dataPath: string,\n base: string | null,\n): Promise<void> {\n const { root, files, slugs, error, warning } = context;\n\n const result = validateFields(fields, data);\n if (!result.ok) {\n for (const issue of result.issues) error(dataPath, issue.path ? `${issue.path}: ${issue.message}` : issue.message);\n }\n\n const raw = JSON.parse((await files.read(dataPath))!) as Record<string, unknown>;\n for (const message of unknownKeys(fields, raw, [])) warning(dataPath, message);\n\n visit(fields, data, [], (field, value, path) => {\n const key = path.join(\".\");\n if (field.kind === \"media\") {\n for (const file of list(mediaPaths(value))) {\n let exists = false;\n try {\n exists = existsSync(within(root, file));\n } catch {\n // A path outside the content root does not exist for Mica.\n }\n if (!exists) error(dataPath, `${key}: the file \"${file}\" does not exist under the content root.`);\n }\n }\n if (field.kind === \"relation\") {\n for (const slug of list(value)) {\n if (!slugs.get(field.collection)?.has(slug)) {\n error(dataPath, `${key}: there is no \"${slug}\" in \"${field.collection}\".`);\n }\n }\n }\n });\n\n if (base === null) return;\n for (const item of await files.list(base)) {\n if (item.type !== \"file\" || !item.name.endsWith(DOCUMENT_EXTENSION)) continue;\n const path = `${base}/${item.name}`;\n if (!namesRichText(fields, documentPath(item.name))) {\n warning(path, \"Not read: no rich-text field has this name.\");\n continue;\n }\n for (const message of markdocWarnings((await files.read(path)) ?? \"\")) warning(path, message);\n }\n}\n\n/**\n * What Markdoc renders differently from Markdown, found in the text outside\n * code. A scan, not a parse: it can warn about too much, never about too little.\n */\nexport function markdocWarnings(source: string): string[] {\n const prose = source.replace(/^(`{3,}|~{3,})[^\\n]*\\n[\\s\\S]*?^\\1[ \\t]*$/gm, \"\").replace(/`[^`\\n]*`/g, \"\");\n const messages: string[] = [];\n const html = [...new Set([...prose.matchAll(/<\\/?([a-zA-Z][a-zA-Z0-9-]*)(?=[\\s/>])[^>\\n]*>/g)].map((match) => match[1]!.toLowerCase()))];\n if (html.length > 0) {\n messages.push(`Raw HTML (${html.map((tag) => `<${tag}>`).join(\", \")}) is not rendered: Markdoc escapes it.`);\n }\n const tags = [...new Set([...prose.matchAll(/\\{%\\s*([a-zA-Z][\\w-]*)/g)].map((match) => match[1]!))];\n if (tags.length > 0) {\n messages.push(\n `Markdoc tags (${tags.join(\", \")}) render only if the website declares them in renderRichText(source, { config }).`,\n );\n }\n if (/\\[\\^[^\\]]+\\]/.test(prose)) messages.push(\"Footnotes ([^1]) are not Markdoc: they render as plain text.\");\n return messages;\n}\n\n/** The paths of a media value: a path, `{ path, alt }`, or a list of either. */\nfunction mediaPaths(value: unknown): unknown[] {\n const values = Array.isArray(value) ? value : [value];\n return values.map((item) => (item !== null && typeof item === \"object\" ? (item as { path?: unknown }).path : item));\n}\n\nfunction list(value: unknown): string[] {\n const values = Array.isArray(value) ? value : [value];\n return values.filter((item): item is string => typeof item === \"string\" && item !== \"\");\n}\n\n/** Calls `onField` for every field with a value, nested fields included. */\nfunction visit(\n fields: FieldMap,\n data: Record<string, unknown>,\n path: PathSegment[],\n onField: (field: AnyField, value: unknown, path: PathSegment[]) => void,\n): void {\n for (const [key, field] of Object.entries(fields)) {\n const value = data[key];\n if (value === undefined || value === null) continue;\n onField(field, value, [...path, key]);\n if (field.kind === \"object\" && isRecord(value)) visit(field.fields, value, [...path, key], onField);\n if (field.kind === \"repeater\" && Array.isArray(value)) {\n value.forEach((row, index) => {\n if (isRecord(row)) visit(field.fields, row, [...path, key, index], onField);\n });\n }\n }\n}\n\n/** Keys in the stored JSON that the model does not have; Mica drops them on the next save. */\nfunction unknownKeys(fields: FieldMap, raw: Record<string, unknown>, path: PathSegment[]): string[] {\n const messages: string[] = [];\n for (const [key, value] of Object.entries(raw)) {\n const at = [...path, key].join(\".\");\n const field = fields[key];\n if (!field) {\n messages.push(`${at}: the model has no such field. Mica drops it on the next save.`);\n } else if (field.kind === \"richText\") {\n messages.push(`${at}: rich text belongs in ${[...path, key].join(\".\")}${DOCUMENT_EXTENSION}, not in the JSON.`);\n } else if (field.kind === \"object\" && isRecord(value)) {\n messages.push(...unknownKeys(field.fields, value, [...path, key]));\n } else if (field.kind === \"repeater\" && Array.isArray(value)) {\n value.forEach((row, index) => {\n if (isRecord(row)) messages.push(...unknownKeys(field.fields, row, [...path, key, index]));\n });\n }\n }\n return messages;\n}\n\n/** Whether a sidecar's dotted path leads to a rich-text field. */\nfunction namesRichText(fields: FieldMap, path: PathSegment[]): boolean {\n let map: FieldMap | undefined = fields;\n for (const [index, segment] of path.entries()) {\n if (typeof segment === \"number\") continue;\n const field: AnyField | undefined = map?.[segment];\n if (!field) return false;\n if (index === path.length - 1) return field.kind === \"richText\";\n map = field.kind === \"object\" || field.kind === \"repeater\" ? field.fields : undefined;\n }\n return false;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === \"object\" && !Array.isArray(value);\n}\n\n/** The report as a person or an agent reads it. */\nexport function formatReport(report: CheckReport, root: string): string {\n const errors = report.problems.filter((problem) => problem.level === \"error\").length;\n const warnings = report.problems.length - errors;\n const lines = [\n report.model\n ? `Checked ${report.entries} ${report.entries === 1 ? \"entry\" : \"entries\"} in ${report.collections} ` +\n `${report.collections === 1 ? \"collection\" : \"collections\"} and ${report.singletons} ` +\n `${report.singletons === 1 ? \"singleton\" : \"singletons\"} in ${relative(process.cwd(), root) || \".\"}.`\n : `Could not read the content model in ${relative(process.cwd(), root) || \".\"}, so no entry was checked.`,\n \"\",\n ];\n for (const problem of report.problems) {\n lines.push(` ${problem.level === \"error\" ? \"error \" : \"warning\"} ${problem.path}`);\n for (const line of problem.message.split(\"\\n\")) lines.push(` ${line.trim()}`);\n }\n if (report.problems.length > 0) lines.push(\"\");\n lines.push(`${errors} ${errors === 1 ? \"error\" : \"errors\"}, ${warnings} ${warnings === 1 ? \"warning\" : \"warnings\"}.`);\n return `${lines.join(\"\\n\")}\\n`;\n}\n"]}
package/dist/cli.d.ts CHANGED
@@ -12,7 +12,7 @@
12
12
  * - it binds to loopback. A container binds 0.0.0.0 because it has to; a
13
13
  * laptop does not, and this editor has no authentication. Anything
14
14
  * reachable by anyone else runs @micacms/self-hosted with an identity
15
- * provider — see docs/self-hosting.md.
15
+ * provider — see https://micacms.com/docs/self-hosting/.
16
16
  * - it talks to a person instead of to a log collector: one short banner
17
17
  * saying where the editor is and what happens when you save, and the
18
18
  * browser opened on it. `MICA_CLI` is how the server is told which of the
@@ -22,11 +22,12 @@
22
22
  * account and no session, so nothing is written outside the content
23
23
  * directory.
24
24
  *
25
- * Explicit environment wins over every flag default here, and a flag wins over
26
- * a `.env` file, because `loadConfig` leaves already-set variables alone.
25
+ * A flag wins over the environment, and the environment over a default. No
26
+ * `.env` file is read: the project's `.env` belongs to the site.
27
27
  */
28
28
  export interface Options {
29
- content: string;
29
+ /** Undefined means "not given": `MICA_CONTENT_ROOT`, else `./content`. */
30
+ content?: string;
30
31
  port?: string;
31
32
  host?: string;
32
33
  /** Undefined means "decide from the terminal" — see `shouldOpenBrowser`. */
@@ -39,5 +40,9 @@ export declare function parse(argv: string[]): Options;
39
40
  * container and a CI job all get a server and no surprises.
40
41
  */
41
42
  export declare function shouldOpenBrowser(options: Options, env: NodeJS.ProcessEnv): boolean;
43
+ /** The content root: the flag or path, else `MICA_CONTENT_ROOT`, else `./content`. */
44
+ export declare function contentRoot(options: Options, env: NodeJS.ProcessEnv): string;
45
+ /** What the server reads: a flag wins over the environment, and the environment over a default. */
46
+ export declare function serverEnvironment(options: Options, env: NodeJS.ProcessEnv, content: string): Record<string, string>;
42
47
  export declare function main(): Promise<void>;
43
48
  //# sourceMappingURL=cli.d.ts.map
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAYH,MAAM,WAAW,OAAO;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,4EAA4E;IAC5E,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AA0BD,wBAAgB,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAgD7C;AAkBD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,CAAC,UAAU,GAAG,OAAO,CAGnF;AAED,wBAAsB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAyD1C"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAaH,MAAM,WAAW,OAAO;IACtB,0EAA0E;IAC1E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,4EAA4E;IAC5E,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AA2BD,wBAAgB,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAgD7C;AAkBD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,CAAC,UAAU,GAAG,OAAO,CAGnF;AAKD,sFAAsF;AACtF,wBAAgB,WAAW,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,CAAC,UAAU,GAAG,MAAM,CAE5E;AAED,mGAAmG;AACnG,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,CAAC,UAAU,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAanH;AAED,wBAAsB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CA0C1C"}
package/dist/cli.js CHANGED
@@ -12,7 +12,7 @@
12
12
  * - it binds to loopback. A container binds 0.0.0.0 because it has to; a
13
13
  * laptop does not, and this editor has no authentication. Anything
14
14
  * reachable by anyone else runs @micacms/self-hosted with an identity
15
- * provider — see docs/self-hosting.md.
15
+ * provider — see https://micacms.com/docs/self-hosting/.
16
16
  * - it talks to a person instead of to a log collector: one short banner
17
17
  * saying where the editor is and what happens when you save, and the
18
18
  * browser opened on it. `MICA_CLI` is how the server is told which of the
@@ -22,8 +22,8 @@
22
22
  * account and no session, so nothing is written outside the content
23
23
  * directory.
24
24
  *
25
- * Explicit environment wins over every flag default here, and a flag wins over
26
- * a `.env` file, because `loadConfig` leaves already-set variables alone.
25
+ * A flag wins over the environment, and the environment over a default. No
26
+ * `.env` file is read: the project's `.env` belongs to the site.
27
27
  */
28
28
  import { existsSync, realpathSync } from "node:fs";
29
29
  import path from "node:path";
@@ -32,6 +32,7 @@ import { MANIFEST_PATH } from "@micacms/core";
32
32
  import { DEFAULT_PORT, VERSION } from "@micacms/runtime";
33
33
  import { checkContent, formatReport } from "./check.js";
34
34
  import { formatInit, initProject, InitError } from "./init.js";
35
+ import { formatMigration, migrateLayout } from "./migrate.js";
35
36
  const USAGE = `
36
37
  mica — edit your Astro project's content, on this machine
37
38
 
@@ -39,6 +40,7 @@ const USAGE = `
39
40
  $ npx @micacms/editor [content directory] open the editor
40
41
  $ npx @micacms/editor init add Mica to this Astro project
41
42
  $ npx @micacms/editor check [content dir] check every entry against the model
43
+ $ npx @micacms/editor migrate-layout [dir] move entries into the layout of the model
42
44
 
43
45
  With @micacms/editor installed in the project, \`npx mica\` is the same command.
44
46
 
@@ -56,7 +58,7 @@ const USAGE = `
56
58
  container with an identity provider. See https://micacms.com/docs/self-hosting/.
57
59
  `;
58
60
  export function parse(argv) {
59
- const options = { content: "./content" };
61
+ const options = {};
60
62
  for (let index = 0; index < argv.length; index++) {
61
63
  const flag = argv[index];
62
64
  const value = () => {
@@ -126,14 +128,37 @@ export function shouldOpenBrowser(options, env) {
126
128
  return options.open;
127
129
  return Boolean(process.stdout.isTTY) && !env["CI"];
128
130
  }
131
+ /** A variable's value, with a blank one counted as not set. */
132
+ const set = (value) => (value?.trim() ? value : undefined);
133
+ /** The content root: the flag or path, else `MICA_CONTENT_ROOT`, else `./content`. */
134
+ export function contentRoot(options, env) {
135
+ return path.resolve(options.content ?? set(env["MICA_CONTENT_ROOT"]) ?? "./content");
136
+ }
137
+ /** What the server reads: a flag wins over the environment, and the environment over a default. */
138
+ export function serverEnvironment(options, env, content) {
139
+ const port = options.port ?? set(env["PORT"]) ?? String(DEFAULT_PORT);
140
+ return {
141
+ // The checks ran on this path, so it is the one the server gets.
142
+ MICA_CONTENT_ROOT: content,
143
+ HOST: options.host ?? set(env["HOST"]) ?? "127.0.0.1",
144
+ PORT: port,
145
+ // It must agree with the port: it scopes the cookie and the Origin check.
146
+ MICA_PUBLIC_URL: (!options.port && set(env["MICA_PUBLIC_URL"])) || `http://localhost:${port}`,
147
+ // A person, not a log collector: one banner instead of startup log lines.
148
+ MICA_CLI: "true",
149
+ MICA_OPEN: String(shouldOpenBrowser(options, env)),
150
+ };
151
+ }
129
152
  export async function main() {
130
153
  const [command, ...rest] = process.argv.slice(2);
131
154
  if (command === "init")
132
155
  return init(rest);
133
156
  if (command === "check")
134
157
  return check(rest);
158
+ if (command === "migrate-layout")
159
+ return migrate(rest);
135
160
  const options = parse(process.argv.slice(2));
136
- const content = path.resolve(options.content);
161
+ const content = contentRoot(options, process.env);
137
162
  if (!existsSync(content)) {
138
163
  report(`There is no content directory at ${content}.`, [
139
164
  "Mica edits the directory your Astro project publishes content into —",
@@ -160,22 +185,7 @@ export async function main() {
160
185
  ]);
161
186
  process.exit(78); // EX_CONFIG
162
187
  }
163
- const defaults = {
164
- CONTENT_ROOT: content,
165
- HOST: options.host ?? "127.0.0.1",
166
- PORT: options.port ?? String(DEFAULT_PORT),
167
- // Tells the server it is talking to a person: one banner instead of the
168
- // startup log lines a collector wants.
169
- MICA_CLI: "true",
170
- MICA_OPEN: String(shouldOpenBrowser(options, process.env)),
171
- };
172
- for (const [key, value] of Object.entries(defaults)) {
173
- process.env[key] ??= value;
174
- }
175
- // MICA_PUBLIC_URL has to agree with the port, and only matters here for
176
- // cookie scope and the Origin check — both of which are this process's own
177
- // address.
178
- process.env["MICA_PUBLIC_URL"] ??= `http://localhost:${process.env["PORT"]}`;
188
+ Object.assign(process.env, serverEnvironment(options, process.env, content));
179
189
  // The server is imported rather than spawned: one process, one signal
180
190
  // handler, and a stack trace that points at the real thing.
181
191
  await import("./index.js");
@@ -194,13 +204,18 @@ function init(argv) {
194
204
  }
195
205
  }
196
206
  async function check(argv) {
197
- const { content } = parse(argv);
198
- const root = path.resolve(content);
207
+ const root = contentRoot(parse(argv), process.env);
199
208
  const result = await checkContent(root);
200
209
  process.stdout.write(formatReport(result, root));
201
210
  if (result.problems.some((problem) => problem.level === "error"))
202
211
  process.exit(1);
203
212
  }
213
+ async function migrate(argv) {
214
+ const result = await migrateLayout(contentRoot(parse(argv), process.env));
215
+ process.stdout.write(formatMigration(result));
216
+ if (result.conflicts.length > 0)
217
+ process.exit(1);
218
+ }
204
219
  // Only when this file *is* the command. Importing it, as the tests do, must
205
220
  // not start a server.
206
221
  //
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\n/**\n * `npx @micacms/editor` — Mica against a project on this machine.\n *\n * The point of this file is that adopting Mica should not mean adopting this\n * repository. Everything it does is set environment variables and hand over to\n * the same entry point a container runs, so there is one server with one\n * configuration path and this is a front door rather than a second one.\n *\n * Two defaults differ from a container's, and both are deliberate:\n *\n * - it binds to loopback. A container binds 0.0.0.0 because it has to; a\n * laptop does not, and this editor has no authentication. Anything\n * reachable by anyone else runs @micacms/self-hosted with an identity\n * provider — see docs/self-hosting.md.\n * - it talks to a person instead of to a log collector: one short banner\n * saying where the editor is and what happens when you save, and the\n * browser opened on it. `MICA_CLI` is how the server is told which of the\n * two audiences it has.\n *\n * There is no state to keep anywhere: the local editor has no database, no\n * account and no session, so nothing is written outside the content\n * directory.\n *\n * Explicit environment wins over every flag default here, and a flag wins over\n * a `.env` file, because `loadConfig` leaves already-set variables alone.\n */\n\nimport { existsSync, realpathSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\n\nimport { MANIFEST_PATH } from \"@micacms/core\";\nimport { DEFAULT_PORT, VERSION } from \"@micacms/runtime\";\n\nimport { checkContent, formatReport } from \"./check.js\";\nimport { formatInit, initProject, InitError } from \"./init.js\";\n\nexport interface Options {\n content: string;\n port?: string;\n host?: string;\n /** Undefined means \"decide from the terminal\" — see `shouldOpenBrowser`. */\n open?: boolean;\n}\n\nconst USAGE = `\n mica — edit your Astro project's content, on this machine\n\n Usage\n $ npx @micacms/editor [content directory] open the editor\n $ npx @micacms/editor init add Mica to this Astro project\n $ npx @micacms/editor check [content dir] check every entry against the model\n\n With @micacms/editor installed in the project, \\`npx mica\\` is the same command.\n\n Options\n -c, --content <dir> The content root (default: ./content)\n -p, --port <number> Port to listen on (default: ${DEFAULT_PORT})\n --host <address> Address to bind (default: 127.0.0.1)\n --no-open Do not open the browser\n -h, --help Show this\n -v, --version Print the version\n\n There is no sign-in here, because there is nothing to sign in to: this mode\n has no identity provider, no account and no user. That is right for your own\n machine and wrong everywhere else — a Mica other people reach runs as a\n container with an identity provider. See https://micacms.com/docs/self-hosting/.\n`;\n\nexport function parse(argv: string[]): Options {\n const options: Options = { content: \"./content\" };\n\n for (let index = 0; index < argv.length; index++) {\n const flag = argv[index]!;\n const value = (): string => {\n const next = argv[++index];\n if (next === undefined || next.startsWith(\"-\")) fail(`${flag} needs a value.`);\n return next!;\n };\n\n switch (flag) {\n case \"-c\":\n case \"--content\":\n options.content = value();\n break;\n case \"-p\":\n case \"--port\":\n options.port = value();\n break;\n case \"--host\":\n options.host = value();\n break;\n case \"--open\":\n options.open = true;\n break;\n case \"--no-open\":\n options.open = false;\n break;\n case \"-h\":\n case \"--help\":\n process.stdout.write(`${USAGE}\\n`);\n process.exit(0);\n\n case \"-v\":\n case \"--version\":\n process.stdout.write(`${VERSION}\\n`);\n process.exit(0);\n\n default:\n // A bare path is the content root, so `npx @micacms/editor ./site/content`\n // does what it looks like it does.\n if (!flag.startsWith(\"-\")) options.content = flag;\n else fail(`Unknown option \"${flag}\".`);\n }\n }\n\n return options;\n}\n\n/**\n * A problem the person can fix, said the way a person reads.\n *\n * The title is the fact, the lines under it are what to do about it. Anything\n * printed here has already decided the command is over: the caller exits.\n */\nfunction report(title: string, lines: string[]): void {\n const body = lines.map((line) => (line ? ` ${line}` : \"\")).join(\"\\n\");\n process.stderr.write(`\\n ${title}\\n\\n${body}\\n\\n`);\n}\n\nfunction fail(message: string): never {\n report(message, [\"Run `npx @micacms/editor --help` to see every option.\"]);\n process.exit(64); // EX_USAGE\n}\n\n/**\n * Opening a browser is a courtesy, so it is offered only where it can be\n * welcome: an interactive terminal that somebody is sitting at. A script, a\n * container and a CI job all get a server and no surprises.\n */\nexport function shouldOpenBrowser(options: Options, env: NodeJS.ProcessEnv): boolean {\n if (options.open !== undefined) return options.open;\n return Boolean(process.stdout.isTTY) && !env[\"CI\"];\n}\n\nexport async function main(): Promise<void> {\n const [command, ...rest] = process.argv.slice(2);\n if (command === \"init\") return init(rest);\n if (command === \"check\") return check(rest);\n\n const options = parse(process.argv.slice(2));\n const content = path.resolve(options.content);\n\n if (!existsSync(content)) {\n report(`There is no content directory at ${content}.`, [\n \"Mica edits the directory your Astro project publishes content into —\",\n \"the `root` in your `defineSite({ root: \\\"./content\\\" })`.\",\n \"\",\n \" npx @micacms/editor ./path/to/content\",\n \"\",\n \"If this is a new project, create the directory and Mica writes the first\",\n \"entry into it.\",\n ]);\n process.exit(64); // EX_USAGE\n }\n\n // A far more useful message than the server's, because here we know the\n // project is on this machine and can name the command that fixes it.\n if (!existsSync(path.join(content, MANIFEST_PATH))) {\n report(`There is no content model at ${path.join(content, MANIFEST_PATH)}.`, [\n \"Mica ships without one: your Astro project defines the model, and building\",\n \"the project writes it there. Run this in the project, then start Mica again:\",\n \"\",\n \" npx astro sync # or build, or start the dev server once\",\n \"\",\n \"If you have not defined a model yet, run `npx @micacms/editor init`, or see\",\n \"https://micacms.com/docs/getting-started/.\",\n ]);\n process.exit(78); // EX_CONFIG\n }\n\n const defaults: Record<string, string> = {\n CONTENT_ROOT: content,\n HOST: options.host ?? \"127.0.0.1\",\n PORT: options.port ?? String(DEFAULT_PORT),\n // Tells the server it is talking to a person: one banner instead of the\n // startup log lines a collector wants.\n MICA_CLI: \"true\",\n MICA_OPEN: String(shouldOpenBrowser(options, process.env)),\n };\n\n for (const [key, value] of Object.entries(defaults)) {\n process.env[key] ??= value;\n }\n // MICA_PUBLIC_URL has to agree with the port, and only matters here for\n // cookie scope and the Origin check — both of which are this process's own\n // address.\n process.env[\"MICA_PUBLIC_URL\"] ??= `http://localhost:${process.env[\"PORT\"]}`;\n\n // The server is imported rather than spawned: one process, one signal\n // handler, and a stack trace that points at the real thing.\n await import(\"./index.js\");\n}\n\nfunction init(argv: string[]): void {\n if (argv.length > 0) fail(`init takes no options; it works in the current directory.`);\n try {\n process.stdout.write(formatInit(initProject(process.cwd())));\n } catch (error) {\n if (!(error instanceof InitError)) throw error;\n report(error.message, []);\n process.exit(64); // EX_USAGE\n }\n}\n\nasync function check(argv: string[]): Promise<void> {\n const { content } = parse(argv);\n const root = path.resolve(content);\n const result = await checkContent(root);\n process.stdout.write(formatReport(result, root));\n if (result.problems.some((problem) => problem.level === \"error\")) process.exit(1);\n}\n\n// Only when this file *is* the command. Importing it, as the tests do, must\n// not start a server.\n//\n// `realpathSync` matters: npm and npx run this file through a symlink in\n// `node_modules/.bin`, and Node resolves `import.meta.url` to the symlink's\n// real target. Comparing against the raw `argv[1]` never matched there, so\n// `npx @micacms/editor` silently did nothing.\nif (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {\n main().catch((error: unknown) => {\n process.stderr.write(`\\n${error instanceof Error ? error.stack : String(error)}\\n`);\n process.exit(1);\n });\n}\n"]}
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\n/**\n * `npx @micacms/editor` — Mica against a project on this machine.\n *\n * The point of this file is that adopting Mica should not mean adopting this\n * repository. Everything it does is set environment variables and hand over to\n * the same entry point a container runs, so there is one server with one\n * configuration path and this is a front door rather than a second one.\n *\n * Two defaults differ from a container's, and both are deliberate:\n *\n * - it binds to loopback. A container binds 0.0.0.0 because it has to; a\n * laptop does not, and this editor has no authentication. Anything\n * reachable by anyone else runs @micacms/self-hosted with an identity\n * provider — see https://micacms.com/docs/self-hosting/.\n * - it talks to a person instead of to a log collector: one short banner\n * saying where the editor is and what happens when you save, and the\n * browser opened on it. `MICA_CLI` is how the server is told which of the\n * two audiences it has.\n *\n * There is no state to keep anywhere: the local editor has no database, no\n * account and no session, so nothing is written outside the content\n * directory.\n *\n * A flag wins over the environment, and the environment over a default. No\n * `.env` file is read: the project's `.env` belongs to the site.\n */\n\nimport { existsSync, realpathSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\n\nimport { MANIFEST_PATH } from \"@micacms/core\";\nimport { DEFAULT_PORT, VERSION } from \"@micacms/runtime\";\n\nimport { checkContent, formatReport } from \"./check.js\";\nimport { formatInit, initProject, InitError } from \"./init.js\";\nimport { formatMigration, migrateLayout } from \"./migrate.js\";\n\nexport interface Options {\n /** Undefined means \"not given\": `MICA_CONTENT_ROOT`, else `./content`. */\n content?: string;\n port?: string;\n host?: string;\n /** Undefined means \"decide from the terminal\" — see `shouldOpenBrowser`. */\n open?: boolean;\n}\n\nconst USAGE = `\n mica — edit your Astro project's content, on this machine\n\n Usage\n $ npx @micacms/editor [content directory] open the editor\n $ npx @micacms/editor init add Mica to this Astro project\n $ npx @micacms/editor check [content dir] check every entry against the model\n $ npx @micacms/editor migrate-layout [dir] move entries into the layout of the model\n\n With @micacms/editor installed in the project, \\`npx mica\\` is the same command.\n\n Options\n -c, --content <dir> The content root (default: ./content)\n -p, --port <number> Port to listen on (default: ${DEFAULT_PORT})\n --host <address> Address to bind (default: 127.0.0.1)\n --no-open Do not open the browser\n -h, --help Show this\n -v, --version Print the version\n\n There is no sign-in here, because there is nothing to sign in to: this mode\n has no identity provider, no account and no user. That is right for your own\n machine and wrong everywhere else — a Mica other people reach runs as a\n container with an identity provider. See https://micacms.com/docs/self-hosting/.\n`;\n\nexport function parse(argv: string[]): Options {\n const options: Options = {};\n\n for (let index = 0; index < argv.length; index++) {\n const flag = argv[index]!;\n const value = (): string => {\n const next = argv[++index];\n if (next === undefined || next.startsWith(\"-\")) fail(`${flag} needs a value.`);\n return next!;\n };\n\n switch (flag) {\n case \"-c\":\n case \"--content\":\n options.content = value();\n break;\n case \"-p\":\n case \"--port\":\n options.port = value();\n break;\n case \"--host\":\n options.host = value();\n break;\n case \"--open\":\n options.open = true;\n break;\n case \"--no-open\":\n options.open = false;\n break;\n case \"-h\":\n case \"--help\":\n process.stdout.write(`${USAGE}\\n`);\n process.exit(0);\n\n case \"-v\":\n case \"--version\":\n process.stdout.write(`${VERSION}\\n`);\n process.exit(0);\n\n default:\n // A bare path is the content root, so `npx @micacms/editor ./site/content`\n // does what it looks like it does.\n if (!flag.startsWith(\"-\")) options.content = flag;\n else fail(`Unknown option \"${flag}\".`);\n }\n }\n\n return options;\n}\n\n/**\n * A problem the person can fix, said the way a person reads.\n *\n * The title is the fact, the lines under it are what to do about it. Anything\n * printed here has already decided the command is over: the caller exits.\n */\nfunction report(title: string, lines: string[]): void {\n const body = lines.map((line) => (line ? ` ${line}` : \"\")).join(\"\\n\");\n process.stderr.write(`\\n ${title}\\n\\n${body}\\n\\n`);\n}\n\nfunction fail(message: string): never {\n report(message, [\"Run `npx @micacms/editor --help` to see every option.\"]);\n process.exit(64); // EX_USAGE\n}\n\n/**\n * Opening a browser is a courtesy, so it is offered only where it can be\n * welcome: an interactive terminal that somebody is sitting at. A script, a\n * container and a CI job all get a server and no surprises.\n */\nexport function shouldOpenBrowser(options: Options, env: NodeJS.ProcessEnv): boolean {\n if (options.open !== undefined) return options.open;\n return Boolean(process.stdout.isTTY) && !env[\"CI\"];\n}\n\n/** A variable's value, with a blank one counted as not set. */\nconst set = (value: string | undefined): string | undefined => (value?.trim() ? value : undefined);\n\n/** The content root: the flag or path, else `MICA_CONTENT_ROOT`, else `./content`. */\nexport function contentRoot(options: Options, env: NodeJS.ProcessEnv): string {\n return path.resolve(options.content ?? set(env[\"MICA_CONTENT_ROOT\"]) ?? \"./content\");\n}\n\n/** What the server reads: a flag wins over the environment, and the environment over a default. */\nexport function serverEnvironment(options: Options, env: NodeJS.ProcessEnv, content: string): Record<string, string> {\n const port = options.port ?? set(env[\"PORT\"]) ?? String(DEFAULT_PORT);\n return {\n // The checks ran on this path, so it is the one the server gets.\n MICA_CONTENT_ROOT: content,\n HOST: options.host ?? set(env[\"HOST\"]) ?? \"127.0.0.1\",\n PORT: port,\n // It must agree with the port: it scopes the cookie and the Origin check.\n MICA_PUBLIC_URL: (!options.port && set(env[\"MICA_PUBLIC_URL\"])) || `http://localhost:${port}`,\n // A person, not a log collector: one banner instead of startup log lines.\n MICA_CLI: \"true\",\n MICA_OPEN: String(shouldOpenBrowser(options, env)),\n };\n}\n\nexport async function main(): Promise<void> {\n const [command, ...rest] = process.argv.slice(2);\n if (command === \"init\") return init(rest);\n if (command === \"check\") return check(rest);\n if (command === \"migrate-layout\") return migrate(rest);\n\n const options = parse(process.argv.slice(2));\n const content = contentRoot(options, process.env);\n\n if (!existsSync(content)) {\n report(`There is no content directory at ${content}.`, [\n \"Mica edits the directory your Astro project publishes content into —\",\n \"the `root` in your `defineSite({ root: \\\"./content\\\" })`.\",\n \"\",\n \" npx @micacms/editor ./path/to/content\",\n \"\",\n \"If this is a new project, create the directory and Mica writes the first\",\n \"entry into it.\",\n ]);\n process.exit(64); // EX_USAGE\n }\n\n // A far more useful message than the server's, because here we know the\n // project is on this machine and can name the command that fixes it.\n if (!existsSync(path.join(content, MANIFEST_PATH))) {\n report(`There is no content model at ${path.join(content, MANIFEST_PATH)}.`, [\n \"Mica ships without one: your Astro project defines the model, and building\",\n \"the project writes it there. Run this in the project, then start Mica again:\",\n \"\",\n \" npx astro sync # or build, or start the dev server once\",\n \"\",\n \"If you have not defined a model yet, run `npx @micacms/editor init`, or see\",\n \"https://micacms.com/docs/getting-started/.\",\n ]);\n process.exit(78); // EX_CONFIG\n }\n\n Object.assign(process.env, serverEnvironment(options, process.env, content));\n\n // The server is imported rather than spawned: one process, one signal\n // handler, and a stack trace that points at the real thing.\n await import(\"./index.js\");\n}\n\nfunction init(argv: string[]): void {\n if (argv.length > 0) fail(`init takes no options; it works in the current directory.`);\n try {\n process.stdout.write(formatInit(initProject(process.cwd())));\n } catch (error) {\n if (!(error instanceof InitError)) throw error;\n report(error.message, []);\n process.exit(64); // EX_USAGE\n }\n}\n\nasync function check(argv: string[]): Promise<void> {\n const root = contentRoot(parse(argv), process.env);\n const result = await checkContent(root);\n process.stdout.write(formatReport(result, root));\n if (result.problems.some((problem) => problem.level === \"error\")) process.exit(1);\n}\n\nasync function migrate(argv: string[]): Promise<void> {\n const result = await migrateLayout(contentRoot(parse(argv), process.env));\n process.stdout.write(formatMigration(result));\n if (result.conflicts.length > 0) process.exit(1);\n}\n\n// Only when this file *is* the command. Importing it, as the tests do, must\n// not start a server.\n//\n// `realpathSync` matters: npm and npx run this file through a symlink in\n// `node_modules/.bin`, and Node resolves `import.meta.url` to the symlink's\n// real target. Comparing against the raw `argv[1]` never matched there, so\n// `npx @micacms/editor` silently did nothing.\nif (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {\n main().catch((error: unknown) => {\n process.stderr.write(`\\n${error instanceof Error ? error.stack : String(error)}\\n`);\n process.exit(1);\n });\n}\n"]}
package/dist/config.d.ts CHANGED
@@ -8,5 +8,6 @@ export type LocalConfig = RuntimeConfig & {
8
8
  MICA_CLI: boolean;
9
9
  MICA_OPEN: boolean;
10
10
  };
11
+ /** Reads the process environment only: a site's `.env` holds the site's settings, not the editor's. */
11
12
  export declare function loadConfig(env?: NodeJS.ProcessEnv): LocalConfig;
12
13
  //# sourceMappingURL=config.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,EASL,KAAK,aAAa,EACnB,MAAM,kBAAkB,CAAC;AA6C1B,MAAM,MAAM,WAAW,GAAG,aAAa,GAAG;IAAE,QAAQ,EAAE,OAAO,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE,CAAC;AAEpF,wBAAgB,UAAU,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,WAAW,CAwC5E"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,EAQL,KAAK,aAAa,EACnB,MAAM,kBAAkB,CAAC;AA6C1B,MAAM,MAAM,WAAW,GAAG,aAAa,GAAG;IAAE,QAAQ,EAAE,OAAO,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE,CAAC;AAEpF,uGAAuG;AACvG,wBAAgB,UAAU,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,WAAW,CAoC5E"}
package/dist/config.js CHANGED
@@ -4,20 +4,20 @@
4
4
  * production and any non-loopback URL or address, with no override.
5
5
  */
6
6
  import { z } from "zod";
7
- import { bool, ConfigError, deriveRuntime, describeConfigIssues, loadEnvFile, LOOPBACK, runtimeEnv, withoutBlanks, } from "@micacms/runtime";
8
- const { NODE_ENV, LOG_LEVEL, HOST, PORT, MICA_PUBLIC_URL, CONTENT_ROOT, MICA_LOCAL_PUBLISH, MAX_UPLOAD_BYTES, ADMIN_ORIGINS, ADMIN_DIR, } = runtimeEnv;
7
+ import { bool, ConfigError, deriveRuntime, describeConfigIssues, LOOPBACK, runtimeEnv, withoutBlanks, } from "@micacms/runtime";
8
+ const { NODE_ENV, MICA_LOG_LEVEL, HOST, PORT, MICA_PUBLIC_URL, MICA_CONTENT_ROOT, MICA_LOCAL_PUBLISH, MICA_MAX_UPLOAD_BYTES, MICA_ADMIN_ORIGINS, MICA_ADMIN_DIR, } = runtimeEnv;
9
9
  const schema = z
10
10
  .object({
11
11
  NODE_ENV,
12
- LOG_LEVEL,
12
+ MICA_LOG_LEVEL,
13
13
  HOST,
14
14
  PORT,
15
15
  MICA_PUBLIC_URL,
16
- CONTENT_ROOT,
16
+ MICA_CONTENT_ROOT,
17
17
  MICA_LOCAL_PUBLISH,
18
- MAX_UPLOAD_BYTES,
19
- ADMIN_ORIGINS,
20
- ADMIN_DIR,
18
+ MICA_MAX_UPLOAD_BYTES,
19
+ MICA_ADMIN_ORIGINS,
20
+ MICA_ADMIN_DIR,
21
21
  /**
22
22
  * Set only by the `mica` bin: a terminal gets one banner, a log collector
23
23
  * gets structured lines.
@@ -30,15 +30,12 @@ const schema = z
30
30
  ...deriveRuntime({
31
31
  ...env,
32
32
  // There is no proxy in front of a loopback server to trust.
33
- TRUST_PROXY: false,
33
+ MICA_TRUST_PROXY: false,
34
34
  }),
35
35
  HOST: env.HOST ?? "127.0.0.1",
36
36
  }));
37
+ /** Reads the process environment only: a site's `.env` holds the site's settings, not the editor's. */
37
38
  export function loadConfig(env = process.env) {
38
- // Only when reading the ambient environment: tests pass an explicit object
39
- // and must not pick up whatever `.env` sits in the working directory.
40
- if (env === process.env)
41
- loadEnvFile();
42
39
  const parsed = schema.safeParse(withoutBlanks(env));
43
40
  if (!parsed.success)
44
41
  throw new ConfigError(describeConfigIssues(parsed.error));
@@ -48,7 +45,7 @@ export function loadConfig(env = process.env) {
48
45
  throw new ConfigError("The local editor has no authentication, and NODE_ENV=production.\n" +
49
46
  " There is no identity provider and no user: every request is allowed.\n" +
50
47
  " A deployment runs @micacms/self-hosted with an OIDC provider instead —\n" +
51
- " see docs/self-hosting.md.");
48
+ " see https://micacms.com/docs/self-hosting/.");
52
49
  }
53
50
  const host = new URL(config.MICA_PUBLIC_URL).hostname;
54
51
  if (!LOOPBACK.has(host)) {
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EACL,IAAI,EACJ,WAAW,EACX,aAAa,EACb,oBAAoB,EACpB,WAAW,EACX,QAAQ,EACR,UAAU,EACV,aAAa,GAEd,MAAM,kBAAkB,CAAC;AAE1B,MAAM,EACJ,QAAQ,EACR,SAAS,EACT,IAAI,EACJ,IAAI,EACJ,eAAe,EACf,YAAY,EACZ,kBAAkB,EAClB,gBAAgB,EAChB,aAAa,EACb,SAAS,GACV,GAAG,UAAU,CAAC;AAEf,MAAM,MAAM,GAAG,CAAC;KACb,MAAM,CAAC;IACN,QAAQ;IACR,SAAS;IACT,IAAI;IACJ,IAAI;IACJ,eAAe;IACf,YAAY;IACZ,kBAAkB;IAClB,gBAAgB;IAChB,aAAa;IACb,SAAS;IAET;;;OAGG;IACH,QAAQ,EAAE,IAAI,CAAC,KAAK,CAAC;IACrB,0EAA0E;IAC1E,SAAS,EAAE,IAAI,CAAC,KAAK,CAAC;CACvB,CAAC;KACD,SAAS,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;IACnB,GAAG,aAAa,CAAC;QACf,GAAG,GAAG;QACN,4DAA4D;QAC5D,WAAW,EAAE,KAAK;KACnB,CAAC;IACF,IAAI,EAAE,GAAG,CAAC,IAAI,IAAI,WAAW;CAC9B,CAAC,CAAC,CAAC;AAIN,MAAM,UAAU,UAAU,CAAC,MAAyB,OAAO,CAAC,GAAG;IAC7D,2EAA2E;IAC3E,sEAAsE;IACtE,IAAI,GAAG,KAAK,OAAO,CAAC,GAAG;QAAE,WAAW,EAAE,CAAC;IAEvC,MAAM,MAAM,GAAG,MAAM,CAAC,SAAS,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC;IACpD,IAAI,CAAC,MAAM,CAAC,OAAO;QAAE,MAAM,IAAI,WAAW,CAAC,oBAAoB,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IAE/E,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC;IAE3B,8FAA8F;IAC9F,IAAI,MAAM,CAAC,YAAY,EAAE,CAAC;QACxB,MAAM,IAAI,WAAW,CACnB,oEAAoE;YAClE,0EAA0E;YAC1E,4EAA4E;YAC5E,6BAA6B,CAChC,CAAC;IACJ,CAAC;IAED,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC,QAAQ,CAAC;IACtD,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;QACxB,MAAM,IAAI,WAAW,CACnB,0EAA0E,IAAI,MAAM;YAClF,4EAA4E;YAC5E,6DAA6D,CAChE,CAAC;IACJ,CAAC;IAED,6EAA6E;IAC7E,oCAAoC;IACpC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;QAC/B,MAAM,IAAI,WAAW,CACnB,wDAAwD,MAAM,CAAC,IAAI,MAAM;YACvE,4EAA4E;YAC5E,0DAA0D,CAC7D,CAAC;IACJ,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC","sourcesContent":["/**\n * The local editor's configuration: the runtime variables minus everything that\n * describes a deployment. The server has no authentication, so it refuses\n * production and any non-loopback URL or address, with no override.\n */\n\nimport { z } from \"zod\";\n\nimport {\n bool,\n ConfigError,\n deriveRuntime,\n describeConfigIssues,\n loadEnvFile,\n LOOPBACK,\n runtimeEnv,\n withoutBlanks,\n type RuntimeConfig,\n} from \"@micacms/runtime\";\n\nconst {\n NODE_ENV,\n LOG_LEVEL,\n HOST,\n PORT,\n MICA_PUBLIC_URL,\n CONTENT_ROOT,\n MICA_LOCAL_PUBLISH,\n MAX_UPLOAD_BYTES,\n ADMIN_ORIGINS,\n ADMIN_DIR,\n} = runtimeEnv;\n\nconst schema = z\n .object({\n NODE_ENV,\n LOG_LEVEL,\n HOST,\n PORT,\n MICA_PUBLIC_URL,\n CONTENT_ROOT,\n MICA_LOCAL_PUBLISH,\n MAX_UPLOAD_BYTES,\n ADMIN_ORIGINS,\n ADMIN_DIR,\n\n /**\n * Set only by the `mica` bin: a terminal gets one banner, a log collector\n * gets structured lines.\n */\n MICA_CLI: bool(false),\n /** Whether that front door should open a browser once it is listening. */\n MICA_OPEN: bool(false),\n })\n .transform((env) => ({\n ...deriveRuntime({\n ...env,\n // There is no proxy in front of a loopback server to trust.\n TRUST_PROXY: false,\n }),\n HOST: env.HOST ?? \"127.0.0.1\",\n }));\n\nexport type LocalConfig = RuntimeConfig & { MICA_CLI: boolean; MICA_OPEN: boolean };\n\nexport function loadConfig(env: NodeJS.ProcessEnv = process.env): LocalConfig {\n // Only when reading the ambient environment: tests pass an explicit object\n // and must not pick up whatever `.env` sits in the working directory.\n if (env === process.env) loadEnvFile();\n\n const parsed = schema.safeParse(withoutBlanks(env));\n if (!parsed.success) throw new ConfigError(describeConfigIssues(parsed.error));\n\n const config = parsed.data;\n\n // The local editor has no authentication, so it must never be reachable from another machine.\n if (config.isProduction) {\n throw new ConfigError(\n \"The local editor has no authentication, and NODE_ENV=production.\\n\" +\n \" There is no identity provider and no user: every request is allowed.\\n\" +\n \" A deployment runs @micacms/self-hosted with an OIDC provider instead —\\n\" +\n \" see docs/self-hosting.md.\",\n );\n }\n\n const host = new URL(config.MICA_PUBLIC_URL).hostname;\n if (!LOOPBACK.has(host)) {\n throw new ConfigError(\n `The local editor has no authentication, but MICA_PUBLIC_URL points at \"${host}\".\\n` +\n \" It is for a developer's own machine only. Anything reachable by anyone\\n\" +\n \" else runs @micacms/self-hosted with an identity provider.\",\n );\n }\n\n // A `localhost` URL with a socket on `0.0.0.0` opens the editor to the whole\n // network. No override, on purpose.\n if (!LOOPBACK.has(config.HOST)) {\n throw new ConfigError(\n `The local editor has no authentication, but HOST is \"${config.HOST}\".\\n` +\n \" It binds to the loopback address and nothing else. Use HOST=127.0.0.1,\\n\" +\n \" or run @micacms/self-hosted with an identity provider.\",\n );\n }\n\n return config;\n}\n"]}
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EACL,IAAI,EACJ,WAAW,EACX,aAAa,EACb,oBAAoB,EACpB,QAAQ,EACR,UAAU,EACV,aAAa,GAEd,MAAM,kBAAkB,CAAC;AAE1B,MAAM,EACJ,QAAQ,EACR,cAAc,EACd,IAAI,EACJ,IAAI,EACJ,eAAe,EACf,iBAAiB,EACjB,kBAAkB,EAClB,qBAAqB,EACrB,kBAAkB,EAClB,cAAc,GACf,GAAG,UAAU,CAAC;AAEf,MAAM,MAAM,GAAG,CAAC;KACb,MAAM,CAAC;IACN,QAAQ;IACR,cAAc;IACd,IAAI;IACJ,IAAI;IACJ,eAAe;IACf,iBAAiB;IACjB,kBAAkB;IAClB,qBAAqB;IACrB,kBAAkB;IAClB,cAAc;IAEd;;;OAGG;IACH,QAAQ,EAAE,IAAI,CAAC,KAAK,CAAC;IACrB,0EAA0E;IAC1E,SAAS,EAAE,IAAI,CAAC,KAAK,CAAC;CACvB,CAAC;KACD,SAAS,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;IACnB,GAAG,aAAa,CAAC;QACf,GAAG,GAAG;QACN,4DAA4D;QAC5D,gBAAgB,EAAE,KAAK;KACxB,CAAC;IACF,IAAI,EAAE,GAAG,CAAC,IAAI,IAAI,WAAW;CAC9B,CAAC,CAAC,CAAC;AAIN,uGAAuG;AACvG,MAAM,UAAU,UAAU,CAAC,MAAyB,OAAO,CAAC,GAAG;IAC7D,MAAM,MAAM,GAAG,MAAM,CAAC,SAAS,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC;IACpD,IAAI,CAAC,MAAM,CAAC,OAAO;QAAE,MAAM,IAAI,WAAW,CAAC,oBAAoB,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IAE/E,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC;IAE3B,8FAA8F;IAC9F,IAAI,MAAM,CAAC,YAAY,EAAE,CAAC;QACxB,MAAM,IAAI,WAAW,CACnB,oEAAoE;YAClE,0EAA0E;YAC1E,4EAA4E;YAC5E,+CAA+C,CAClD,CAAC;IACJ,CAAC;IAED,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC,QAAQ,CAAC;IACtD,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;QACxB,MAAM,IAAI,WAAW,CACnB,0EAA0E,IAAI,MAAM;YAClF,4EAA4E;YAC5E,6DAA6D,CAChE,CAAC;IACJ,CAAC;IAED,6EAA6E;IAC7E,oCAAoC;IACpC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;QAC/B,MAAM,IAAI,WAAW,CACnB,wDAAwD,MAAM,CAAC,IAAI,MAAM;YACvE,4EAA4E;YAC5E,0DAA0D,CAC7D,CAAC;IACJ,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC","sourcesContent":["/**\n * The local editor's configuration: the runtime variables minus everything that\n * describes a deployment. The server has no authentication, so it refuses\n * production and any non-loopback URL or address, with no override.\n */\n\nimport { z } from \"zod\";\n\nimport {\n bool,\n ConfigError,\n deriveRuntime,\n describeConfigIssues,\n LOOPBACK,\n runtimeEnv,\n withoutBlanks,\n type RuntimeConfig,\n} from \"@micacms/runtime\";\n\nconst {\n NODE_ENV,\n MICA_LOG_LEVEL,\n HOST,\n PORT,\n MICA_PUBLIC_URL,\n MICA_CONTENT_ROOT,\n MICA_LOCAL_PUBLISH,\n MICA_MAX_UPLOAD_BYTES,\n MICA_ADMIN_ORIGINS,\n MICA_ADMIN_DIR,\n} = runtimeEnv;\n\nconst schema = z\n .object({\n NODE_ENV,\n MICA_LOG_LEVEL,\n HOST,\n PORT,\n MICA_PUBLIC_URL,\n MICA_CONTENT_ROOT,\n MICA_LOCAL_PUBLISH,\n MICA_MAX_UPLOAD_BYTES,\n MICA_ADMIN_ORIGINS,\n MICA_ADMIN_DIR,\n\n /**\n * Set only by the `mica` bin: a terminal gets one banner, a log collector\n * gets structured lines.\n */\n MICA_CLI: bool(false),\n /** Whether that front door should open a browser once it is listening. */\n MICA_OPEN: bool(false),\n })\n .transform((env) => ({\n ...deriveRuntime({\n ...env,\n // There is no proxy in front of a loopback server to trust.\n MICA_TRUST_PROXY: false,\n }),\n HOST: env.HOST ?? \"127.0.0.1\",\n }));\n\nexport type LocalConfig = RuntimeConfig & { MICA_CLI: boolean; MICA_OPEN: boolean };\n\n/** Reads the process environment only: a site's `.env` holds the site's settings, not the editor's. */\nexport function loadConfig(env: NodeJS.ProcessEnv = process.env): LocalConfig {\n const parsed = schema.safeParse(withoutBlanks(env));\n if (!parsed.success) throw new ConfigError(describeConfigIssues(parsed.error));\n\n const config = parsed.data;\n\n // The local editor has no authentication, so it must never be reachable from another machine.\n if (config.isProduction) {\n throw new ConfigError(\n \"The local editor has no authentication, and NODE_ENV=production.\\n\" +\n \" There is no identity provider and no user: every request is allowed.\\n\" +\n \" A deployment runs @micacms/self-hosted with an OIDC provider instead —\\n\" +\n \" see https://micacms.com/docs/self-hosting/.\",\n );\n }\n\n const host = new URL(config.MICA_PUBLIC_URL).hostname;\n if (!LOOPBACK.has(host)) {\n throw new ConfigError(\n `The local editor has no authentication, but MICA_PUBLIC_URL points at \"${host}\".\\n` +\n \" It is for a developer's own machine only. Anything reachable by anyone\\n\" +\n \" else runs @micacms/self-hosted with an identity provider.\",\n );\n }\n\n // A `localhost` URL with a socket on `0.0.0.0` opens the editor to the whole\n // network. No override, on purpose.\n if (!LOOPBACK.has(config.HOST)) {\n throw new ConfigError(\n `The local editor has no authentication, but HOST is \"${config.HOST}\".\\n` +\n \" It binds to the loopback address and nothing else. Use HOST=127.0.0.1,\\n\" +\n \" or run @micacms/self-hosted with an identity provider.\",\n );\n }\n\n return config;\n}\n"]}
package/dist/guards.js CHANGED
@@ -23,7 +23,7 @@ export function hostGuard() {
23
23
  return c.text("This is a local Mica editor. It answers only to this machine —\n" +
24
24
  `"${host}" is not it.\n\n` +
25
25
  "To let other people edit, deploy @micacms/self-hosted with an identity\n" +
26
- "provider: https://github.com/micacms/mica/blob/main/docs/self-hosting.md\n", 403);
26
+ "provider: https://micacms.com/docs/self-hosting/\n", 403);
27
27
  }
28
28
  await next();
29
29
  };
@@ -1 +1 @@
1
- {"version":3,"file":"guards.js","sourceRoot":"","sources":["../src/guards.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAMH,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,CAAC,WAAW,EAAE,WAAW,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC;AAEpE,kEAAkE;AAClE,SAAS,UAAU,CAAC,MAAc;IAChC,IAAI,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QAC3B,MAAM,GAAG,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAChC,OAAO,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,GAAG,CAAC,CAAC,CAAC;IACrD,CAAC;IACD,OAAO,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;AACpC,CAAC;AAED,MAAM,UAAU,SAAS;IACvB,OAAO,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE;QACvB,MAAM,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;QAExC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC,EAAE,CAAC;YACjD,OAAO,CAAC,CAAC,IAAI,CACX,kEAAkE;gBAChE,IAAI,IAAI,kBAAkB;gBAC1B,0EAA0E;gBAC1E,4EAA4E,EAC9E,GAAG,CACJ,CAAC;QACJ,CAAC;QAED,MAAM,IAAI,EAAE,CAAC;IACf,CAAC,CAAC;AACJ,CAAC","sourcesContent":["/**\n * The Host allowlist.\n *\n * Under DNS rebinding, a page re-resolves its own name to `127.0.0.1` and the\n * browser then treats requests to it as same-origin. Writes still fail on the\n * Origin check; *reads* would succeed. Local mode keeps no session or token,\n * so this server answers only to names that are this machine. It applies in\n * front of everything, the health endpoints included.\n */\n\nimport type { MiddlewareHandler } from \"hono\";\n\nimport type { Env } from \"@micacms/runtime\";\n\nconst ALLOWED = new Set([\"localhost\", \"127.0.0.1\", \"[::1]\", \"::1\"]);\n\n/** The hostname part of a Host header, brackets kept for IPv6. */\nfunction hostnameOf(header: string): string {\n if (header.startsWith(\"[\")) {\n const end = header.indexOf(\"]\");\n return end < 0 ? header : header.slice(0, end + 1);\n }\n return header.split(\":\")[0] ?? \"\";\n}\n\nexport function hostGuard(): MiddlewareHandler<Env> {\n return async (c, next) => {\n const host = c.req.header(\"host\") ?? \"\";\n\n if (!ALLOWED.has(hostnameOf(host).toLowerCase())) {\n return c.text(\n \"This is a local Mica editor. It answers only to this machine —\\n\" +\n `\"${host}\" is not it.\\n\\n` +\n \"To let other people edit, deploy @micacms/self-hosted with an identity\\n\" +\n \"provider: https://github.com/micacms/mica/blob/main/docs/self-hosting.md\\n\",\n 403,\n );\n }\n\n await next();\n };\n}\n"]}
1
+ {"version":3,"file":"guards.js","sourceRoot":"","sources":["../src/guards.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAMH,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,CAAC,WAAW,EAAE,WAAW,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC;AAEpE,kEAAkE;AAClE,SAAS,UAAU,CAAC,MAAc;IAChC,IAAI,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QAC3B,MAAM,GAAG,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAChC,OAAO,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,GAAG,CAAC,CAAC,CAAC;IACrD,CAAC;IACD,OAAO,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;AACpC,CAAC;AAED,MAAM,UAAU,SAAS;IACvB,OAAO,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE;QACvB,MAAM,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;QAExC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC,EAAE,CAAC;YACjD,OAAO,CAAC,CAAC,IAAI,CACX,kEAAkE;gBAChE,IAAI,IAAI,kBAAkB;gBAC1B,0EAA0E;gBAC1E,oDAAoD,EACtD,GAAG,CACJ,CAAC;QACJ,CAAC;QAED,MAAM,IAAI,EAAE,CAAC;IACf,CAAC,CAAC;AACJ,CAAC","sourcesContent":["/**\n * The Host allowlist.\n *\n * Under DNS rebinding, a page re-resolves its own name to `127.0.0.1` and the\n * browser then treats requests to it as same-origin. Writes still fail on the\n * Origin check; *reads* would succeed. Local mode keeps no session or token,\n * so this server answers only to names that are this machine. It applies in\n * front of everything, the health endpoints included.\n */\n\nimport type { MiddlewareHandler } from \"hono\";\n\nimport type { Env } from \"@micacms/runtime\";\n\nconst ALLOWED = new Set([\"localhost\", \"127.0.0.1\", \"[::1]\", \"::1\"]);\n\n/** The hostname part of a Host header, brackets kept for IPv6. */\nfunction hostnameOf(header: string): string {\n if (header.startsWith(\"[\")) {\n const end = header.indexOf(\"]\");\n return end < 0 ? header : header.slice(0, end + 1);\n }\n return header.split(\":\")[0] ?? \"\";\n}\n\nexport function hostGuard(): MiddlewareHandler<Env> {\n return async (c, next) => {\n const host = c.req.header(\"host\") ?? \"\";\n\n if (!ALLOWED.has(hostnameOf(host).toLowerCase())) {\n return c.text(\n \"This is a local Mica editor. It answers only to this machine —\\n\" +\n `\"${host}\" is not it.\\n\\n` +\n \"To let other people edit, deploy @micacms/self-hosted with an identity\\n\" +\n \"provider: https://micacms.com/docs/self-hosting/\\n\",\n 403,\n );\n }\n\n await next();\n };\n}\n"]}
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@
4
4
  * content directory and installs no auth code that can be misconfigured.
5
5
  */
6
6
  import { MANIFEST_PATH } from "@micacms/core";
7
- import { ConfigError, contentFiles, createApp, createLogger, createRuntimeContext, loadSchema, SchemaError, schemaFingerprint, schemaOrigin, startServer, VERSION, } from "@micacms/runtime";
7
+ import { ConfigError, contentFiles, createApp, createLogger, createRuntimeContext, layoutProblems, loadSchema, SchemaError, schemaFingerprint, schemaOrigin, startServer, VERSION, } from "@micacms/runtime";
8
8
  import { editorUiDist } from "./admin-dir.js";
9
9
  import { portInUse, startupBanner, supportsColor } from "./banner.js";
10
10
  import { loadConfig } from "./config.js";
@@ -22,7 +22,7 @@ async function main() {
22
22
  }
23
23
  throw error;
24
24
  }
25
- const logger = createLogger({ level: config.LOG_LEVEL, pretty: true });
25
+ const logger = createLogger({ level: config.MICA_LOG_LEVEL, pretty: true });
26
26
  // A bad or absent model is a project mistake, not a crash: exit like a bad
27
27
  // environment, without a stack trace.
28
28
  const files = contentFiles({ root: config.contentRoot });
@@ -37,6 +37,11 @@ async function main() {
37
37
  }
38
38
  throw error;
39
39
  }
40
+ const misplaced = await layoutProblems(files, model);
41
+ if (misplaced.length > 0) {
42
+ process.stderr.write(`\n${misplaced.join("\n\n")}\n\n`);
43
+ process.exit(78); // EX_CONFIG
44
+ }
40
45
  const schemaRevision = await files.revision(MANIFEST_PATH);
41
46
  const ctx = createRuntimeContext({
42
47
  config,
@@ -50,7 +55,7 @@ async function main() {
50
55
  });
51
56
  const app = createApp(ctx, {
52
57
  guards: [hostGuard()],
53
- adminDir: config.ADMIN_DIR ?? editorUiDist(),
58
+ adminDir: config.MICA_ADMIN_DIR ?? editorUiDist(),
54
59
  });
55
60
  const fingerprint = await schemaFingerprint(model);
56
61
  startServer({
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The local editor's process entry point: the developer's checkout, the\n * auth-neutral runtime and the Host allowlist. It writes nothing outside the\n * content directory and installs no auth code that can be misconfigured.\n */\n\nimport { MANIFEST_PATH } from \"@micacms/core\";\nimport {\n ConfigError,\n contentFiles,\n createApp,\n createLogger,\n createRuntimeContext,\n loadSchema,\n SchemaError,\n schemaFingerprint,\n schemaOrigin,\n startServer,\n VERSION,\n} from \"@micacms/runtime\";\n\nimport { editorUiDist } from \"./admin-dir.js\";\nimport { portInUse, startupBanner, supportsColor } from \"./banner.js\";\nimport { loadConfig } from \"./config.js\";\nimport { hostGuard } from \"./guards.js\";\nimport { openInBrowser } from \"./open.js\";\n\nasync function main(): Promise<void> {\n let config;\n try {\n config = loadConfig();\n } catch (error) {\n if (error instanceof ConfigError) {\n process.stderr.write(`\\n${error.message}\\n\\n`);\n process.exit(78); // EX_CONFIG\n }\n throw error;\n }\n\n const logger = createLogger({ level: config.LOG_LEVEL, pretty: true });\n\n // A bad or absent model is a project mistake, not a crash: exit like a bad\n // environment, without a stack trace.\n const files = contentFiles({ root: config.contentRoot });\n\n let model;\n try {\n model = await loadSchema(files);\n } catch (error) {\n if (error instanceof SchemaError) {\n process.stderr.write(`\\n${error.message}\\n\\n`);\n process.exit(78); // EX_CONFIG\n }\n throw error;\n }\n\n const schemaRevision = await files.revision(MANIFEST_PATH);\n\n const ctx = createRuntimeContext({\n config,\n model,\n files,\n schemaRevision,\n logger,\n operational: () => ({\n auth: \"none — the local editor has no accounts and no sign-in\",\n }),\n });\n\n const app = createApp(ctx, {\n guards: [hostGuard()],\n adminDir: config.ADMIN_DIR ?? editorUiDist(),\n });\n\n const fingerprint = await schemaFingerprint(model);\n\n startServer({\n app,\n host: config.HOST,\n port: config.PORT,\n logger,\n\n onListening() {\n // A person at a terminal gets a short banner; a log collector gets every field.\n if (config.MICA_CLI) {\n const useColor = supportsColor(process.stdout, process.env);\n\n process.stdout.write(\n startupBanner(\n {\n version: VERSION,\n url: config.MICA_PUBLIC_URL,\n contentRoot: files.origin,\n collections: Object.keys(model.collections).length,\n singletons: Object.keys(model.singletons).length,\n },\n useColor,\n ),\n );\n\n if (config.MICA_OPEN) openInBrowser(`${config.MICA_PUBLIC_URL}/admin/`);\n return;\n }\n\n logger.info(\"Mica local editor started\", {\n url: config.MICA_PUBLIC_URL,\n listening: `${config.HOST}:${config.PORT}`,\n auth: \"none (local editor; loopback only)\",\n content: files.origin,\n schema: schemaOrigin(files),\n schemaFingerprint: fingerprint,\n collections: Object.keys(model.collections).length,\n });\n },\n\n portInUseMessage: () =>\n portInUse(config.PORT, config.HOST, supportsColor(process.stderr, process.env)),\n\n shutdown: () => ctx.shutdown(),\n });\n}\n\nmain().catch((error: unknown) => {\n process.stderr.write(`Failed to start: ${error instanceof Error ? error.stack : String(error)}\\n`);\n process.exit(1);\n});\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The local editor's process entry point: the developer's checkout, the\n * auth-neutral runtime and the Host allowlist. It writes nothing outside the\n * content directory and installs no auth code that can be misconfigured.\n */\n\nimport { MANIFEST_PATH } from \"@micacms/core\";\nimport {\n ConfigError,\n contentFiles,\n createApp,\n createLogger,\n createRuntimeContext,\n layoutProblems,\n loadSchema,\n SchemaError,\n schemaFingerprint,\n schemaOrigin,\n startServer,\n VERSION,\n} from \"@micacms/runtime\";\n\nimport { editorUiDist } from \"./admin-dir.js\";\nimport { portInUse, startupBanner, supportsColor } from \"./banner.js\";\nimport { loadConfig } from \"./config.js\";\nimport { hostGuard } from \"./guards.js\";\nimport { openInBrowser } from \"./open.js\";\n\nasync function main(): Promise<void> {\n let config;\n try {\n config = loadConfig();\n } catch (error) {\n if (error instanceof ConfigError) {\n process.stderr.write(`\\n${error.message}\\n\\n`);\n process.exit(78); // EX_CONFIG\n }\n throw error;\n }\n\n const logger = createLogger({ level: config.MICA_LOG_LEVEL, pretty: true });\n\n // A bad or absent model is a project mistake, not a crash: exit like a bad\n // environment, without a stack trace.\n const files = contentFiles({ root: config.contentRoot });\n\n let model;\n try {\n model = await loadSchema(files);\n } catch (error) {\n if (error instanceof SchemaError) {\n process.stderr.write(`\\n${error.message}\\n\\n`);\n process.exit(78); // EX_CONFIG\n }\n throw error;\n }\n\n const misplaced = await layoutProblems(files, model);\n if (misplaced.length > 0) {\n process.stderr.write(`\\n${misplaced.join(\"\\n\\n\")}\\n\\n`);\n process.exit(78); // EX_CONFIG\n }\n\n const schemaRevision = await files.revision(MANIFEST_PATH);\n\n const ctx = createRuntimeContext({\n config,\n model,\n files,\n schemaRevision,\n logger,\n operational: () => ({\n auth: \"none — the local editor has no accounts and no sign-in\",\n }),\n });\n\n const app = createApp(ctx, {\n guards: [hostGuard()],\n adminDir: config.MICA_ADMIN_DIR ?? editorUiDist(),\n });\n\n const fingerprint = await schemaFingerprint(model);\n\n startServer({\n app,\n host: config.HOST,\n port: config.PORT,\n logger,\n\n onListening() {\n // A person at a terminal gets a short banner; a log collector gets every field.\n if (config.MICA_CLI) {\n const useColor = supportsColor(process.stdout, process.env);\n\n process.stdout.write(\n startupBanner(\n {\n version: VERSION,\n url: config.MICA_PUBLIC_URL,\n contentRoot: files.origin,\n collections: Object.keys(model.collections).length,\n singletons: Object.keys(model.singletons).length,\n },\n useColor,\n ),\n );\n\n if (config.MICA_OPEN) openInBrowser(`${config.MICA_PUBLIC_URL}/admin/`);\n return;\n }\n\n logger.info(\"Mica local editor started\", {\n url: config.MICA_PUBLIC_URL,\n listening: `${config.HOST}:${config.PORT}`,\n auth: \"none (local editor; loopback only)\",\n content: files.origin,\n schema: schemaOrigin(files),\n schemaFingerprint: fingerprint,\n collections: Object.keys(model.collections).length,\n });\n },\n\n portInUseMessage: () =>\n portInUse(config.PORT, config.HOST, supportsColor(process.stderr, process.env)),\n\n shutdown: () => ctx.shutdown(),\n });\n}\n\nmain().catch((error: unknown) => {\n process.stderr.write(`Failed to start: ${error instanceof Error ? error.stack : String(error)}\\n`);\n process.exit(1);\n});\n"]}
package/dist/init.d.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * `mica init` — adds Mica to an Astro project: a starter model and the content
3
3
  * collections. It never overwrites a file.
4
4
  */
5
- export declare const MODEL = "import {\n collection,\n date,\n defineSite,\n media,\n richText,\n singleton,\n slug,\n text,\n} from \"@micacms/core\";\n\nexport const site = defineSite({\n root: \"./content\",\n\n collections: {\n news: collection({\n label: \"News\",\n labelSingular: \"Article\",\n fields: {\n title: text({ label: \"Title\", required: true, maxLength: 120, listed: true }),\n slug: slug({ from: \"title\" }),\n publishedOn: date({ label: \"Published on\", listed: true }),\n cover: media({ accept: \"image\" }),\n body: richText(),\n },\n }),\n },\n\n singletons: {\n settings: singleton({\n label: \"Website\",\n fields: {\n siteName: text({ label: \"Site name\", required: true }),\n tagline: text({ label: \"Tagline\" }),\n },\n }),\n },\n});\n";
5
+ export declare const MODEL = "import {\n collection,\n date,\n defineSite,\n media,\n richText,\n singleton,\n slug,\n text,\n} from \"@micacms/core\";\n\nexport const site = defineSite({\n root: \"./content\",\n\n collections: {\n news: collection({\n label: \"News\",\n labelSingular: \"Article\",\n fields: {\n title: text({ label: \"Title\", required: true, maxLength: 120, listed: true }),\n slug: slug({ from: \"title\" }),\n publishedOn: date({ label: \"Published on\", listed: true }),\n cover: media({ accept: \"image\", alt: \"required\" }),\n body: richText(),\n },\n }),\n },\n\n singletons: {\n settings: singleton({\n label: \"Website\",\n fields: {\n siteName: text({ label: \"Site name\", required: true }),\n tagline: text({ label: \"Tagline\" }),\n },\n }),\n },\n});\n";
6
6
  export interface InitResult {
7
7
  /** Files and directories made, relative to the project. */
8
8
  created: string[];
@@ -1 +1 @@
1
- {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,eAAO,MAAM,KAAK,o1BAsCjB,CAAC;AAcF,MAAM,WAAW,UAAU;IACzB,2DAA2D;IAC3D,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,sDAAsD;IACtD,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,0CAA0C;IAC1C,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,qBAAa,SAAU,SAAQ,KAAK;IACzB,IAAI,SAAe;CAC7B;AAED,mDAAmD;AACnD,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,CAqDvD;AAYD,mDAAmD;AACnD,wBAAgB,UAAU,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CAOrD"}
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,eAAO,MAAM,KAAK,u2BAsCjB,CAAC;AAcF,MAAM,WAAW,UAAU;IACzB,2DAA2D;IAC3D,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,sDAAsD;IACtD,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,0CAA0C;IAC1C,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,qBAAa,SAAU,SAAQ,KAAK;IACzB,IAAI,SAAe;CAC7B;AAED,mDAAmD;AACnD,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,CA2DvD;AAuBD,mDAAmD;AACnD,wBAAgB,UAAU,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CAOrD"}
package/dist/init.js CHANGED
@@ -26,7 +26,7 @@ export const site = defineSite({
26
26
  title: text({ label: "Title", required: true, maxLength: 120, listed: true }),
27
27
  slug: slug({ from: "title" }),
28
28
  publishedOn: date({ label: "Published on", listed: true }),
29
- cover: media({ accept: "image" }),
29
+ cover: media({ accept: "image", alt: "required" }),
30
30
  body: richText(),
31
31
  },
32
32
  }),
@@ -103,11 +103,27 @@ export function initProject(project) {
103
103
  result.next.push(`Install the packages: npm install ${missing.join(" ")}`);
104
104
  if (!dependencies["@micacms/editor"])
105
105
  result.next.push("Install the editor: npm install --save-dev @micacms/editor");
106
+ const scripts = scriptsToSet(pkg.scripts ?? {}, Boolean(dependencies["@astrojs/check"]));
107
+ if (scripts.length > 0)
108
+ result.next.push(`Set in "scripts" in package.json:\n\n${scripts.map((line) => ` ${line}`).join(",\n")}`);
106
109
  result.next.push(model
107
110
  ? "Add the website's collections and singletons to the model in src/mica.ts."
108
111
  : "Replace the starter model in src/mica.ts (news, settings) with the website's model.", "Write the model to content/.mica/schema.json: npx astro sync", "Check the content: npx mica check", "When the content is in place, open the editor in a browser: npx mica");
109
112
  return result;
110
113
  }
114
+ /** `npm run mica` opens the editor; `npm run check` checks the content, and the types when `@astrojs/check` is there. */
115
+ function scriptsToSet(scripts, astroCheck) {
116
+ const missing = [];
117
+ if (!scripts["mica"])
118
+ missing.push(`"mica": "mica"`);
119
+ // Without @astrojs/check, `astro check` asks to install it, and a script cannot answer.
120
+ const check = scripts["check"];
121
+ if (!check)
122
+ missing.push(`"check": "${astroCheck ? "mica check && astro check" : "mica check"}"`);
123
+ else if (!check.includes("mica check"))
124
+ missing.push(`"check": ${JSON.stringify(`mica check && ${check}`)}`);
125
+ return missing;
126
+ }
111
127
  function existingFile(project, candidates) {
112
128
  return candidates.find((candidate) => existsSync(path.join(project, candidate)));
113
129
  }
package/dist/init.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"init.js","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `mica init` — adds Mica to an Astro project: a starter model and the content\n * collections. It never overwrites a file.\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport path from \"node:path\";\n\nexport const MODEL = `import {\n collection,\n date,\n defineSite,\n media,\n richText,\n singleton,\n slug,\n text,\n} from \"@micacms/core\";\n\nexport const site = defineSite({\n root: \"./content\",\n\n collections: {\n news: collection({\n label: \"News\",\n labelSingular: \"Article\",\n fields: {\n title: text({ label: \"Title\", required: true, maxLength: 120, listed: true }),\n slug: slug({ from: \"title\" }),\n publishedOn: date({ label: \"Published on\", listed: true }),\n cover: media({ accept: \"image\" }),\n body: richText(),\n },\n }),\n },\n\n singletons: {\n settings: singleton({\n label: \"Website\",\n fields: {\n siteName: text({ label: \"Site name\", required: true }),\n tagline: text({ label: \"Tagline\" }),\n },\n }),\n },\n});\n`;\n\nconst COLLECTIONS = `import { micaCollections } from \"@micacms/astro\";\n\nimport { site } from \"./mica\";\n\nexport const collections = micaCollections(site);\n`;\n\nconst MERGE = ` import { micaCollections } from \"@micacms/astro\";\n import { site } from \"./mica\";\n\n export const collections = { ...micaCollections(site), /* your collections */ };`;\n\nexport interface InitResult {\n /** Files and directories made, relative to the project. */\n created: string[];\n /** Files that already existed and were left alone. */\n kept: string[];\n /** What the person or agent does next. */\n next: string[];\n}\n\nexport class InitError extends Error {\n override name = \"InitError\";\n}\n\n/** Adds Mica to the Astro project in `project`. */\nexport function initProject(project: string): InitResult {\n const manifest = path.join(project, \"package.json\");\n if (!existsSync(manifest)) {\n throw new InitError(`There is no package.json in ${project}. Create an Astro project first: npm create astro@latest`);\n }\n let pkg: { dependencies?: Record<string, string>; devDependencies?: Record<string, string> };\n try {\n pkg = JSON.parse(readFileSync(manifest, \"utf8\")) as typeof pkg;\n } catch (cause) {\n throw new InitError(`${manifest} is not valid JSON: ${(cause as Error).message}`);\n }\n const dependencies = { ...pkg.dependencies, ...pkg.devDependencies };\n if (!dependencies[\"astro\"]) {\n throw new InitError(`${manifest} has no \"astro\" dependency. Run mica init in an Astro project.`);\n }\n\n const result: InitResult = { created: [], kept: [], next: [] };\n\n const model = existingFile(project, [\"src/mica.ts\", \"src/mica.mts\", \"src/mica.js\", \"src/mica.mjs\"]);\n if (model) result.kept.push(model);\n else write(project, \"src/mica.ts\", MODEL, result);\n\n const config = existingFile(project, [\n \"src/content.config.ts\",\n \"src/content.config.mts\",\n \"src/content.config.js\",\n \"src/content.config.mjs\",\n \"src/content/config.ts\",\n \"src/content/config.mts\",\n \"src/content/config.js\",\n \"src/content/config.mjs\",\n ]);\n if (config) {\n result.kept.push(config);\n if (!readFileSync(path.join(project, config), \"utf8\").includes(\"micaCollections\")) {\n result.next.push(`Add the Mica collections to ${config}:\\n\\n${MERGE}`);\n }\n } else {\n write(project, \"src/content.config.ts\", COLLECTIONS, result);\n }\n\n const missing = [\"@micacms/astro\", \"@micacms/core\"].filter((name) => !dependencies[name]);\n if (missing.length > 0) result.next.push(`Install the packages: npm install ${missing.join(\" \")}`);\n if (!dependencies[\"@micacms/editor\"]) result.next.push(\"Install the editor: npm install --save-dev @micacms/editor\");\n result.next.push(\n model\n ? \"Add the website's collections and singletons to the model in src/mica.ts.\"\n : \"Replace the starter model in src/mica.ts (news, settings) with the website's model.\",\n \"Write the model to content/.mica/schema.json: npx astro sync\",\n \"Check the content: npx mica check\",\n \"When the content is in place, open the editor in a browser: npx mica\",\n );\n return result;\n}\n\nfunction existingFile(project: string, candidates: string[]): string | undefined {\n return candidates.find((candidate) => existsSync(path.join(project, candidate)));\n}\n\nfunction write(project: string, file: string, contents: string, result: InitResult): void {\n mkdirSync(path.dirname(path.join(project, file)), { recursive: true });\n writeFileSync(path.join(project, file), contents, { flag: \"wx\" });\n result.created.push(file);\n}\n\n/** The result as a person or an agent reads it. */\nexport function formatInit(result: InitResult): string {\n const lines: string[] = [];\n for (const file of result.created) lines.push(` created ${file}`);\n for (const file of result.kept) lines.push(` kept ${file}`);\n lines.push(\"\", \"Next:\", \"\");\n result.next.forEach((step, index) => lines.push(` ${index + 1}. ${step}`, ...(step.includes(\"\\n\") ? [\"\"] : [])));\n return `${lines.join(\"\\n\")}\\n`;\n}\n"]}
1
+ {"version":3,"file":"init.js","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `mica init` — adds Mica to an Astro project: a starter model and the content\n * collections. It never overwrites a file.\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport path from \"node:path\";\n\nexport const MODEL = `import {\n collection,\n date,\n defineSite,\n media,\n richText,\n singleton,\n slug,\n text,\n} from \"@micacms/core\";\n\nexport const site = defineSite({\n root: \"./content\",\n\n collections: {\n news: collection({\n label: \"News\",\n labelSingular: \"Article\",\n fields: {\n title: text({ label: \"Title\", required: true, maxLength: 120, listed: true }),\n slug: slug({ from: \"title\" }),\n publishedOn: date({ label: \"Published on\", listed: true }),\n cover: media({ accept: \"image\", alt: \"required\" }),\n body: richText(),\n },\n }),\n },\n\n singletons: {\n settings: singleton({\n label: \"Website\",\n fields: {\n siteName: text({ label: \"Site name\", required: true }),\n tagline: text({ label: \"Tagline\" }),\n },\n }),\n },\n});\n`;\n\nconst COLLECTIONS = `import { micaCollections } from \"@micacms/astro\";\n\nimport { site } from \"./mica\";\n\nexport const collections = micaCollections(site);\n`;\n\nconst MERGE = ` import { micaCollections } from \"@micacms/astro\";\n import { site } from \"./mica\";\n\n export const collections = { ...micaCollections(site), /* your collections */ };`;\n\nexport interface InitResult {\n /** Files and directories made, relative to the project. */\n created: string[];\n /** Files that already existed and were left alone. */\n kept: string[];\n /** What the person or agent does next. */\n next: string[];\n}\n\nexport class InitError extends Error {\n override name = \"InitError\";\n}\n\n/** Adds Mica to the Astro project in `project`. */\nexport function initProject(project: string): InitResult {\n const manifest = path.join(project, \"package.json\");\n if (!existsSync(manifest)) {\n throw new InitError(`There is no package.json in ${project}. Create an Astro project first: npm create astro@latest`);\n }\n let pkg: {\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n scripts?: Record<string, string>;\n };\n try {\n pkg = JSON.parse(readFileSync(manifest, \"utf8\")) as typeof pkg;\n } catch (cause) {\n throw new InitError(`${manifest} is not valid JSON: ${(cause as Error).message}`);\n }\n const dependencies = { ...pkg.dependencies, ...pkg.devDependencies };\n if (!dependencies[\"astro\"]) {\n throw new InitError(`${manifest} has no \"astro\" dependency. Run mica init in an Astro project.`);\n }\n\n const result: InitResult = { created: [], kept: [], next: [] };\n\n const model = existingFile(project, [\"src/mica.ts\", \"src/mica.mts\", \"src/mica.js\", \"src/mica.mjs\"]);\n if (model) result.kept.push(model);\n else write(project, \"src/mica.ts\", MODEL, result);\n\n const config = existingFile(project, [\n \"src/content.config.ts\",\n \"src/content.config.mts\",\n \"src/content.config.js\",\n \"src/content.config.mjs\",\n \"src/content/config.ts\",\n \"src/content/config.mts\",\n \"src/content/config.js\",\n \"src/content/config.mjs\",\n ]);\n if (config) {\n result.kept.push(config);\n if (!readFileSync(path.join(project, config), \"utf8\").includes(\"micaCollections\")) {\n result.next.push(`Add the Mica collections to ${config}:\\n\\n${MERGE}`);\n }\n } else {\n write(project, \"src/content.config.ts\", COLLECTIONS, result);\n }\n\n const missing = [\"@micacms/astro\", \"@micacms/core\"].filter((name) => !dependencies[name]);\n if (missing.length > 0) result.next.push(`Install the packages: npm install ${missing.join(\" \")}`);\n if (!dependencies[\"@micacms/editor\"]) result.next.push(\"Install the editor: npm install --save-dev @micacms/editor\");\n const scripts = scriptsToSet(pkg.scripts ?? {}, Boolean(dependencies[\"@astrojs/check\"]));\n if (scripts.length > 0) result.next.push(`Set in \"scripts\" in package.json:\\n\\n${scripts.map((line) => ` ${line}`).join(\",\\n\")}`);\n result.next.push(\n model\n ? \"Add the website's collections and singletons to the model in src/mica.ts.\"\n : \"Replace the starter model in src/mica.ts (news, settings) with the website's model.\",\n \"Write the model to content/.mica/schema.json: npx astro sync\",\n \"Check the content: npx mica check\",\n \"When the content is in place, open the editor in a browser: npx mica\",\n );\n return result;\n}\n\n/** `npm run mica` opens the editor; `npm run check` checks the content, and the types when `@astrojs/check` is there. */\nfunction scriptsToSet(scripts: Record<string, string>, astroCheck: boolean): string[] {\n const missing: string[] = [];\n if (!scripts[\"mica\"]) missing.push(`\"mica\": \"mica\"`);\n // Without @astrojs/check, `astro check` asks to install it, and a script cannot answer.\n const check = scripts[\"check\"];\n if (!check) missing.push(`\"check\": \"${astroCheck ? \"mica check && astro check\" : \"mica check\"}\"`);\n else if (!check.includes(\"mica check\")) missing.push(`\"check\": ${JSON.stringify(`mica check && ${check}`)}`);\n return missing;\n}\n\nfunction existingFile(project: string, candidates: string[]): string | undefined {\n return candidates.find((candidate) => existsSync(path.join(project, candidate)));\n}\n\nfunction write(project: string, file: string, contents: string, result: InitResult): void {\n mkdirSync(path.dirname(path.join(project, file)), { recursive: true });\n writeFileSync(path.join(project, file), contents, { flag: \"wx\" });\n result.created.push(file);\n}\n\n/** The result as a person or an agent reads it. */\nexport function formatInit(result: InitResult): string {\n const lines: string[] = [];\n for (const file of result.created) lines.push(` created ${file}`);\n for (const file of result.kept) lines.push(` kept ${file}`);\n lines.push(\"\", \"Next:\", \"\");\n result.next.forEach((step, index) => lines.push(` ${index + 1}. ${step}`, ...(step.includes(\"\\n\") ? [\"\"] : [])));\n return `${lines.join(\"\\n\")}\\n`;\n}\n"]}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * `mica migrate-layout`: moves the entries that a model change left in the
3
+ * other layout. The developer runs it in the working tree and commits the
4
+ * result with the model change; Git is the undo.
5
+ */
6
+ export interface LayoutMigration {
7
+ /** Each entry moved, as content-root paths. */
8
+ moved: Array<{
9
+ from: string;
10
+ to: string;
11
+ }>;
12
+ /** Rich text and media of an entry that became a file: no field reads them now. */
13
+ removed: string[];
14
+ /** Files that Mica did not write, left in the old folder. */
15
+ kept: string[];
16
+ /** Entries not moved, because the target already exists. */
17
+ conflicts: string[];
18
+ }
19
+ /** Moves every entry in the other layout into the layout that the model gives. */
20
+ export declare function migrateLayout(root: string): Promise<LayoutMigration>;
21
+ /** The result as a person or an agent reads it. */
22
+ export declare function formatMigration(result: LayoutMigration): string;
23
+ //# sourceMappingURL=migrate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migrate.d.ts","sourceRoot":"","sources":["../src/migrate.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAsBH,MAAM,WAAW,eAAe;IAC9B,+CAA+C;IAC/C,KAAK,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC3C,mFAAmF;IACnF,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,6DAA6D;IAC7D,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,4DAA4D;IAC5D,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB;AAED,kFAAkF;AAClF,wBAAsB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAgC1E;AAmDD,mDAAmD;AACnD,wBAAgB,eAAe,CAAC,MAAM,EAAE,eAAe,GAAG,MAAM,CAS/D"}
@@ -0,0 +1,115 @@
1
+ /**
2
+ * `mica migrate-layout`: moves the entries that a model change left in the
3
+ * other layout. The developer runs it in the working tree and commits the
4
+ * result with the model change; Git is the undo.
5
+ */
6
+ import { existsSync } from "node:fs";
7
+ import { mkdir, readdir, rename, rm, rmdir } from "node:fs/promises";
8
+ import path from "node:path";
9
+ import { collectReferences, DOCUMENT_EXTENSION, entryLayout, INDEX_FILE, MEDIA_DIR, misplacedFiles, readCollection, readSingleton, } from "@micacms/core";
10
+ import { within } from "@micacms/core/node";
11
+ import { contentFiles, loadSchema } from "@micacms/runtime";
12
+ /** Moves every entry in the other layout into the layout that the model gives. */
13
+ export async function migrateLayout(root) {
14
+ const files = contentFiles({ root });
15
+ const site = await loadSchema(files);
16
+ const result = { moved: [], removed: [], kept: [], conflicts: [] };
17
+ const parts = [
18
+ ...Object.entries(site.collections).map(([name, def]) => [name, def.fields, "collection"]),
19
+ ...Object.entries(site.singletons).map(([name, def]) => [name, def.fields, "singleton"]),
20
+ ];
21
+ const formerFolders = [];
22
+ for (const [name, fields, kind] of parts) {
23
+ // The model decides the direction: `pages/index.json` is an entry named `index` in a file collection.
24
+ const toFolder = entryLayout(fields) === "directory";
25
+ for (const from of await misplacedFiles(files, fields, name, kind)) {
26
+ const to = toFolder ? `${from.slice(0, -".json".length)}/${INDEX_FILE}` : `${path.posix.dirname(from)}.json`;
27
+ if (existsSync(within(root, to))) {
28
+ result.conflicts.push(`${from}: ${to} exists too. Merge the two by hand.`);
29
+ continue;
30
+ }
31
+ await mkdir(path.dirname(within(root, to)), { recursive: true });
32
+ await rename(within(root, from), within(root, to));
33
+ if (!toFolder)
34
+ formerFolders.push(path.posix.dirname(from));
35
+ result.moved.push({ from, to });
36
+ }
37
+ }
38
+ const used = await usedMedia(files, site);
39
+ for (const folder of formerFolders)
40
+ await emptyFolder(root, folder, used, result);
41
+ return result;
42
+ }
43
+ /**
44
+ * Every media path that an entry uses, after the moves. Null when a collection
45
+ * cannot be read: then no media file is removed.
46
+ */
47
+ async function usedMedia(files, site) {
48
+ const used = new Set();
49
+ const note = (fields, data) => {
50
+ for (const reference of collectReferences(fields, data))
51
+ if (reference.kind === "media")
52
+ used.add(reference.path);
53
+ };
54
+ try {
55
+ for (const [name, def] of Object.entries(site.collections)) {
56
+ for (const entry of await readCollection(files, def, name))
57
+ note(def.fields, entry.data);
58
+ }
59
+ for (const [name, def] of Object.entries(site.singletons)) {
60
+ const data = await readSingleton(files, def, name);
61
+ if (data)
62
+ note(def.fields, data);
63
+ }
64
+ }
65
+ catch {
66
+ return null;
67
+ }
68
+ return used;
69
+ }
70
+ /** Removes the sidecars and the unused media of a former folder entry; keeps anything else. */
71
+ async function emptyFolder(root, folder, used, result) {
72
+ for (const item of await readdir(within(root, folder), { withFileTypes: true, recursive: true })) {
73
+ if (!item.isFile())
74
+ continue;
75
+ const file = path.relative(path.resolve(root), path.join(item.parentPath, item.name)).split(path.sep).join("/");
76
+ const inMedia = file.startsWith(`${folder}/${MEDIA_DIR}/`);
77
+ const sidecar = path.posix.dirname(file) === folder && file.endsWith(DOCUMENT_EXTENSION);
78
+ if (inMedia && (used === null || used.has(file))) {
79
+ result.kept.push(`${file} (${used ? "another entry uses it" : "a collection could not be read"})`);
80
+ }
81
+ else if (inMedia || sidecar) {
82
+ await rm(within(root, file));
83
+ result.removed.push(file);
84
+ }
85
+ else {
86
+ result.kept.push(`${file} (not written by Mica)`);
87
+ }
88
+ }
89
+ await removeEmpty(within(root, folder));
90
+ }
91
+ async function removeEmpty(directory) {
92
+ for (const item of await readdir(directory, { withFileTypes: true })) {
93
+ if (item.isDirectory())
94
+ await removeEmpty(path.join(directory, item.name));
95
+ }
96
+ if ((await readdir(directory)).length === 0)
97
+ await rmdir(directory);
98
+ }
99
+ /** The result as a person or an agent reads it. */
100
+ export function formatMigration(result) {
101
+ const lines = [];
102
+ for (const { from, to } of result.moved)
103
+ lines.push(` moved ${from} -> ${to}`);
104
+ for (const file of result.removed)
105
+ lines.push(` removed ${file}`);
106
+ for (const file of result.kept)
107
+ lines.push(` kept ${file}`);
108
+ for (const conflict of result.conflicts)
109
+ lines.push(` error ${conflict}`);
110
+ if (lines.length === 0)
111
+ return "Every entry is in the layout of the model. Nothing to move.\n";
112
+ lines.push("", "Next: run `npx mica check`, look at `git status`, and commit the result with the model change.");
113
+ return `${lines.join("\n")}\n`;
114
+ }
115
+ //# sourceMappingURL=migrate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migrate.js","sourceRoot":"","sources":["../src/migrate.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `mica migrate-layout`: moves the entries that a model change left in the\n * other layout. The developer runs it in the working tree and commits the\n * result with the model change; Git is the undo.\n */\n\nimport { existsSync } from \"node:fs\";\nimport { mkdir, readdir, rename, rm, rmdir } from \"node:fs/promises\";\nimport path from \"node:path\";\n\nimport {\n collectReferences,\n DOCUMENT_EXTENSION,\n entryLayout,\n INDEX_FILE,\n MEDIA_DIR,\n misplacedFiles,\n readCollection,\n readSingleton,\n type ContentSource,\n type FieldMap,\n type Site,\n} from \"@micacms/core\";\nimport { within } from \"@micacms/core/node\";\nimport { contentFiles, loadSchema } from \"@micacms/runtime\";\n\nexport interface LayoutMigration {\n /** Each entry moved, as content-root paths. */\n moved: Array<{ from: string; to: string }>;\n /** Rich text and media of an entry that became a file: no field reads them now. */\n removed: string[];\n /** Files that Mica did not write, left in the old folder. */\n kept: string[];\n /** Entries not moved, because the target already exists. */\n conflicts: string[];\n}\n\n/** Moves every entry in the other layout into the layout that the model gives. */\nexport async function migrateLayout(root: string): Promise<LayoutMigration> {\n const files = contentFiles({ root });\n const site = await loadSchema(files);\n const result: LayoutMigration = { moved: [], removed: [], kept: [], conflicts: [] };\n\n const parts: Array<[string, FieldMap, \"collection\" | \"singleton\"]> = [\n ...Object.entries(site.collections).map(([name, def]) => [name, def.fields, \"collection\"] as [string, FieldMap, \"collection\"]),\n ...Object.entries(site.singletons).map(([name, def]) => [name, def.fields, \"singleton\"] as [string, FieldMap, \"singleton\"]),\n ];\n\n const formerFolders: string[] = [];\n for (const [name, fields, kind] of parts) {\n // The model decides the direction: `pages/index.json` is an entry named `index` in a file collection.\n const toFolder = entryLayout(fields) === \"directory\";\n for (const from of await misplacedFiles(files, fields, name, kind)) {\n const to = toFolder ? `${from.slice(0, -\".json\".length)}/${INDEX_FILE}` : `${path.posix.dirname(from)}.json`;\n\n if (existsSync(within(root, to))) {\n result.conflicts.push(`${from}: ${to} exists too. Merge the two by hand.`);\n continue;\n }\n\n await mkdir(path.dirname(within(root, to)), { recursive: true });\n await rename(within(root, from), within(root, to));\n if (!toFolder) formerFolders.push(path.posix.dirname(from));\n result.moved.push({ from, to });\n }\n }\n\n const used = await usedMedia(files, site);\n for (const folder of formerFolders) await emptyFolder(root, folder, used, result);\n return result;\n}\n\n/**\n * Every media path that an entry uses, after the moves. Null when a collection\n * cannot be read: then no media file is removed.\n */\nasync function usedMedia(files: ContentSource, site: Site): Promise<Set<string> | null> {\n const used = new Set<string>();\n const note = (fields: FieldMap, data: Record<string, unknown>) => {\n for (const reference of collectReferences(fields, data)) if (reference.kind === \"media\") used.add(reference.path);\n };\n try {\n for (const [name, def] of Object.entries(site.collections)) {\n for (const entry of await readCollection(files, def, name)) note(def.fields, entry.data);\n }\n for (const [name, def] of Object.entries(site.singletons)) {\n const data = await readSingleton(files, def, name);\n if (data) note(def.fields, data);\n }\n } catch {\n return null;\n }\n return used;\n}\n\n/** Removes the sidecars and the unused media of a former folder entry; keeps anything else. */\nasync function emptyFolder(root: string, folder: string, used: Set<string> | null, result: LayoutMigration): Promise<void> {\n for (const item of await readdir(within(root, folder), { withFileTypes: true, recursive: true })) {\n if (!item.isFile()) continue;\n const file = path.relative(path.resolve(root), path.join(item.parentPath, item.name)).split(path.sep).join(\"/\");\n const inMedia = file.startsWith(`${folder}/${MEDIA_DIR}/`);\n const sidecar = path.posix.dirname(file) === folder && file.endsWith(DOCUMENT_EXTENSION);\n if (inMedia && (used === null || used.has(file))) {\n result.kept.push(`${file} (${used ? \"another entry uses it\" : \"a collection could not be read\"})`);\n } else if (inMedia || sidecar) {\n await rm(within(root, file));\n result.removed.push(file);\n } else {\n result.kept.push(`${file} (not written by Mica)`);\n }\n }\n await removeEmpty(within(root, folder));\n}\n\nasync function removeEmpty(directory: string): Promise<void> {\n for (const item of await readdir(directory, { withFileTypes: true })) {\n if (item.isDirectory()) await removeEmpty(path.join(directory, item.name));\n }\n if ((await readdir(directory)).length === 0) await rmdir(directory);\n}\n\n/** The result as a person or an agent reads it. */\nexport function formatMigration(result: LayoutMigration): string {\n const lines: string[] = [];\n for (const { from, to } of result.moved) lines.push(` moved ${from} -> ${to}`);\n for (const file of result.removed) lines.push(` removed ${file}`);\n for (const file of result.kept) lines.push(` kept ${file}`);\n for (const conflict of result.conflicts) lines.push(` error ${conflict}`);\n if (lines.length === 0) return \"Every entry is in the layout of the model. Nothing to move.\\n\";\n lines.push(\"\", \"Next: run `npx mica check`, look at `git status`, and commit the result with the model change.\");\n return `${lines.join(\"\\n\")}\\n`;\n}\n"]}
package/package.json CHANGED
@@ -1,9 +1,16 @@
1
1
  {
2
2
  "name": "@micacms/editor",
3
- "version": "0.5.0",
3
+ "version": "0.6.0-rc.1",
4
4
  "type": "module",
5
5
  "description": "The local Mica editor — `npx @micacms/editor` against a checkout on your own machine. No authentication, no database, loopback only.",
6
6
  "license": "MIT",
7
+ "homepage": "https://micacms.com",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/micacms/mica.git",
11
+ "directory": "apps/local-editor"
12
+ },
13
+ "bugs": "https://github.com/micacms/mica/issues",
7
14
  "author": "Alexander Müdespacher",
8
15
  "keywords": [
9
16
  "mica",
@@ -23,9 +30,9 @@
23
30
  "dependencies": {
24
31
  "hono": "^4.13.7",
25
32
  "zod": "^4.3.6",
26
- "@micacms/core": "0.5.0",
27
- "@micacms/editor-ui": "0.5.0",
28
- "@micacms/runtime": "0.5.0"
33
+ "@micacms/core": "0.6.0-rc.1",
34
+ "@micacms/editor-ui": "0.6.0-rc.1",
35
+ "@micacms/runtime": "0.6.0-rc.1"
29
36
  },
30
37
  "devDependencies": {
31
38
  "@types/node": "^22.20.1",
@@ -33,14 +40,14 @@
33
40
  "typescript": "^5.7.3"
34
41
  },
35
42
  "engines": {
36
- "node": ">=22.12.0"
43
+ "node": ">=24"
37
44
  },
38
45
  "publishConfig": {
39
46
  "access": "public"
40
47
  },
41
48
  "scripts": {
42
49
  "build": "tsc -p tsconfig.json",
43
- "dev": "tsx watch --clear-screen=false --include ../../examples/muster-consulting/content/.mica/schema.json src/index.ts",
50
+ "dev": "tsx watch --env-file=dev.env --clear-screen=false --include ../../examples/muster-consulting/content/.mica/schema.json src/index.ts",
44
51
  "start": "node dist/index.js",
45
52
  "typecheck": "tsc -p tsconfig.test.json",
46
53
  "test": "tsc -p tsconfig.test.json && node --test --import tsx test/*.test.ts"