@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/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAEzD,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AACxD,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAU/D,MAAM,KAAK,GAAG;;;;;;;;;;;;wDAY0C,YAAY;;;;;;;;;;CAUnE,CAAC;AAEF,MAAM,UAAU,KAAK,CAAC,IAAc;IAClC,MAAM,OAAO,GAAY,EAAE,OAAO,EAAE,WAAW,EAAE,CAAC;IAElD,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC;QACjD,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAE,CAAC;QAC1B,MAAM,KAAK,GAAG,GAAW,EAAE;YACzB,MAAM,IAAI,GAAG,IAAI,CAAC,EAAE,KAAK,CAAC,CAAC;YAC3B,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;gBAAE,IAAI,CAAC,GAAG,IAAI,iBAAiB,CAAC,CAAC;YAC/E,OAAO,IAAK,CAAC;QACf,CAAC,CAAC;QAEF,QAAQ,IAAI,EAAE,CAAC;YACb,KAAK,IAAI,CAAC;YACV,KAAK,WAAW;gBACd,OAAO,CAAC,OAAO,GAAG,KAAK,EAAE,CAAC;gBAC1B,MAAM;YACR,KAAK,IAAI,CAAC;YACV,KAAK,QAAQ;gBACX,OAAO,CAAC,IAAI,GAAG,KAAK,EAAE,CAAC;gBACvB,MAAM;YACR,KAAK,QAAQ;gBACX,OAAO,CAAC,IAAI,GAAG,KAAK,EAAE,CAAC;gBACvB,MAAM;YACR,KAAK,QAAQ;gBACX,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;gBACpB,MAAM;YACR,KAAK,WAAW;gBACd,OAAO,CAAC,IAAI,GAAG,KAAK,CAAC;gBACrB,MAAM;YACR,KAAK,IAAI,CAAC;YACV,KAAK,QAAQ;gBACX,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,KAAK,IAAI,CAAC,CAAC;gBACnC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAElB,KAAK,IAAI,CAAC;YACV,KAAK,WAAW;gBACd,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,OAAO,IAAI,CAAC,CAAC;gBACrC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAElB;gBACE,2EAA2E;gBAC3E,mCAAmC;gBACnC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;oBAAE,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC;;oBAC7C,IAAI,CAAC,mBAAmB,IAAI,IAAI,CAAC,CAAC;QAC3C,CAAC;IACH,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;;GAKG;AACH,SAAS,MAAM,CAAC,KAAa,EAAE,KAAe;IAC5C,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACvE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,KAAK,OAAO,IAAI,MAAM,CAAC,CAAC;AACtD,CAAC;AAED,SAAS,IAAI,CAAC,OAAe;IAC3B,MAAM,CAAC,OAAO,EAAE,CAAC,uDAAuD,CAAC,CAAC,CAAC;IAC3E,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,WAAW;AAC/B,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,OAAgB,EAAE,GAAsB;IACxE,IAAI,OAAO,CAAC,IAAI,KAAK,SAAS;QAAE,OAAO,OAAO,CAAC,IAAI,CAAC;IACpD,OAAO,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;AACrD,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,IAAI;IACxB,MAAM,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACjD,IAAI,OAAO,KAAK,MAAM;QAAE,OAAO,IAAI,CAAC,IAAI,CAAC,CAAC;IAC1C,IAAI,OAAO,KAAK,OAAO;QAAE,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC;IAE5C,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAC7C,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IAE9C,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;QACzB,MAAM,CAAC,oCAAoC,OAAO,GAAG,EAAE;YACrD,sEAAsE;YACtE,2DAA2D;YAC3D,EAAE;YACF,yCAAyC;YACzC,EAAE;YACF,0EAA0E;YAC1E,gBAAgB;SACjB,CAAC,CAAC;QACH,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,WAAW;IAC/B,CAAC;IAED,wEAAwE;IACxE,qEAAqE;IACrE,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,aAAa,CAAC,CAAC,EAAE,CAAC;QACnD,MAAM,CAAC,gCAAgC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,aAAa,CAAC,GAAG,EAAE;YAC3E,4EAA4E;YAC5E,8EAA8E;YAC9E,EAAE;YACF,mEAAmE;YACnE,EAAE;YACF,6EAA6E;YAC7E,4CAA4C;SAC7C,CAAC,CAAC;QACH,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,YAAY;IAChC,CAAC;IAED,MAAM,QAAQ,GAA2B;QACvC,YAAY,EAAE,OAAO;QACrB,IAAI,EAAE,OAAO,CAAC,IAAI,IAAI,WAAW;QACjC,IAAI,EAAE,OAAO,CAAC,IAAI,IAAI,MAAM,CAAC,YAAY,CAAC;QAC1C,wEAAwE;QACxE,uCAAuC;QACvC,QAAQ,EAAE,MAAM;QAChB,SAAS,EAAE,MAAM,CAAC,iBAAiB,CAAC,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC;KAC3D,CAAC;IAEF,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;QACpD,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,KAAK,CAAC;IAC7B,CAAC;IACD,wEAAwE;IACxE,2EAA2E;IAC3E,WAAW;IACX,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC,KAAK,oBAAoB,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;IAE7E,sEAAsE;IACtE,4DAA4D;IAC5D,MAAM,MAAM,CAAC,YAAY,CAAC,CAAC;AAC7B,CAAC;AAED,SAAS,IAAI,CAAC,IAAc;IAC1B,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC;QAAE,IAAI,CAAC,2DAA2D,CAAC,CAAC;IACvF,IAAI,CAAC;QACH,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,WAAW,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC;IAC/D,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,CAAC,CAAC,KAAK,YAAY,SAAS,CAAC;YAAE,MAAM,KAAK,CAAC;QAC/C,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;QAC1B,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,WAAW;IAC/B,CAAC;AACH,CAAC;AAED,KAAK,UAAU,KAAK,CAAC,IAAc;IACjC,MAAM,EAAE,OAAO,EAAE,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC;IAChC,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IACnC,MAAM,MAAM,GAAG,MAAM,YAAY,CAAC,IAAI,CAAC,CAAC;IACxC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,YAAY,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC;IACjD,IAAI,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,KAAK,OAAO,CAAC;QAAE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACpF,CAAC;AAED,4EAA4E;AAC5E,sBAAsB;AACtB,EAAE;AACF,yEAAyE;AACzE,4EAA4E;AAC5E,2EAA2E;AAC3E,8CAA8C;AAC9C,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,KAAK,aAAa,CAAC,YAAY,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAC7F,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;QAC9B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACpF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC,CAAC;AACL,CAAC","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 start` — 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 { formatImport, importCollections, ImportError } from \"./import.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 start [content dir] 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 $ npx @micacms/editor import [names] move Astro collections into Mica\n --from <file> Astro's data store (default: node_modules/.astro/data-store.json)\n --map <c.field=key> take a field from another key; @body is the Markdown body\n --order <c=field> the initial order of a sortable collection\n\n With @micacms/editor installed in the project, \\`npx mica\\` is the same command.\n\n Options of start\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 start ./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 command and 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 if (command === \"import\") return importCommand(rest);\n // The first word is always a command, so that a directory named `init` is not one.\n if (command === undefined || command === \"-h\" || command === \"--help\") return void process.stdout.write(`${USAGE}\\n`);\n if (command === \"-v\" || command === \"--version\") return void process.stdout.write(`${VERSION}\\n`);\n if (command !== \"start\") fail(`Unknown command \"${command}\". To open the editor: npx @micacms/editor start`);\n\n const options = parse(rest);\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 start ./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\nasync function importCommand(argv: string[]): Promise<void> {\n // `astro sync` and `astro build` write the store into node_modules; `astro dev` into .astro.\n const found = [\"node_modules/.astro/data-store.json\", \".astro/data-store.json\"].find((file) => existsSync(file));\n const options = { store: found ?? \"node_modules/.astro/data-store.json\", map: {} as Record<string, string>, order: {} as Record<string, string>, only: [] as string[], content: undefined as string | undefined };\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 const pair = (text: string): [string, string] => {\n const at = text.indexOf(\"=\");\n if (at < 1 || at === text.length - 1) fail(`${flag} takes name=value, for example team.image.alt=alt.`);\n return [text.slice(0, at), text.slice(at + 1)];\n };\n if (flag === \"--from\") options.store = value();\n else if (flag === \"--content\" || flag === \"-c\") options.content = value();\n else if (flag === \"--map\") { const [to, from] = pair(value()); options.map[to] = from; }\n else if (flag === \"--order\") { const [name, field] = pair(value()); options.order[name] = field; }\n else if (flag.startsWith(\"-\")) fail(`Unknown option \"${flag}\".`);\n else options.only.push(flag);\n }\n try {\n const result = await importCollections({\n root: contentRoot({ content: options.content }, process.env),\n project: process.cwd(),\n store: path.resolve(options.store),\n map: options.map,\n order: options.order,\n only: options.only,\n });\n process.stdout.write(formatImport(result));\n } catch (error) {\n if (!(error instanceof ImportError)) throw error;\n report(error.message, []);\n process.exit(1);\n }\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"]}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Reads the flattened format of `devalue`, which Astro uses for
3
+ * `.astro/data-store.json`: a JSON array whose first item is the root, and in
4
+ * which every other value refers to items by their index. Only what Astro
5
+ * writes is read: objects, arrays, Map, Set, Date and the special numbers.
6
+ */
7
+ export declare function unflatten(flat: unknown[]): unknown;
8
+ //# sourceMappingURL=devalue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"devalue.d.ts","sourceRoot":"","sources":["../src/devalue.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAWH,wBAAgB,SAAS,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,OAAO,CAiDlD"}
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Reads the flattened format of `devalue`, which Astro uses for
3
+ * `.astro/data-store.json`: a JSON array whose first item is the root, and in
4
+ * which every other value refers to items by their index. Only what Astro
5
+ * writes is read: objects, arrays, Map, Set, Date and the special numbers.
6
+ */
7
+ const SPECIAL = {
8
+ [-1]: undefined,
9
+ [-2]: undefined, // a hole in an array
10
+ [-3]: Number.NaN,
11
+ [-4]: Number.POSITIVE_INFINITY,
12
+ [-5]: Number.NEGATIVE_INFINITY,
13
+ [-6]: -0,
14
+ };
15
+ export function unflatten(flat) {
16
+ const done = new Map();
17
+ const value = (index) => {
18
+ if (index < 0)
19
+ return SPECIAL[index];
20
+ if (done.has(index))
21
+ return done.get(index);
22
+ const item = flat[index];
23
+ if (Array.isArray(item)) {
24
+ if (typeof item[0] === "string") {
25
+ const [type, ...rest] = item;
26
+ if (type === "Date")
27
+ return keep(index, new Date(rest[0]));
28
+ if (type === "Map") {
29
+ const map = keep(index, new Map());
30
+ for (let i = 0; i < rest.length; i += 2)
31
+ map.set(value(rest[i]), value(rest[i + 1]));
32
+ return map;
33
+ }
34
+ if (type === "Set") {
35
+ const set = keep(index, new Set());
36
+ for (const entry of rest)
37
+ set.add(value(entry));
38
+ return set;
39
+ }
40
+ if (type === "null") {
41
+ const object = keep(index, Object.create(null));
42
+ for (let i = 0; i < rest.length; i += 2)
43
+ object[rest[i]] = value(rest[i + 1]);
44
+ return object;
45
+ }
46
+ throw new Error(`The data store holds a value of type "${type}", which this version of Mica cannot read.`);
47
+ }
48
+ const array = keep(index, []);
49
+ for (const entry of item)
50
+ array.push(value(entry));
51
+ return array;
52
+ }
53
+ if (item !== null && typeof item === "object") {
54
+ const object = keep(index, {});
55
+ for (const [key, entry] of Object.entries(item))
56
+ object[key] = value(entry);
57
+ return object;
58
+ }
59
+ return keep(index, item);
60
+ };
61
+ function keep(index, result) {
62
+ done.set(index, result);
63
+ return result;
64
+ }
65
+ return value(0);
66
+ }
67
+ //# sourceMappingURL=devalue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"devalue.js","sourceRoot":"","sources":["../src/devalue.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Reads the flattened format of `devalue`, which Astro uses for\n * `.astro/data-store.json`: a JSON array whose first item is the root, and in\n * which every other value refers to items by their index. Only what Astro\n * writes is read: objects, arrays, Map, Set, Date and the special numbers.\n */\n\nconst SPECIAL: Record<number, unknown> = {\n [-1]: undefined,\n [-2]: undefined, // a hole in an array\n [-3]: Number.NaN,\n [-4]: Number.POSITIVE_INFINITY,\n [-5]: Number.NEGATIVE_INFINITY,\n [-6]: -0,\n};\n\nexport function unflatten(flat: unknown[]): unknown {\n const done = new Map<number, unknown>();\n\n const value = (index: number): unknown => {\n if (index < 0) return SPECIAL[index];\n if (done.has(index)) return done.get(index);\n const item = flat[index];\n\n if (Array.isArray(item)) {\n if (typeof item[0] === \"string\") {\n const [type, ...rest] = item as [string, ...unknown[]];\n if (type === \"Date\") return keep(index, new Date(rest[0] as string));\n if (type === \"Map\") {\n const map = keep(index, new Map<unknown, unknown>());\n for (let i = 0; i < rest.length; i += 2) map.set(value(rest[i] as number), value(rest[i + 1] as number));\n return map;\n }\n if (type === \"Set\") {\n const set = keep(index, new Set<unknown>());\n for (const entry of rest) set.add(value(entry as number));\n return set;\n }\n if (type === \"null\") {\n const object = keep(index, Object.create(null) as Record<string, unknown>);\n for (let i = 0; i < rest.length; i += 2) object[rest[i] as string] = value(rest[i + 1] as number);\n return object;\n }\n throw new Error(`The data store holds a value of type \"${type}\", which this version of Mica cannot read.`);\n }\n const array = keep(index, [] as unknown[]);\n for (const entry of item as number[]) array.push(value(entry));\n return array;\n }\n\n if (item !== null && typeof item === \"object\") {\n const object = keep(index, {} as Record<string, unknown>);\n for (const [key, entry] of Object.entries(item as Record<string, number>)) object[key] = value(entry);\n return object;\n }\n\n return keep(index, item);\n };\n\n function keep<T>(index: number, result: T): T {\n done.set(index, result);\n return result;\n }\n\n return value(0);\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"]}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * `mica import`: moves the entries of Astro content collections into Mica.
3
+ *
4
+ * It reads `.astro/data-store.json`, where Astro keeps every entry after its
5
+ * own loader and schema ran, so one importer covers `glob()`, `file()`,
6
+ * Markdown, MDX frontmatter, JSON and YAML alike. Each field of the Mica model
7
+ * takes the field with the same name, unless `--map` names another; values are
8
+ * converted to the stored form and written with the editor's own serializer.
9
+ */
10
+ /** The `--map` value for the Markdown body. Not `$body`: a shell would expand it. */
11
+ export declare const BODY = "@body";
12
+ export interface ImportOptions {
13
+ /** The Mica content root. */
14
+ root: string;
15
+ /** The Astro project, which the entries' file paths are relative to. */
16
+ project: string;
17
+ /** Astro's data store from before Mica replaced the collections: `node_modules/.astro/data-store.json`. */
18
+ store: string;
19
+ /** `collection.targetField` (dots for fields in an object, and `.alt`) → source key; `@body` is the Markdown body. Not inside repeater rows. */
20
+ map?: Record<string, string>;
21
+ /** Collection → the number field that gives the initial manual order. */
22
+ order?: Record<string, string>;
23
+ /** Only these collections and singletons; all that match by name when empty. */
24
+ only?: string[];
25
+ }
26
+ export interface ImportResult {
27
+ imported: Array<{
28
+ name: string;
29
+ entries: number;
30
+ }>;
31
+ problems: string[];
32
+ }
33
+ export declare class ImportError extends Error {
34
+ name: string;
35
+ }
36
+ export declare function importCollections(options: ImportOptions): Promise<ImportResult>;
37
+ /** The result as a person or an agent reads it. */
38
+ export declare function formatImport(result: ImportResult): string;
39
+ //# sourceMappingURL=import.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"import.d.ts","sourceRoot":"","sources":["../src/import.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAqBH,qFAAqF;AACrF,eAAO,MAAM,IAAI,UAAU,CAAC;AAK5B,MAAM,WAAW,aAAa;IAC5B,6BAA6B;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,wEAAwE;IACxE,OAAO,EAAE,MAAM,CAAC;IAChB,2GAA2G;IAC3G,KAAK,EAAE,MAAM,CAAC;IACd,gJAAgJ;IAChJ,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,yEAAyE;IACzE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,gFAAgF;IAChF,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACnD,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AASD,qBAAa,WAAY,SAAQ,KAAK;IAC3B,IAAI,SAAiB;CAC/B;AAED,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC,CAQrF;AA8PD,mDAAmD;AACnD,wBAAgB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CAKzD"}
package/dist/import.js ADDED
@@ -0,0 +1,268 @@
1
+ /**
2
+ * `mica import`: moves the entries of Astro content collections into Mica.
3
+ *
4
+ * It reads `.astro/data-store.json`, where Astro keeps every entry after its
5
+ * own loader and schema ran, so one importer covers `glob()`, `file()`,
6
+ * Markdown, MDX frontmatter, JSON and YAML alike. Each field of the Mica model
7
+ * takes the field with the same name, unless `--map` names another; values are
8
+ * converted to the stored form and written with the editor's own serializer.
9
+ */
10
+ import { existsSync, readFileSync } from "node:fs";
11
+ import { copyFile, mkdir, writeFile } from "node:fs/promises";
12
+ import path from "node:path";
13
+ import { collectionEntryPath, entryFiles, serializeEntry, singletonPath, slugify, validateFields, } from "@micacms/core";
14
+ import { within } from "@micacms/core/node";
15
+ import { contentFiles, loadSchema } from "@micacms/runtime";
16
+ import { unflatten } from "./devalue.js";
17
+ /** The `--map` value for the Markdown body. Not `$body`: a shell would expand it. */
18
+ export const BODY = "@body";
19
+ /** Astro's marker for a path that `image()` resolves against the entry's file. */
20
+ const IMAGE_MARKER = "__ASTRO_IMAGE_";
21
+ export class ImportError extends Error {
22
+ name = "ImportError";
23
+ }
24
+ export async function importCollections(options) {
25
+ try {
26
+ return await importAll(options);
27
+ }
28
+ catch (error) {
29
+ // Any refusal reads as one, not as a stack trace.
30
+ if (error instanceof ImportError)
31
+ throw error;
32
+ throw new ImportError(error.message, { cause: error });
33
+ }
34
+ }
35
+ async function importAll(options) {
36
+ const site = await loadSchema(contentFiles({ root: options.root }));
37
+ const astro = readStore(options.store);
38
+ const result = { imported: [], problems: [] };
39
+ const map = options.map ?? {};
40
+ const names = [...Object.keys(site.collections), ...Object.keys(site.singletons)].filter((name) => astro.has(name) && (!options.only?.length || options.only.includes(name)));
41
+ for (const name of options.only ?? []) {
42
+ if (!astro.has(name))
43
+ result.problems.push(`${name}: the data store has no collection with this name.`);
44
+ }
45
+ if (names.length === 0)
46
+ throw new ImportError("No Astro collection has the name of a Mica collection or singleton.");
47
+ // Before the first write, so that a refusal leaves nothing half done.
48
+ for (const name of names) {
49
+ const fields = (site.singletons[name] ?? site.collections[name]).fields;
50
+ const target = site.singletons[name] ? singletonPath(fields, name) : name;
51
+ if (existsSync(within(options.root, target))) {
52
+ throw new ImportError(`${target} exists in the content root. Delete it to import "${name}" again.`);
53
+ }
54
+ if (!site.singletons[name]) {
55
+ const empty = astro.get(name).find((entry) => slugify(entry.id.replace(/\//g, "-")) === "");
56
+ if (empty)
57
+ throw new ImportError(`${name}: the id "${empty.id}" gives no slug. Rename the entry first.`);
58
+ }
59
+ }
60
+ for (const name of names) {
61
+ const entries = astro.get(name);
62
+ const singleton = site.singletons[name];
63
+ const fields = (singleton ?? site.collections[name]).fields;
64
+ if (singleton && entries.length !== 1) {
65
+ result.problems.push(`${name}: a singleton takes one entry, and the collection has ${entries.length}. Not imported.`);
66
+ continue;
67
+ }
68
+ const used = new Set();
69
+ let bodyTaken = false;
70
+ const slugs = new Map();
71
+ const written = [];
72
+ for (const entry of entries) {
73
+ const slug = slugify(entry.id.replace(/\//g, "-"));
74
+ if (!singleton && slugs.has(slug)) {
75
+ result.problems.push(`${name}: "${entry.id}" and "${slugs.get(slug)}" both become "${slug}". Not imported: "${entry.id}".`);
76
+ continue;
77
+ }
78
+ slugs.set(slug, entry.id);
79
+ const base = singleton ? singletonPath(fields, name) : collectionEntryPath(fields, name, slug);
80
+ const context = { options, map, name, entry, base, slug, used, problems: result.problems, copies: [], body: false };
81
+ const data = convertFields(fields, entry.data, "", context, singleton ? undefined : slug);
82
+ bodyTaken ||= context.body;
83
+ const check = validateFields(fields, data);
84
+ if (!check.ok) {
85
+ for (const issue of check.issues)
86
+ result.problems.push(`${name}/${slug}: ${issue.path}: ${issue.message}`);
87
+ }
88
+ for (const [file, contents] of Object.entries(entryFiles(fields, base, serializeEntry(fields, data)))) {
89
+ await mkdir(path.dirname(within(options.root, file)), { recursive: true });
90
+ await writeFile(within(options.root, file), contents);
91
+ }
92
+ for (const { from, to } of context.copies) {
93
+ await mkdir(path.dirname(within(options.root, to)), { recursive: true });
94
+ await copyFile(from, within(options.root, to));
95
+ }
96
+ written.push({ slug, source: entry });
97
+ }
98
+ const orderField = options.order?.[name];
99
+ if (orderField) {
100
+ const def = site.collections[name];
101
+ if (!def?.sortable)
102
+ result.problems.push(`${name}: --order needs a collection with \`sortable: true\`.`);
103
+ else {
104
+ const position = (item) => {
105
+ const value = Number(item.source.data[orderField]);
106
+ return Number.isFinite(value) ? value : Number.POSITIVE_INFINITY;
107
+ };
108
+ const order = [...written].sort((a, b) => position(a) - position(b)).map((item) => item.slug);
109
+ await writeFile(within(options.root, `${name}/_order.json`), `${JSON.stringify(order, null, 2)}\n`);
110
+ used.add(orderField);
111
+ }
112
+ }
113
+ if (!bodyTaken && entries.some((entry) => entry.body?.trim())) {
114
+ result.problems.push(`${name}: not imported: the Markdown body. The model has no rich-text field \`body\`; map one with --map ${name}.<field>=@body.`);
115
+ }
116
+ const left = new Set(entries.flatMap((entry) => Object.keys(entry.data)).filter((key) => !used.has(key)));
117
+ if (left.size > 0)
118
+ result.problems.push(`${name}: not imported: ${[...left].join(", ")}. Map them with --map, or leave them out.`);
119
+ result.imported.push({ name, entries: written.length });
120
+ }
121
+ return result;
122
+ }
123
+ function convertFields(fields, source, prefix, context, slug) {
124
+ const data = {};
125
+ for (const [key, field] of Object.entries(fields)) {
126
+ const at = prefix ? `${prefix}.${key}` : key;
127
+ if (field.kind === "slug" && prefix === "" && slug !== undefined) {
128
+ data[key] = slug;
129
+ continue;
130
+ }
131
+ data[key] = convert(field, read(source, at, key, context), at, context);
132
+ }
133
+ return data;
134
+ }
135
+ /** The source value for a target field: mapped, else the key of the same name. */
136
+ function read(source, at, key, context) {
137
+ const mapped = context.map[`${context.name}.${at}`];
138
+ if (mapped === BODY) {
139
+ context.body = true;
140
+ return context.entry.body?.trim() || null;
141
+ }
142
+ if (mapped !== undefined) {
143
+ context.used.add(mapped.split(".")[0]);
144
+ return mapped.split(".").reduce((value, part) => (isRecord(value) ? value[part] : undefined), context.entry.data);
145
+ }
146
+ if (!at.includes("."))
147
+ context.used.add(key);
148
+ return source[key];
149
+ }
150
+ function convert(field, value, at, context) {
151
+ const where = `${context.name}/${context.slug}: ${at}`;
152
+ switch (field.kind) {
153
+ case "richText":
154
+ // The body goes to the rich-text field named `body`, unless a map says otherwise.
155
+ if (value == null && at === "body" && !(`${context.name}.body` in context.map) && context.entry.body?.trim()) {
156
+ context.body = true;
157
+ return context.entry.body.trim();
158
+ }
159
+ return value ?? null;
160
+ case "date":
161
+ return toDate(value, where, context, field.includeTime === true);
162
+ case "relation": {
163
+ const one = (item) => (isRecord(item) ? slugify(String(item["id"] ?? "")) : typeof item === "string" ? slugify(item) : null);
164
+ return field.multiple ? (Array.isArray(value) ? value.map(one).filter(Boolean) : []) : one(value);
165
+ }
166
+ case "media": {
167
+ const one = (item, index) => {
168
+ const stored = copyMedia(item, `${where}${index === undefined ? "" : `.${index}`}`, context);
169
+ if (stored === null || !field.alt)
170
+ return stored;
171
+ const alt = context.map[`${context.name}.${at}.alt`];
172
+ if (alt !== undefined)
173
+ context.used.add(alt.split(".")[0]);
174
+ const text = alt === undefined ? null : alt.split(".").reduce((v, p) => (isRecord(v) ? v[p] : undefined), context.entry.data);
175
+ return { path: stored, alt: typeof text === "string" ? text : null };
176
+ };
177
+ if (field.multiple)
178
+ return Array.isArray(value) ? value.map((item, index) => one(item, index)).filter((item) => item !== null) : [];
179
+ return one(value);
180
+ }
181
+ case "object": {
182
+ // A map can fill an object from flat keys, for example `blog.seo.title=metaTitle`.
183
+ const mapped = Object.keys(context.map).some((key) => key.startsWith(`${context.name}.${at}.`));
184
+ return isRecord(value) || mapped ? convertFields(field.fields, isRecord(value) ? value : {}, at, context) : null;
185
+ }
186
+ case "repeater":
187
+ return Array.isArray(value)
188
+ ? value.map((row, index) => (isRecord(row) ? convertFields(field.fields, row, `${at}.${index}`, { ...context, map: {} }) : {}))
189
+ : [];
190
+ case "select":
191
+ return field.multiple ? (Array.isArray(value) ? value : []) : (value ?? null);
192
+ case "boolean":
193
+ return typeof value === "boolean" ? value : (field.default ?? false);
194
+ case "number":
195
+ return typeof value === "number" ? value : value == null ? null : Number(value);
196
+ default:
197
+ return value instanceof Date ? value.toISOString() : (value ?? null);
198
+ }
199
+ }
200
+ /**
201
+ * A date as `YYYY-MM-DD` in UTC (a local-time date would give the day before
202
+ * east of UTC), or a full ISO timestamp for `date({ includeTime: true })`.
203
+ */
204
+ function toDate(value, where, context, withTime) {
205
+ if (value == null || value === "")
206
+ return null;
207
+ const parsed = value instanceof Date
208
+ ? value
209
+ : typeof value === "string" && /^\d{4}-\d{2}-\d{2}/.test(value)
210
+ ? new Date(value.length === 10 ? `${value}T00:00:00Z` : value)
211
+ : new Date(`${String(value)} UTC`);
212
+ if (Number.isNaN(parsed.getTime())) {
213
+ context.problems.push(`${where}: "${String(value)}" is not a date.`);
214
+ return null;
215
+ }
216
+ return withTime ? parsed.toISOString() : parsed.toISOString().slice(0, 10);
217
+ }
218
+ /** Copies a media file into the entry's `media/` folder, and gives the stored path. */
219
+ function copyMedia(value, where, context) {
220
+ if (typeof value !== "string" || value === "")
221
+ return null;
222
+ const raw = value.startsWith(IMAGE_MARKER) ? value.slice(IMAGE_MARKER.length) : value;
223
+ if (/^[a-z]+:\/\//i.test(raw)) {
224
+ context.problems.push(`${where}: "${raw}" is a URL. Download the file and add it in the editor.`);
225
+ return null;
226
+ }
227
+ const from = raw.startsWith("/")
228
+ ? path.join(context.options.project, raw)
229
+ : path.resolve(context.options.project, path.dirname(context.entry.filePath ?? "."), raw);
230
+ if (!existsSync(from)) {
231
+ context.problems.push(`${where}: the file "${raw}" does not exist.`);
232
+ return null;
233
+ }
234
+ const taken = new Set(context.copies.map((copy) => path.posix.basename(copy.to)));
235
+ let name = path.basename(from);
236
+ for (let n = 2; taken.has(name); n++)
237
+ name = `${path.parse(from).name}-${n}${path.extname(from)}`;
238
+ context.copies.push({ from, to: `${context.base}/media/${name}` });
239
+ return `media/${name}`;
240
+ }
241
+ /** Collection name → its entries, from Astro's data store. */
242
+ function readStore(file) {
243
+ if (!existsSync(file)) {
244
+ throw new ImportError(`There is no data store at ${file}. Run \`npx astro sync\` before Mica replaces the collections, and keep a copy.`);
245
+ }
246
+ const root = unflatten(JSON.parse(readFileSync(file, "utf8")));
247
+ if (!(root instanceof Map))
248
+ throw new ImportError(`${file} is not an Astro data store.`);
249
+ const collections = new Map();
250
+ for (const [name, entries] of root) {
251
+ if (typeof name !== "string" || name.includes("::") || !(entries instanceof Map))
252
+ continue;
253
+ collections.set(name, [...entries.values()]);
254
+ }
255
+ return collections;
256
+ }
257
+ function isRecord(value) {
258
+ return value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date);
259
+ }
260
+ /** The result as a person or an agent reads it. */
261
+ export function formatImport(result) {
262
+ const lines = result.imported.map(({ name, entries }) => ` imported ${name}: ${entries} ${entries === 1 ? "entry" : "entries"}`);
263
+ for (const problem of result.problems)
264
+ lines.push(` check ${problem}`);
265
+ lines.push("", "Next: run `npx mica check`, and compare the website with the version before.");
266
+ return `${lines.join("\n")}\n`;
267
+ }
268
+ //# sourceMappingURL=import.js.map