@minamocms/framework 0.0.0-stage → 0.10.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/CHANGELOG.md +234 -0
- package/LICENSE +105 -0
- package/README.md +133 -2
- package/dist/boot.d.ts +19 -0
- package/dist/boot.js +22 -0
- package/dist/config.d.ts +18 -0
- package/dist/config.js +44 -0
- package/dist/create-site/cli.d.ts +2 -0
- package/dist/create-site/cli.js +31 -0
- package/dist/create-site/generate-site.d.ts +12 -0
- package/dist/create-site/generate-site.js +168 -0
- package/dist/create-site/mint-token-cli.d.ts +2 -0
- package/dist/create-site/mint-token-cli.js +26 -0
- package/dist/create-site/mint-token.d.ts +7 -0
- package/dist/create-site/mint-token.js +49 -0
- package/dist/create-site/template/AGENTS.md +473 -0
- package/dist/create-site/template/content/menus/footerCompany.json +9 -0
- package/dist/create-site/template/content/menus/footerProduct.json +9 -0
- package/dist/create-site/template/content/menus/footerResources.json +9 -0
- package/dist/create-site/template/content/menus/main.json +9 -0
- package/dist/create-site/template/content/pages/404.json +23 -0
- package/dist/create-site/template/content/pages/about/careers.json +22 -0
- package/dist/create-site/template/content/pages/about/team.json +22 -0
- package/dist/create-site/template/content/pages/about.json +22 -0
- package/dist/create-site/template/content/pages/docs/deployment.json +22 -0
- package/dist/create-site/template/content/pages/docs/getting-started/quickstart.json +22 -0
- package/dist/create-site/template/content/pages/docs/getting-started.json +24 -0
- package/dist/create-site/template/content/pages/docs.json +22 -0
- package/dist/create-site/template/content/pages/index.json +182 -0
- package/dist/create-site/template/content/redirects.json +4 -0
- package/dist/create-site/template/dockerignore +6 -0
- package/dist/create-site/template/gitignore +11 -0
- package/dist/create-site/template/theme/assets/site.js +49 -0
- package/dist/create-site/template/theme/assets/style.css +1223 -0
- package/dist/create-site/template/theme/blocks/button.liquid +21 -0
- package/dist/create-site/template/theme/blocks/comparison-row.liquid +19 -0
- package/dist/create-site/template/theme/blocks/faq-item.liquid +22 -0
- package/dist/create-site/template/theme/blocks/feature-card.liquid +23 -0
- package/dist/create-site/template/theme/blocks/logo-mark.liquid +13 -0
- package/dist/create-site/template/theme/blocks/pricing-tier.liquid +42 -0
- package/dist/create-site/template/theme/blocks/step-card.liquid +19 -0
- package/dist/create-site/template/theme/blocks/testimonial-card.liquid +25 -0
- package/dist/create-site/template/theme/config/site_settings.json +11 -0
- package/dist/create-site/template/theme/layouts/theme.liquid +102 -0
- package/dist/create-site/template/theme/sections/comparison.liquid +41 -0
- package/dist/create-site/template/theme/sections/cta-banner.liquid +36 -0
- package/dist/create-site/template/theme/sections/faq.liquid +27 -0
- package/dist/create-site/template/theme/sections/feature-grid.liquid +27 -0
- package/dist/create-site/template/theme/sections/hero.liquid +48 -0
- package/dist/create-site/template/theme/sections/how-it-works.liquid +27 -0
- package/dist/create-site/template/theme/sections/pricing.liquid +27 -0
- package/dist/create-site/template/theme/sections/social-proof.liquid +21 -0
- package/dist/create-site/template/theme/sections/testimonials.liquid +27 -0
- package/dist/create-site/template/theme/snippets/icon.liquid +24 -0
- package/dist/create-site/template/theme/snippets/logo.liquid +7 -0
- package/dist/create-site/template/theme/snippets/responsive-media.liquid +107 -0
- package/dist/create-site/template/theme/snippets/site-name.liquid +1 -0
- package/dist/create-site/template/vhost/Dockerfile +33 -0
- package/dist/create-site/template/vhost/docker-entrypoint.sh +58 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -0
- package/dist/media/drivers/driver.d.ts +11 -0
- package/dist/media/drivers/driver.js +1 -0
- package/dist/media/drivers/local-fs-driver.d.ts +3 -0
- package/dist/media/drivers/local-fs-driver.js +81 -0
- package/dist/media/filename.d.ts +2 -0
- package/dist/media/filename.js +54 -0
- package/dist/media/manage-media.d.ts +19 -0
- package/dist/media/manage-media.js +45 -0
- package/dist/media/seed-media-cli.d.ts +2 -0
- package/dist/media/seed-media-cli.js +22 -0
- package/dist/media/seed-media.d.ts +11 -0
- package/dist/media/seed-media.js +76 -0
- package/dist/migrations/index.d.ts +3 -0
- package/dist/migrations/index.js +83 -0
- package/dist/pull-site/cli.d.ts +2 -0
- package/dist/pull-site/cli.js +78 -0
- package/dist/pull-site/prompt-token.d.ts +16 -0
- package/dist/pull-site/prompt-token.js +69 -0
- package/dist/pull-site/prompts.d.ts +22 -0
- package/dist/pull-site/prompts.js +127 -0
- package/dist/pull-site/pull-site.d.ts +23 -0
- package/dist/pull-site/pull-site.js +211 -0
- package/dist/renderer/engine.d.ts +2 -0
- package/dist/renderer/engine.js +48 -0
- package/dist/renderer/render-cache.d.ts +11 -0
- package/dist/renderer/render-cache.js +11 -0
- package/dist/renderer/render-page.d.ts +34 -0
- package/dist/renderer/render-page.js +209 -0
- package/dist/renderer/theme-templates.d.ts +7 -0
- package/dist/renderer/theme-templates.js +82 -0
- package/dist/routes/admin-redirect.d.ts +5 -0
- package/dist/routes/admin-redirect.js +26 -0
- package/dist/routes/assets.d.ts +6 -0
- package/dist/routes/assets.js +30 -0
- package/dist/routes/batch.d.ts +10 -0
- package/dist/routes/batch.js +148 -0
- package/dist/routes/capabilities.d.ts +6 -0
- package/dist/routes/capabilities.js +27 -0
- package/dist/routes/content.d.ts +8 -0
- package/dist/routes/content.js +169 -0
- package/dist/routes/deleted-pages.d.ts +8 -0
- package/dist/routes/deleted-pages.js +9 -0
- package/dist/routes/drafts.d.ts +10 -0
- package/dist/routes/drafts.js +73 -0
- package/dist/routes/git.d.ts +8 -0
- package/dist/routes/git.js +158 -0
- package/dist/routes/index.d.ts +12 -0
- package/dist/routes/index.js +86 -0
- package/dist/routes/links.d.ts +8 -0
- package/dist/routes/links.js +18 -0
- package/dist/routes/media-public.d.ts +11 -0
- package/dist/routes/media-public.js +125 -0
- package/dist/routes/media.d.ts +9 -0
- package/dist/routes/media.js +80 -0
- package/dist/routes/menus.d.ts +8 -0
- package/dist/routes/menus.js +148 -0
- package/dist/routes/preview-revision.d.ts +10 -0
- package/dist/routes/preview-revision.js +96 -0
- package/dist/routes/preview.d.ts +10 -0
- package/dist/routes/preview.js +56 -0
- package/dist/routes/public.d.ts +10 -0
- package/dist/routes/public.js +123 -0
- package/dist/routes/publish.d.ts +10 -0
- package/dist/routes/publish.js +139 -0
- package/dist/routes/redirects.d.ts +8 -0
- package/dist/routes/redirects.js +132 -0
- package/dist/routes/search-public.d.ts +6 -0
- package/dist/routes/search-public.js +104 -0
- package/dist/routes/search.d.ts +8 -0
- package/dist/routes/search.js +15 -0
- package/dist/routes/settings.d.ts +10 -0
- package/dist/routes/settings.js +68 -0
- package/dist/routes/sitemap.d.ts +7 -0
- package/dist/routes/sitemap.js +79 -0
- package/dist/routes/theme.d.ts +10 -0
- package/dist/routes/theme.js +116 -0
- package/dist/schemas/instance.schema.json +17 -0
- package/dist/schemas/menu.schema.json +24 -0
- package/dist/schemas/page.schema.json +26 -0
- package/dist/schemas/redirects.schema.json +24 -0
- package/dist/search/drivers/driver.d.ts +9 -0
- package/dist/search/drivers/driver.js +1 -0
- package/dist/search/drivers/node-sqlite-driver.d.ts +7 -0
- package/dist/search/drivers/node-sqlite-driver.js +19 -0
- package/dist/search/query-content.d.ts +32 -0
- package/dist/search/query-content.js +207 -0
- package/dist/search/query-index.d.ts +5 -0
- package/dist/search/query-index.js +21 -0
- package/dist/search/rebuild-index.d.ts +3 -0
- package/dist/search/rebuild-index.js +320 -0
- package/dist/server-config.d.ts +23 -0
- package/dist/server-config.js +237 -0
- package/dist/server.d.ts +13 -0
- package/dist/server.js +235 -0
- package/dist/services/batch.d.ts +35 -0
- package/dist/services/batch.js +145 -0
- package/dist/services/checkpoint.d.ts +4 -0
- package/dist/services/checkpoint.js +45 -0
- package/dist/services/content-read.d.ts +27 -0
- package/dist/services/content-read.js +151 -0
- package/dist/services/delete-content.d.ts +12 -0
- package/dist/services/delete-content.js +166 -0
- package/dist/services/deleted-pages.d.ts +11 -0
- package/dist/services/deleted-pages.js +83 -0
- package/dist/services/dev-tunnel.d.ts +5 -0
- package/dist/services/dev-tunnel.js +5 -0
- package/dist/services/drafts.d.ts +16 -0
- package/dist/services/drafts.js +164 -0
- package/dist/services/etag.d.ts +2 -0
- package/dist/services/etag.js +30 -0
- package/dist/services/fs-walk.d.ts +1 -0
- package/dist/services/fs-walk.js +26 -0
- package/dist/services/git-commit.d.ts +3 -0
- package/dist/services/git-commit.js +12 -0
- package/dist/services/git-history.d.ts +28 -0
- package/dist/services/git-history.js +131 -0
- package/dist/services/git-revert.d.ts +12 -0
- package/dist/services/git-revert.js +140 -0
- package/dist/services/git.d.ts +16 -0
- package/dist/services/git.js +127 -0
- package/dist/services/interval-job.d.ts +4 -0
- package/dist/services/interval-job.js +9 -0
- package/dist/services/ip-allowlist.d.ts +7 -0
- package/dist/services/ip-allowlist.js +31 -0
- package/dist/services/links.d.ts +22 -0
- package/dist/services/links.js +169 -0
- package/dist/services/manage-menus.d.ts +15 -0
- package/dist/services/manage-menus.js +138 -0
- package/dist/services/manage-redirects.d.ts +25 -0
- package/dist/services/manage-redirects.js +156 -0
- package/dist/services/menu-references.d.ts +3 -0
- package/dist/services/menu-references.js +38 -0
- package/dist/services/menus.d.ts +9 -0
- package/dist/services/menus.js +43 -0
- package/dist/services/migration-runner.d.ts +16 -0
- package/dist/services/migration-runner.js +169 -0
- package/dist/services/mime-types.d.ts +3 -0
- package/dist/services/mime-types.js +43 -0
- package/dist/services/move.d.ts +16 -0
- package/dist/services/move.js +236 -0
- package/dist/services/path-safety.d.ts +6 -0
- package/dist/services/path-safety.js +112 -0
- package/dist/services/pid-file.d.ts +13 -0
- package/dist/services/pid-file.js +54 -0
- package/dist/services/post-urls.d.ts +3 -0
- package/dist/services/post-urls.js +27 -0
- package/dist/services/prepared-operation.d.ts +4 -0
- package/dist/services/prepared-operation.js +1 -0
- package/dist/services/publish.d.ts +15 -0
- package/dist/services/publish.js +313 -0
- package/dist/services/rate-limit-config.d.ts +9 -0
- package/dist/services/rate-limit-config.js +22 -0
- package/dist/services/redirects.d.ts +27 -0
- package/dist/services/redirects.js +175 -0
- package/dist/services/reindex-on-write.d.ts +3 -0
- package/dist/services/reindex-on-write.js +30 -0
- package/dist/services/resolve-blog-url.d.ts +11 -0
- package/dist/services/resolve-blog-url.js +31 -0
- package/dist/services/resolve-url.d.ts +11 -0
- package/dist/services/resolve-url.js +20 -0
- package/dist/services/site-settings.d.ts +28 -0
- package/dist/services/site-settings.js +116 -0
- package/dist/services/startup-checks.d.ts +10 -0
- package/dist/services/startup-checks.js +81 -0
- package/dist/services/static-file.d.ts +7 -0
- package/dist/services/static-file.js +37 -0
- package/dist/services/theme-component-file.d.ts +6 -0
- package/dist/services/theme-component-file.js +39 -0
- package/dist/services/theme-files.d.ts +33 -0
- package/dist/services/theme-files.js +148 -0
- package/dist/services/theme-page-templates.d.ts +7 -0
- package/dist/services/theme-page-templates.js +53 -0
- package/dist/services/theme-schemas.d.ts +2 -0
- package/dist/services/theme-schemas.js +137 -0
- package/dist/services/token-auth.d.ts +10 -0
- package/dist/services/token-auth.js +64 -0
- package/dist/services/urls.d.ts +2 -0
- package/dist/services/urls.js +20 -0
- package/dist/services/validation.d.ts +26 -0
- package/dist/services/validation.js +195 -0
- package/dist/services/write-queue.d.ts +1 -0
- package/dist/services/write-queue.js +14 -0
- package/dist/site-check/cli.d.ts +2 -0
- package/dist/site-check/cli.js +48 -0
- package/dist/site-check/run-check.d.ts +12 -0
- package/dist/site-check/run-check.js +188 -0
- package/dist/site-sync/cli-helpers.d.ts +23 -0
- package/dist/site-sync/cli-helpers.js +75 -0
- package/dist/site-sync/deploy.d.ts +36 -0
- package/dist/site-sync/deploy.js +124 -0
- package/dist/site-sync/live-content.d.ts +30 -0
- package/dist/site-sync/live-content.js +68 -0
- package/dist/site-sync/prompts.d.ts +30 -0
- package/dist/site-sync/prompts.js +237 -0
- package/dist/site-sync/pull-cli.d.ts +2 -0
- package/dist/site-sync/pull-cli.js +75 -0
- package/dist/site-sync/pull-site.d.ts +28 -0
- package/dist/site-sync/pull-site.js +171 -0
- package/dist/site-sync/push-cli.d.ts +2 -0
- package/dist/site-sync/push-cli.js +268 -0
- package/dist/site-sync/push-site.d.ts +96 -0
- package/dist/site-sync/push-site.js +376 -0
- package/dist/site-sync/remote-site.d.ts +38 -0
- package/dist/site-sync/remote-site.js +122 -0
- package/dist/site-sync/sync-record.d.ts +26 -0
- package/dist/site-sync/sync-record.js +76 -0
- package/dist/site-sync/theme-sync.d.ts +34 -0
- package/dist/site-sync/theme-sync.js +128 -0
- package/dist/stop-site/cli.d.ts +2 -0
- package/dist/stop-site/cli.js +28 -0
- package/dist/stop-site/stop-site.d.ts +26 -0
- package/dist/stop-site/stop-site.js +56 -0
- package/dist/theme-state.d.ts +20 -0
- package/dist/theme-state.js +33 -0
- package/dist/upgrade/changelog.d.ts +8 -0
- package/dist/upgrade/changelog.js +46 -0
- package/dist/upgrade/finish.d.ts +9 -0
- package/dist/upgrade/finish.js +30 -0
- package/dist/upgrade/owned-files.d.ts +11 -0
- package/dist/upgrade/owned-files.js +42 -0
- package/dist/upgrade/package-names.d.ts +4 -0
- package/dist/upgrade/package-names.js +26 -0
- package/dist/upgrade/upgrade-cli.d.ts +2 -0
- package/dist/upgrade/upgrade-cli.js +170 -0
- package/package.json +64 -4
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { readdirSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { parseThemeComponentFile } from "./theme-component-file.js";
|
|
4
|
+
import { requiredFieldsHaveValidDefaults } from "./validation.js";
|
|
5
|
+
// themeRoot is agent configuration (the configured site's theme
|
|
6
|
+
// directory), not a request-supplied :path parameter. This walk is
|
|
7
|
+
// deliberately NOT the Group B path-sanitisation helper and must
|
|
8
|
+
// never be reused for untrusted request paths.
|
|
9
|
+
//
|
|
10
|
+
// Flat *.liquid files, one per type, named directly (e.g. hero.liquid,
|
|
11
|
+
// media-text.liquid) - no subfolder per type. Mirrors theme-templates.ts's
|
|
12
|
+
// loadFlatTemplates walk exactly (already established for snippets/
|
|
13
|
+
// layouts), extended to extract the embedded {% schema %} block instead
|
|
14
|
+
// of returning the raw file contents.
|
|
15
|
+
//
|
|
16
|
+
// kind is only used to word each warning ("Section" vs "Block") -
|
|
17
|
+
// callers already know which directory they asked for.
|
|
18
|
+
function loadTypeSchemas(typesDir, kind) {
|
|
19
|
+
const schemas = {};
|
|
20
|
+
const acceptsBlocks = {};
|
|
21
|
+
const warnings = [];
|
|
22
|
+
let entries;
|
|
23
|
+
try {
|
|
24
|
+
entries = readdirSync(typesDir, { withFileTypes: true })
|
|
25
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith('.liquid'))
|
|
26
|
+
.map((entry) => entry.name);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return { schemas, acceptsBlocks, warnings };
|
|
30
|
+
}
|
|
31
|
+
for (const fileName of entries) {
|
|
32
|
+
const type = fileName.slice(0, -'.liquid'.length);
|
|
33
|
+
const label = `${kind} type "${type}" (${fileName}) was excluded from the theme`;
|
|
34
|
+
let source;
|
|
35
|
+
try {
|
|
36
|
+
source = readFileSync(join(typesDir, fileName), 'utf-8');
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const parsed = parseThemeComponentFile(source);
|
|
42
|
+
if (!parsed) {
|
|
43
|
+
warnings.push(`${label}: no valid {% schema %} block found (missing, or not parseable JSON).`);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
// A type whose required settings fields lack usable defaults is
|
|
47
|
+
// skipped the same way a malformed schema block already is -
|
|
48
|
+
// never a boot failure, just excluded from what gets registered
|
|
49
|
+
// (guide-theme-authoring.md, Group L).
|
|
50
|
+
if (!requiredFieldsHaveValidDefaults(parsed.schema)) {
|
|
51
|
+
warnings.push(`${label}: a property listed in "required" has no valid "default" (see guide-theme-authoring.md).`);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
schemas[type] = parsed.schema;
|
|
55
|
+
// The only place "does this type support nested blocks" is ever
|
|
56
|
+
// expressed - a markup convention (does the template loop
|
|
57
|
+
// blocksHtml), not a schema field (guide-theme-authoring.md).
|
|
58
|
+
acceptsBlocks[type] = parsed.markup.includes('blocksHtml');
|
|
59
|
+
}
|
|
60
|
+
return { schemas, acceptsBlocks, warnings };
|
|
61
|
+
}
|
|
62
|
+
// theme/config/site_settings.json: the site-wide settings the theme
|
|
63
|
+
// defines, in the same JSON Schema form a section's {% schema %} uses,
|
|
64
|
+
// vetted the same way. No file means no site settings, which is fine; a
|
|
65
|
+
// file that can't be used is left out with a warning, like a broken
|
|
66
|
+
// section.
|
|
67
|
+
//
|
|
68
|
+
// Not Shopify's config/settings_schema.json, which 0.7.0 and 0.7.1 used:
|
|
69
|
+
// Shopify's VS Code extension checks any file of that name against
|
|
70
|
+
// Shopify's own format (a list of groups), marking Minamo's as wrong -
|
|
71
|
+
// which is how one real site had its file rewritten into Shopify's form.
|
|
72
|
+
// A settings_schema.json in Minamo's form is still read, with a warning
|
|
73
|
+
// to rename it; one in Shopify's form (or anything else) is Shopify's,
|
|
74
|
+
// and left alone.
|
|
75
|
+
const SETTINGS_FILE = 'site_settings.json';
|
|
76
|
+
const OLD_SETTINGS_FILE = 'settings_schema.json';
|
|
77
|
+
function readConfigFile(themeRoot, name) {
|
|
78
|
+
try {
|
|
79
|
+
return readFileSync(join(themeRoot, 'config', name), 'utf-8');
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function loadSettingsSchema(themeRoot) {
|
|
86
|
+
const source = readConfigFile(themeRoot, SETTINGS_FILE);
|
|
87
|
+
if (source === null) {
|
|
88
|
+
return loadOldSettingsSchema(themeRoot);
|
|
89
|
+
}
|
|
90
|
+
const label = `Site settings (config/${SETTINGS_FILE}) were excluded from the theme`;
|
|
91
|
+
let schema;
|
|
92
|
+
try {
|
|
93
|
+
schema = JSON.parse(source);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return { warning: `${label}: the file is not valid JSON.` };
|
|
97
|
+
}
|
|
98
|
+
if (typeof schema !== 'object' || schema === null || Array.isArray(schema)) {
|
|
99
|
+
return { warning: `${label}: the file must hold a JSON Schema object, the same form as a section's {% schema %}.` };
|
|
100
|
+
}
|
|
101
|
+
if (!requiredFieldsHaveValidDefaults(schema)) {
|
|
102
|
+
return { warning: `${label}: a property listed in "required" has no valid "default" (see guide-theme-authoring.md).` };
|
|
103
|
+
}
|
|
104
|
+
return { schema };
|
|
105
|
+
}
|
|
106
|
+
function loadOldSettingsSchema(themeRoot) {
|
|
107
|
+
const source = readConfigFile(themeRoot, OLD_SETTINGS_FILE);
|
|
108
|
+
if (source === null) {
|
|
109
|
+
return {};
|
|
110
|
+
}
|
|
111
|
+
let schema;
|
|
112
|
+
try {
|
|
113
|
+
schema = JSON.parse(source);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return {};
|
|
117
|
+
}
|
|
118
|
+
if (typeof schema !== 'object' || schema === null || Array.isArray(schema) || !requiredFieldsHaveValidDefaults(schema)) {
|
|
119
|
+
return {};
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
schema,
|
|
123
|
+
warning: `Site settings are defined in config/${OLD_SETTINGS_FILE}, the name Shopify uses for its own, different format (its VS Code extension marks the file as wrong). Rename it to config/${SETTINGS_FILE}; nothing else changes.`,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
export function loadThemeSchemas(themeRoot) {
|
|
127
|
+
const sections = loadTypeSchemas(join(themeRoot, 'sections'), 'Section');
|
|
128
|
+
const blocks = loadTypeSchemas(join(themeRoot, 'blocks'), 'Block');
|
|
129
|
+
const settings = loadSettingsSchema(themeRoot);
|
|
130
|
+
return {
|
|
131
|
+
sections: sections.schemas,
|
|
132
|
+
blocks: blocks.schemas,
|
|
133
|
+
acceptsBlocks: { sections: sections.acceptsBlocks, blocks: blocks.acceptsBlocks },
|
|
134
|
+
...(settings.schema ? { settings: settings.schema } : {}),
|
|
135
|
+
warnings: [...sections.warnings, ...blocks.warnings, ...(settings.warning ? [settings.warning] : [])],
|
|
136
|
+
};
|
|
137
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { FastifyRequest } from 'fastify';
|
|
2
|
+
import type { Scope, TokenEntry } from '../server-config.ts';
|
|
3
|
+
export type AuthReason = 'missing-token' | 'invalid-token' | 'missing-scope';
|
|
4
|
+
export declare class AuthError extends Error {
|
|
5
|
+
readonly reason: AuthReason;
|
|
6
|
+
readonly statusCode: number;
|
|
7
|
+
constructor(reason: AuthReason, message: string);
|
|
8
|
+
}
|
|
9
|
+
export declare function verifyToken(tokens: TokenEntry[], presentedToken: string): Scope[] | null;
|
|
10
|
+
export declare function requireScope(tokens: TokenEntry[], scope: Scope): (request: FastifyRequest) => Promise<void>;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { createHash, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
// The first domain error in this codebase's established XxxError+
|
|
3
|
+
// reason convention that has to cross the Fastify HTTP boundary -
|
|
4
|
+
// every prior one throws pre-HTTP (at boot) or predates Phase 2
|
|
5
|
+
// entirely. statusCode is set explicitly in the constructor so the
|
|
6
|
+
// existing global error handler (server.ts, which branches on
|
|
7
|
+
// error.statusCode) passes these through with their real message
|
|
8
|
+
// intact, never sanitising them as a generic 500 - confirmed
|
|
9
|
+
// empirically that a preHandler-thrown error with .statusCode set
|
|
10
|
+
// reaches that same handler correctly.
|
|
11
|
+
export class AuthError extends Error {
|
|
12
|
+
reason;
|
|
13
|
+
statusCode;
|
|
14
|
+
constructor(reason, message) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = 'AuthError';
|
|
17
|
+
this.reason = reason;
|
|
18
|
+
this.statusCode = reason === 'missing-scope' ? 403 : 401;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const BEARER_PREFIX = 'Bearer ';
|
|
22
|
+
function hashToken(token) {
|
|
23
|
+
return createHash('sha256').update(token).digest();
|
|
24
|
+
}
|
|
25
|
+
// Never compare token digests (or hex strings) with === or
|
|
26
|
+
// Buffer.equals() - neither is guaranteed constant-time. Only
|
|
27
|
+
// timingSafeEqual, which is why both sides must first be reduced to
|
|
28
|
+
// a fixed-length digest: it throws on mismatched buffer lengths, and
|
|
29
|
+
// a raw incoming token is never the same length as another token in
|
|
30
|
+
// general (confirmed empirically before this was written).
|
|
31
|
+
export function verifyToken(tokens, presentedToken) {
|
|
32
|
+
const presentedDigest = hashToken(presentedToken);
|
|
33
|
+
for (const entry of tokens) {
|
|
34
|
+
const storedDigest = Buffer.from(entry.hash, 'hex');
|
|
35
|
+
if (timingSafeEqual(presentedDigest, storedDigest)) {
|
|
36
|
+
return entry.scopes;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
// Applied as a per-route preHandler option
|
|
42
|
+
// (fastify.get(path, { preHandler: requireScope(tokens, 'content') }, handler)),
|
|
43
|
+
// not a whole-file addHook: a single route-group file may eventually
|
|
44
|
+
// mix scopes (e.g. a content-scoped GET and a media-scoped POST in
|
|
45
|
+
// the same file), so the check is granular to the route, not the
|
|
46
|
+
// file. Each route-group file still registers plainly, never wrapped
|
|
47
|
+
// in fastify-plugin's fp(), preserving the per-plugin encapsulation
|
|
48
|
+
// this design depends on.
|
|
49
|
+
export function requireScope(tokens, scope) {
|
|
50
|
+
return async function scopeGuard(request) {
|
|
51
|
+
const header = request.headers.authorization;
|
|
52
|
+
if (!header || !header.startsWith(BEARER_PREFIX)) {
|
|
53
|
+
throw new AuthError('missing-token', 'Missing or malformed Authorization header');
|
|
54
|
+
}
|
|
55
|
+
const presentedToken = header.slice(BEARER_PREFIX.length);
|
|
56
|
+
const scopes = verifyToken(tokens, presentedToken);
|
|
57
|
+
if (scopes === null) {
|
|
58
|
+
throw new AuthError('invalid-token', 'The presented token does not match any configured token');
|
|
59
|
+
}
|
|
60
|
+
if (!scopes.includes(scope)) {
|
|
61
|
+
throw new AuthError('missing-scope', `This route requires the "${scope}" scope`);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Pure, filesystem-free mapping between a public URL and a page's path
|
|
2
|
+
// relative to pagesRoot. A page with children is a file (about.json)
|
|
3
|
+
// sitting alongside a same-named sibling directory (about/) holding
|
|
4
|
+
// descendants, so this mapping never needs to know whether a given
|
|
5
|
+
// page currently has children - unlike an about/index.json convention,
|
|
6
|
+
// which would make resolution filesystem-dependent.
|
|
7
|
+
export function urlToPagePath(url) {
|
|
8
|
+
const trimmed = url.replace(/^\/+/, '').replace(/\/+$/, '');
|
|
9
|
+
if (trimmed === '') {
|
|
10
|
+
return 'index.json';
|
|
11
|
+
}
|
|
12
|
+
return `${trimmed}.json`;
|
|
13
|
+
}
|
|
14
|
+
export function pagePathToUrl(relativePagePath) {
|
|
15
|
+
const withoutExtension = relativePagePath.replace(/\.json$/, '');
|
|
16
|
+
if (withoutExtension === 'index') {
|
|
17
|
+
return '/';
|
|
18
|
+
}
|
|
19
|
+
return `/${withoutExtension}`;
|
|
20
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface ValidationError {
|
|
2
|
+
path: string;
|
|
3
|
+
message: string;
|
|
4
|
+
keyword: string;
|
|
5
|
+
}
|
|
6
|
+
export interface ValidationResult {
|
|
7
|
+
valid: boolean;
|
|
8
|
+
errors: ValidationError[];
|
|
9
|
+
}
|
|
10
|
+
export interface ThemeSchemas {
|
|
11
|
+
sections: Record<string, object>;
|
|
12
|
+
blocks: Record<string, object>;
|
|
13
|
+
acceptsBlocks: {
|
|
14
|
+
sections: Record<string, boolean>;
|
|
15
|
+
blocks: Record<string, boolean>;
|
|
16
|
+
};
|
|
17
|
+
settings?: object;
|
|
18
|
+
warnings?: string[];
|
|
19
|
+
}
|
|
20
|
+
export declare function requiredFieldsHaveValidDefaults(schema: object): boolean;
|
|
21
|
+
export declare function validateInstance(instance: unknown, kind: 'section' | 'block', themeSchemas: ThemeSchemas): ValidationResult;
|
|
22
|
+
export declare function validatePage(page: unknown, themeSchemas: ThemeSchemas): ValidationResult;
|
|
23
|
+
export declare function validateMenu(menu: unknown): ValidationResult;
|
|
24
|
+
export declare function validateRedirects(redirects: unknown): ValidationResult;
|
|
25
|
+
export declare function validateContent(relativePath: string, content: unknown, themeSchemas: ThemeSchemas): ValidationResult;
|
|
26
|
+
export declare function validateSiteSettings(settings: unknown, themeSchemas: ThemeSchemas): ValidationResult;
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { Ajv } from 'ajv';
|
|
4
|
+
// strict: false because theme schema.json files are authored by theme
|
|
5
|
+
// authors, not agent code, and are validated the same lenient way
|
|
6
|
+
// everywhere (see theme-schemas.ts).
|
|
7
|
+
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
8
|
+
// The nine format values guide-theme-authoring.md/AGENTS.md actually
|
|
9
|
+
// document (richtext/image/video/textarea/uri/date/color/range/toggle -
|
|
10
|
+
// uri and date are real standard JSON Schema formats already understood
|
|
11
|
+
// without this) are still, correctly, UI hints only: registering them
|
|
12
|
+
// as a literal no-op format (the `true` here, not a real validator
|
|
13
|
+
// function) doesn't make Ajv enforce anything about them, it only
|
|
14
|
+
// stops it printing "unknown format \"x\" ignored" for values that are
|
|
15
|
+
// completely expected. A theme author's genuine typo (e.g. "iamge")
|
|
16
|
+
// still isn't in this list, so it still warns - this only silences the
|
|
17
|
+
// ones we ourselves tell theme authors to use, not unknown-format
|
|
18
|
+
// warnings in general.
|
|
19
|
+
// "url" (the link field's documented name) and "uri" (its name before)
|
|
20
|
+
// are hints too: a link is a site path like "/about" as often as a full
|
|
21
|
+
// address, which a real uri check would reject.
|
|
22
|
+
for (const format of ['richtext', 'image', 'textarea', 'color', 'range', 'toggle', 'video', 'url', 'uri']) {
|
|
23
|
+
ajv.addFormat(format, true);
|
|
24
|
+
}
|
|
25
|
+
const schemasDir = join(import.meta.dirname, '..', 'schemas');
|
|
26
|
+
function readSchema(filename) {
|
|
27
|
+
return JSON.parse(readFileSync(join(schemasDir, filename), 'utf-8'));
|
|
28
|
+
}
|
|
29
|
+
const instanceSchema = readSchema('instance.schema.json');
|
|
30
|
+
ajv.addSchema(instanceSchema);
|
|
31
|
+
const validatePageEnvelope = ajv.compile(readSchema('page.schema.json'));
|
|
32
|
+
const validateMenuEnvelope = ajv.compile(readSchema('menu.schema.json'));
|
|
33
|
+
const validateRedirectsEnvelope = ajv.compile(readSchema('redirects.schema.json'));
|
|
34
|
+
const validateInstanceEnvelope = ajv.compile(instanceSchema);
|
|
35
|
+
function normaliseErrors(errors) {
|
|
36
|
+
if (!errors) {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
return errors.map((error) => {
|
|
40
|
+
let path = error.instancePath;
|
|
41
|
+
if (error.keyword === 'required') {
|
|
42
|
+
const { missingProperty } = error.params;
|
|
43
|
+
path = `${error.instancePath}/${missingProperty}`;
|
|
44
|
+
}
|
|
45
|
+
else if (error.keyword === 'additionalProperties') {
|
|
46
|
+
const { additionalProperty } = error.params;
|
|
47
|
+
path = `${error.instancePath}/${additionalProperty}`;
|
|
48
|
+
}
|
|
49
|
+
return { path, message: error.message ?? '', keyword: error.keyword };
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function runValidator(validate, data) {
|
|
53
|
+
const valid = validate(data);
|
|
54
|
+
return { valid: Boolean(valid), errors: normaliseErrors(validate.errors) };
|
|
55
|
+
}
|
|
56
|
+
function prefixErrors(errors, prefix) {
|
|
57
|
+
return errors.map((error) => ({ ...error, path: `${prefix}${error.path}` }));
|
|
58
|
+
}
|
|
59
|
+
// The theme-authored schema JSON is passed through verbatim end to end
|
|
60
|
+
// (see theme-schemas.ts), so an optional allowedBlocks array rides
|
|
61
|
+
// through the whole pipeline for free, exactly like the "title" keyword
|
|
62
|
+
// already does - never a real Ajv keyword, just a plain top-level
|
|
63
|
+
// annotation read directly off the object. Absent/malformed means
|
|
64
|
+
// unrestricted, matching every theme file written before this existed.
|
|
65
|
+
function allowedBlockTypesOf(schema) {
|
|
66
|
+
const value = schema?.allowedBlocks;
|
|
67
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === 'string') ? value : undefined;
|
|
68
|
+
}
|
|
69
|
+
// A theme author's own settings schema must declare a "default" for
|
|
70
|
+
// every property it lists as required, and that default must itself
|
|
71
|
+
// satisfy the property's own sub-schema - a bare "default": "" against
|
|
72
|
+
// a minLength: 1 field would technically have a default but still
|
|
73
|
+
// leave a pre-filled instance invalid, defeating the reason this
|
|
74
|
+
// exists (see theme-schemas.ts, which skips a type failing this check
|
|
75
|
+
// exactly like it already skips a malformed {% schema %} block).
|
|
76
|
+
export function requiredFieldsHaveValidDefaults(schema) {
|
|
77
|
+
const { required, properties } = schema;
|
|
78
|
+
if (!Array.isArray(required)) {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
const propertyMap = (properties && typeof properties === 'object' ? properties : {});
|
|
82
|
+
return required.every((name) => {
|
|
83
|
+
if (typeof name !== 'string') {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
const propertySchema = propertyMap[name];
|
|
87
|
+
if (typeof propertySchema !== 'object' || propertySchema === null || !('default' in propertySchema)) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
// ajv.validate compiles+runs propertySchema in isolation, detached
|
|
91
|
+
// from whatever schema it was pulled out of - a "$ref" pointing at
|
|
92
|
+
// a "$defs" entry declared on the parent (not inside this property
|
|
93
|
+
// sub-schema itself) can't resolve here, and Ajv throws
|
|
94
|
+
// (MissingRefError) rather than returning false. That's a real
|
|
95
|
+
// theme-authoring mistake (this function's whole contract assumes
|
|
96
|
+
// a schema self-contained enough to validate standalone), not a
|
|
97
|
+
// reason to crash the caller - loadTypeSchemas already treats a
|
|
98
|
+
// false return here as "type excluded, boot warning printed", so
|
|
99
|
+
// folding a thrown validation error into that exact same false
|
|
100
|
+
// keeps every possible way a required field's default can be
|
|
101
|
+
// unusable on the one graceful path, instead of one of them being
|
|
102
|
+
// the sole exception that takes the whole server down.
|
|
103
|
+
try {
|
|
104
|
+
return ajv.validate(propertySchema, propertySchema.default) === true;
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
export function validateInstance(instance, kind, themeSchemas) {
|
|
112
|
+
const envelopeResult = runValidator(validateInstanceEnvelope, instance);
|
|
113
|
+
if (!envelopeResult.valid) {
|
|
114
|
+
return envelopeResult;
|
|
115
|
+
}
|
|
116
|
+
const typed = instance;
|
|
117
|
+
const typeSchemas = kind === 'section' ? themeSchemas.sections : themeSchemas.blocks;
|
|
118
|
+
const typeSchema = typeSchemas[typed.type];
|
|
119
|
+
const errors = [];
|
|
120
|
+
if (!typeSchema) {
|
|
121
|
+
errors.push({
|
|
122
|
+
path: '/type',
|
|
123
|
+
message: `Unknown ${kind} type "${typed.type}"`,
|
|
124
|
+
keyword: 'unknownType',
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
const settingsValidate = ajv.compile(typeSchema);
|
|
129
|
+
const settingsResult = runValidator(settingsValidate, typed.settings);
|
|
130
|
+
errors.push(...prefixErrors(settingsResult.errors, '/settings'));
|
|
131
|
+
}
|
|
132
|
+
if (typed.blocks) {
|
|
133
|
+
const allowedBlocks = allowedBlockTypesOf(typeSchema);
|
|
134
|
+
typed.blocks.forEach((block, index) => {
|
|
135
|
+
const blockType = block?.type;
|
|
136
|
+
if (allowedBlocks && typeof blockType === 'string' && !allowedBlocks.includes(blockType)) {
|
|
137
|
+
errors.push({
|
|
138
|
+
path: `/blocks/${index}/type`,
|
|
139
|
+
message: `Block type "${blockType}" is not allowed here`,
|
|
140
|
+
keyword: 'disallowedBlockType',
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
const blockResult = validateInstance(block, 'block', themeSchemas);
|
|
144
|
+
errors.push(...prefixErrors(blockResult.errors, `/blocks/${index}`));
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
return { valid: errors.length === 0, errors };
|
|
148
|
+
}
|
|
149
|
+
function validateSectionedContent(envelopeValidate, content, themeSchemas) {
|
|
150
|
+
const envelopeResult = runValidator(envelopeValidate, content);
|
|
151
|
+
if (!envelopeResult.valid) {
|
|
152
|
+
return envelopeResult;
|
|
153
|
+
}
|
|
154
|
+
const typed = content;
|
|
155
|
+
const errors = [];
|
|
156
|
+
typed.sections.forEach((section, index) => {
|
|
157
|
+
const sectionResult = validateInstance(section, 'section', themeSchemas);
|
|
158
|
+
errors.push(...prefixErrors(sectionResult.errors, `/sections/${index}`));
|
|
159
|
+
});
|
|
160
|
+
return { valid: errors.length === 0, errors };
|
|
161
|
+
}
|
|
162
|
+
export function validatePage(page, themeSchemas) {
|
|
163
|
+
return validateSectionedContent(validatePageEnvelope, page, themeSchemas);
|
|
164
|
+
}
|
|
165
|
+
export function validateMenu(menu) {
|
|
166
|
+
return runValidator(validateMenuEnvelope, menu);
|
|
167
|
+
}
|
|
168
|
+
// Never routed through validateContent's dispatcher below: redirects.json
|
|
169
|
+
// lives at the site root, not under content/, so it never has a
|
|
170
|
+
// content/-relative path for that dispatcher to match against.
|
|
171
|
+
export function validateRedirects(redirects) {
|
|
172
|
+
return runValidator(validateRedirectsEnvelope, redirects);
|
|
173
|
+
}
|
|
174
|
+
// Dispatches on the relative path's prefix, not an in-file `type`
|
|
175
|
+
// field: a menu has no `type` field at all, and the correct schema to
|
|
176
|
+
// parse against must be known before the content is even validated.
|
|
177
|
+
export function validateContent(relativePath, content, themeSchemas) {
|
|
178
|
+
if (relativePath.startsWith('menus/')) {
|
|
179
|
+
return validateMenu(content);
|
|
180
|
+
}
|
|
181
|
+
return validatePage(content, themeSchemas);
|
|
182
|
+
}
|
|
183
|
+
// Site settings (content/settings.json) against the theme's own
|
|
184
|
+
// settings schema. A theme that defines none accepts none.
|
|
185
|
+
export function validateSiteSettings(settings, themeSchemas) {
|
|
186
|
+
if (typeof settings !== 'object' || settings === null || Array.isArray(settings)) {
|
|
187
|
+
return { valid: false, errors: [{ path: '', message: 'must be an object', keyword: 'type' }] };
|
|
188
|
+
}
|
|
189
|
+
if (!themeSchemas.settings) {
|
|
190
|
+
return Object.keys(settings).length === 0
|
|
191
|
+
? { valid: true, errors: [] }
|
|
192
|
+
: { valid: false, errors: [{ path: '', message: 'the theme defines no site settings', keyword: 'noSettingsSchema' }] };
|
|
193
|
+
}
|
|
194
|
+
return runValidator(ajv.compile(themeSchemas.settings), settings);
|
|
195
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function enqueue<T>(job: () => Promise<T>): Promise<T>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// The single in-process write queue: all disk and git mutations go
|
|
2
|
+
// through enqueue(), one job at a time (constraint 6). Ordering only -
|
|
3
|
+
// no domain knowledge of what "clean" means for a failed job. That is
|
|
4
|
+
// each job's own responsibility (see drafts.ts, publish.ts).
|
|
5
|
+
//
|
|
6
|
+
// In-memory only, does not survive restart: the API only reports
|
|
7
|
+
// success after a write (and commit, where applicable) completes, so an
|
|
8
|
+
// in-flight request during a crash simply fails and the caller retries.
|
|
9
|
+
let tail = Promise.resolve();
|
|
10
|
+
export function enqueue(job) {
|
|
11
|
+
const result = tail.then(job, job);
|
|
12
|
+
tail = result.then(() => undefined, () => undefined);
|
|
13
|
+
return result;
|
|
14
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { runSiteCheck } from "./run-check.js";
|
|
4
|
+
// Run from vhost/ (see the scaffold's own "check" script), so the
|
|
5
|
+
// site root is one level up - the exact same relative relationship
|
|
6
|
+
// SERVER_JS in create-site/generate-site.ts already relies on.
|
|
7
|
+
const siteRoot = resolve(process.cwd(), '..');
|
|
8
|
+
const KIND_LABELS = {
|
|
9
|
+
schema: 'Theme schema',
|
|
10
|
+
'render-error': 'Render error',
|
|
11
|
+
'missing-asset': 'Missing asset',
|
|
12
|
+
'broken-link': 'Broken link',
|
|
13
|
+
'misplaced-media': 'Misplaced media',
|
|
14
|
+
};
|
|
15
|
+
function printGrouped(findings) {
|
|
16
|
+
const byKind = new Map();
|
|
17
|
+
for (const finding of findings) {
|
|
18
|
+
const list = byKind.get(finding.kind) ?? [];
|
|
19
|
+
list.push(finding);
|
|
20
|
+
byKind.set(finding.kind, list);
|
|
21
|
+
}
|
|
22
|
+
for (const [kind, list] of byKind) {
|
|
23
|
+
console.log(`\n${KIND_LABELS[kind]} (${list.length}):`);
|
|
24
|
+
for (const finding of list) {
|
|
25
|
+
const location = finding.pageUrl ? ` ${finding.pageUrl}: ` : ' ';
|
|
26
|
+
console.log(`${location}${finding.message}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function printSuggestions(suggestions) {
|
|
31
|
+
if (suggestions.length === 0) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
console.log(`\nSuggestions (${suggestions.length}):`);
|
|
35
|
+
for (const suggestion of suggestions) {
|
|
36
|
+
console.log(` ${suggestion}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const result = await runSiteCheck(siteRoot);
|
|
40
|
+
if (result.ok) {
|
|
41
|
+
console.log('No problems found.');
|
|
42
|
+
printSuggestions(result.suggestions);
|
|
43
|
+
process.exit(0);
|
|
44
|
+
}
|
|
45
|
+
console.log(`${result.findings.length} problem${result.findings.length === 1 ? '' : 's'} found:`);
|
|
46
|
+
printGrouped(result.findings);
|
|
47
|
+
printSuggestions(result.suggestions);
|
|
48
|
+
process.exit(1);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type CheckFindingKind = 'schema' | 'render-error' | 'missing-asset' | 'broken-link' | 'misplaced-media';
|
|
2
|
+
export interface CheckFinding {
|
|
3
|
+
kind: CheckFindingKind;
|
|
4
|
+
message: string;
|
|
5
|
+
pageUrl?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface CheckResult {
|
|
8
|
+
ok: boolean;
|
|
9
|
+
findings: CheckFinding[];
|
|
10
|
+
suggestions: string[];
|
|
11
|
+
}
|
|
12
|
+
export declare function runSiteCheck(siteRoot: string): Promise<CheckResult>;
|