@shapio/cms 0.5.1 → 0.5.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 CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { $ as parseLockFile, A as buildEditorManifest, B as RESERVED_DEFINITION_API_KEYS, C as loopbackUrl, D as BEFORE_EVENTS, E as AFTER_EVENTS, F as lockById, G as THEME_VARIANTS, H as RESERVED_TYPE_NAMES, I as update, J as hashDefinition, K as checkThemeContrast, L as STORAGE_DRIVERS, N as listForAssets, O as EXTENSION_JOB_PREFIX, P as listLiveByDriver, Q as parseDefinition, R as describeError, S as createUrlBuilder, T as loadProjectConfig, U as SCHEMA_KIND_DIRECTORIES, V as RESERVED_FIELD_API_KEYS, W as SITES_DIRECTORY, X as lockEntriesForSite, Y as lockCoversSite, Z as mergePulledLock, _ as listAllSites, a as loadConfig, at as toTypeName, b as generateToken, ct as withSiteCovered, d as migrateToLatest, dt as createStableId, et as schemaFilePath, f as SHAPIO_VERSION, ft as validateRichText, g as createSite, i as assertDatabaseReachable, it as suggestPlural, k as EXTENSION_ROUTE_ROOT, lt as RICHTEXT_FORMAT, mt as sqliteDriverOf, n as startServer, nt as serializeDefinition, o as loadEnvFileIfPresent, ot as upgradeLockFile, p as createMediaStorage, pt as createDb, q as foldApiKey, r as copyObjectVerified, rt as serializeLockFile, st as validateSchema, t as startDedicatedWorker, tt as scopeDirectory, u as getPendingMigrations, ut as SAFE_LINK_PATTERN, v as createAdminDirect, w as SITE_KEY_PATTERN, x as createLogger, y as SYSTEM_CLI_ACTOR } from "./worker-BU-V4qbH.js";
2
+ import { $ as parseLockFile, A as buildEditorManifest, B as RESERVED_DEFINITION_API_KEYS, C as loopbackUrl, D as BEFORE_EVENTS, E as AFTER_EVENTS, F as lockById, G as THEME_VARIANTS, H as RESERVED_TYPE_NAMES, I as update, J as hashDefinition, K as checkThemeContrast, L as STORAGE_DRIVERS, N as listForAssets, O as EXTENSION_JOB_PREFIX, P as listLiveByDriver, Q as parseDefinition, R as describeError, S as createUrlBuilder, T as loadProjectConfig, U as SCHEMA_KIND_DIRECTORIES, V as RESERVED_FIELD_API_KEYS, W as SITES_DIRECTORY, X as lockEntriesForSite, Y as lockCoversSite, Z as mergePulledLock, _ as listAllSites, a as loadConfig, at as toTypeName, b as generateToken, ct as withSiteCovered, d as migrateToLatest, dt as createStableId, et as schemaFilePath, f as SHAPIO_VERSION, ft as validateRichText, g as createSite, i as assertDatabaseReachable, it as suggestPlural, k as EXTENSION_ROUTE_ROOT, lt as RICHTEXT_FORMAT, mt as sqliteDriverOf, n as startServer, nt as serializeDefinition, o as loadEnvFileIfPresent, ot as upgradeLockFile, p as createMediaStorage, pt as createDb, q as foldApiKey, r as copyObjectVerified, rt as serializeLockFile, st as validateSchema, t as startDedicatedWorker, tt as scopeDirectory, u as getPendingMigrations, ut as SAFE_LINK_PATTERN, v as createAdminDirect, w as SITE_KEY_PATTERN, x as createLogger, y as SYSTEM_CLI_ACTOR } from "./worker-W5eY5lSj.js";
3
3
  import { createDecipheriv, createHash, scryptSync } from "node:crypto";
4
4
  import { createReadStream, createWriteStream, existsSync, openAsBlob } from "node:fs";
5
5
  import { basename, dirname, extname, isAbsolute, join, posix, relative, resolve } from "node:path";
@@ -2290,12 +2290,18 @@ const planStrapiSchema = (schemas) => {
2290
2290
  };
2291
2291
  const taken = /* @__PURE__ */ new Set();
2292
2292
  const plans = /* @__PURE__ */ new Map();
2293
+ /** The API ID for `name`; a reserved or repeated name gets a suffix, which the plan lists. */
2294
+ const claimApiKey = (schema, name) => {
2295
+ const apiKey = definitionApiKey(name, taken);
2296
+ if (apiKey !== toCamelCase(name)) context.notes.push(`${schema.uid} is named ${apiKey} (the name is reserved or already taken).`);
2297
+ return apiKey;
2298
+ };
2293
2299
  for (const schema of contentTypes) {
2294
- const apiKey = definitionApiKey(schema.info?.singularName ?? schema.uid.split(".").at(-1) ?? "model", taken);
2295
- plans.set(schema.uid, planDefinition(schema, apiKey, context));
2300
+ const name = schema.info?.singularName ?? schema.uid.split(".").at(-1) ?? "model";
2301
+ plans.set(schema.uid, planDefinition(schema, claimApiKey(schema, name), context));
2296
2302
  }
2297
2303
  const baseName = componentBaseNames(components);
2298
- for (const schema of components) plans.set(schema.uid, planDefinition(schema, definitionApiKey(baseName(schema), taken), context));
2304
+ for (const schema of components) plans.set(schema.uid, planDefinition(schema, claimApiKey(schema, baseName(schema)), context));
2299
2305
  if (contentTypes.some((schema) => schema.options?.draftAndPublish === false)) context.notes.push(`Draft and publish is turned on for ${contentTypes.filter((schema) => schema.options?.draftAndPublish === false).map((schema) => plans.get(schema.uid)?.definition.apiKey).join(", ")} (off in Strapi), so imported entries are reviewed before they go live.`);
2300
2306
  return {
2301
2307
  plans,
@@ -2337,6 +2343,8 @@ const indexLinks = (links) => {
2337
2343
  const stringOf = (value) => typeof value === "string" ? value : void 0;
2338
2344
  /** Strapi IDs and numeric scalars as text (objects are not scalars and give undefined). */
2339
2345
  const textOf = (value) => typeof value === "string" ? value : typeof value === "number" || typeof value === "bigint" ? String(value) : void 0;
2346
+ /** Strapi's `name` may lack the extension (its example project's names do); `ext` always has it. */
2347
+ const withExtension = (name, ext) => name.toLowerCase().endsWith(ext.toLowerCase()) ? name : `${name}${ext}`;
2340
2348
  const fileMedia = (data, root) => {
2341
2349
  const hash = stringOf(data.hash);
2342
2350
  const ext = stringOf(data.ext) ?? "";
@@ -2345,9 +2353,10 @@ const fileMedia = (data, root) => {
2345
2353
  const alt = stringOf(data.alternativeText);
2346
2354
  const caption = stringOf(data.caption);
2347
2355
  const mimeType = stringOf(data.mime);
2356
+ const name = stringOf(data.name);
2348
2357
  return {
2349
2358
  sourceId: `file:${id}`,
2350
- filename: stringOf(data.name) ?? `${hash}${ext}`,
2359
+ filename: name ? withExtension(name, ext) : `${hash}${ext}`,
2351
2360
  path: join(root, "assets", "uploads", `${hash}${ext}`),
2352
2361
  ...mimeType ? { mimeType } : {},
2353
2362
  ...alt ? { alt } : {},
@@ -3649,13 +3658,39 @@ const runMap = async (options, io) => {
3649
3658
  for (const failure of changeSets.failed) io.stderr(` ! change set item ${failure.key}: ${failure.message}\n`);
3650
3659
  return printSummary(run, source, changeSets.failed.length);
3651
3660
  };
3661
+ /**
3662
+ * The site a `--plan` writes its models for. `--plan` runs offline (no URL or token), so it cannot ask the
3663
+ * instance which site a token works on: it takes `--site`, else `SHAPIO_SITE`, else the primary site, whose key
3664
+ * is always `default`. On a single-site instance that is the only site, so the planned models apply with an
3665
+ * ordinary admin token. `--shared` plans models shared with all sites instead (applying them needs a network
3666
+ * admin token).
3667
+ */
3668
+ const PRIMARY_SITE_KEY = "default";
3669
+ const resolvePlanSite = (options, env) => {
3670
+ const named = options.site?.trim() || void 0;
3671
+ if (options.shared) {
3672
+ if (named) throw new UsageError$1("Pass either --site <key> or --shared, not both");
3673
+ return {
3674
+ site: void 0,
3675
+ explicit: true
3676
+ };
3677
+ }
3678
+ const site = named ?? (env.SHAPIO_SITE?.trim() || void 0);
3679
+ return site ? {
3680
+ site,
3681
+ explicit: true
3682
+ } : {
3683
+ site: PRIMARY_SITE_KEY,
3684
+ explicit: false
3685
+ };
3686
+ };
3652
3687
  /** Relative to the working directory when inside it, else absolute (what to paste into the next command). */
3653
3688
  const display = (path) => {
3654
3689
  const shown = relative(process.cwd(), path);
3655
3690
  return !shown ? "." : shown.startsWith("..") || isAbsolute(shown) ? path : shown;
3656
3691
  };
3657
3692
  /** What `--plan` wrote and what to do next. */
3658
- const formatPlanSummary = (source, definitions, dir, command, site) => {
3693
+ const formatPlanSummary = (source, definitions, dir, command, { site, explicit }) => {
3659
3694
  const keyOf = new Map(source.definitions.map((definition) => [definition.apiKey, definition.key]));
3660
3695
  const lines = [`Planned ${definitions.length} definition(s) in ${display(`${dir}/${PLAN_SCHEMA_DIR}`)}:`];
3661
3696
  for (const definition of definitions) {
@@ -3666,8 +3701,8 @@ const formatPlanSummary = (source, definitions, dir, command, site) => {
3666
3701
  lines.push(`Media: ${source.media.length} file(s) to import.`);
3667
3702
  lines.push("Every model has draft and publish on: nothing goes live until you ship the change set(s) the import opens.");
3668
3703
  lines.push(...source.notes);
3669
- lines.push(site ? `The models and components will belong to site "${site}".` : "No --site given: the models and components will be shared with all sites. Plan again with --site <key> to keep them on one site.");
3670
- const siteFlag = site ? ` --site ${site}` : "";
3704
+ lines.push(!site ? "The models and components will be shared with all sites (--shared): applying them needs a network admin token." : explicit ? `The models and components will belong to site "${site}".` : `The models and components will belong to the primary site ("${site}"). Plan with --site <key> for another site, or --shared to share them with all sites.`);
3705
+ const siteFlag = site && explicit ? ` --site ${site}` : "";
3671
3706
  lines.push("Next:", " 1. Review the files: rename API IDs or delete fields you do not want (entries follow the stable IDs).", ` 2. shapio schema apply --dir ${display(`${dir}/${PLAN_SCHEMA_DIR}`)} --lock ${display(`${dir}/${PLAN_LOCK_FILE}`)}${siteFlag}`, ` 3. shapio import ${command} --map ${display(dir)}${siteFlag}`);
3672
3707
  return `${lines.join("\n")}\n`;
3673
3708
  };
@@ -3681,6 +3716,10 @@ const parse = (args, spec) => {
3681
3716
  type: "boolean",
3682
3717
  default: false
3683
3718
  },
3719
+ shared: {
3720
+ type: "boolean",
3721
+ default: false
3722
+ },
3684
3723
  url: { type: "string" },
3685
3724
  token: { type: "string" },
3686
3725
  site: { type: "string" },
@@ -3692,28 +3731,33 @@ const parse = (args, spec) => {
3692
3731
  if (values.plan === void 0 === (values.map === void 0)) throw new UsageError$1("Pass either --plan <dir> (with the export file) or --map <dir>");
3693
3732
  if (values.plan !== void 0 && !positionals[0]) throw new UsageError$1("--plan needs the export file");
3694
3733
  if (values.map !== void 0 && positionals[0]) throw new UsageError$1("--map reads the export recorded in the plan: leave the file out");
3734
+ if (values.map !== void 0 && values.shared) throw new UsageError$1("--shared is a --plan option: --map imports where the plan put the models");
3735
+ if (values.shared && values.site !== void 0) throw new UsageError$1("Pass either --site <key> or --shared, not both");
3695
3736
  return {
3696
3737
  values,
3697
3738
  file: positionals[0]
3698
3739
  };
3699
3740
  };
3700
- /** `--site`, else SHAPIO_SITE: the site the plan's schema belongs to and the map step writes to. */
3741
+ /** `--site`, else SHAPIO_SITE: the site `--map` writes to (else the site the plan recorded). */
3701
3742
  const siteOf = (values, io) => (values.site ?? io.env.SHAPIO_SITE)?.trim() || void 0;
3702
3743
  const runPlan = async (spec, file, values, io) => {
3703
3744
  const dir = resolve(String(values.plan));
3704
3745
  const path = resolve(file);
3705
3746
  await checkPlanDirectory(dir, values.force === true);
3747
+ const planSite = resolvePlanSite({
3748
+ site: values.site,
3749
+ shared: values.shared === true
3750
+ }, io.env);
3706
3751
  const source = await spec.plan(path, dir, values);
3707
- const site = siteOf(values, io);
3708
3752
  const { definitions } = await writePlan(dir, source, {
3709
3753
  kind: source.kind,
3710
3754
  path,
3711
3755
  sha256: await sha256File(path)
3712
3756
  }, {
3713
3757
  force: values.force === true,
3714
- site
3758
+ site: planSite.site
3715
3759
  });
3716
- io.stdout(formatPlanSummary(source, definitions, dir, spec.name, site));
3760
+ io.stdout(formatPlanSummary(source, definitions, dir, spec.name, planSite));
3717
3761
  return 0;
3718
3762
  };
3719
3763
  const runMapStep = (spec, values, io) => {
@@ -3759,7 +3803,7 @@ const importStrapiCommand = createImporterCommand({
3759
3803
  name: "strapi",
3760
3804
  label: "Strapi",
3761
3805
  summary: "Import a Strapi 5 export: --plan writes schema files, --map creates drafts and change sets",
3762
- usage: "shapio import strapi <export.tar.gz[.enc]> --plan <dir> [--key <encryption key>] [--site <key>] [--force]\n shapio import strapi --map <dir> [--url <origin>] [--token <admin token>] [--site <key>]\n --plan unpacks the export into <dir>/source (--key for an encrypted export) and writes the planned models\n and components (<dir>/schema) and <dir>/import-map.json; it sends nothing. With --site (or SHAPIO_SITE)\n they belong to that site (<dir>/schema/sites/<key>/); without, they are shared (--map\n without --site imports into the planned site). Apply the schema with\n `shapio schema apply --dir <dir>/schema --lock <dir>/schema-lock.json [--site <key>]`, then run --map:\n it uploads the media, creates every document as a draft and opens change sets with the published ones.\n Strapi 5 only.",
3806
+ usage: "shapio import strapi <export.tar[.gz[.enc]]> --plan <dir> [--key <encryption key>] [--site <key> | --shared] [--force]\n shapio import strapi --map <dir> [--url <origin>] [--token <admin token>] [--site <key>]\n --plan unpacks the export into <dir>/source (--key for an encrypted export) and writes the planned models\n and components (<dir>/schema) and <dir>/import-map.json; it sends nothing. They belong to one site\n (<dir>/schema/sites/<key>/): --site (or SHAPIO_SITE), else the primary site (\"default\"); --shared shares\n them with all sites (applying them needs a network admin token). --map without --site imports into the\n planned site. Apply the schema with\n `shapio schema apply --dir <dir>/schema --lock <dir>/schema-lock.json [--site <key>]`, then run --map:\n it uploads the media, creates every document as a draft and opens change sets with the published ones.\n Strapi 5 only.",
3763
3807
  options: ["key"],
3764
3808
  plan: async (file, dir, values) => {
3765
3809
  await extractStrapiExport(file, sourceDir(dir), typeof values.key === "string" ? values.key : void 0);
@@ -4353,7 +4397,7 @@ const importWordPressCommand = createImporterCommand({
4353
4397
  name: "wordpress",
4354
4398
  label: "WordPress",
4355
4399
  summary: "Import a WordPress export (WXR): --plan writes schema files, --map creates drafts and change sets",
4356
- usage: "shapio import wordpress <export.xml> --plan <dir> [--site <key>] [--force]\n shapio import wordpress --map <dir> [--url <origin>] [--token <admin token>] [--site <key>] [--media-dir <uploads dir>]\n --plan reads the export and writes the planned models (<dir>/schema) and <dir>/import-map.json; it sends nothing.\n With --site (or SHAPIO_SITE) the models belong to that site (<dir>/schema/sites/<key>/); without, they are shared;\n --map without --site then imports into the planned site.\n Apply the models with `shapio schema apply --dir <dir>/schema --lock <dir>/schema-lock.json [--site <key>]`, then run --map:\n it uploads the media (downloaded from the site, or read from --media-dir, a copy of wp-content/uploads),\n creates every post and page as a draft, and opens change sets with the published ones. Re-run --map to resume.",
4400
+ usage: "shapio import wordpress <export.xml> --plan <dir> [--site <key> | --shared] [--force]\n shapio import wordpress --map <dir> [--url <origin>] [--token <admin token>] [--site <key>] [--media-dir <uploads dir>]\n --plan reads the export and writes the planned models (<dir>/schema) and <dir>/import-map.json; it sends nothing.\n The models belong to one site (<dir>/schema/sites/<key>/): --site (or SHAPIO_SITE), else the primary site\n (\"default\"); --shared shares them with all sites (applying them needs a network admin token).\n --map without --site imports into the planned site.\n Apply the models with `shapio schema apply --dir <dir>/schema --lock <dir>/schema-lock.json [--site <key>]`, then run --map:\n it uploads the media (downloaded from the site, or read from --media-dir, a copy of wp-content/uploads),\n creates every post and page as a draft, and opens change sets with the published ones. Re-run --map to resume.",
4357
4401
  options: ["media-dir"],
4358
4402
  plan: async (file) => wordpressSource(await readWxr(file)),
4359
4403
  load: async (map, _dir, values) => wordpressSource(await readWxr(map.source.path), mediaDirOf(values))