@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.
- package/README.md +7 -7
- package/dist/cli.mjs +80 -40
- 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,
|
|
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
|
-
|
|
1238
|
-
|
|
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 });
|