@anyday/sanity-workspace-manager 0.2.0 → 0.5.0
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 +45 -21
- package/dist/cli.mjs +84 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -43,19 +43,36 @@ npx @anyday/sanity-workspace-manager register --api https://anyday-workspace-man
|
|
|
43
43
|
npx @anyday/sanity-workspace-manager init [dir] [options]
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
`dir` is the site repo, and defaults to the current folder. `init` asks
|
|
46
|
+
`dir` is the site repo, and defaults to the current folder. `init` asks five questions, each with a
|
|
47
47
|
suggestion you can accept with Enter:
|
|
48
48
|
|
|
49
|
-
| Question | Suggested from
|
|
50
|
-
| ------------------------------------------- |
|
|
51
|
-
| Project type id, short and lowercase | The repo name in the git remote, e.g. `acme-site`
|
|
52
|
-
| Name shown in the app | The same repo name, capitalised
|
|
53
|
-
| Sanity project to copy schemas from | A `projectId: '…'` written in the Studio's config, if there is one
|
|
54
|
-
| Sanity organisation to create workspaces in | Looked up from that project with your login
|
|
49
|
+
| Question | Suggested from |
|
|
50
|
+
| ------------------------------------------- | ------------------------------------------------------------------------------------- |
|
|
51
|
+
| Project type id, short and lowercase | The repo name in the git remote, e.g. `acme-site` |
|
|
52
|
+
| Name shown in the app | The same repo name, capitalised |
|
|
53
|
+
| Sanity project to copy schemas from | A `projectId: '…'` written in the Studio's config, if there is one |
|
|
54
|
+
| Sanity organisation to create workspaces in | Looked up from that project with your login |
|
|
55
|
+
| Vercel team for the sites | Nothing: press Enter for Anyday's team; give a team id only for the client's own team |
|
|
55
56
|
|
|
56
57
|
The id is permanent: jobs store it and it prefixes each site's Vercel project. Registering a
|
|
57
58
|
different id later creates a second project type instead of updating this one.
|
|
58
59
|
|
|
60
|
+
### Which command to run
|
|
61
|
+
|
|
62
|
+
| Command | Questions | Starter pages | Use it when |
|
|
63
|
+
| ---------------------------------- | ------------------------ | ---------------------------------------------- | --------------------------------------------- |
|
|
64
|
+
| `init --live` | Yes, then a list to pick | The pages you pick (none: one empty home page) | Usually: real theme, colours and pages |
|
|
65
|
+
| `init --live --starter home,about` | Yes, no list | Those pages | You already know which pages |
|
|
66
|
+
| `init --live --yes` | None, suggestions taken | One empty home page | Scripts, or no starter content wanted |
|
|
67
|
+
| `init` | Yes | One empty home page | The project has no content yet, or no sign-in |
|
|
68
|
+
|
|
69
|
+
Without `--live`, `init` reads only the repo's files, so the manifest has the structure but no
|
|
70
|
+
values: colour defaults are `#000000`, theme defaults and `studioOrigins` are empty. Fill them in
|
|
71
|
+
by hand, or run again with `--live --force` once the project has content.
|
|
72
|
+
|
|
73
|
+
Starter pages are only what **Start fresh** writes. **Copy from a workspace** is picked in the
|
|
74
|
+
Workspaces app for each new workspace, and reads its source live; it needs no `init` option.
|
|
75
|
+
|
|
59
76
|
### What it reads
|
|
60
77
|
|
|
61
78
|
| Source | Used for |
|
|
@@ -63,23 +80,25 @@ different id later creates a second project type instead of updating this one.
|
|
|
63
80
|
| git remote and default branch | `site.repo`, `site.branch`, the suggested id and name |
|
|
64
81
|
| `.env.example`, names only | `siteEnv`, the variables each new site needs |
|
|
65
82
|
| `sanity.schema.json` | copy sections, singletons, home page, language documents, settings fields, colour and font templates |
|
|
66
|
-
| the live project, with `--live` | theme defaults, colour defaults and
|
|
83
|
+
| the live project, with `--live` | theme defaults, colour defaults, starter pages, and its deployed Studio for `studioOrigins` |
|
|
67
84
|
|
|
68
85
|
`sanity.schema.json` is looked for in the repo root, `frontend`, `web`, `site`, `studio`, `apps`
|
|
69
86
|
and each folder in `apps`.
|
|
70
87
|
|
|
71
88
|
### Options
|
|
72
89
|
|
|
73
|
-
| Option
|
|
74
|
-
|
|
|
75
|
-
| `--yes`
|
|
76
|
-
| `--project <id>`
|
|
77
|
-
| `--org <id>`
|
|
78
|
-
| `--
|
|
79
|
-
| `--
|
|
80
|
-
| `--
|
|
81
|
-
| `--
|
|
82
|
-
| `--
|
|
90
|
+
| Option | Does |
|
|
91
|
+
| -------------------- | ----------------------------------------------------------------------------------------------- |
|
|
92
|
+
| `--yes` | Asks nothing: takes every suggestion and skips the starter list |
|
|
93
|
+
| `--project <id>` | The Sanity project new workspaces copy their schema from |
|
|
94
|
+
| `--org <id>` | The Sanity organisation new workspaces are created in (found with `--live`) |
|
|
95
|
+
| `--vercel-team <id>` | The client's own Vercel team for the sites; leave out for Anyday's team |
|
|
96
|
+
| `--studio <origin>` | The Studio that opens new workspaces, e.g. `https://acme.sanity.studio` (found with `--live`) |
|
|
97
|
+
| `--live` | Reads the current theme, settings and starter pages from that project |
|
|
98
|
+
| `--dataset <name>` | The dataset `--live` reads (default `production`) |
|
|
99
|
+
| `--starter <pages>` | Starter pages without the list: `home`, slugs or document ids, comma-separated (needs `--live`) |
|
|
100
|
+
| `--force` | Replaces an existing `workspace-manager.json`, keeping its file-backed theme defaults (fonts) |
|
|
101
|
+
| `--print` | Prints the draft instead of writing it |
|
|
83
102
|
|
|
84
103
|
### Starter pages
|
|
85
104
|
|
|
@@ -89,19 +108,20 @@ Starter pages are what a new workspace starts with when it doesn't copy another
|
|
|
89
108
|
```text
|
|
90
109
|
Pages of the live site:
|
|
91
110
|
1. Home (/home) ← home
|
|
92
|
-
2. About
|
|
111
|
+
2. About us (/about-us)
|
|
93
112
|
3. Accessibility (/accessibility)
|
|
94
113
|
Starter pages, by number (e.g. 1,3; Enter for one empty home page):
|
|
95
114
|
```
|
|
96
115
|
|
|
97
116
|
Pick by number, or press Enter for one empty home page. To skip the list, name the pages with
|
|
98
117
|
`--starter`: `home` means whichever page the settings mark as the home page, and any other entry
|
|
99
|
-
is a page's slug or document id, e.g. `--starter home,about-
|
|
118
|
+
is a page's slug or document id, e.g. `--starter home,about-us`. When translations share a
|
|
100
119
|
slug, the home page's language wins. With `--yes` and no `--starter`, the starter is one empty
|
|
101
120
|
home page.
|
|
102
121
|
|
|
103
122
|
Starter pages get `{{siteName}}` and `{{language}}` where the source site's name and language
|
|
104
|
-
were.
|
|
123
|
+
were. Fonts are left out of theme defaults, since uploaded fonts are often licensed per brand: add
|
|
124
|
+
them by hand when they may be reused, and a later `--force` keeps them.
|
|
105
125
|
|
|
106
126
|
### Review before registering
|
|
107
127
|
|
|
@@ -115,6 +135,9 @@ listed afterwards. Check these in particular:
|
|
|
115
135
|
- **Colour roles** come with empty descriptions. Add a short one each, saying where the colour is
|
|
116
136
|
used, so whoever sets up a brand knows what they are picking.
|
|
117
137
|
- **Language documents and singletons** are guessed from type names (`…Settings`, `l10n.locale`).
|
|
138
|
+
- **Vercel team:** sites go to Anyday's team unless you gave a team id. A client's own team needs
|
|
139
|
+
its token on the API as `VERCEL_TOKEN_<team id>` (a secret, added on Vercel by Anyday, never in
|
|
140
|
+
the manifest); `register` warns while it is missing.
|
|
118
141
|
|
|
119
142
|
## register
|
|
120
143
|
|
|
@@ -130,6 +153,7 @@ workspace manager. The live API is `https://anyday-workspace-manager-sanity-api.
|
|
|
130
153
|
- Registering again replaces the saved copy for that `id`, so a changed manifest needs no delete
|
|
131
154
|
first.
|
|
132
155
|
- It prints `Registered <name> (<id>)` when saved.
|
|
156
|
+
- It warns when the manifest names a Vercel team the API has no token for yet.
|
|
133
157
|
|
|
134
158
|
## Signing in
|
|
135
159
|
|
package/dist/cli.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { existsSync as existsSync3, writeFileSync } from "node:fs";
|
|
4
|
+
import { existsSync as existsSync3, readFileSync as readFileSync4, writeFileSync } from "node:fs";
|
|
5
5
|
import { resolve } from "node:path";
|
|
6
6
|
import { createInterface } from "node:readline/promises";
|
|
7
7
|
import { parseArgs } from "node:util";
|
|
@@ -236,6 +236,14 @@ var workspace_manager_schema_default = {
|
|
|
236
236
|
description: "The Sanity project whose latest deployed Studio schema each workspace gets.",
|
|
237
237
|
$ref: "#/definitions/name"
|
|
238
238
|
},
|
|
239
|
+
studioOrigins: {
|
|
240
|
+
description: "Origins of the Studios that open these workspaces, e.g. https://acme.sanity.studio; every new workspace allows them.",
|
|
241
|
+
type: "array",
|
|
242
|
+
items: {
|
|
243
|
+
type: "string",
|
|
244
|
+
pattern: "^https?://[^/]+$"
|
|
245
|
+
}
|
|
246
|
+
},
|
|
239
247
|
languages: {
|
|
240
248
|
type: "object",
|
|
241
249
|
required: ["document", "setupTypes"],
|
|
@@ -411,6 +419,7 @@ function configFrom(manifest) {
|
|
|
411
419
|
site: manifest.site,
|
|
412
420
|
siteEnv: manifest.siteEnv,
|
|
413
421
|
schemaSourceProjectId: manifest.schemaSourceProjectId,
|
|
422
|
+
studioOrigins: manifest.studioOrigins ?? [],
|
|
414
423
|
languageDocument: languages && ((code) => fill(languages.document, {
|
|
415
424
|
code,
|
|
416
425
|
codeUpper: code.toUpperCase(),
|
|
@@ -431,6 +440,7 @@ function configFrom(manifest) {
|
|
|
431
440
|
},
|
|
432
441
|
brand: {
|
|
433
442
|
colorRoles: brand.colorRoles,
|
|
443
|
+
fonts: brand.theme.font !== void 0,
|
|
434
444
|
documents: (values, homePageId) => brandDocuments(brand, values, homePageId)
|
|
435
445
|
}
|
|
436
446
|
};
|
|
@@ -558,11 +568,18 @@ function draftManifest(repo, answers, live = {}) {
|
|
|
558
568
|
);
|
|
559
569
|
if (!repo.repo) note("No GitHub remote found: set site.repo to owner/name");
|
|
560
570
|
if (!repo.hasRevalidateRoute) note("No /api/revalidate route found: check site.revalidatePath");
|
|
571
|
+
if (answers.vercelTeamId)
|
|
572
|
+
note(
|
|
573
|
+
`Sites go to Vercel team ${answers.vercelTeamId}: the API needs VERCEL_TOKEN_${answers.vercelTeamId} before setups reach Vercel`
|
|
574
|
+
);
|
|
575
|
+
if (answers.studioOrigins?.length) note(`Studio: ${answers.studioOrigins.join(", ")}`);
|
|
576
|
+
else note('No Studio found: add studioOrigins, e.g. ["https://acme.sanity.studio"]');
|
|
561
577
|
return {
|
|
562
578
|
manifest: {
|
|
563
579
|
id: answers.id,
|
|
564
580
|
title: answers.title,
|
|
565
581
|
organizationId: answers.organizationId,
|
|
582
|
+
...answers.vercelTeamId ? { vercelTeamId: answers.vercelTeamId } : {},
|
|
566
583
|
site: {
|
|
567
584
|
repo: repo.repo ?? "owner/name",
|
|
568
585
|
branch: repo.branch ?? "main",
|
|
@@ -570,6 +587,7 @@ function draftManifest(repo, answers, live = {}) {
|
|
|
570
587
|
},
|
|
571
588
|
siteEnv,
|
|
572
589
|
schemaSourceProjectId: answers.schemaSourceProjectId,
|
|
590
|
+
...answers.studioOrigins?.length ? { studioOrigins: answers.studioOrigins } : {},
|
|
573
591
|
...languages ? { languages } : {},
|
|
574
592
|
content: {
|
|
575
593
|
sections,
|
|
@@ -754,6 +772,17 @@ function placeholders(value, siteName, language, key, parent) {
|
|
|
754
772
|
}
|
|
755
773
|
return value;
|
|
756
774
|
}
|
|
775
|
+
function keepFileDefaults(draft, existing) {
|
|
776
|
+
const defaults = draft.manifest.brand.theme.defaults;
|
|
777
|
+
const previous = existing?.brand?.theme?.defaults ?? {};
|
|
778
|
+
const kept = Object.keys(previous).filter(
|
|
779
|
+
(field) => !(field in defaults) && hasReference(previous[field])
|
|
780
|
+
);
|
|
781
|
+
for (const field of kept) defaults[field] = previous[field];
|
|
782
|
+
if (kept.length)
|
|
783
|
+
draft.notes.push(`Kept from the existing manifest's theme defaults: ${kept.join(", ")}`);
|
|
784
|
+
return kept;
|
|
785
|
+
}
|
|
757
786
|
var hasReference = (value) => JSON.stringify(value ?? null).includes('"_ref":');
|
|
758
787
|
function references(value) {
|
|
759
788
|
if (Array.isArray(value)) return value.flatMap(references);
|
|
@@ -785,12 +814,29 @@ function sanityToken() {
|
|
|
785
814
|
if (!existsSync(config)) return null;
|
|
786
815
|
return JSON.parse(readFileSync(config, "utf8")).authToken ?? null;
|
|
787
816
|
}
|
|
788
|
-
async function
|
|
817
|
+
async function projectInfo(projectId, token) {
|
|
789
818
|
const response = await fetch(`https://api.sanity.io/v2021-06-07/projects/${projectId}`, {
|
|
790
819
|
headers: { Authorization: `Bearer ${token}` }
|
|
791
820
|
});
|
|
792
821
|
if (!response.ok) throw new Error(`Couldn't read project ${projectId}: ${response.status}`);
|
|
793
|
-
return
|
|
822
|
+
return await response.json();
|
|
823
|
+
}
|
|
824
|
+
async function organizationOf(projectId, token) {
|
|
825
|
+
return (await projectInfo(projectId, token)).organizationId ?? "";
|
|
826
|
+
}
|
|
827
|
+
async function studioOriginsOf(projectId, token) {
|
|
828
|
+
const response = await fetch(
|
|
829
|
+
`https://api.sanity.io/v2024-08-01/projects/${projectId}/user-applications`,
|
|
830
|
+
{ headers: { Authorization: `Bearer ${token}` } }
|
|
831
|
+
);
|
|
832
|
+
if (!response.ok) throw new Error(`Couldn't list ${projectId}'s Studios: ${response.status}`);
|
|
833
|
+
const apps = await response.json();
|
|
834
|
+
const origins = apps.filter((app) => app.type === "studio").map(
|
|
835
|
+
(app) => app.urlType === "external" ? new URL(app.appHost).origin : `https://${app.appHost}.sanity.studio`
|
|
836
|
+
);
|
|
837
|
+
const { studioHost } = await projectInfo(projectId, token);
|
|
838
|
+
if (studioHost) origins.push(`https://${studioHost}.sanity.studio`);
|
|
839
|
+
return [...new Set(origins)];
|
|
794
840
|
}
|
|
795
841
|
var liveClient = ({ projectId, dataset, token }) => createClient({
|
|
796
842
|
projectId,
|
|
@@ -939,6 +985,9 @@ Options:
|
|
|
939
985
|
--yes Accept the suggested id, title and project without asking
|
|
940
986
|
--project <id> The Sanity project new workspaces copy their schema from
|
|
941
987
|
--org <id> The Sanity organisation new workspaces are created in (found with --live)
|
|
988
|
+
--vercel-team <id> The client's own Vercel team for the sites (default: Anyday's team)
|
|
989
|
+
--studio <origin> The Studio that opens new workspaces, e.g. https://acme.sanity.studio
|
|
990
|
+
(found with --live from the project's deployed Studio)
|
|
942
991
|
--live Read the current theme, settings and starter pages from that project
|
|
943
992
|
--dataset <name> Dataset to read with --live (default: production)
|
|
944
993
|
--starter <pages> Comma-separated pages to copy as starter content: \`home\`, slugs or
|
|
@@ -965,20 +1014,23 @@ async function registerCommand(argv) {
|
|
|
965
1014
|
if (!token) throw new Error("register needs SANITY_AUTH_TOKEN or `npx sanity login`");
|
|
966
1015
|
const saved = await register({ dir: dirOf(positionals[0]), api, token });
|
|
967
1016
|
console.log(`Registered ${saved.title} (${saved.id})`);
|
|
1017
|
+
for (const warning of saved.warnings ?? []) console.error(`Warning: ${warning}`);
|
|
968
1018
|
}
|
|
969
1019
|
async function init(argv) {
|
|
970
1020
|
const { values, positionals } = parseArgs({
|
|
971
1021
|
args: argv,
|
|
972
1022
|
allowPositionals: true,
|
|
973
1023
|
options: {
|
|
974
|
-
yes: { type: "boolean", default: false },
|
|
975
|
-
project: { type: "string" },
|
|
976
|
-
org: { type: "string" },
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
1024
|
+
"yes": { type: "boolean", default: false },
|
|
1025
|
+
"project": { type: "string" },
|
|
1026
|
+
"org": { type: "string" },
|
|
1027
|
+
"studio": { type: "string" },
|
|
1028
|
+
"vercel-team": { type: "string" },
|
|
1029
|
+
"live": { type: "boolean", default: false },
|
|
1030
|
+
"dataset": { type: "string", default: "production" },
|
|
1031
|
+
"starter": { type: "string" },
|
|
1032
|
+
"force": { type: "boolean", default: false },
|
|
1033
|
+
"print": { type: "boolean", default: false }
|
|
982
1034
|
}
|
|
983
1035
|
});
|
|
984
1036
|
const root = dirOf(positionals[0]);
|
|
@@ -991,7 +1043,11 @@ async function init(argv) {
|
|
|
991
1043
|
const orgFor = async (projectId) => values.org ?? (token && projectId ? await organizationOf(projectId, token) : "");
|
|
992
1044
|
const repo = readRepo(root);
|
|
993
1045
|
const suggested = { ...suggestNames(repo.repo), project: values.project ?? repo.projectId ?? "" };
|
|
994
|
-
const answers = values.yes ? {
|
|
1046
|
+
const answers = values.yes ? {
|
|
1047
|
+
...suggested,
|
|
1048
|
+
org: await orgFor(suggested.project),
|
|
1049
|
+
vercelTeam: values["vercel-team"] ?? ""
|
|
1050
|
+
} : await ask({ ...suggested, vercelTeam: values["vercel-team"] ?? "" }, orgFor);
|
|
995
1051
|
if (!answers.project)
|
|
996
1052
|
throw new Error("Pass --project <id>: the Sanity project to copy schemas from");
|
|
997
1053
|
if (!answers.org) throw new Error("Pass --org <id>, or --live to read it from the project");
|
|
@@ -999,7 +1055,9 @@ async function init(argv) {
|
|
|
999
1055
|
id: answers.id,
|
|
1000
1056
|
title: answers.title,
|
|
1001
1057
|
organizationId: answers.org,
|
|
1002
|
-
|
|
1058
|
+
...answers.vercelTeam ? { vercelTeamId: answers.vercelTeam } : {},
|
|
1059
|
+
schemaSourceProjectId: answers.project,
|
|
1060
|
+
studioOrigins: values.studio ? [values.studio.replace(/\/+$/, "")] : token ? await studioOriginsOf(answers.project, token) : []
|
|
1003
1061
|
};
|
|
1004
1062
|
let draft = draftManifest(repo, base);
|
|
1005
1063
|
if (token) {
|
|
@@ -1022,6 +1080,7 @@ async function init(argv) {
|
|
|
1022
1080
|
});
|
|
1023
1081
|
draft = draftManifest(repo, base, live);
|
|
1024
1082
|
}
|
|
1083
|
+
if (existsSync3(out)) keepFileDefaults(draft, readJson(out));
|
|
1025
1084
|
configFrom(parseManifest(draft.manifest));
|
|
1026
1085
|
const json = `${JSON.stringify(draft.manifest, null, 2)}
|
|
1027
1086
|
`;
|
|
@@ -1032,6 +1091,13 @@ async function init(argv) {
|
|
|
1032
1091
|
Check before registering:`);
|
|
1033
1092
|
for (const note of draft.notes) console.error(` - ${note}`);
|
|
1034
1093
|
}
|
|
1094
|
+
function readJson(path) {
|
|
1095
|
+
try {
|
|
1096
|
+
return JSON.parse(readFileSync4(path, "utf8"));
|
|
1097
|
+
} catch {
|
|
1098
|
+
return null;
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1035
1101
|
async function ask(suggested, orgFor) {
|
|
1036
1102
|
const prompt = createInterface({ input: process.stdin, output: process.stderr });
|
|
1037
1103
|
const question = async (label, fallback) => (await prompt.question(`${label}${fallback ? ` (${fallback})` : ""}: `)).trim() || fallback;
|
|
@@ -1040,7 +1106,11 @@ async function ask(suggested, orgFor) {
|
|
|
1040
1106
|
const title = await question("Name shown in the app", suggested.title);
|
|
1041
1107
|
const project = await question("Sanity project to copy schemas from", suggested.project);
|
|
1042
1108
|
const org = await question("Sanity organisation to create workspaces in", await orgFor(project));
|
|
1043
|
-
|
|
1109
|
+
const vercelTeam = await question(
|
|
1110
|
+
"Vercel team for the sites, only if it's the client's own (Enter for Anyday's)",
|
|
1111
|
+
suggested.vercelTeam
|
|
1112
|
+
);
|
|
1113
|
+
return { id, title, project, org, vercelTeam };
|
|
1044
1114
|
} finally {
|
|
1045
1115
|
prompt.close();
|
|
1046
1116
|
}
|