@anyday/sanity-workspace-manager 0.6.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +7 -7
  2. package/dist/cli.mjs +80 -40
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -65,7 +65,7 @@ workspace-manager.json linguist-generated=true
65
65
  | `organizationId` | The Sanity organisation new workspaces are created in |
66
66
  | `vercelTeamId` | The client's own Vercel team; left out for Anyday's |
67
67
  | `site` | The GitHub repo and branch each new site deploys from |
68
- | `studioOrigins` | The Studios that open new workspaces |
68
+ | `studioOrigins` | The Studios that open new workspaces, incl. a site's `/studio`; picked from the live project on first run |
69
69
  | `sections` | Each content type with its label in the Copy picker, or `false` to leave it out |
70
70
  | `colorRoles` | Each theme colour with a short description of where it's used, shown to whoever sets up a brand |
71
71
  | `themeFiles` | Theme fields with uploaded files (fonts) to copy into new sites; fonts are often licensed per brand |
@@ -106,12 +106,12 @@ A renamed field reads as one removed and one added, so `generate` asks rather th
106
106
 
107
107
  ### What it reads
108
108
 
109
- | Source | Used for |
110
- | ----------------------------- | -------------------------------------------------------------------------------------------- |
111
- | git remote and default branch | `site`, the suggested id and name |
112
- | `.env.example`, names only | `siteEnv`, the variables each new site needs |
113
- | `sanity.schema.json` | copy sections, singletons, home page, language documents, settings fields, colours and fonts |
114
- | the live project | organisation, Studio, theme colours and defaults, fonts named in `themeFiles`, starter pages |
109
+ | Source | Used for |
110
+ | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
111
+ | git remote and default branch | `site`, the suggested id and name |
112
+ | `.env.example`, names only | `siteEnv`, the variables each new site needs (`*_WORKSPACES_API_URL` if the site has one) |
113
+ | `sanity.schema.json` | copy sections, singletons, home page, language documents, settings fields, colours and fonts |
114
+ | the live project | organisation, Studios and CORS origins to pick from, theme colours and defaults, fonts named in `themeFiles`, starter pages |
115
115
 
116
116
  `sanity.schema.json` is looked for in the repo root, `frontend`, `web`, `site`, `studio`, `apps`
117
117
  and each folder in `apps`.
package/dist/cli.mjs CHANGED
@@ -233,6 +233,10 @@ var workspace_manager_schema_default = {
233
233
  },
234
234
  baseUrl: {
235
235
  $ref: "#/definitions/name"
236
+ },
237
+ workspacesApiUrl: {
238
+ description: "Set to the Workspaces API's address, so the site's own /studio lists every workspace.",
239
+ $ref: "#/definitions/name"
236
240
  }
237
241
  }
238
242
  },
@@ -583,6 +587,9 @@ function draftManifest(repo, answers, live = {}, choices) {
583
587
  return [key, found ?? ENV_DEFAULTS[key]];
584
588
  })
585
589
  );
590
+ const apiUrlNames = repo.envNames.filter((name) => /WORKSPACES_API_URL$/.test(name));
591
+ const apiUrl = apiUrlNames.find((name) => name.startsWith("NEXT_PUBLIC_")) ?? apiUrlNames[0];
592
+ if (apiUrl) siteEnv.workspacesApiUrl = apiUrl;
586
593
  if (!repo.repo) note("No GitHub remote found: set site.repo to owner/name");
587
594
  if (!repo.hasRevalidateRoute) note("No /api/revalidate route found: check site.revalidatePath");
588
595
  if (answers.vercelTeamId)
@@ -1201,9 +1208,65 @@ function configFromManifest(manifest, schema) {
1201
1208
  };
1202
1209
  }
1203
1210
 
1211
+ // src/generate/starter.ts
1212
+ var StarterError = class extends Error {
1213
+ };
1214
+ function resolveStarter(entries, pages, homeId) {
1215
+ const home = pages.find((page) => page._id === homeId);
1216
+ return entries.map((entry) => {
1217
+ if (entry === "home") {
1218
+ if (!homeId)
1219
+ throw new StarterError("The live settings name no home page: pass its slug instead");
1220
+ return homeId;
1221
+ }
1222
+ if (pages.some((page) => page._id === entry)) return entry;
1223
+ let matches = pages.filter((page) => page.slug === entry);
1224
+ if (matches.length > 1 && home?.language)
1225
+ matches = matches.filter((page) => page.language === home.language);
1226
+ if (matches.length === 1) return matches[0]._id;
1227
+ if (!matches.length) throw new StarterError(`No page with the slug or id "${entry}"`);
1228
+ throw new StarterError(
1229
+ `Several pages have the slug "${entry}": pass one of ${matches.map((page) => page._id).join(", ")}`
1230
+ );
1231
+ });
1232
+ }
1233
+ function pickable(pages, homeId) {
1234
+ const home = pages.find((page) => page._id === homeId);
1235
+ const shown = home?.language ? pages.filter((page) => page.language === home.language) : pages;
1236
+ return [...shown].sort(
1237
+ (a, b) => a._id === homeId ? -1 : b._id === homeId ? 1 : (a.title ?? "").localeCompare(b.title ?? "")
1238
+ );
1239
+ }
1240
+ function parsePick(answer, count) {
1241
+ const picked = answer.split(/[\s,]+/).filter(Boolean).map((part) => Number(part));
1242
+ const wrong = picked.filter((n) => !Number.isInteger(n) || n < 1 || n > count);
1243
+ if (wrong.length) throw new Error(`Pick numbers from 1 to ${count}`);
1244
+ return [...new Set(picked)].map((n) => n - 1);
1245
+ }
1246
+
1204
1247
  // src/generate/resolve.ts
1205
1248
  var yes = (answer) => /^y(es)?$/i.test(answer.trim());
1206
1249
  var ROLE_NAMES = { siteName: "site name", domain: "domain", logo: "logo", favicon: "favicon" };
1250
+ async function pickStudioOrigins(ask, saved, found) {
1251
+ const candidates = [.../* @__PURE__ */ new Set([...saved, ...found.studios, ...found.cors])];
1252
+ if (!candidates.length) return [];
1253
+ const chosen = saved.length ? saved : found.studios;
1254
+ const label = (origin) => saved.includes(origin) ? "saved" : found.studios.includes(origin) ? "Studio" : "CORS list";
1255
+ const list = candidates.map((origin, index) => ` ${index + 1}. ${origin} \u2190 ${label(origin)}`);
1256
+ const suggestion = candidates.flatMap((origin, index) => chosen.includes(origin) ? [index + 1] : []).join(",");
1257
+ let text = `Addresses that run this project's Studio, e.g. a site's /studio:
1258
+ ${list.join("\n")}
1259
+ By number`;
1260
+ for (; ; ) {
1261
+ try {
1262
+ return parsePick(await ask({ text, suggestion }), candidates.length).map(
1263
+ (index) => candidates[index]
1264
+ );
1265
+ } catch (error) {
1266
+ text = `${error.message}. ${text}`;
1267
+ }
1268
+ }
1269
+ }
1207
1270
  async function resolveIdentity(options) {
1208
1271
  const { saved, flags, repo, ask, lookups, note } = options;
1209
1272
  const known = { ...saved, ...flags };
@@ -1233,9 +1296,11 @@ async function resolveIdentity(options) {
1233
1296
  };
1234
1297
  if (!site.repo) throw new Error("No GitHub remote found: add one, or set site.repo in the config");
1235
1298
  let studioOrigins = known.studioOrigins;
1236
- if (!studioOrigins?.length || options.askAgain) {
1237
- const found = await lookups.studioOriginsOf?.(project).catch(() => void 0);
1238
- studioOrigins = found?.length ? found : studioOrigins;
1299
+ if (!flags.studioOrigins && (!studioOrigins?.length || options.askAgain)) {
1300
+ studioOrigins = await pickStudioOrigins(ask, saved.studioOrigins ?? [], {
1301
+ studios: await lookups.studioOriginsOf?.(project).catch(() => []) ?? [],
1302
+ cors: await lookups.corsOriginsOf?.(project).catch(() => []) ?? []
1303
+ });
1239
1304
  }
1240
1305
  if (!studioOrigins?.length)
1241
1306
  note(`No Studio found: add studioOrigins to the config, e.g. ["https://acme.sanity.studio"]`);
@@ -1494,6 +1559,16 @@ async function studioOriginsOf(projectId, token) {
1494
1559
  if (studioHost) origins.push(`https://${studioHost}.sanity.studio`);
1495
1560
  return [...new Set(origins)];
1496
1561
  }
1562
+ async function corsOriginsOf(projectId, token) {
1563
+ const response = await fetch(`https://api.sanity.io/v2021-06-07/projects/${projectId}/cors`, {
1564
+ headers: { Authorization: `Bearer ${token}` }
1565
+ });
1566
+ if (!response.ok) throw new Error(`Couldn't list ${projectId}'s CORS origins: ${response.status}`);
1567
+ const entries = await response.json();
1568
+ return entries.filter(
1569
+ ({ origin, allowCredentials }) => allowCredentials && origin.startsWith("https://") && !origin.includes("*") && !/^https:\/\/(localhost|127\.0\.0\.1)(:|$)/.test(origin)
1570
+ ).map(({ origin }) => origin.replace(/\/+$/, ""));
1571
+ }
1497
1572
  var liveClient = ({ projectId, dataset, token }) => createClient({
1498
1573
  projectId,
1499
1574
  dataset,
@@ -1575,42 +1650,6 @@ function readRepo(root) {
1575
1650
  };
1576
1651
  }
1577
1652
 
1578
- // src/generate/starter.ts
1579
- var StarterError = class extends Error {
1580
- };
1581
- function resolveStarter(entries, pages, homeId) {
1582
- const home = pages.find((page) => page._id === homeId);
1583
- return entries.map((entry) => {
1584
- if (entry === "home") {
1585
- if (!homeId)
1586
- throw new StarterError("The live settings name no home page: pass its slug instead");
1587
- return homeId;
1588
- }
1589
- if (pages.some((page) => page._id === entry)) return entry;
1590
- let matches = pages.filter((page) => page.slug === entry);
1591
- if (matches.length > 1 && home?.language)
1592
- matches = matches.filter((page) => page.language === home.language);
1593
- if (matches.length === 1) return matches[0]._id;
1594
- if (!matches.length) throw new StarterError(`No page with the slug or id "${entry}"`);
1595
- throw new StarterError(
1596
- `Several pages have the slug "${entry}": pass one of ${matches.map((page) => page._id).join(", ")}`
1597
- );
1598
- });
1599
- }
1600
- function pickable(pages, homeId) {
1601
- const home = pages.find((page) => page._id === homeId);
1602
- const shown = home?.language ? pages.filter((page) => page.language === home.language) : pages;
1603
- return [...shown].sort(
1604
- (a, b) => a._id === homeId ? -1 : b._id === homeId ? 1 : (a.title ?? "").localeCompare(b.title ?? "")
1605
- );
1606
- }
1607
- function parsePick(answer, count) {
1608
- const picked = answer.split(/[\s,]+/).filter(Boolean).map((part) => Number(part));
1609
- const wrong = picked.filter((n) => !Number.isInteger(n) || n < 1 || n > count);
1610
- if (wrong.length) throw new Error(`Pick numbers from 1 to ${count}`);
1611
- return [...new Set(picked)].map((n) => n - 1);
1612
- }
1613
-
1614
1653
  // src/register.ts
1615
1654
  import { readFileSync as readFileSync4 } from "node:fs";
1616
1655
  import { join as join3 } from "node:path";
@@ -1758,7 +1797,8 @@ async function generate(argv) {
1758
1797
  note,
1759
1798
  lookups: token ? {
1760
1799
  organizationOf: (projectId) => organizationOf(projectId, token),
1761
- studioOriginsOf: (projectId) => studioOriginsOf(projectId, token)
1800
+ studioOriginsOf: (projectId) => studioOriginsOf(projectId, token),
1801
+ corsOriginsOf: (projectId) => corsOriginsOf(projectId, token)
1762
1802
  } : {}
1763
1803
  });
1764
1804
  const choices = await resolveChoices({ saved, schema, ask, askAgain, note });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anyday/sanity-workspace-manager",
3
- "version": "0.6.0",
3
+ "version": "0.7.1",
4
4
  "description": "Generates and checks a site repo's workspace-manager.json for the Anyday workspace manager",
5
5
  "repository": {
6
6
  "type": "git",