@micacms/editor 0.5.0 → 0.6.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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
@@ -22,7 +22,7 @@ localhost:6422
22
22
  ## Running it against a project
23
23
 
24
24
  ```bash
25
- npx @micacms/editor --content ./content
25
+ npx @micacms/editor start --content ./content
26
26
  ```
27
27
 
28
28
  The published package carries the API and the built admin, so this is one
@@ -44,10 +44,13 @@ 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
48
+ npx @micacms/editor import # move Astro collections into Mica
47
49
  ```
48
50
 
49
51
  `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
52
+ overwrites a file. It also names the `mica` and `check` scripts that
53
+ `package.json` does not have yet. `check` reads every entry the way Mica and the
51
54
  website read it, and reports what they would skip or refuse: fields that do
52
55
  not validate, missing media, relations to entries that do not exist, slugs
53
56
  that do not match their file names, and files that are not read at all. It
@@ -60,14 +63,12 @@ are also `npx mica init` and `npx mica check`.
60
63
  rush update
61
64
  rush build --to tag:packages
62
65
 
63
- cd apps/local-editor
64
- cp .env.example .env
65
-
66
66
  rush dev # API on :6422, admin on :5173
67
67
  ```
68
68
 
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
69
+ `rush dev` reads `dev.env`, which sets
70
+ `MICA_CONTENT_ROOT=../../examples/muster-consulting/content`, so the server edits
71
+ the example project's content. There is nothing to seed: the
71
72
  content is already there, in Git.
72
73
 
73
74
  The server has no content model of its own. It reads one from the content
@@ -79,8 +80,10 @@ no avatar and no account menu, because there is nothing behind them.
79
80
 
80
81
  `rush dev` runs this app and the Vite dev server together. Vite
81
82
  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.
83
+ `MICA_ADMIN_ORIGINS=http://localhost:5173` for its Origin check, which
84
+ `dev.env` sets. The published editor reads no `.env` file: the project's `.env`
85
+ belongs to the site. Every variable is at
86
+ <https://micacms.com/docs/configuration/>.
84
87
 
85
88
  ## What lives where
86
89
 
package/dist/banner.js CHANGED
@@ -83,6 +83,6 @@ export function portInUse(port, host, useColor) {
83
83
  return (`\n ${c.bold(`Port ${port} is already in use on ${host}.`)}\n\n` +
84
84
  ` Something else is listening there — another Mica, most likely.\n` +
85
85
  ` Stop it, or give this one a different port:\n\n` +
86
- ` npx @micacms/editor --port ${port + 1}\n\n`);
86
+ ` npx @micacms/editor start --port ${port + 1}\n\n`);
87
87
  }
88
88
  //# sourceMappingURL=banner.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"banner.js","sourceRoot":"","sources":["../src/banner.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What `npx @micacms/editor` says when it works.\n *\n * The startup log lines are written for whoever is reading a container's\n * output later: every field that could matter, flat, greppable, and in no\n * particular order of importance. A person who has just typed a command needs\n * the opposite — the address to open, the directory being edited, and what\n * pressing Save will do to their repository. Three facts, in that order,\n * because the first minute decides whether there is a second one.\n *\n * Everything here is decoration over information the server already has. It is\n * printed instead of the startup log lines, never as well: two accounts of the\n * same event, disagreeing in tone, is worse than either alone.\n */\n\nimport path from \"node:path\";\n\nexport interface Banner {\n version: string;\n /** The origin the person should open, without the `/admin/` path. */\n url: string;\n contentRoot: string;\n collections: number;\n singletons: number;\n}\n\n/** ANSI, or nothing at all when the output is not a terminal. */\nexport function colors(enabled: boolean) {\n const wrap = (code: string) => (text: string) => (enabled ? `\u001b[${code}m${text}\u001b[0m` : text);\n return {\n bold: wrap(\"1\"),\n dim: wrap(\"2\"),\n brand: wrap(\"36\"),\n label: wrap(\"2\"),\n };\n}\n\n/**\n * True unless something says otherwise.\n *\n * `NO_COLOR` is the convention (no-color.org); a pipe or a file is the other\n * case, and escape codes in a redirected log help nobody.\n */\nexport function supportsColor(stream: { isTTY?: boolean }, env: NodeJS.ProcessEnv): boolean {\n if (env[\"NO_COLOR\"]) return false;\n if (env[\"FORCE_COLOR\"]) return true;\n return Boolean(stream.isTTY);\n}\n\n/** Paths inside the working directory read better as the relative ones. */\nfunction readable(target: string, cwd = process.cwd()): string {\n const relative = path.relative(cwd, target);\n if (!relative || relative.startsWith(\"..\") || path.isAbsolute(relative)) return target;\n return `./${relative}`;\n}\n\n/** \"3 collections and 2 singletons\", or whichever half is not zero. */\nfunction modelSummary(collections: number, singletons: number): string {\n const plural = (count: number, word: string) => `${count} ${word}${count === 1 ? \"\" : \"s\"}`;\n const parts: string[] = [];\n if (collections > 0) parts.push(plural(collections, \"collection\"));\n if (singletons > 0) parts.push(plural(singletons, \"singleton\"));\n return parts.join(\" and \") || \"an empty model\";\n}\n\nexport function startupBanner(banner: Banner, useColor: boolean): string {\n const c = colors(useColor);\n const editor = `${banner.url}/admin/`;\n\n const row = (label: string, value: string): string =>\n ` ${c.label(label.padEnd(9))} ${value}`;\n\n const lines = [\n \"\",\n ` ${c.brand(\"◆\")} ${c.bold(\"Mica\")} ${c.dim(banner.version)}`,\n \"\",\n row(\"Editor\", c.bold(editor)),\n row(\"Content\", `${readable(banner.contentRoot)} ${c.dim(`— ${modelSummary(banner.collections, banner.singletons)}`)}`),\n row(\"Saving\", c.dim(\"writes files into that directory; Publish commits and pushes them.\")),\n \"\",\n ` ${c.dim(\"No sign-in on your own machine — the link above opens straight into the editor.\")}`,\n ` ${c.dim(\"Mica keeps no state of its own here: everything you save is a file in that directory.\")}`,\n \"\",\n ` ${c.dim(\"Run your site's dev server beside this to see the changes. Ctrl+C stops Mica.\")}`,\n \"\",\n ];\n\n return `${lines.join(\"\\n\")}\\n`;\n}\n\n/**\n * The one failure this command is most likely to hit, and the one the operating\n * system explains worst: `EADDRINUSE` alone tells nobody which port or what to\n * do instead.\n */\nexport function portInUse(port: number, host: string, useColor: boolean): string {\n const c = colors(useColor);\n return (\n `\\n ${c.bold(`Port ${port} is already in use on ${host}.`)}\\n\\n` +\n ` Something else is listening there — another Mica, most likely.\\n` +\n ` Stop it, or give this one a different port:\\n\\n` +\n ` npx @micacms/editor --port ${port + 1}\\n\\n`\n );\n}\n"]}
1
+ {"version":3,"file":"banner.js","sourceRoot":"","sources":["../src/banner.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,IAAI,MAAM,WAAW,CAAC;AAW7B,iEAAiE;AACjE,MAAM,UAAU,MAAM,CAAC,OAAgB;IACrC,MAAM,IAAI,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,CAAC,IAAY,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,IAAI,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IAC5F,OAAO;QACL,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC;QACf,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC;QACd,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC;QACjB,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC;KACjB,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAAC,MAA2B,EAAE,GAAsB;IAC/E,IAAI,GAAG,CAAC,UAAU,CAAC;QAAE,OAAO,KAAK,CAAC;IAClC,IAAI,GAAG,CAAC,aAAa,CAAC;QAAE,OAAO,IAAI,CAAC;IACpC,OAAO,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC/B,CAAC;AAED,2EAA2E;AAC3E,SAAS,QAAQ,CAAC,MAAc,EAAE,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE;IACnD,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IAC5C,IAAI,CAAC,QAAQ,IAAI,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC;QAAE,OAAO,MAAM,CAAC;IACvF,OAAO,KAAK,QAAQ,EAAE,CAAC;AACzB,CAAC;AAED,uEAAuE;AACvE,SAAS,YAAY,CAAC,WAAmB,EAAE,UAAkB;IAC3D,MAAM,MAAM,GAAG,CAAC,KAAa,EAAE,IAAY,EAAE,EAAE,CAAC,GAAG,KAAK,IAAI,IAAI,GAAG,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC;IAC5F,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,IAAI,WAAW,GAAG,CAAC;QAAE,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,YAAY,CAAC,CAAC,CAAC;IACnE,IAAI,UAAU,GAAG,CAAC;QAAE,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE,WAAW,CAAC,CAAC,CAAC;IAChE,OAAO,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,gBAAgB,CAAC;AACjD,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,MAAc,EAAE,QAAiB;IAC7D,MAAM,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC;IAC3B,MAAM,MAAM,GAAG,GAAG,MAAM,CAAC,GAAG,SAAS,CAAC;IAEtC,MAAM,GAAG,GAAG,CAAC,KAAa,EAAE,KAAa,EAAU,EAAE,CACnD,KAAK,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,KAAK,EAAE,CAAC;IAE5C,MAAM,KAAK,GAAG;QACZ,EAAE;QACF,KAAK,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE;QAC9D,EAAE;QACF,GAAG,CAAC,QAAQ,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC7B,GAAG,CAAC,SAAS,EAAE,GAAG,QAAQ,CAAC,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,KAAK,YAAY,CAAC,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,UAAU,CAAC,EAAE,CAAC,EAAE,CAAC;QACtH,GAAG,CAAC,QAAQ,EAAE,CAAC,CAAC,GAAG,CAAC,oEAAoE,CAAC,CAAC;QAC1F,EAAE;QACF,KAAK,CAAC,CAAC,GAAG,CAAC,iFAAiF,CAAC,EAAE;QAC/F,KAAK,CAAC,CAAC,GAAG,CAAC,uFAAuF,CAAC,EAAE;QACrG,EAAE;QACF,KAAK,CAAC,CAAC,GAAG,CAAC,+EAA+E,CAAC,EAAE;QAC7F,EAAE;KACH,CAAC;IAEF,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC;AACjC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,SAAS,CAAC,IAAY,EAAE,IAAY,EAAE,QAAiB;IACrE,MAAM,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC;IAC3B,OAAO,CACL,OAAO,CAAC,CAAC,IAAI,CAAC,QAAQ,IAAI,yBAAyB,IAAI,GAAG,CAAC,MAAM;QACjE,oEAAoE;QACpE,mDAAmD;QACnD,wCAAwC,IAAI,GAAG,CAAC,MAAM,CACvD,CAAC;AACJ,CAAC","sourcesContent":["/**\n * What `npx @micacms/editor` says when it works.\n *\n * The startup log lines are written for whoever is reading a container's\n * output later: every field that could matter, flat, greppable, and in no\n * particular order of importance. A person who has just typed a command needs\n * the opposite — the address to open, the directory being edited, and what\n * pressing Save will do to their repository. Three facts, in that order,\n * because the first minute decides whether there is a second one.\n *\n * Everything here is decoration over information the server already has. It is\n * printed instead of the startup log lines, never as well: two accounts of the\n * same event, disagreeing in tone, is worse than either alone.\n */\n\nimport path from \"node:path\";\n\nexport interface Banner {\n version: string;\n /** The origin the person should open, without the `/admin/` path. */\n url: string;\n contentRoot: string;\n collections: number;\n singletons: number;\n}\n\n/** ANSI, or nothing at all when the output is not a terminal. */\nexport function colors(enabled: boolean) {\n const wrap = (code: string) => (text: string) => (enabled ? `\u001b[${code}m${text}\u001b[0m` : text);\n return {\n bold: wrap(\"1\"),\n dim: wrap(\"2\"),\n brand: wrap(\"36\"),\n label: wrap(\"2\"),\n };\n}\n\n/**\n * True unless something says otherwise.\n *\n * `NO_COLOR` is the convention (no-color.org); a pipe or a file is the other\n * case, and escape codes in a redirected log help nobody.\n */\nexport function supportsColor(stream: { isTTY?: boolean }, env: NodeJS.ProcessEnv): boolean {\n if (env[\"NO_COLOR\"]) return false;\n if (env[\"FORCE_COLOR\"]) return true;\n return Boolean(stream.isTTY);\n}\n\n/** Paths inside the working directory read better as the relative ones. */\nfunction readable(target: string, cwd = process.cwd()): string {\n const relative = path.relative(cwd, target);\n if (!relative || relative.startsWith(\"..\") || path.isAbsolute(relative)) return target;\n return `./${relative}`;\n}\n\n/** \"3 collections and 2 singletons\", or whichever half is not zero. */\nfunction modelSummary(collections: number, singletons: number): string {\n const plural = (count: number, word: string) => `${count} ${word}${count === 1 ? \"\" : \"s\"}`;\n const parts: string[] = [];\n if (collections > 0) parts.push(plural(collections, \"collection\"));\n if (singletons > 0) parts.push(plural(singletons, \"singleton\"));\n return parts.join(\" and \") || \"an empty model\";\n}\n\nexport function startupBanner(banner: Banner, useColor: boolean): string {\n const c = colors(useColor);\n const editor = `${banner.url}/admin/`;\n\n const row = (label: string, value: string): string =>\n ` ${c.label(label.padEnd(9))} ${value}`;\n\n const lines = [\n \"\",\n ` ${c.brand(\"◆\")} ${c.bold(\"Mica\")} ${c.dim(banner.version)}`,\n \"\",\n row(\"Editor\", c.bold(editor)),\n row(\"Content\", `${readable(banner.contentRoot)} ${c.dim(`— ${modelSummary(banner.collections, banner.singletons)}`)}`),\n row(\"Saving\", c.dim(\"writes files into that directory; Publish commits and pushes them.\")),\n \"\",\n ` ${c.dim(\"No sign-in on your own machine — the link above opens straight into the editor.\")}`,\n ` ${c.dim(\"Mica keeps no state of its own here: everything you save is a file in that directory.\")}`,\n \"\",\n ` ${c.dim(\"Run your site's dev server beside this to see the changes. Ctrl+C stops Mica.\")}`,\n \"\",\n ];\n\n return `${lines.join(\"\\n\")}\\n`;\n}\n\n/**\n * The one failure this command is most likely to hit, and the one the operating\n * system explains worst: `EADDRINUSE` alone tells nobody which port or what to\n * do instead.\n */\nexport function portInUse(port: number, host: string, useColor: boolean): string {\n const c = colors(useColor);\n return (\n `\\n ${c.bold(`Port ${port} is already in use on ${host}.`)}\\n\\n` +\n ` Something else is listening there — another Mica, most likely.\\n` +\n ` Stop it, or give this one a different port:\\n\\n` +\n ` npx @micacms/editor start --port ${port + 1}\\n\\n`\n );\n}\n"]}
@@ -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":"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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 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
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * `npx @micacms/editor` — Mica against a project on this machine.
3
+ * `npx @micacms/editor start` — Mica against a project on this machine.
4
4
  *
5
5
  * The point of this file is that adopting Mica should not mean adopting this
6
6
  * repository. Everything it does is set environment variables and hand over to
@@ -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;AAcH,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;AA+BD,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,CA+C1C"}
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * `npx @micacms/editor` — Mica against a project on this machine.
3
+ * `npx @micacms/editor start` — Mica against a project on this machine.
4
4
  *
5
5
  * The point of this file is that adopting Mica should not mean adopting this
6
6
  * repository. Everything it does is set environment variables and hand over to
@@ -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,17 +32,24 @@ 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 { formatImport, importCollections, ImportError } from "./import.js";
36
+ import { formatMigration, migrateLayout } from "./migrate.js";
35
37
  const USAGE = `
36
38
  mica — edit your Astro project's content, on this machine
37
39
 
38
40
  Usage
39
- $ npx @micacms/editor [content directory] open the editor
41
+ $ npx @micacms/editor start [content dir] open the editor
40
42
  $ npx @micacms/editor init add Mica to this Astro project
41
43
  $ npx @micacms/editor check [content dir] check every entry against the model
44
+ $ npx @micacms/editor migrate-layout [dir] move entries into the layout of the model
45
+ $ npx @micacms/editor import [names] move Astro collections into Mica
46
+ --from <file> Astro's data store (default: node_modules/.astro/data-store.json)
47
+ --map <c.field=key> take a field from another key; @body is the Markdown body
48
+ --order <c=field> the initial order of a sortable collection
42
49
 
43
50
  With @micacms/editor installed in the project, \`npx mica\` is the same command.
44
51
 
45
- Options
52
+ Options of start
46
53
  -c, --content <dir> The content root (default: ./content)
47
54
  -p, --port <number> Port to listen on (default: ${DEFAULT_PORT})
48
55
  --host <address> Address to bind (default: 127.0.0.1)
@@ -56,7 +63,7 @@ const USAGE = `
56
63
  container with an identity provider. See https://micacms.com/docs/self-hosting/.
57
64
  `;
58
65
  export function parse(argv) {
59
- const options = { content: "./content" };
66
+ const options = {};
60
67
  for (let index = 0; index < argv.length; index++) {
61
68
  const flag = argv[index];
62
69
  const value = () => {
@@ -92,7 +99,7 @@ export function parse(argv) {
92
99
  process.stdout.write(`${VERSION}\n`);
93
100
  process.exit(0);
94
101
  default:
95
- // A bare path is the content root, so `npx @micacms/editor ./site/content`
102
+ // A bare path is the content root, so `npx @micacms/editor start ./site/content`
96
103
  // does what it looks like it does.
97
104
  if (!flag.startsWith("-"))
98
105
  options.content = flag;
@@ -113,7 +120,7 @@ function report(title, lines) {
113
120
  process.stderr.write(`\n ${title}\n\n${body}\n\n`);
114
121
  }
115
122
  function fail(message) {
116
- report(message, ["Run `npx @micacms/editor --help` to see every option."]);
123
+ report(message, ["Run `npx @micacms/editor --help` to see every command and option."]);
117
124
  process.exit(64); // EX_USAGE
118
125
  }
119
126
  /**
@@ -126,20 +133,52 @@ export function shouldOpenBrowser(options, env) {
126
133
  return options.open;
127
134
  return Boolean(process.stdout.isTTY) && !env["CI"];
128
135
  }
136
+ /** A variable's value, with a blank one counted as not set. */
137
+ const set = (value) => (value?.trim() ? value : undefined);
138
+ /** The content root: the flag or path, else `MICA_CONTENT_ROOT`, else `./content`. */
139
+ export function contentRoot(options, env) {
140
+ return path.resolve(options.content ?? set(env["MICA_CONTENT_ROOT"]) ?? "./content");
141
+ }
142
+ /** What the server reads: a flag wins over the environment, and the environment over a default. */
143
+ export function serverEnvironment(options, env, content) {
144
+ const port = options.port ?? set(env["PORT"]) ?? String(DEFAULT_PORT);
145
+ return {
146
+ // The checks ran on this path, so it is the one the server gets.
147
+ MICA_CONTENT_ROOT: content,
148
+ HOST: options.host ?? set(env["HOST"]) ?? "127.0.0.1",
149
+ PORT: port,
150
+ // It must agree with the port: it scopes the cookie and the Origin check.
151
+ MICA_PUBLIC_URL: (!options.port && set(env["MICA_PUBLIC_URL"])) || `http://localhost:${port}`,
152
+ // A person, not a log collector: one banner instead of startup log lines.
153
+ MICA_CLI: "true",
154
+ MICA_OPEN: String(shouldOpenBrowser(options, env)),
155
+ };
156
+ }
129
157
  export async function main() {
130
158
  const [command, ...rest] = process.argv.slice(2);
131
159
  if (command === "init")
132
160
  return init(rest);
133
161
  if (command === "check")
134
162
  return check(rest);
135
- const options = parse(process.argv.slice(2));
136
- const content = path.resolve(options.content);
163
+ if (command === "migrate-layout")
164
+ return migrate(rest);
165
+ if (command === "import")
166
+ return importCommand(rest);
167
+ // The first word is always a command, so that a directory named `init` is not one.
168
+ if (command === undefined || command === "-h" || command === "--help")
169
+ return void process.stdout.write(`${USAGE}\n`);
170
+ if (command === "-v" || command === "--version")
171
+ return void process.stdout.write(`${VERSION}\n`);
172
+ if (command !== "start")
173
+ fail(`Unknown command "${command}". To open the editor: npx @micacms/editor start`);
174
+ const options = parse(rest);
175
+ const content = contentRoot(options, process.env);
137
176
  if (!existsSync(content)) {
138
177
  report(`There is no content directory at ${content}.`, [
139
178
  "Mica edits the directory your Astro project publishes content into —",
140
179
  "the `root` in your `defineSite({ root: \"./content\" })`.",
141
180
  "",
142
- " npx @micacms/editor ./path/to/content",
181
+ " npx @micacms/editor start ./path/to/content",
143
182
  "",
144
183
  "If this is a new project, create the directory and Mica writes the first",
145
184
  "entry into it.",
@@ -160,22 +199,7 @@ export async function main() {
160
199
  ]);
161
200
  process.exit(78); // EX_CONFIG
162
201
  }
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"]}`;
202
+ Object.assign(process.env, serverEnvironment(options, process.env, content));
179
203
  // The server is imported rather than spawned: one process, one signal
180
204
  // handler, and a stack trace that points at the real thing.
181
205
  await import("./index.js");
@@ -194,13 +218,71 @@ function init(argv) {
194
218
  }
195
219
  }
196
220
  async function check(argv) {
197
- const { content } = parse(argv);
198
- const root = path.resolve(content);
221
+ const root = contentRoot(parse(argv), process.env);
199
222
  const result = await checkContent(root);
200
223
  process.stdout.write(formatReport(result, root));
201
224
  if (result.problems.some((problem) => problem.level === "error"))
202
225
  process.exit(1);
203
226
  }
227
+ async function migrate(argv) {
228
+ const result = await migrateLayout(contentRoot(parse(argv), process.env));
229
+ process.stdout.write(formatMigration(result));
230
+ if (result.conflicts.length > 0)
231
+ process.exit(1);
232
+ }
233
+ async function importCommand(argv) {
234
+ // `astro sync` and `astro build` write the store into node_modules; `astro dev` into .astro.
235
+ const found = ["node_modules/.astro/data-store.json", ".astro/data-store.json"].find((file) => existsSync(file));
236
+ const options = { store: found ?? "node_modules/.astro/data-store.json", map: {}, order: {}, only: [], content: undefined };
237
+ for (let index = 0; index < argv.length; index++) {
238
+ const flag = argv[index];
239
+ const value = () => {
240
+ const next = argv[++index];
241
+ if (next === undefined || next.startsWith("-"))
242
+ fail(`${flag} needs a value.`);
243
+ return next;
244
+ };
245
+ const pair = (text) => {
246
+ const at = text.indexOf("=");
247
+ if (at < 1 || at === text.length - 1)
248
+ fail(`${flag} takes name=value, for example team.image.alt=alt.`);
249
+ return [text.slice(0, at), text.slice(at + 1)];
250
+ };
251
+ if (flag === "--from")
252
+ options.store = value();
253
+ else if (flag === "--content" || flag === "-c")
254
+ options.content = value();
255
+ else if (flag === "--map") {
256
+ const [to, from] = pair(value());
257
+ options.map[to] = from;
258
+ }
259
+ else if (flag === "--order") {
260
+ const [name, field] = pair(value());
261
+ options.order[name] = field;
262
+ }
263
+ else if (flag.startsWith("-"))
264
+ fail(`Unknown option "${flag}".`);
265
+ else
266
+ options.only.push(flag);
267
+ }
268
+ try {
269
+ const result = await importCollections({
270
+ root: contentRoot({ content: options.content }, process.env),
271
+ project: process.cwd(),
272
+ store: path.resolve(options.store),
273
+ map: options.map,
274
+ order: options.order,
275
+ only: options.only,
276
+ });
277
+ process.stdout.write(formatImport(result));
278
+ }
279
+ catch (error) {
280
+ if (!(error instanceof ImportError))
281
+ throw error;
282
+ report(error.message, []);
283
+ process.exit(1);
284
+ }
285
+ }
204
286
  // Only when this file *is* the command. Importing it, as the tests do, must
205
287
  // not start a server.
206
288
  //