@lupinum/ginko 0.0.0-stage → 0.4.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/CHANGELOG.md +75 -0
- package/LICENSE +21 -0
- package/README.md +99 -2
- package/dist/cli/index.d.mts +17 -0
- package/dist/cli/index.d.ts +17 -0
- package/dist/cli/index.mjs +679 -0
- package/dist/index.d.mts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.mjs +15 -0
- package/dist/internal/search.d.mts +25 -0
- package/dist/internal/search.d.ts +25 -0
- package/dist/internal/search.mjs +85 -0
- package/dist/internal.d.mts +496 -0
- package/dist/internal.d.ts +496 -0
- package/dist/internal.mjs +903 -0
- package/dist/nuxt/module.d.mts +29 -0
- package/dist/nuxt/module.d.ts +29 -0
- package/dist/nuxt/module.mjs +364 -0
- package/dist/nuxt/runtime/GinkoImage.d.mts +39 -0
- package/dist/nuxt/runtime/GinkoImage.mjs +33 -0
- package/dist/nuxt/runtime/GinkoMarkdown.d.mts +36 -0
- package/dist/nuxt/runtime/GinkoMarkdown.mjs +24 -0
- package/dist/nuxt/runtime/composables.d.mts +121 -0
- package/dist/nuxt/runtime/composables.mjs +182 -0
- package/dist/nuxt/runtime/fetch.d.mts +26 -0
- package/dist/nuxt/runtime/fetch.mjs +50 -0
- package/dist/nuxt/runtime/markdown.d.mts +13 -0
- package/dist/nuxt/runtime/markdown.mjs +17 -0
- package/dist/nuxt/runtime/og/GinkoCard.vue +48 -0
- package/dist/nuxt/runtime/plugin.d.mts +2 -0
- package/dist/nuxt/runtime/plugin.mjs +89 -0
- package/dist/nuxt/runtime/preview.vue +52 -0
- package/dist/nuxt/runtime/schema.d.ts +50 -0
- package/dist/nuxt/runtime/server/bundles.d.mts +4 -0
- package/dist/nuxt/runtime/server/bundles.mjs +18 -0
- package/dist/nuxt/runtime/server/files-md.d.mts +2 -0
- package/dist/nuxt/runtime/server/files-md.mjs +8 -0
- package/dist/nuxt/runtime/server/files.d.mts +2 -0
- package/dist/nuxt/runtime/server/files.mjs +9 -0
- package/dist/nuxt/runtime/server/headers.d.mts +2 -0
- package/dist/nuxt/runtime/server/headers.mjs +9 -0
- package/dist/nuxt/runtime/server/llms-full.d.mts +2 -0
- package/dist/nuxt/runtime/server/llms-full.mjs +13 -0
- package/dist/nuxt/runtime/server/llms.d.mts +2 -0
- package/dist/nuxt/runtime/server/llms.mjs +13 -0
- package/dist/nuxt/runtime/server/markdown-trees.d.mts +2 -0
- package/dist/nuxt/runtime/server/markdown-trees.mjs +2 -0
- package/dist/nuxt/runtime/server/markdown.d.mts +2 -0
- package/dist/nuxt/runtime/server/markdown.mjs +35 -0
- package/dist/nuxt/runtime/server/redirects.d.mts +2 -0
- package/dist/nuxt/runtime/server/redirects.mjs +17 -0
- package/dist/nuxt/runtime/server/robots.d.mts +2 -0
- package/dist/nuxt/runtime/server/robots.mjs +8 -0
- package/dist/nuxt/runtime/server/search.d.mts +2 -0
- package/dist/nuxt/runtime/server/search.mjs +9 -0
- package/dist/nuxt/runtime/server/site-config.d.mts +14 -0
- package/dist/nuxt/runtime/server/site-config.mjs +9 -0
- package/dist/nuxt/runtime/server/sitemap-source.d.mts +2 -0
- package/dist/nuxt/runtime/server/sitemap-source.mjs +14 -0
- package/dist/nuxt/runtime/server/sitemap-xml.d.mts +24 -0
- package/dist/nuxt/runtime/server/sitemap-xml.mjs +88 -0
- package/dist/nuxt/runtime/server/sitemap.d.mts +2 -0
- package/dist/nuxt/runtime/server/sitemap.mjs +15 -0
- package/dist/nuxt/runtime/shared.d.mts +35 -0
- package/dist/nuxt/runtime/shared.mjs +9 -0
- package/dist/nuxt/runtime/today.client.d.mts +2 -0
- package/dist/nuxt/runtime/today.client.mjs +18 -0
- package/dist/search.d.mts +1 -0
- package/dist/search.d.ts +1 -0
- package/dist/search.mjs +2 -0
- package/dist/shared/ginko.BBkZW2nW.d.ts +68 -0
- package/dist/shared/ginko.BDF3bW0C.d.ts +213 -0
- package/dist/shared/ginko.BTUfZCfa.d.mts +172 -0
- package/dist/shared/ginko.BWpep204.mjs +62 -0
- package/dist/shared/ginko.Bwan3BB-.d.mts +68 -0
- package/dist/shared/ginko.C-QjDn3O.mjs +4944 -0
- package/dist/shared/ginko.C90fPpMP.mjs +544 -0
- package/dist/shared/ginko.CQvxhyez.mjs +40 -0
- package/dist/shared/ginko.CrNRBkS5.mjs +1075 -0
- package/dist/shared/ginko.D-fNojn9.d.mts +1554 -0
- package/dist/shared/ginko.D-fNojn9.d.ts +1554 -0
- package/dist/shared/ginko.D4ApSEyg.d.mts +57 -0
- package/dist/shared/ginko.D4ApSEyg.d.ts +57 -0
- package/dist/shared/ginko.Dc0oNcDv.d.ts +172 -0
- package/dist/shared/ginko.Dht0xxE5.mjs +96 -0
- package/dist/shared/ginko.Mn30dmlm.mjs +20 -0
- package/dist/shared/ginko.akHewCGt.d.mts +213 -0
- package/dist/vue.d.mts +45 -0
- package/dist/vue.d.ts +45 -0
- package/dist/vue.mjs +84 -0
- package/i18n/de.json +204 -0
- package/i18n/en.json +204 -0
- package/i18n/es.json +204 -0
- package/package.json +85 -3
|
@@ -0,0 +1,679 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, realpathSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { join, dirname, resolve } from 'node:path';
|
|
4
|
+
import { pathToFileURL, fileURLToPath } from 'node:url';
|
|
5
|
+
import { f as fileLinkIssues } from '../shared/ginko.Dht0xxE5.mjs';
|
|
6
|
+
import { n as coreMessage, i as isTranslatable } from '../shared/ginko.C-QjDn3O.mjs';
|
|
7
|
+
import { s as serializeSchema, a as checkSite, G as validate, g as SITE_BUDGET, A as platformSchema, j as checkSchema } from '../shared/ginko.CrNRBkS5.mjs';
|
|
8
|
+
import { e as defaultsFor, h as fieldsChanged, r as releaseData, m as markdownTrees, g as diffSchema } from '../shared/ginko.C90fPpMP.mjs';
|
|
9
|
+
import { mkdir, writeFile, rm, readdir } from 'node:fs/promises';
|
|
10
|
+
import { execFile } from 'node:child_process';
|
|
11
|
+
import 'comark';
|
|
12
|
+
import 'comark/plugins/alert';
|
|
13
|
+
import 'comark/plugins/attributes';
|
|
14
|
+
import 'comark/plugins/components';
|
|
15
|
+
import 'comark/plugins/emoji';
|
|
16
|
+
import 'comark/plugins/footnotes';
|
|
17
|
+
import 'comark/plugins/frontmatter';
|
|
18
|
+
import 'comark/plugins/math';
|
|
19
|
+
import 'comark/plugins/mermaid';
|
|
20
|
+
import 'comark/plugins/task-list';
|
|
21
|
+
import 'comark/plugins/toc';
|
|
22
|
+
import '@lupinum/ginko-i18n/core';
|
|
23
|
+
import '../shared/ginko.CQvxhyez.mjs';
|
|
24
|
+
|
|
25
|
+
class CliError extends Error {
|
|
26
|
+
code;
|
|
27
|
+
constructor(message, code = "INVALID") {
|
|
28
|
+
super(message);
|
|
29
|
+
this.code = code;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
33
|
+
function parseReleaseState(value) {
|
|
34
|
+
if (!isObject(value)) throw new CliError("The platform did not return a valid state.", "UNAVAILABLE");
|
|
35
|
+
const { site, schema, schemaRev, contentRev, migrations, entries } = value;
|
|
36
|
+
const valid = typeof site === "string" && isObject(schema) && isObject(schema.bricks) && Array.isArray(schema.migrations) && isObject(schema.retired) && typeof schemaRev === "number" && typeof contentRev === "number" && Array.isArray(migrations) && Array.isArray(entries) && entries.every((e) => isObject(e) && typeof e.id === "string" && typeof e.brick === "string" && isObject(e.data));
|
|
37
|
+
if (!valid) throw new CliError("The platform returned an incomplete state.", "UNAVAILABLE");
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
function parseExport(value, locator, problem) {
|
|
41
|
+
const optionalNumber = (v) => v === void 0 || typeof v === "number";
|
|
42
|
+
const optionalString = (v) => v === void 0 || typeof v === "string";
|
|
43
|
+
const valid = isObject(value) && value.format === 1 && typeof value.site === "string" && typeof value.name === "string" && typeof value.exportedAt === "string" && isObject(value.schema) && isObject(value.schema.bricks) && typeof value.schemaRev === "number" && typeof value.contentRev === "number" && Array.isArray(value.entries) && value.entries.every((e) => isObject(e) && typeof e.id === "string" && typeof e.brick === "string" && (e.kind === "single" || e.kind === "item") && typeof e.order === "number" && isObject(e.data) && isObject(e.prints) && typeof e.rev === "number" && optionalNumber(e.deletedAt)) && Array.isArray(value.files) && value.files.every((f) => isObject(f) && typeof f.id === "string" && typeof f.name === "string" && typeof f.mime === "string" && typeof f.size === "number" && typeof f[locator] === "string" && optionalNumber(f.width) && optionalNumber(f.height) && optionalString(f.folder) && optionalString(f.sha256) && (f.kept === void 0 || typeof f.kept === "boolean") && (f.focus === void 0 || isObject(f.focus) && typeof f.focus.x === "number" && typeof f.focus.y === "number")) && (value.folders === void 0 || Array.isArray(value.folders) && value.folders.every((f) => isObject(f) && typeof f.id === "string" && typeof f.name === "string" && optionalString(f.parent)));
|
|
44
|
+
if (!valid) throw new CliError(problem, "UNAVAILABLE" );
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function loadConfig(path, revision = 0) {
|
|
49
|
+
const url = pathToFileURL(path).href + (revision > 0 ? `?rev=${revision}` : "");
|
|
50
|
+
let module;
|
|
51
|
+
try {
|
|
52
|
+
module = await import(url);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
throw new CliError(`ginko.config.ts could not be loaded (${path}): ${error instanceof Error ? error.message : String(error)}`);
|
|
55
|
+
}
|
|
56
|
+
const config = isObject(module) ? module.default : void 0;
|
|
57
|
+
if (!isObject(config) || typeof config.site !== "string" || !isObject(config.bricks)) {
|
|
58
|
+
throw new CliError(`${path} needs a default export with defineSite(\u2026).`);
|
|
59
|
+
}
|
|
60
|
+
return config;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function english(m) {
|
|
64
|
+
return coreMessage("en", m.key, m.params);
|
|
65
|
+
}
|
|
66
|
+
function explainRejected(rejected) {
|
|
67
|
+
const lines = ["The schema changes in a way that is not allowed:"];
|
|
68
|
+
for (const r of rejected) {
|
|
69
|
+
lines.push(` \u2717 ${r.path}: ${english(r)}`);
|
|
70
|
+
const fix = r.fixMsg ? english(r.fixMsg) : r.fix;
|
|
71
|
+
if (fix) lines.push(` \u2192 ${fix}`);
|
|
72
|
+
}
|
|
73
|
+
return lines;
|
|
74
|
+
}
|
|
75
|
+
function explainProblems(problems) {
|
|
76
|
+
return ["The release would not be accepted:", ...problems.map((p) => ` \u2717 ${p}`)];
|
|
77
|
+
}
|
|
78
|
+
function explainChanges(changes) {
|
|
79
|
+
return changes.map((c) => ` + ${english(c)}`);
|
|
80
|
+
}
|
|
81
|
+
function kib(bytes) {
|
|
82
|
+
return `${Math.ceil(bytes / 1024)} KiB`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function checkFileLinks(input) {
|
|
86
|
+
const schema = serializeSchema(input.config);
|
|
87
|
+
if (!Object.values(schema.bricks).some((b) => b.source === "files")) return true;
|
|
88
|
+
let content;
|
|
89
|
+
try {
|
|
90
|
+
content = await import('@lupinum/ginko-content');
|
|
91
|
+
} catch (error) {
|
|
92
|
+
if (error.code !== "ERR_MODULE_NOT_FOUND") throw error;
|
|
93
|
+
input.log("Install @lupinum/ginko-content to check file content.");
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
const contentDir = join(input.root, "content");
|
|
97
|
+
const publicDir = join(input.root, "public");
|
|
98
|
+
const result = await content.buildFileBundles({ contentDir, publicDir, schema, drafts: false });
|
|
99
|
+
let ok = true;
|
|
100
|
+
for (const e of result.errors) {
|
|
101
|
+
input.log(content.errorLine(e));
|
|
102
|
+
ok = false;
|
|
103
|
+
}
|
|
104
|
+
const issues = await fileLinkIssues({ bundles: Object.values(result.bundles), schema, site: input.config, publicDir, root: input.root, log: input.log, fetch: input.fetch, external: true });
|
|
105
|
+
for (const i of issues) {
|
|
106
|
+
input.log(`content/${i.id}: ${english(i)}`);
|
|
107
|
+
if (i.severity === "error") ok = false;
|
|
108
|
+
}
|
|
109
|
+
return ok;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const BUDGET = SITE_BUDGET;
|
|
113
|
+
const byteSize = (value) => new TextEncoder().encode(JSON.stringify(value ?? null)).byteLength;
|
|
114
|
+
async function buildCandidate(state, site, schema) {
|
|
115
|
+
const problems = checkSite(site, schema);
|
|
116
|
+
const applied = new Set([...state.schema.migrations, ...state.migrations].map((m) => m.id));
|
|
117
|
+
const fresh = schema.migrations.filter((m) => !applied.has(m.id));
|
|
118
|
+
const writes = /* @__PURE__ */ new Map();
|
|
119
|
+
const targets = /* @__PURE__ */ new Map();
|
|
120
|
+
for (const descriptor of fresh) {
|
|
121
|
+
const target = `${descriptor.brick}.${descriptor.fills}`;
|
|
122
|
+
const other = targets.get(target);
|
|
123
|
+
if (other !== void 0) {
|
|
124
|
+
problems.push(`Migrations \u201C${other}\u201D and \u201C${descriptor.id}\u201D both fill \u201C${target}\u201D. Remove one of them.`);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
targets.set(target, descriptor.id);
|
|
128
|
+
const field = schema.bricks[descriptor.brick]?.fields[descriptor.fills];
|
|
129
|
+
const migration = Object.hasOwn(site.migrations ?? {}, descriptor.id) ? site.migrations?.[descriptor.id] : void 0;
|
|
130
|
+
if (!field || !migration) {
|
|
131
|
+
problems.push(`Migration \u201C${descriptor.id}\u201D has no target field \u201C${target}\u201D.`);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (field.type === "slug") {
|
|
135
|
+
problems.push(`Migration \u201C${descriptor.id}\u201D cannot set an address (slug).`);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
for (const entry of state.entries) {
|
|
139
|
+
if (entry.brick !== descriptor.brick) continue;
|
|
140
|
+
let value;
|
|
141
|
+
try {
|
|
142
|
+
value = migrate(migration, descriptor, field, schema, entry.data);
|
|
143
|
+
} catch (error) {
|
|
144
|
+
problems.push(`Migration \u201C${descriptor.id}\u201D fails for entry ${entry.id}: ${error instanceof Error ? error.message : String(error)}`);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
writes.set(entry.id, { ...writes.get(entry.id), [descriptor.fills]: value });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const changed = [];
|
|
151
|
+
const created = [];
|
|
152
|
+
const checked = [];
|
|
153
|
+
for (const [key, brick] of Object.entries(schema.bricks)) {
|
|
154
|
+
if (!Object.hasOwn(state.schema.bricks, key)) {
|
|
155
|
+
if (brick.kind === "single") created.push({ brick: key, data: defaultsFor(brick.fields) });
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
const all = fieldsChanged(state.schema, schema, key);
|
|
159
|
+
for (const entry of state.entries) {
|
|
160
|
+
if (entry.brick !== key) continue;
|
|
161
|
+
const next = releaseData(state.schema.bricks[key].fields, brick.fields, entry.data, writes.get(entry.id) ?? {});
|
|
162
|
+
if (next.roots.length) changed.push({ id: entry.id, brick: key, data: next.data });
|
|
163
|
+
if (next.roots.length || all) checked.push({ id: entry.id, brick: key, data: next.data });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
for (const entry of checked) {
|
|
167
|
+
for (const issue of (await validate(schema, entry.brick, entry.data, { mode: "release" })).errors) {
|
|
168
|
+
problems.push(`Entry ${entry.id} (${entry.brick}), ${issue.path || "entry"}: ${english(issue)}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
for (const single of created) {
|
|
172
|
+
for (const issue of (await validate(schema, single.brick, single.data, { mode: "release" })).errors) {
|
|
173
|
+
problems.push(`The new single brick \u201C${single.brick}\u201D, ${issue.path || "entry"}: ${english(issue)}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const changedData = new Map(changed.map((c) => [c.id, c.data]));
|
|
177
|
+
const bytes = { site: 0, largest: 0 };
|
|
178
|
+
const count = (size, label) => {
|
|
179
|
+
bytes.site += size;
|
|
180
|
+
bytes.largest = Math.max(bytes.largest, size);
|
|
181
|
+
if (size > BUDGET.entryBytes) problems.push(`${label} is too large (at most 256 KiB).`);
|
|
182
|
+
};
|
|
183
|
+
const sizeOf = async (brick, data) => {
|
|
184
|
+
const fields = (schema.bricks[brick] ?? state.schema.bricks[brick])?.fields ?? {};
|
|
185
|
+
return byteSize(data) + byteSize(await markdownTrees(fields, data));
|
|
186
|
+
};
|
|
187
|
+
for (const entry of state.entries) count(await sizeOf(entry.brick, changedData.get(entry.id) ?? entry.data), `Entry ${entry.id} (${entry.brick})`);
|
|
188
|
+
for (const single of created) count(await sizeOf(single.brick, single.data), `The new single brick \u201C${single.brick}\u201D`);
|
|
189
|
+
if (bytes.site > BUDGET.siteBytes) problems.push("The site has reached its storage budget (4 MiB).");
|
|
190
|
+
const liveEntries = state.entries.filter((entry) => entry.deletedAt === null).length;
|
|
191
|
+
if (liveEntries + created.length > BUDGET.entries) problems.push(`The site has reached its limit of ${BUDGET.entries} entries.`);
|
|
192
|
+
return {
|
|
193
|
+
schema,
|
|
194
|
+
writes: [...writes].map(([entry, fields]) => ({ entry, fields })),
|
|
195
|
+
changed,
|
|
196
|
+
created,
|
|
197
|
+
problems,
|
|
198
|
+
bytes
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
function migrate(migration, descriptor, field, schema, data) {
|
|
202
|
+
const mainLocale = schema.locales[0];
|
|
203
|
+
const fields = schema.bricks[descriptor.brick].fields;
|
|
204
|
+
if (!isTranslatable(field)) {
|
|
205
|
+
return migration.compute(structuredClone(data), { locales: schema.locales, mainLocale }) ?? null;
|
|
206
|
+
}
|
|
207
|
+
const out = {};
|
|
208
|
+
for (const locale of schema.locales) {
|
|
209
|
+
const value = migration.compute(forLocale(fields, data, locale), { locales: schema.locales, mainLocale, locale });
|
|
210
|
+
if (value !== void 0 && value !== null) out[locale] = value;
|
|
211
|
+
}
|
|
212
|
+
return out;
|
|
213
|
+
}
|
|
214
|
+
function forLocale(fields, data, locale) {
|
|
215
|
+
const out = {};
|
|
216
|
+
for (const [name, value] of Object.entries(data)) {
|
|
217
|
+
const field = Object.hasOwn(fields, name) ? fields[name] : void 0;
|
|
218
|
+
if (field && isTranslatable(field)) {
|
|
219
|
+
const picked = isObject(value) ? value[locale] : void 0;
|
|
220
|
+
if (picked !== void 0) out[name] = picked;
|
|
221
|
+
} else if (field?.type === "list" && Array.isArray(value)) {
|
|
222
|
+
out[name] = value.map((row) => isObject(row) ? forLocale(field.of, row, locale) : row);
|
|
223
|
+
} else {
|
|
224
|
+
out[name] = value;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return out;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function plan(state, site) {
|
|
231
|
+
const schema = platformSchema(serializeSchema(site));
|
|
232
|
+
if (checkSchema(schema).length > 0) return { schema, changes: [], rejected: [], candidate: await buildCandidate(state, site, schema) };
|
|
233
|
+
const known = /* @__PURE__ */ new Map();
|
|
234
|
+
for (const m of [...state.schema.migrations, ...state.migrations]) {
|
|
235
|
+
known.set(m.id, { id: m.id, brick: m.brick, fills: m.fills, fingerprint: m.fingerprint });
|
|
236
|
+
}
|
|
237
|
+
const { changes, rejected } = diffSchema({ ...state.schema, migrations: [...known.values()] }, schema);
|
|
238
|
+
if (rejected.length > 0) return { schema, changes, rejected, candidate: null };
|
|
239
|
+
return { schema, changes, rejected, candidate: await buildCandidate(state, site, schema) };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function check(input) {
|
|
243
|
+
const problems = checkSite(input.config);
|
|
244
|
+
if (problems.length > 0) {
|
|
245
|
+
input.log("ginko.config.ts is invalid:");
|
|
246
|
+
for (const problem of problems) input.log(` \u2717 ${problem}`);
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
const image = checkSeoImage(input.config, input.root, input.log);
|
|
250
|
+
const links = await checkFileLinks({ config: input.config, root: input.root, log: input.log, fetch: input.fetch }) && image;
|
|
251
|
+
if (!input.connect) {
|
|
252
|
+
if (links) input.log("OK: schema and links are fine.");
|
|
253
|
+
return links;
|
|
254
|
+
}
|
|
255
|
+
try {
|
|
256
|
+
return await checkRelease({ config: input.config, platform: input.connect(), log: input.log }) && links;
|
|
257
|
+
} catch (error) {
|
|
258
|
+
if (links) throw error;
|
|
259
|
+
input.log(`Skipped the comparison with production: ${error instanceof Error ? error.message : String(error)}`);
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function checkSeoImage(config, root, log) {
|
|
264
|
+
const image = config.seo?.image;
|
|
265
|
+
if (!image || !image.startsWith("/") || image.startsWith("//")) return true;
|
|
266
|
+
const file = image.split(/[?#]/)[0];
|
|
267
|
+
if (existsSync(join(root, "public", file))) return true;
|
|
268
|
+
log(`ginko.config.ts: seo.image "${image}" is missing. Fix: add the file public${file}, or change seo.image.`);
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
async function checkRelease(input) {
|
|
272
|
+
const state = await input.platform.state(input.config.site);
|
|
273
|
+
if (state.site !== input.config.site) throw new CliError(`The key belongs to the site \u201C${state.site}\u201D, not to \u201C${input.config.site}\u201D.`, "FORBIDDEN");
|
|
274
|
+
const result = await plan(state, input.config);
|
|
275
|
+
input.log(`Site \u201C${state.site}\u201D: schema version ${state.schemaRev}, content ${state.contentRev}.`);
|
|
276
|
+
if (result.rejected.length > 0) {
|
|
277
|
+
for (const line of explainRejected(result.rejected)) input.log(line);
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
if (result.changes.length > 0) {
|
|
281
|
+
input.log("Allowed changes:");
|
|
282
|
+
for (const line of explainChanges(result.changes)) input.log(line);
|
|
283
|
+
} else {
|
|
284
|
+
input.log("No schema changes.");
|
|
285
|
+
}
|
|
286
|
+
const candidate = result.candidate;
|
|
287
|
+
const migrations = result.schema.migrations.filter((m) => !state.migrations.some((s) => s.id === m.id) && !state.schema.migrations.some((s) => s.id === m.id));
|
|
288
|
+
if (migrations.length > 0) input.log(`New migrations: ${migrations.map((m) => m.id).join(", ")}. Writes: ${candidate.writes.length} entries.`);
|
|
289
|
+
input.log(`Size: site ${kib(candidate.bytes.site)} of 4 MiB, largest entry ${kib(candidate.bytes.largest)} (limit 256 KiB).`);
|
|
290
|
+
if (candidate.problems.length > 0) {
|
|
291
|
+
for (const line of explainProblems(candidate.problems)) input.log(line);
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
input.log("OK: A release with this schema would be accepted.");
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
class PlatformError extends Error {
|
|
299
|
+
code;
|
|
300
|
+
status;
|
|
301
|
+
details;
|
|
302
|
+
constructor(code, message, status, details) {
|
|
303
|
+
super(message);
|
|
304
|
+
this.code = code;
|
|
305
|
+
this.status = status;
|
|
306
|
+
this.details = details;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
const PUSH_KEY_FORMAT = /^ginko_([a-z][a-z0-9-]{1,40})_[a-z0-9]{32}$/;
|
|
310
|
+
const READ_TIMEOUT_MS = 3e4;
|
|
311
|
+
const WRITE_TIMEOUT_MS = 6e4;
|
|
312
|
+
const UNKNOWN_OUTCOME = "It is unknown whether the release arrived. The same commit can run again without risk.";
|
|
313
|
+
function checkPlatformUrl(url) {
|
|
314
|
+
let parsed;
|
|
315
|
+
try {
|
|
316
|
+
parsed = new URL(url);
|
|
317
|
+
} catch {
|
|
318
|
+
throw new CliError("GINKO_PLATFORM is not a valid URL.", "USAGE");
|
|
319
|
+
}
|
|
320
|
+
const local = ["localhost", "127.0.0.1", "[::1]"].includes(parsed.hostname);
|
|
321
|
+
if (parsed.protocol !== "https:" && !(parsed.protocol === "http:" && local)) {
|
|
322
|
+
throw new CliError("GINKO_PLATFORM must start with https:// (http:// only for localhost, 127.0.0.1 and [::1]).", "USAGE");
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
function createPlatform(options) {
|
|
326
|
+
checkPlatformUrl(options.url);
|
|
327
|
+
const key = options.key.trim();
|
|
328
|
+
if (!PUSH_KEY_FORMAT.test(key)) throw new CliError("GINKO_KEY is not a valid push key (expected: ginko_<site>_<32 characters a-z0-9>).", "USAGE");
|
|
329
|
+
const base = options.url.replace(/\/+$/, "");
|
|
330
|
+
async function call(path, init = {}, deadlineMs = READ_TIMEOUT_MS, note = "") {
|
|
331
|
+
const signal = AbortSignal.timeout(deadlineMs);
|
|
332
|
+
const after = note ? ` ${note}` : "";
|
|
333
|
+
let response;
|
|
334
|
+
try {
|
|
335
|
+
response = await options.fetch(`${base}${path}`, {
|
|
336
|
+
...init,
|
|
337
|
+
headers: { authorization: `Bearer ${key}`, ...init.body ? { "content-type": "application/json" } : {} },
|
|
338
|
+
// A redirect would re-send the key or the release body to another address.
|
|
339
|
+
redirect: "manual",
|
|
340
|
+
signal
|
|
341
|
+
});
|
|
342
|
+
} catch {
|
|
343
|
+
throw new CliError(signal.aborted ? timeoutMessage(deadlineMs, note) : "The platform is not reachable. Check GINKO_PLATFORM and your network.", "UNAVAILABLE");
|
|
344
|
+
}
|
|
345
|
+
if (response.type === "opaqueredirect" || response.status >= 300 && response.status < 400) {
|
|
346
|
+
throw new CliError("The platform redirects; check GINKO_PLATFORM.", "USAGE");
|
|
347
|
+
}
|
|
348
|
+
let text;
|
|
349
|
+
try {
|
|
350
|
+
text = await response.text();
|
|
351
|
+
} catch {
|
|
352
|
+
throw new CliError(signal.aborted ? timeoutMessage(deadlineMs, note) : `The connection to the platform broke while reading the response.${after}`, "UNAVAILABLE");
|
|
353
|
+
}
|
|
354
|
+
let body;
|
|
355
|
+
try {
|
|
356
|
+
body = JSON.parse(text);
|
|
357
|
+
} catch {
|
|
358
|
+
body = void 0;
|
|
359
|
+
}
|
|
360
|
+
if (!response.ok) {
|
|
361
|
+
const error = isObject(body) ? body : {};
|
|
362
|
+
if (response.status === 404 && typeof error.code !== "string") {
|
|
363
|
+
throw new CliError("No Ginko platform answers at this address (HTTP 404); check GINKO_PLATFORM.", "USAGE");
|
|
364
|
+
}
|
|
365
|
+
throw new PlatformError(
|
|
366
|
+
typeof error.code === "string" ? error.code : `HTTP_${response.status}`,
|
|
367
|
+
typeof error.message === "string" ? error.message.replaceAll(key, "\u2026") : `The platform responded with HTTP ${response.status}.`,
|
|
368
|
+
response.status,
|
|
369
|
+
error.details
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
return body;
|
|
373
|
+
}
|
|
374
|
+
function timeoutMessage(deadlineMs, note) {
|
|
375
|
+
return `The platform did not respond within ${deadlineMs / 1e3} s.${note ? ` ${note}` : ""}`;
|
|
376
|
+
}
|
|
377
|
+
return {
|
|
378
|
+
async state(site) {
|
|
379
|
+
return parseReleaseState(await call(`/release/state?site=${encodeURIComponent(site)}`));
|
|
380
|
+
},
|
|
381
|
+
async release(body) {
|
|
382
|
+
const answer = await call("/release", { method: "POST", body: JSON.stringify(body) }, WRITE_TIMEOUT_MS, UNKNOWN_OUTCOME);
|
|
383
|
+
if (!isObject(answer) || typeof answer.schemaRev !== "number" || typeof answer.contentRev !== "number") {
|
|
384
|
+
throw new CliError(`The platform did not send a valid response to the release. ${UNKNOWN_OUTCOME}`, "UNAVAILABLE");
|
|
385
|
+
}
|
|
386
|
+
return { schemaRev: answer.schemaRev, contentRev: answer.contentRev };
|
|
387
|
+
},
|
|
388
|
+
async exportSite(site) {
|
|
389
|
+
return parseExport(await call(`/export?site=${encodeURIComponent(site)}`), "url", "The platform did not return a valid export.");
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const EXTENSIONS = {
|
|
395
|
+
"image/jpeg": ".jpg",
|
|
396
|
+
"image/png": ".png",
|
|
397
|
+
"image/webp": ".webp",
|
|
398
|
+
"image/avif": ".avif",
|
|
399
|
+
"application/pdf": ".pdf"
|
|
400
|
+
};
|
|
401
|
+
function siteOfKey(key) {
|
|
402
|
+
const match = PUSH_KEY_FORMAT.exec(key.trim());
|
|
403
|
+
if (!match) throw new CliError("GINKO_KEY is not a valid key.", "USAGE");
|
|
404
|
+
return match[1];
|
|
405
|
+
}
|
|
406
|
+
function defaultDir(site, now) {
|
|
407
|
+
return `ginko-export-${site}-${now.toISOString().slice(0, 10)}`;
|
|
408
|
+
}
|
|
409
|
+
function pathOf(file) {
|
|
410
|
+
return `files/${file.id}${EXTENSIONS[file.mime] ?? ""}`;
|
|
411
|
+
}
|
|
412
|
+
async function requireEmpty(dir) {
|
|
413
|
+
let names;
|
|
414
|
+
try {
|
|
415
|
+
names = await readdir(dir);
|
|
416
|
+
} catch (error) {
|
|
417
|
+
const code = error.code;
|
|
418
|
+
if (code === "ENOENT") return;
|
|
419
|
+
if (code === "ENOTDIR") throw new CliError(`\u201C${dir}\u201D is not a folder.`, "USAGE");
|
|
420
|
+
throw error;
|
|
421
|
+
}
|
|
422
|
+
if (names.length > 0) throw new CliError(`The folder \u201C${dir}\u201D is not empty. Choose a new folder.`, "USAGE");
|
|
423
|
+
}
|
|
424
|
+
async function exportSite(input) {
|
|
425
|
+
const { dir } = input;
|
|
426
|
+
await requireEmpty(dir);
|
|
427
|
+
const answer = await input.platform.exportSite(input.site);
|
|
428
|
+
await mkdir(join(dir, "files"), { recursive: true });
|
|
429
|
+
try {
|
|
430
|
+
const files = [];
|
|
431
|
+
for (const file of answer.files) {
|
|
432
|
+
const { url, ...info } = file;
|
|
433
|
+
const path = pathOf(file);
|
|
434
|
+
let bytes;
|
|
435
|
+
try {
|
|
436
|
+
const response = await input.fetch(url);
|
|
437
|
+
if (!response.ok) throw new CliError(`The file \u201C${file.name}\u201D could not be downloaded (HTTP ${response.status}).`, "UNAVAILABLE");
|
|
438
|
+
bytes = new Uint8Array(await response.arrayBuffer());
|
|
439
|
+
} catch (error) {
|
|
440
|
+
if (error instanceof CliError) throw error;
|
|
441
|
+
throw new CliError(`The file \u201C${file.name}\u201D could not be downloaded.`, "UNAVAILABLE");
|
|
442
|
+
}
|
|
443
|
+
if (bytes.byteLength !== file.size) {
|
|
444
|
+
throw new CliError(`The file \u201C${file.name}\u201D is incomplete: ${bytes.byteLength} instead of ${file.size} bytes.`, "UNAVAILABLE");
|
|
445
|
+
}
|
|
446
|
+
await writeFile(join(dir, path), bytes);
|
|
447
|
+
files.push({ ...info, path });
|
|
448
|
+
input.log(`File ${files.length}/${answer.files.length}: ${path}`);
|
|
449
|
+
}
|
|
450
|
+
const out = { ...answer, files };
|
|
451
|
+
await writeFile(join(dir, "export.json"), `${JSON.stringify(out, null, 2)}
|
|
452
|
+
`);
|
|
453
|
+
} catch (error) {
|
|
454
|
+
await rm(dir, { recursive: true, force: true });
|
|
455
|
+
throw error;
|
|
456
|
+
}
|
|
457
|
+
return { dir, entries: answer.entries.length, files: answer.files.length };
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const RETRY_WAITS_MS = [2e3, 4e3, 8e3, 16e3, 32e3];
|
|
461
|
+
async function release(input) {
|
|
462
|
+
const site = input.config.site;
|
|
463
|
+
for (let attempt = 0; ; attempt++) {
|
|
464
|
+
if (attempt > 0 && await input.branchHead() !== input.commit) return { deploy: false, reason: "superseded" };
|
|
465
|
+
const state = await input.platform.state(site);
|
|
466
|
+
if (state.site !== site) throw new CliError(`The key belongs to the site \u201C${state.site}\u201D, not to \u201C${site}\u201D.`, "FORBIDDEN");
|
|
467
|
+
const result = await plan(state, input.config);
|
|
468
|
+
if (result.rejected.length > 0) throw new CliError(explainRejected(result.rejected).join("\n"));
|
|
469
|
+
if (!result.candidate) throw new Error("Internal error: candidate is missing.");
|
|
470
|
+
if (result.candidate.problems.length > 0) throw new CliError(explainProblems(result.candidate.problems).join("\n"));
|
|
471
|
+
try {
|
|
472
|
+
const answer = await input.platform.release({
|
|
473
|
+
site,
|
|
474
|
+
commit: input.commit,
|
|
475
|
+
expectedSchemaRev: state.schemaRev,
|
|
476
|
+
expectedContentRev: state.contentRev,
|
|
477
|
+
schema: result.candidate.schema,
|
|
478
|
+
writes: result.candidate.writes
|
|
479
|
+
});
|
|
480
|
+
return { deploy: true, ...answer };
|
|
481
|
+
} catch (error) {
|
|
482
|
+
if (error instanceof PlatformError && error.code === "CHANGED_SINCE" && attempt < RETRY_WAITS_MS.length) {
|
|
483
|
+
const wait = RETRY_WAITS_MS[attempt];
|
|
484
|
+
input.log(`The site changed during the release. Retrying in ${wait / 1e3} s.`);
|
|
485
|
+
await input.sleep(wait);
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
throw error;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function realDeps() {
|
|
494
|
+
return {
|
|
495
|
+
env: process.env,
|
|
496
|
+
git: (args) => new Promise((done, fail) => {
|
|
497
|
+
execFile("git", args, (error, stdout) => error ? fail(error) : done(stdout));
|
|
498
|
+
}),
|
|
499
|
+
stdout: (text) => {
|
|
500
|
+
process.stdout.write(text);
|
|
501
|
+
},
|
|
502
|
+
log: (line) => {
|
|
503
|
+
process.stderr.write(`${line}
|
|
504
|
+
`);
|
|
505
|
+
},
|
|
506
|
+
fetch: (input, init) => fetch(input, init)
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
function codeOf(error) {
|
|
510
|
+
return error instanceof CliError || error instanceof PlatformError ? error.code : "UNEXPECTED";
|
|
511
|
+
}
|
|
512
|
+
function exitCodeOf(error) {
|
|
513
|
+
const code = codeOf(error);
|
|
514
|
+
if (code === "UNAUTHORIZED" || code === "FORBIDDEN") return 4;
|
|
515
|
+
if (code === "USAGE") return 2;
|
|
516
|
+
if (code === "UNAVAILABLE" || code === "CHANGED_SINCE") return 3;
|
|
517
|
+
if (error instanceof PlatformError) {
|
|
518
|
+
if (error.status === 401 || error.status === 403) return 4;
|
|
519
|
+
if (error.status >= 500 || error.status === 408 || error.status === 429) return 3;
|
|
520
|
+
}
|
|
521
|
+
return 1;
|
|
522
|
+
}
|
|
523
|
+
async function runCommand(deps, command) {
|
|
524
|
+
try {
|
|
525
|
+
const fields = await command();
|
|
526
|
+
deps.stdout(`${JSON.stringify({ ok: true, ...fields })}
|
|
527
|
+
`);
|
|
528
|
+
return 0;
|
|
529
|
+
} catch (error) {
|
|
530
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
531
|
+
const code = codeOf(error);
|
|
532
|
+
deps.log(error instanceof PlatformError ? `Error ${code}: ${message}` : message);
|
|
533
|
+
deps.stdout(`${JSON.stringify({ ok: false, error: { code, message } })}
|
|
534
|
+
`);
|
|
535
|
+
return exitCodeOf(error);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
function parseArgs(argv) {
|
|
539
|
+
const args = { flags: /* @__PURE__ */ new Set() };
|
|
540
|
+
for (let i = 0; i < argv.length; i++) {
|
|
541
|
+
const arg = argv[i];
|
|
542
|
+
if (arg === "--config" || arg.startsWith("--config=")) {
|
|
543
|
+
const value = arg === "--config" ? argv[++i] : arg.slice("--config=".length);
|
|
544
|
+
if (!value) throw new CliError("--config needs a path.", "USAGE");
|
|
545
|
+
args.config = value;
|
|
546
|
+
} else if (arg.startsWith("-")) args.flags.add(arg);
|
|
547
|
+
else if (args.command === void 0) args.command = arg;
|
|
548
|
+
else if (args.folder === void 0) args.folder = arg;
|
|
549
|
+
else throw new CliError(`Unexpected argument \u201C${arg}\u201D.`, "USAGE");
|
|
550
|
+
}
|
|
551
|
+
return args;
|
|
552
|
+
}
|
|
553
|
+
async function runCli(argv, deps, cli) {
|
|
554
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
555
|
+
deps.stdout(cli.usage);
|
|
556
|
+
return 0;
|
|
557
|
+
}
|
|
558
|
+
if (cli.version && argv.includes("--version")) {
|
|
559
|
+
deps.stdout(`${cli.version()}
|
|
560
|
+
`);
|
|
561
|
+
return 0;
|
|
562
|
+
}
|
|
563
|
+
return runCommand(deps, async () => {
|
|
564
|
+
const args = parseArgs(argv);
|
|
565
|
+
if (args.command === void 0) throw new CliError("No command given. Run it with --help to see the commands.", "USAGE");
|
|
566
|
+
const command = Object.hasOwn(cli.commands, args.command) ? cli.commands[args.command] : void 0;
|
|
567
|
+
if (!command) throw new CliError(`Unknown command \u201C${args.command}\u201D. Run it with --help to see the commands.`, "USAGE");
|
|
568
|
+
const name = `${cli.prefix}${args.command}`;
|
|
569
|
+
const flag = [...args.flags].find((f) => f !== "--json" && !command.flags?.includes(f));
|
|
570
|
+
if (flag) throw new CliError(`Unknown option \u201C${flag}\u201D for ${name}.`, "USAGE");
|
|
571
|
+
if (args.folder !== void 0 && !command.folder) throw new CliError(`Unexpected argument \u201C${args.folder}\u201D for ${name}.`, "USAGE");
|
|
572
|
+
if (args.folder === void 0 && command.folder === "required") throw new CliError(`${name} needs a folder: ${name} <folder>.`, "USAGE");
|
|
573
|
+
return command.run(args);
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
function env(deps, ...names) {
|
|
577
|
+
const value = names.map((name) => deps.env[name]).find(Boolean);
|
|
578
|
+
if (!value) throw new CliError(`The environment variable ${names[0]} is missing.`, "USAGE");
|
|
579
|
+
return value;
|
|
580
|
+
}
|
|
581
|
+
const PLATFORM_ENV = ["GINKO_PLATFORM", "NUXT_PUBLIC_GINKO_PLATFORM"];
|
|
582
|
+
function connectPlatform(deps) {
|
|
583
|
+
return createPlatform({ url: env(deps, ...PLATFORM_ENV), key: env(deps, "GINKO_KEY"), fetch: deps.fetch });
|
|
584
|
+
}
|
|
585
|
+
function isEntryPoint(url) {
|
|
586
|
+
return process.argv[1] !== void 0 && realpathSync(process.argv[1]) === fileURLToPath(url);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const USAGE = `Usage: ginko <command> [--config <file>]
|
|
590
|
+
|
|
591
|
+
Commands:
|
|
592
|
+
check [--offline] Check ginko.config.ts and the links in content/, and for platform bricks the schema against
|
|
593
|
+
production. Writes nothing. Platform bricks need GINKO_PLATFORM and GINKO_KEY, unless --offline
|
|
594
|
+
skips the comparison with production.
|
|
595
|
+
release Deploy the schema and content to production (CI). Needs GINKO_PLATFORM, GINKO_KEY and
|
|
596
|
+
GINKO_COMMIT (or GITHUB_SHA). Deploy the site only when it prints "deploy":true.
|
|
597
|
+
export [folder] Save the schema, entries and files from production to a new or empty folder
|
|
598
|
+
(default: ginko-export-<site>-<yyyy-mm-dd>). Needs GINKO_PLATFORM and GINKO_KEY.
|
|
599
|
+
|
|
600
|
+
Options:
|
|
601
|
+
--config <file> Path to ginko.config.ts (default: ./ginko.config.ts)
|
|
602
|
+
-h, --help Show this help
|
|
603
|
+
--version Show the version
|
|
604
|
+
|
|
605
|
+
Every run prints one JSON line on stdout. Exit codes: 0 ok, 1 invalid or rejected, 2 usage,
|
|
606
|
+
3 platform unavailable (run again), 4 key not allowed.
|
|
607
|
+
`;
|
|
608
|
+
function version() {
|
|
609
|
+
return JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8")).version;
|
|
610
|
+
}
|
|
611
|
+
const COMMIT_ENV = ["GINKO_COMMIT", "GITHUB_SHA"];
|
|
612
|
+
async function branchHead(deps) {
|
|
613
|
+
const remote = deps.env.GINKO_REMOTE || "origin";
|
|
614
|
+
const branch = deps.env.GINKO_BRANCH || "main";
|
|
615
|
+
let stdout;
|
|
616
|
+
try {
|
|
617
|
+
stdout = await deps.git(["ls-remote", remote, `refs/heads/${branch}`]);
|
|
618
|
+
} catch {
|
|
619
|
+
throw new CliError(`Could not read ${remote}/${branch} (git ls-remote ${remote} refs/heads/${branch}).`, "UNAVAILABLE");
|
|
620
|
+
}
|
|
621
|
+
const sha = stdout.trim().split(/\s+/)[0];
|
|
622
|
+
if (!sha) throw new CliError(`The branch ${branch} does not exist on ${remote}. Set GINKO_BRANCH and GINKO_REMOTE.`, "USAGE");
|
|
623
|
+
return sha;
|
|
624
|
+
}
|
|
625
|
+
const sleep = (ms) => new Promise((done) => setTimeout(done, ms));
|
|
626
|
+
function main(argv, deps = realDeps()) {
|
|
627
|
+
const configPathOf = (args) => resolve(args.config ?? "ginko.config.ts");
|
|
628
|
+
return runCli(argv, deps, {
|
|
629
|
+
prefix: "ginko ",
|
|
630
|
+
usage: USAGE,
|
|
631
|
+
version,
|
|
632
|
+
commands: {
|
|
633
|
+
check: {
|
|
634
|
+
flags: ["--offline"],
|
|
635
|
+
async run(args) {
|
|
636
|
+
const configPath = configPathOf(args);
|
|
637
|
+
const config = await loadConfig(configPath);
|
|
638
|
+
const offline = args.flags.has("--offline");
|
|
639
|
+
const platformBricks = Object.values(config.bricks).some((b) => b.source !== "files");
|
|
640
|
+
const compared = platformBricks && !offline;
|
|
641
|
+
const ok = await check({
|
|
642
|
+
config,
|
|
643
|
+
connect: compared ? () => connectPlatform(deps) : void 0,
|
|
644
|
+
root: dirname(configPath),
|
|
645
|
+
fetch: deps.fetch,
|
|
646
|
+
log: deps.log
|
|
647
|
+
});
|
|
648
|
+
if (platformBricks && offline) deps.log("Skipped the comparison with production (--offline).");
|
|
649
|
+
if (!ok) throw new CliError("ginko check found problems. Each one is listed on stderr.");
|
|
650
|
+
return { compared };
|
|
651
|
+
}
|
|
652
|
+
},
|
|
653
|
+
release: {
|
|
654
|
+
async run(args) {
|
|
655
|
+
const commit = env(deps, ...COMMIT_ENV);
|
|
656
|
+
if (await branchHead(deps) !== commit) return { deploy: false, reason: "superseded" };
|
|
657
|
+
const config = await loadConfig(configPathOf(args));
|
|
658
|
+
return release({ config, commit, platform: connectPlatform(deps), branchHead: () => branchHead(deps), sleep, log: deps.log });
|
|
659
|
+
}
|
|
660
|
+
},
|
|
661
|
+
// Works on the hosted content only, so it needs no config.
|
|
662
|
+
export: {
|
|
663
|
+
folder: "optional",
|
|
664
|
+
async run(args) {
|
|
665
|
+
const platform = connectPlatform(deps);
|
|
666
|
+
const site = siteOfKey(env(deps, "GINKO_KEY"));
|
|
667
|
+
return exportSite({ platform, site, dir: args.folder ?? defaultDir(site, /* @__PURE__ */ new Date()), fetch: deps.fetch, log: deps.log });
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
if (isEntryPoint(import.meta.url)) {
|
|
674
|
+
void main(process.argv.slice(2)).then((code) => {
|
|
675
|
+
process.exitCode = code;
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
export { main };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { L as LinkConfig, M as Migration, b as MigrationContext, R as Redirect, c as RouteTemplate, S as SiteInput, e as collection, f as defineSite, s as single } from './shared/ginko.akHewCGt.mjs';
|
|
2
|
+
export { B as Brick, d as BrickSource, k as ChoiceOption, e as CollectionBrick, m as ComponentDef, a as ComponentRegistry, C as Currency, q as DataOf, t as Field, F as Fields, v as FileValue, y as ImageValue, z as ItemOf, J as Json, c as Label, L as Locale, Q as MarkdownProfile, U as MarkdownToc, M as MarkdownTree, X as MarkdownValue, Z as Money, f as SingleBrick, T as Task, a8 as TocLink, ab as UnknownChoice, V as Visibility, W as WeekInfo, aj as defineComponents, ao as f, au as plainText, ax as startOfWeek } from './shared/ginko.D-fNojn9.mjs';
|
|
3
|
+
export { C as Condition, N as NavNode, Q as Query, d as SeoImage, j as formatMoney, s as safeHref } from './shared/ginko.BTUfZCfa.mjs';
|
|
4
|
+
import './shared/ginko.Bwan3BB-.mjs';
|
|
5
|
+
import '@lupinum/ginko-i18n/core';
|
|
6
|
+
import 'comark';
|