@janga/norna 0.7.13 → 0.7.15
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 +15 -9
- package/bin/norna-cli.mjs +11 -4
- package/docs/README.md +71 -43
- package/docs/commands.md +34 -20
- package/docs/configuration.md +43 -62
- package/docs/content.md +34 -7
- package/docs/design/command-organization.md +15 -8
- package/docs/design/norna-diagram-design.md +6 -6
- package/docs/design/site-examples-structure.md +1 -1
- package/docs/engine-development.md +22 -9
- package/docs/getting-started.md +29 -16
- package/docs/how-to/embedded-site.md +1 -1
- package/docs/images-and-metadata.md +3 -3
- package/docs/local-development.md +11 -3
- package/docs/public-files.md +105 -0
- package/docs/publishing.md +4 -8
- package/docs/routes.md +6 -7
- package/docs/site-files.md +144 -0
- package/docs/sitewide-content.md +47 -33
- package/docs/theme.md +37 -49
- package/docs/typography.md +25 -31
- package/examples/complete-sites/dog-shelter-multi-page/README.md +8 -0
- package/examples/complete-sites/{dog-shelter-single-page/site/config.md → dog-shelter-multi-page/site/config.yaml} +0 -2
- package/examples/complete-sites/dog-shelter-multi-page/site/sitewide-content.yaml +6 -0
- package/examples/complete-sites/dog-shelter-multi-page/site/{theme.md → theme.yaml} +0 -2
- package/examples/complete-sites/dog-shelter-single-page/README.md +8 -0
- package/examples/complete-sites/{dog-shelter-multi-page/site/config.md → dog-shelter-single-page/site/config.yaml} +0 -2
- package/examples/complete-sites/dog-shelter-single-page/site/sitewide-content.yaml +6 -0
- package/examples/complete-sites/dog-shelter-single-page/site/{theme.md → theme.yaml} +0 -2
- package/examples/feature-demos/media-and-surfaces/.astro/data-store.json +1 -1
- package/examples/feature-demos/media-and-surfaces/README.md +1 -1
- package/examples/feature-demos/media-and-surfaces/site/{config.md → config.yaml} +0 -2
- package/examples/feature-demos/media-and-surfaces/site/content.md +2 -2
- package/examples/feature-demos/media-and-surfaces/site/routes/020-surfaces/{route-content.md → content.md} +3 -7
- package/examples/feature-demos/media-and-surfaces/site/routes/020-surfaces/theme.yaml +3 -0
- package/examples/feature-demos/media-and-surfaces/site/{sitewide-content.md → sitewide-content.yaml} +1 -3
- package/examples/feature-demos/media-and-surfaces/site/{theme.md → theme.yaml} +0 -2
- package/examples/feature-demos/sitewide-content/README.md +8 -0
- package/examples/feature-demos/sitewide-content/site/{config.md → config.yaml} +0 -2
- package/examples/feature-demos/sitewide-content/site/routes/010-identity/{route-content.md → content.md} +3 -3
- package/examples/feature-demos/sitewide-content/site/routes/020-notices/{route-content.md → content.md} +1 -1
- package/examples/feature-demos/sitewide-content/site/routes/030-footer/{route-content.md → content.md} +1 -1
- package/examples/feature-demos/sitewide-content/site/{sitewide-content.md → sitewide-content.yaml} +1 -11
- package/examples/feature-demos/sitewide-content/site/{theme.md → theme.yaml} +0 -2
- package/examples/feature-demos/theme-presets/site/config.yaml +1 -0
- package/examples/feature-demos/theme-presets/site/routes/010-portfolio/{theme.md → theme.yaml} +0 -2
- package/examples/feature-demos/theme-presets/site/routes/020-documentation/{theme.md → theme.yaml} +0 -2
- package/examples/feature-demos/theme-presets/site/routes/030-project/theme.yaml +1 -0
- package/examples/feature-demos/theme-presets/site/routes/040-statement/{route-content.md → content.md} +1 -1
- package/examples/feature-demos/theme-presets/site/routes/040-statement/{theme.md → theme.yaml} +0 -2
- package/examples/feature-demos/theme-presets/site/{sitewide-content.md → sitewide-content.yaml} +1 -3
- package/examples/feature-demos/theme-presets/site/theme.yaml +1 -0
- package/fixtures/basic/site/{config.md → config.yaml} +0 -2
- package/fixtures/basic/site/content.md +3 -3
- package/fixtures/basic/site/{sitewide-content.md → sitewide-content.yaml} +0 -2
- package/fixtures/basic/site/{theme.md → theme.yaml} +3 -6
- package/fixtures/content-model-v2/site/{config.md → config.yaml} +0 -2
- package/fixtures/content-model-v2/site/theme.yaml +4 -0
- package/package.json +10 -3
- package/schemas/config.schema.json +77 -0
- package/schemas/content-frontmatter.schema.json +81 -0
- package/schemas/manifest.json +9 -0
- package/schemas/sitewide-content.schema.json +215 -0
- package/schemas/theme.schema.json +1208 -0
- package/scripts/check-config.mjs +18 -11
- package/scripts/export-theme-preset.mjs +1 -1
- package/scripts/generate-schemas.mjs +283 -0
- package/scripts/init-site.mjs +22 -27
- package/scripts/lib/documentation-links.mjs +8 -0
- package/scripts/lib/editor-language-service.mjs +609 -0
- package/scripts/lib/example-sites.mjs +2 -2
- package/scripts/lib/git-status.mjs +7 -0
- package/scripts/lib/logo-assets.mjs +15 -10
- package/scripts/lib/norna-markdown-blocks.mjs +90 -11
- package/scripts/lib/presentation.mjs +23 -12
- package/scripts/lib/project-config.mjs +17 -41
- package/scripts/lib/public-asset-conventions.mjs +112 -0
- package/scripts/lib/schema-definitions.mjs +213 -0
- package/scripts/lib/schema-value-definitions.mjs +86 -0
- package/scripts/lib/site-content.mjs +24 -18
- package/scripts/lib/site-paths.mjs +4 -8
- package/scripts/lib/sitewide-content.mjs +24 -0
- package/scripts/lib/theme-config.mjs +15 -35
- package/scripts/lib/theme-presets.mjs +76 -66
- package/scripts/lib/typography.mjs +15 -15
- package/scripts/lib/yaml-config.mjs +50 -0
- package/scripts/list-theme-presets.mjs +15 -0
- package/scripts/release.mjs +84 -8
- package/scripts/show-typography.mjs +22 -46
- package/scripts/sync-content-sections.mjs +2 -2
- package/scripts/test-client-javascript.mjs +28 -28
- package/scripts/test-content-check.mjs +21 -26
- package/scripts/test-content-model-v2.mjs +72 -40
- package/scripts/test-documentation.mjs +70 -4
- package/scripts/test-editor-language-service.mjs +158 -0
- package/scripts/test-engine-commands.mjs +65 -34
- package/scripts/test-package-check.mjs +37 -30
- package/scripts/test-project-config.mjs +15 -20
- package/scripts/test-schemas.mjs +102 -0
- package/scripts/test-temporary-visibility.mjs +5 -13
- package/scripts/test-theme-presets.mjs +64 -37
- package/src/components/CardList.astro +2 -5
- package/src/components/ImageCarousel.astro +21 -21
- package/src/components/{GalleryGrid.astro → ImageStack.astro} +12 -12
- package/src/components/SectionNavigationScript.astro +28 -0
- package/src/components/SiteNavigation.astro +6 -5
- package/src/components/SitePage.astro +8 -8
- package/src/components/SiteSection.astro +18 -21
- package/src/content.config.ts +17 -209
- package/src/layouts/BaseLayout.astro +9 -9
- package/src/lib/{galleryImages.ts → imagePresentation.ts} +7 -7
- package/src/lib/routeThemes.ts +9 -29
- package/src/lib/sectionContent.ts +3 -3
- package/src/lib/sitePublicAssets.ts +5 -21
- package/src/styles/global.css +73 -73
- package/starters/basic/.github/workflows/deploy.yml +1 -1
- package/starters/basic/README.md +16 -13
- package/starters/basic/package.json +3 -2
- package/starters/basic/site/{config.md → config.yaml} +0 -2
- package/starters/basic/site/content.md +1 -1
- package/starters/basic/site/{sitewide-content.md → sitewide-content.yaml} +1 -3
- package/starters/basic/site/{theme.md → theme.yaml} +8 -15
- package/starters/project/.github/workflows/deploy.yml +1 -1
- package/starters/project/README.md +22 -15
- package/starters/project/package.json +3 -2
- package/starters/project/site/{config.md → config.yaml} +0 -2
- package/starters/project/site/{sitewide-content.md → sitewide-content.yaml} +1 -3
- package/starters/project/site/{theme.md → theme.yaml} +4 -8
- package/docs/site-structure.md +0 -98
- package/examples/complete-sites/dog-shelter-multi-page/site/sitewide-content.md +0 -16
- package/examples/complete-sites/dog-shelter-single-page/site/sitewide-content.md +0 -16
- package/examples/feature-demos/media-and-surfaces/site/routes/020-surfaces/theme.md +0 -8
- package/examples/feature-demos/theme-presets/site/config.md +0 -3
- package/examples/feature-demos/theme-presets/site/routes/030-project/theme.md +0 -3
- package/examples/feature-demos/theme-presets/site/theme.md +0 -3
- package/fixtures/content-model-v2/site/theme.md +0 -7
- package/scripts/lib/sitewide-config.mjs +0 -34
- package/src/components/MobileSectionNavigationScript.astro +0 -14
- /package/examples/complete-sites/dog-shelter-multi-page/site/routes/010-dogs/{route-content.md → content.md} +0 -0
- /package/examples/complete-sites/dog-shelter-multi-page/site/routes/020-adopt/{route-content.md → content.md} +0 -0
- /package/examples/feature-demos/media-and-surfaces/site/routes/010-media/{route-content.md → content.md} +0 -0
- /package/examples/feature-demos/theme-presets/site/routes/010-portfolio/{route-content.md → content.md} +0 -0
- /package/examples/feature-demos/theme-presets/site/routes/020-documentation/{route-content.md → content.md} +0 -0
- /package/examples/feature-demos/theme-presets/site/routes/030-project/{route-content.md → content.md} +0 -0
- /package/fixtures/content-model-v2/site/routes/010-guide/{route-content.md → content.md} +0 -0
- /package/starters/project/site/routes/010-guide/{route-content.md → content.md} +0 -0
|
@@ -4,7 +4,8 @@ import { mkdir, mkdtemp, readFile, realpath, rm, writeFile } from 'node:fs/promi
|
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
|
-
import {
|
|
7
|
+
import { getShortGitStatusPath, parseShortGitStatus } from './lib/git-status.mjs';
|
|
8
|
+
import { typographyProfiles, typographyRhythms } from './lib/typography.mjs';
|
|
8
9
|
|
|
9
10
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
10
11
|
const cliPath = path.join(repoRoot, 'bin', 'norna.mjs');
|
|
@@ -18,6 +19,10 @@ const runCli = (args, options = {}) => spawnSync(process.execPath, [cliPath, ...
|
|
|
18
19
|
});
|
|
19
20
|
|
|
20
21
|
try {
|
|
22
|
+
const releaseStatus = parseShortGitStatus(' M package-lock.json\n M schemas/theme.schema.json\n');
|
|
23
|
+
assert.deepEqual(releaseStatus, [' M package-lock.json', ' M schemas/theme.schema.json']);
|
|
24
|
+
assert.equal(getShortGitStatusPath(releaseStatus[0]), 'package-lock.json');
|
|
25
|
+
|
|
21
26
|
const versionResult = runCli(['engine:version']);
|
|
22
27
|
assert.equal(versionResult.status, 0, versionResult.stderr || versionResult.stdout);
|
|
23
28
|
assert.match(versionResult.stdout, /norna engine:version/);
|
|
@@ -35,7 +40,7 @@ try {
|
|
|
35
40
|
|
|
36
41
|
const directSiteRoot = path.join(tempRoot, 'current-directory-site');
|
|
37
42
|
await mkdir(directSiteRoot, { recursive: true });
|
|
38
|
-
await writeFile(path.join(directSiteRoot, 'config.
|
|
43
|
+
await writeFile(path.join(directSiteRoot, 'config.yaml'), 'url: https://example.com/\n');
|
|
39
44
|
await writeFile(path.join(directSiteRoot, 'content.md'), '---\ntitle: Direct Site\ndescription: Direct site fixture.\n---\n\n## Intro {#intro}\n\nText.\n');
|
|
40
45
|
const directSiteProjectRoot = await realpath(tempRoot);
|
|
41
46
|
const directSiteDoctorResult = runCli(['doctor'], {
|
|
@@ -49,29 +54,36 @@ try {
|
|
|
49
54
|
const initResult = runCli(['init', initializedSiteRoot]);
|
|
50
55
|
assert.equal(initResult.status, 0, initResult.stderr || initResult.stdout);
|
|
51
56
|
assert.match(initResult.stdout, /Created norna site at /);
|
|
57
|
+
assert.match(initResult.stdout, /npm run norna:dev/);
|
|
52
58
|
|
|
53
59
|
const initializedPackageJson = JSON.parse(await readFile(path.join(initializedSiteRoot, 'package.json'), 'utf8'));
|
|
54
60
|
assert.equal(initializedPackageJson.dependencies['@janga/norna'], packageJson.version);
|
|
61
|
+
assert.equal(initializedPackageJson.scripts['norna:check'], 'norna check');
|
|
55
62
|
assert.equal(initializedPackageJson.scripts['norna:engine:update'], 'norna engine:update');
|
|
56
63
|
assert.equal(initializedPackageJson.scripts['norna:engine:version'], 'norna engine:version');
|
|
64
|
+
assert.equal(initializedPackageJson.scripts['norna:theme:presets'], 'norna theme:presets');
|
|
57
65
|
assert.equal(initializedPackageJson.scripts['norna:theme:export'], 'norna theme:export');
|
|
58
66
|
assert.equal(initializedPackageJson.scripts['engine:update'], undefined);
|
|
59
67
|
assert.equal(initializedPackageJson.scripts['engine:version'], undefined);
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
assert.
|
|
63
|
-
assert.match(
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
assert.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
const checkResult = runCli(['--site-dir', path.join(initializedSiteRoot, 'site'), 'check']);
|
|
69
|
+
assert.equal(checkResult.status, 0, checkResult.stderr || checkResult.stdout);
|
|
70
|
+
assert.match(checkResult.stdout, /Config check passed\./);
|
|
71
|
+
assert.match(checkResult.stdout, /Content check passed\./);
|
|
72
|
+
|
|
73
|
+
const profilesResult = runCli(['typography', 'profiles']);
|
|
74
|
+
assert.equal(profilesResult.status, 0, profilesResult.stderr || profilesResult.stdout);
|
|
75
|
+
assert.match(profilesResult.stdout, /restrained:/);
|
|
76
|
+
assert.match(profilesResult.stdout, /reading:/);
|
|
77
|
+
assert.match(profilesResult.stdout, /rhythms:/);
|
|
78
|
+
assert.match(profilesResult.stdout, /normal:/);
|
|
79
|
+
for (const [profileName, profile] of Object.entries(typographyProfiles)) {
|
|
80
|
+
for (const [level, heading] of Object.entries(profile.headings)) {
|
|
81
|
+
assert.equal(heading.size, 'medium', `${profileName} ${level} should use medium size`);
|
|
82
|
+
assert.ok([400, 500, 600, 700].includes(heading.weight), `${profileName} ${level} should use a supported weight`);
|
|
71
83
|
}
|
|
72
|
-
assert.equal(
|
|
73
|
-
assert.ok(['narrow', 'normal', 'wide'].includes(
|
|
74
|
-
assert.equal(
|
|
84
|
+
assert.equal(profile.body.size, 'medium', `${profileName} body should use medium size`);
|
|
85
|
+
assert.ok(['narrow', 'normal', 'wide'].includes(profile.body.width), `${profileName} body should use a supported width`);
|
|
86
|
+
assert.equal(profile.caption.size, 'medium', `${profileName} caption should use medium size`);
|
|
75
87
|
}
|
|
76
88
|
for (const [rhythmName, rhythm] of Object.entries(typographyRhythms)) {
|
|
77
89
|
for (const [level, heading] of Object.entries(rhythm.headings)) {
|
|
@@ -87,25 +99,26 @@ try {
|
|
|
87
99
|
assert.match(showResult.stdout, /theme:/);
|
|
88
100
|
assert.match(showResult.stdout, /pages:/);
|
|
89
101
|
assert.match(showResult.stdout, /\s+\/:/);
|
|
90
|
-
assert.match(showResult.stdout, /value:
|
|
91
|
-
assert.match(showResult.stdout, /source: "site\/theme\.
|
|
102
|
+
assert.match(showResult.stdout, /value: restrained/);
|
|
103
|
+
assert.match(showResult.stdout, /source: "site\/theme\.yaml"/);
|
|
92
104
|
assert.match(showResult.stdout, /intro:/);
|
|
93
105
|
|
|
94
106
|
const initAgainResult = runCli(['init', initializedSiteRoot]);
|
|
95
107
|
assert.notEqual(initAgainResult.status, 0);
|
|
96
108
|
assert.match(initAgainResult.stderr, /Target directory must be empty/);
|
|
97
109
|
|
|
98
|
-
const
|
|
99
|
-
const
|
|
100
|
-
assert.equal(
|
|
101
|
-
const
|
|
102
|
-
assert.equal(
|
|
103
|
-
assert.equal(
|
|
104
|
-
assert.equal(
|
|
105
|
-
assert.equal(
|
|
106
|
-
|
|
107
|
-
await readFile(path.join(
|
|
108
|
-
await readFile(path.join(
|
|
110
|
+
const customStandaloneSiteRoot = path.join(tempRoot, 'custom-standalone-site');
|
|
111
|
+
const customStandaloneInitResult = runCli(['init', customStandaloneSiteRoot, '--type', 'standalone', '--site-dir', 'presentation']);
|
|
112
|
+
assert.equal(customStandaloneInitResult.status, 0, customStandaloneInitResult.stderr || customStandaloneInitResult.stdout);
|
|
113
|
+
const customStandalonePackageJson = JSON.parse(await readFile(path.join(customStandaloneSiteRoot, 'package.json'), 'utf8'));
|
|
114
|
+
assert.equal(customStandalonePackageJson.scripts.dev, 'npm run norna:dev --');
|
|
115
|
+
assert.equal(customStandalonePackageJson.scripts.build, 'npm run norna:build');
|
|
116
|
+
assert.equal(customStandalonePackageJson.scripts['norna:dev'], 'norna --site-dir presentation dev:local');
|
|
117
|
+
assert.equal(customStandalonePackageJson.scripts['norna:check'], 'norna --site-dir presentation check');
|
|
118
|
+
assert.equal(customStandalonePackageJson.scripts['norna:build'], 'norna --site-dir presentation build');
|
|
119
|
+
await readFile(path.join(customStandaloneSiteRoot, 'presentation', 'content.md'));
|
|
120
|
+
await readFile(path.join(customStandaloneSiteRoot, 'presentation', 'config.yaml'));
|
|
121
|
+
await readFile(path.join(customStandaloneSiteRoot, 'presentation', 'theme.yaml'));
|
|
109
122
|
|
|
110
123
|
const mixedProjectRoot = path.join(tempRoot, 'mixed-project');
|
|
111
124
|
await mkdir(mixedProjectRoot, { recursive: true });
|
|
@@ -127,11 +140,12 @@ try {
|
|
|
127
140
|
assert.equal(mixedPackageJson.scripts.build, 'node build-app.mjs');
|
|
128
141
|
assert.equal(mixedPackageJson.scripts.dev, undefined);
|
|
129
142
|
assert.equal(mixedPackageJson.scripts['norna:dev'], 'norna --site-dir presentation dev:local');
|
|
143
|
+
assert.equal(mixedPackageJson.scripts['norna:check'], 'norna --site-dir presentation check');
|
|
130
144
|
assert.equal(mixedPackageJson.scripts['norna:engine:update'], 'norna --site-dir presentation engine:update');
|
|
131
145
|
assert.equal(mixedPackageJson.scripts['norna:engine:version'], 'norna --site-dir presentation engine:version');
|
|
132
146
|
await readFile(path.join(mixedProjectRoot, 'presentation', 'content.md'));
|
|
133
|
-
await readFile(path.join(mixedProjectRoot, 'presentation', 'config.
|
|
134
|
-
await readFile(path.join(mixedProjectRoot, 'presentation', 'theme.
|
|
147
|
+
await readFile(path.join(mixedProjectRoot, 'presentation', 'config.yaml'));
|
|
148
|
+
await readFile(path.join(mixedProjectRoot, 'presentation', 'theme.yaml'));
|
|
135
149
|
|
|
136
150
|
const conflictProjectRoot = path.join(tempRoot, 'conflict-project');
|
|
137
151
|
await mkdir(conflictProjectRoot, { recursive: true });
|
|
@@ -149,11 +163,11 @@ try {
|
|
|
149
163
|
assert.match(conflictInitResult.stderr, /Refusing to overwrite existing npm scripts/);
|
|
150
164
|
assert.match(conflictInitResult.stderr, /norna:dev/);
|
|
151
165
|
|
|
152
|
-
const
|
|
166
|
+
const standaloneIntoExistingProjectResult = runCli(['init', '.', '--type', 'standalone'], {
|
|
153
167
|
cwd: mixedProjectRoot,
|
|
154
168
|
});
|
|
155
|
-
assert.notEqual(
|
|
156
|
-
assert.match(
|
|
169
|
+
assert.notEqual(standaloneIntoExistingProjectResult.status, 0);
|
|
170
|
+
assert.match(standaloneIntoExistingProjectResult.stderr, /use --type embedded/);
|
|
157
171
|
|
|
158
172
|
const updateFromEngineResult = runCli(['engine:update', '--skip-checks']);
|
|
159
173
|
assert.notEqual(updateFromEngineResult.status, 0);
|
|
@@ -165,6 +179,23 @@ try {
|
|
|
165
179
|
});
|
|
166
180
|
assert.equal(releaseHelpResult.status, 0, releaseHelpResult.stderr || releaseHelpResult.stdout);
|
|
167
181
|
assert.match(releaseHelpResult.stdout, /Usage: node scripts\/release\.mjs/);
|
|
182
|
+
const releaseSource = await readFile(path.join(repoRoot, 'scripts', 'release.mjs'), 'utf8');
|
|
183
|
+
const releaseSteps = [
|
|
184
|
+
"['version', releaseType, '--no-git-tag-version']",
|
|
185
|
+
"['run', 'schemas:generate']",
|
|
186
|
+
"['test']",
|
|
187
|
+
"['commit', '-m', `Release ${releaseTag}`]",
|
|
188
|
+
"['run', 'release:publish']",
|
|
189
|
+
"['push', '--follow-tags']",
|
|
190
|
+
];
|
|
191
|
+
for (const step of releaseSteps) assert.ok(releaseSource.includes(step), `Missing release step: ${step}`);
|
|
192
|
+
for (let index = 1; index < releaseSteps.length; index += 1) {
|
|
193
|
+
assert.ok(
|
|
194
|
+
releaseSource.indexOf(releaseSteps[index - 1]) < releaseSource.indexOf(releaseSteps[index]),
|
|
195
|
+
`Release step is out of order: ${releaseSteps[index]}`,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
assert.match(releaseSource, /rollbackPreparedRelease/);
|
|
168
199
|
|
|
169
200
|
console.log('ok - engine commands report versions, initialize sites, guard engine self-updates, and document release usage');
|
|
170
201
|
} finally {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
1
2
|
import { cp, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
2
3
|
import { spawn } from 'node:child_process';
|
|
3
4
|
import { tmpdir } from 'node:os';
|
|
@@ -155,40 +156,49 @@ try {
|
|
|
155
156
|
assertFileExists(path.join(unpackDir, 'package', 'docs', 'README.md')),
|
|
156
157
|
assertFileExists(path.join(unpackDir, 'package', 'docs', 'configuration.md')),
|
|
157
158
|
assertFileExists(path.join(unpackDir, 'package', 'docs', 'images-and-metadata.md')),
|
|
159
|
+
assertFileExists(path.join(unpackDir, 'package', 'schemas', 'manifest.json')),
|
|
160
|
+
assertFileExists(path.join(unpackDir, 'package', 'schemas', 'config.schema.json')),
|
|
161
|
+
assertFileExists(path.join(unpackDir, 'package', 'schemas', 'content-frontmatter.schema.json')),
|
|
162
|
+
assertFileExists(path.join(unpackDir, 'package', 'schemas', 'sitewide-content.schema.json')),
|
|
163
|
+
assertFileExists(path.join(unpackDir, 'package', 'schemas', 'theme.schema.json')),
|
|
158
164
|
assertFileExists(path.join(packagedStarterRoot, '.github', 'workflows', 'deploy.yml')),
|
|
159
165
|
assertFileExists(path.join(packagedStarterRoot, 'package.json')),
|
|
160
166
|
assertFileExists(path.join(packagedStarterRoot, 'README.md')),
|
|
161
|
-
assertFileExists(path.join(packagedStarterRoot, 'site', 'config.
|
|
162
|
-
assertFileExists(path.join(packagedStarterRoot, 'site', 'theme.
|
|
163
|
-
assertFileExists(path.join(packagedStarterRoot, 'site', 'sitewide-content.
|
|
167
|
+
assertFileExists(path.join(packagedStarterRoot, 'site', 'config.yaml')),
|
|
168
|
+
assertFileExists(path.join(packagedStarterRoot, 'site', 'theme.yaml')),
|
|
169
|
+
assertFileExists(path.join(packagedStarterRoot, 'site', 'sitewide-content.yaml')),
|
|
164
170
|
assertFileExists(path.join(packagedStarterRoot, 'site', 'content.md')),
|
|
165
171
|
assertFileExists(path.join(packagedStarterRoot, 'site', 'images', 'work', '.gitkeep')),
|
|
166
172
|
assertFileExists(path.join(packagedStarterRoot, 'site', 'public', 'robots.txt')),
|
|
167
|
-
assertFileMissing(path.join(packagedStarterRoot, 'site', 'config.mjs')),
|
|
168
173
|
]);
|
|
169
174
|
await assertFileIncludes(
|
|
170
175
|
path.join(packagedStarterRoot, '.github', 'workflows', 'deploy.yml'),
|
|
171
176
|
'node-version: 24.18.0',
|
|
172
177
|
);
|
|
178
|
+
await assertFileIncludes(
|
|
179
|
+
path.join(packagedStarterRoot, '.github', 'workflows', 'deploy.yml'),
|
|
180
|
+
'run: npm run norna:build',
|
|
181
|
+
);
|
|
173
182
|
await cp(packagedStarterRoot, siteProjectRoot, {
|
|
174
183
|
recursive: true,
|
|
175
184
|
});
|
|
176
185
|
const packageJsonPath = path.join(siteProjectRoot, 'package.json');
|
|
177
186
|
const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8'));
|
|
187
|
+
assert.equal(packageJson.scripts['norna:check'], 'norna check');
|
|
178
188
|
packageJson.name = 'norna-package-check-site';
|
|
179
189
|
packageJson.dependencies['@janga/norna'] = tarballPath;
|
|
180
190
|
await writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`);
|
|
181
|
-
const packageCheckConfigPath = path.join(siteProjectRoot, 'site', 'config.
|
|
191
|
+
const packageCheckConfigPath = path.join(siteProjectRoot, 'site', 'config.yaml');
|
|
182
192
|
const packageCheckConfig = await readFile(packageCheckConfigPath, 'utf8');
|
|
183
193
|
await writeFile(
|
|
184
194
|
packageCheckConfigPath,
|
|
185
195
|
packageCheckConfig.replace('url: https://example.com/', 'url: https://example.com/site/'),
|
|
186
196
|
);
|
|
187
|
-
const packageCheckSitewidePath = path.join(siteProjectRoot, 'site', 'sitewide-content.
|
|
197
|
+
const packageCheckSitewidePath = path.join(siteProjectRoot, 'site', 'sitewide-content.yaml');
|
|
188
198
|
const packageCheckSitewide = await readFile(packageCheckSitewidePath, 'utf8');
|
|
189
199
|
await writeFile(
|
|
190
200
|
packageCheckSitewidePath,
|
|
191
|
-
packageCheckSitewide.replace('
|
|
201
|
+
packageCheckSitewide.replace('label: Example Site', 'label: Package Check Logo\n logo:\n height: 2.6rem'),
|
|
192
202
|
);
|
|
193
203
|
await writeFile(
|
|
194
204
|
path.join(siteProjectRoot, 'site', 'public', 'logo.svg'),
|
|
@@ -221,7 +231,7 @@ This section verifies styled section headings.
|
|
|
221
231
|
This section keeps the package check independent from user-facing starter copy.
|
|
222
232
|
`);
|
|
223
233
|
await mkdir(path.join(siteProjectRoot, 'site', 'routes', '010-about'), { recursive: true });
|
|
224
|
-
await writeFile(path.join(siteProjectRoot, 'site', 'routes', '010-about', '
|
|
234
|
+
await writeFile(path.join(siteProjectRoot, 'site', 'routes', '010-about', 'content.md'), `---
|
|
225
235
|
title: About the site
|
|
226
236
|
description: Route used by package checks.
|
|
227
237
|
navigation:
|
|
@@ -240,16 +250,16 @@ This route verifies that packaged norna sites can build route pages.
|
|
|
240
250
|
await runInherit(nornaBinPath, ['init', initializedSiteRoot], { cwd: tempRoot, env: npmEnv });
|
|
241
251
|
await Promise.all([
|
|
242
252
|
assertFileExists(path.join(initializedSiteRoot, 'package.json')),
|
|
243
|
-
assertFileExists(path.join(initializedSiteRoot, 'site', 'config.
|
|
244
|
-
assertFileExists(path.join(initializedSiteRoot, 'site', 'theme.
|
|
253
|
+
assertFileExists(path.join(initializedSiteRoot, 'site', 'config.yaml')),
|
|
254
|
+
assertFileExists(path.join(initializedSiteRoot, 'site', 'theme.yaml')),
|
|
245
255
|
assertFileMissing(path.join(initializedSiteRoot, '.DS_Store')),
|
|
246
256
|
assertFileMissing(path.join(initializedSiteRoot, 'site', '.DS_Store')),
|
|
247
|
-
assertFileMissing(path.join(initializedSiteRoot, 'site', 'config.mjs')),
|
|
248
257
|
]);
|
|
249
258
|
await runInherit(npxBin, ['norna', 'engine:version'], { cwd: path.join(siteProjectRoot, 'site', 'images', 'work'), env: npmEnv });
|
|
250
259
|
await runInherit(npxBin, ['norna', 'doctor'], { cwd: path.join(siteProjectRoot, 'site', 'images', 'work'), env: npmEnv });
|
|
251
260
|
await runInherit(npxBin, ['norna', 'config:check'], { cwd: path.join(siteProjectRoot, 'site', 'images', 'work'), env: npmEnv });
|
|
252
261
|
await runInherit(npxBin, ['norna', 'content:check'], { cwd: siteProjectRoot, env: npmEnv });
|
|
262
|
+
await runInherit(npxBin, ['norna', 'check'], { cwd: siteProjectRoot, env: npmEnv });
|
|
253
263
|
await runInherit(npxBin, ['norna', 'build'], { cwd: siteProjectRoot, env: npmEnv });
|
|
254
264
|
await assertFileExists(path.join(siteProjectRoot, 'site', '.norna', 'public', 'robots.txt'));
|
|
255
265
|
await assertFileExists(path.join(siteProjectRoot, 'dist', 'robots.txt'));
|
|
@@ -404,47 +414,44 @@ This route verifies that packaged norna sites can build route pages.
|
|
|
404
414
|
);
|
|
405
415
|
const siteContentPath = path.join(siteProjectRoot, 'site', 'content.md');
|
|
406
416
|
const siteContent = await readFile(siteContentPath, 'utf8');
|
|
407
|
-
const siteThemePath = path.join(siteProjectRoot, 'site', 'theme.
|
|
417
|
+
const siteThemePath = path.join(siteProjectRoot, 'site', 'theme.yaml');
|
|
408
418
|
const siteTheme = await readFile(siteThemePath, 'utf8');
|
|
409
|
-
await writeFile(siteThemePath, siteTheme.replace('\n
|
|
419
|
+
await writeFile(siteThemePath, siteTheme.replace('\n profile: restrained', '\n profile: noisy'));
|
|
410
420
|
await runExpectFailure(
|
|
411
421
|
npxBin,
|
|
412
422
|
['norna', 'build'],
|
|
413
423
|
{ cwd: siteProjectRoot, env: npmEnv },
|
|
414
|
-
'typography
|
|
424
|
+
'Unknown typography profile: noisy',
|
|
415
425
|
);
|
|
416
426
|
await writeFile(
|
|
417
427
|
siteThemePath,
|
|
418
|
-
siteTheme.replace('\
|
|
428
|
+
siteTheme.replace('\npalette: dark', '\npalette: neon'),
|
|
419
429
|
);
|
|
420
430
|
await runExpectFailure(
|
|
421
431
|
npxBin,
|
|
422
432
|
['norna', 'build'],
|
|
423
433
|
{ cwd: siteProjectRoot, env: npmEnv },
|
|
424
|
-
'
|
|
434
|
+
'palette',
|
|
425
435
|
);
|
|
426
436
|
await writeFile(
|
|
427
437
|
siteThemePath,
|
|
428
|
-
siteTheme.replace('
|
|
438
|
+
siteTheme.replace('\nsectionSurfaces: [base, soft, emphasis]', '\nsectionSurfaces: [base, glowing]'),
|
|
429
439
|
);
|
|
430
440
|
await runExpectFailure(
|
|
431
441
|
npxBin,
|
|
432
442
|
['norna', 'build'],
|
|
433
443
|
{ cwd: siteProjectRoot, env: npmEnv },
|
|
434
|
-
'
|
|
444
|
+
'Unknown section surface "glowing"',
|
|
435
445
|
);
|
|
436
446
|
await writeFile(
|
|
437
447
|
siteThemePath,
|
|
438
|
-
siteTheme.replace(
|
|
439
|
-
' sectionSurfaces:\n mode: cycle',
|
|
440
|
-
' sectionSurfaces:\n mode: cycle\n sequence: [base, base]',
|
|
441
|
-
),
|
|
448
|
+
siteTheme.replace('\nsectionSurfaces: [base, soft, emphasis]', '\nsectionSurfaces: [base, base]'),
|
|
442
449
|
);
|
|
443
450
|
await runExpectFailure(
|
|
444
451
|
npxBin,
|
|
445
452
|
['norna', 'build'],
|
|
446
453
|
{ cwd: siteProjectRoot, env: npmEnv },
|
|
447
|
-
'
|
|
454
|
+
'Each section surface may appear only once',
|
|
448
455
|
);
|
|
449
456
|
await writeFile(siteThemePath, siteTheme);
|
|
450
457
|
await writeFile(
|
|
@@ -465,40 +472,40 @@ This route verifies that packaged norna sites can build route pages.
|
|
|
465
472
|
siteContentPath,
|
|
466
473
|
siteContent.replace(
|
|
467
474
|
'description: Site used by package checks.',
|
|
468
|
-
'description: Site used by package checks.\
|
|
475
|
+
'description: Site used by package checks.\ntypography:\n overrides:\n body:\n lineHeight: tight',
|
|
469
476
|
),
|
|
470
477
|
);
|
|
471
478
|
await runExpectFailure(
|
|
472
479
|
npxBin,
|
|
473
480
|
['norna', 'build'],
|
|
474
481
|
{ cwd: siteProjectRoot, env: npmEnv },
|
|
475
|
-
'
|
|
482
|
+
'typography',
|
|
476
483
|
);
|
|
477
484
|
await writeFile(
|
|
478
485
|
siteContentPath,
|
|
479
486
|
siteContent.replace(
|
|
480
487
|
' intro: {}',
|
|
481
|
-
' intro:\n
|
|
488
|
+
' intro:\n typography:\n overrides:\n caption:\n spacingBefore: wide',
|
|
482
489
|
),
|
|
483
490
|
);
|
|
484
491
|
await runExpectFailure(
|
|
485
492
|
npxBin,
|
|
486
493
|
['norna', 'build'],
|
|
487
494
|
{ cwd: siteProjectRoot, env: npmEnv },
|
|
488
|
-
'sections.intro: Unrecognized key: "
|
|
495
|
+
'sections.intro: Unrecognized key: "typography"',
|
|
489
496
|
);
|
|
490
497
|
await writeFile(
|
|
491
498
|
siteContentPath,
|
|
492
499
|
siteContent.replace(
|
|
493
500
|
' intro: {}',
|
|
494
|
-
' intro:\n
|
|
501
|
+
' intro:\n palette: dark',
|
|
495
502
|
),
|
|
496
503
|
);
|
|
497
504
|
await runExpectFailure(
|
|
498
505
|
npxBin,
|
|
499
506
|
['norna', 'build'],
|
|
500
507
|
{ cwd: siteProjectRoot, env: npmEnv },
|
|
501
|
-
'sections.intro: Unrecognized key: "
|
|
508
|
+
'sections.intro: Unrecognized key: "palette"',
|
|
502
509
|
);
|
|
503
510
|
await writeFile(
|
|
504
511
|
siteThemePath,
|
|
@@ -514,7 +521,7 @@ This route verifies that packaged norna sites can build route pages.
|
|
|
514
521
|
'typography.fontFamily',
|
|
515
522
|
);
|
|
516
523
|
await writeFile(siteThemePath, siteTheme);
|
|
517
|
-
const siteConfigPath = path.join(siteProjectRoot, 'site', 'config.
|
|
524
|
+
const siteConfigPath = path.join(siteProjectRoot, 'site', 'config.yaml');
|
|
518
525
|
const siteConfig = await readFile(siteConfigPath, 'utf8');
|
|
519
526
|
await writeFile(
|
|
520
527
|
siteConfigPath,
|
|
@@ -19,13 +19,13 @@ const importScript = `
|
|
|
19
19
|
}));
|
|
20
20
|
`;
|
|
21
21
|
|
|
22
|
-
const createSite = async (name, config
|
|
22
|
+
const createSite = async (name, config) => {
|
|
23
23
|
const projectRoot = path.join(tempRoot, name);
|
|
24
24
|
const siteDir = path.join(projectRoot, 'site');
|
|
25
25
|
await mkdir(siteDir, { recursive: true });
|
|
26
|
-
await writeFile(path.join(siteDir,
|
|
26
|
+
await writeFile(path.join(siteDir, 'config.yaml'), config);
|
|
27
27
|
await writeFile(path.join(siteDir, 'content.md'), '---\ntitle: Config test\ndescription: Config test.\n---\n\n## Intro {#intro}\n\nText.\n');
|
|
28
|
-
await writeFile(path.join(siteDir, 'theme.
|
|
28
|
+
await writeFile(path.join(siteDir, 'theme.yaml'), 'preset: documentation\n');
|
|
29
29
|
return { projectRoot, siteDir };
|
|
30
30
|
};
|
|
31
31
|
|
|
@@ -50,7 +50,7 @@ const assertFailure = (result, expectedPattern) => {
|
|
|
50
50
|
};
|
|
51
51
|
|
|
52
52
|
try {
|
|
53
|
-
const minimalSite = await createSite('minimal', '
|
|
53
|
+
const minimalSite = await createSite('minimal', 'url: https://example.com/docs\n');
|
|
54
54
|
const minimalResult = loadConfig(minimalSite);
|
|
55
55
|
assert.equal(minimalResult.status, 0, minimalResult.stderr);
|
|
56
56
|
assert.deepEqual(JSON.parse(minimalResult.stdout), {
|
|
@@ -58,7 +58,7 @@ try {
|
|
|
58
58
|
language: 'en',
|
|
59
59
|
labels: {
|
|
60
60
|
dismissBanner: 'Dismiss notice',
|
|
61
|
-
|
|
61
|
+
images: 'Images',
|
|
62
62
|
note: 'Note',
|
|
63
63
|
pageNavigation: 'On this page',
|
|
64
64
|
siteBanners: 'Site notices',
|
|
@@ -69,13 +69,13 @@ try {
|
|
|
69
69
|
url: 'https://example.com/docs/',
|
|
70
70
|
});
|
|
71
71
|
|
|
72
|
-
const localizedSite = await createSite('localized', '
|
|
72
|
+
const localizedSite = await createSite('localized', 'url: https://example.com/\nlanguage: sv-SE\nscrollBehavior: smooth\n');
|
|
73
73
|
const localizedResult = loadConfig(localizedSite);
|
|
74
74
|
assert.equal(localizedResult.status, 0, localizedResult.stderr);
|
|
75
75
|
const localizedConfig = JSON.parse(localizedResult.stdout);
|
|
76
76
|
assert.equal(localizedConfig.language, 'sv-SE');
|
|
77
77
|
assert.equal(localizedConfig.labels.skipToContent, 'Hoppa till innehållet');
|
|
78
|
-
assert.equal(localizedConfig.scrollBehavior, '
|
|
78
|
+
assert.equal(localizedConfig.scrollBehavior, 'smooth');
|
|
79
79
|
|
|
80
80
|
const overrideResult = loadConfig(minimalSite, {
|
|
81
81
|
NORNA_SITE_URL: 'http://127.0.0.1:4567/preview',
|
|
@@ -91,34 +91,29 @@ try {
|
|
|
91
91
|
);
|
|
92
92
|
|
|
93
93
|
assertFailure(
|
|
94
|
-
loadConfig(await createSite('unknown-field', '
|
|
94
|
+
loadConfig(await createSite('unknown-field', 'url: https://example.com/\nbasePath: /docs/\n')),
|
|
95
95
|
/not a valid top-level config field/,
|
|
96
96
|
);
|
|
97
97
|
assertFailure(
|
|
98
|
-
loadConfig(await createSite('
|
|
99
|
-
/
|
|
98
|
+
loadConfig(await createSite('invalid-yaml', 'url: https://example.com/\nConfiguration prose.\n')),
|
|
99
|
+
/contains invalid YAML/,
|
|
100
100
|
);
|
|
101
101
|
assertFailure(
|
|
102
|
-
loadConfig(await createSite('unsupported-language', '
|
|
102
|
+
loadConfig(await createSite('unsupported-language', 'url: https://example.com/\nlanguage: de\n')),
|
|
103
103
|
/has no built-in Norna UI text/,
|
|
104
104
|
);
|
|
105
105
|
assertFailure(
|
|
106
|
-
loadConfig(await createSite('invalid-url', '
|
|
106
|
+
loadConfig(await createSite('invalid-url', 'url: example.com\n')),
|
|
107
107
|
/url must be an absolute URL/,
|
|
108
108
|
);
|
|
109
109
|
assertFailure(
|
|
110
|
-
loadConfig(await createSite('invalid-scroll-behavior', '
|
|
111
|
-
/scrollBehavior must be one of instant,
|
|
110
|
+
loadConfig(await createSite('invalid-scroll-behavior', 'url: https://example.com/\nscrollBehavior: slow\n')),
|
|
111
|
+
/scrollBehavior must be one of instant, smooth/,
|
|
112
112
|
);
|
|
113
113
|
assertFailure(
|
|
114
|
-
loadConfig(await createSite('obsolete-smooth-scroll', '
|
|
114
|
+
loadConfig(await createSite('obsolete-smooth-scroll', 'url: https://example.com/\nsmoothScroll: true\n')),
|
|
115
115
|
/not a valid top-level config field/,
|
|
116
116
|
);
|
|
117
|
-
assertFailure(
|
|
118
|
-
loadConfig(await createSite('legacy', 'export default {};\n', { legacy: true })),
|
|
119
|
-
/Replace the obsolete config\.mjs with a frontmatter-only config\.md file/,
|
|
120
|
-
);
|
|
121
|
-
|
|
122
117
|
console.log('Project config test passed.');
|
|
123
118
|
} finally {
|
|
124
119
|
await rm(tempRoot, { force: true, recursive: true });
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
8
|
+
const packageJson = JSON.parse(await readFile(path.join(root, 'package.json'), 'utf8'));
|
|
9
|
+
const expectedDocumentationRef = `v${packageJson.version}`;
|
|
10
|
+
const filenames = [
|
|
11
|
+
'config.schema.json',
|
|
12
|
+
'theme.schema.json',
|
|
13
|
+
'sitewide-content.schema.json',
|
|
14
|
+
'content-frontmatter.schema.json',
|
|
15
|
+
];
|
|
16
|
+
const requiredRichHelp = {
|
|
17
|
+
'config.schema.json': ['url', 'language', 'scrollBehavior'],
|
|
18
|
+
'theme.schema.json': ['preset', 'layout', 'images', 'typography', 'palette', 'sectionSurfaces'],
|
|
19
|
+
'sitewide-content.schema.json': ['navigation', 'banners', 'footer'],
|
|
20
|
+
'content-frontmatter.schema.json': ['title', 'description', 'navigation', 'sections'],
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const githubHeadingAnchor = (heading) => heading
|
|
24
|
+
.toLowerCase()
|
|
25
|
+
.replace(/<[^>]+>/g, '')
|
|
26
|
+
.replace(/[`*_~]/g, '')
|
|
27
|
+
.replace(/[^\p{L}\p{N}\s-]/gu, '')
|
|
28
|
+
.trim()
|
|
29
|
+
.replace(/\s+/g, '-');
|
|
30
|
+
|
|
31
|
+
const assertDocumentationLinks = async (markdownDescription, location) => {
|
|
32
|
+
const links = [...markdownDescription.matchAll(
|
|
33
|
+
/https:\/\/github\.com\/janga\/norna\/blob\/([^/\s]+)\/docs\/([^\s)#]+)(?:#([^\s)]+))?/g,
|
|
34
|
+
)];
|
|
35
|
+
assert.ok(links.length > 0, `${location} has no documentation link.`);
|
|
36
|
+
|
|
37
|
+
for (const [, reference, filename, anchor] of links) {
|
|
38
|
+
assert.equal(reference, expectedDocumentationRef, `${location} uses documentation ref ${reference}.`);
|
|
39
|
+
const documentationPath = path.join(root, 'docs', decodeURIComponent(filename));
|
|
40
|
+
assert.ok(existsSync(documentationPath), `${location} links to missing docs/${filename}.`);
|
|
41
|
+
if (!anchor) continue;
|
|
42
|
+
|
|
43
|
+
const source = await readFile(documentationPath, 'utf8');
|
|
44
|
+
const anchors = source
|
|
45
|
+
.split('\n')
|
|
46
|
+
.filter((line) => /^#{1,6}\s/.test(line))
|
|
47
|
+
.map((line) => githubHeadingAnchor(line.replace(/^#{1,6}\s+/, '')));
|
|
48
|
+
assert.ok(anchors.includes(anchor), `${location} links to missing anchor docs/${filename}#${anchor}.`);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const visitSchema = (schema, location) => {
|
|
53
|
+
assert.equal(schema.enum, undefined, `${location} uses an undescribed enum.`);
|
|
54
|
+
if (schema.properties) {
|
|
55
|
+
for (const [name, property] of Object.entries(schema.properties)) {
|
|
56
|
+
assert.ok(property.description, `${location}.${name} has no description.`);
|
|
57
|
+
visitSchema(property, `${location}.${name}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (schema.items) visitSchema(schema.items, `${location}[]`);
|
|
61
|
+
for (const keyword of ['allOf', 'anyOf', 'oneOf']) {
|
|
62
|
+
for (const [index, candidate] of (schema[keyword] ?? []).entries()) {
|
|
63
|
+
if (Object.hasOwn(candidate, 'const')) {
|
|
64
|
+
assert.ok(candidate.title, `${location}.${keyword}[${index}] has no title.`);
|
|
65
|
+
assert.ok(candidate.description, `${location}.${keyword}[${index}] has no description.`);
|
|
66
|
+
}
|
|
67
|
+
visitSchema(candidate, `${location}.${keyword}[${index}]`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
for (const filename of filenames) {
|
|
73
|
+
const schema = JSON.parse(await readFile(path.join(root, 'schemas', filename), 'utf8'));
|
|
74
|
+
assert.ok(schema.description, `${filename} has no root description.`);
|
|
75
|
+
assert.match(schema.markdownDescription, /^```yaml\n/, `${filename} rich help must start with YAML syntax.`);
|
|
76
|
+
await assertDocumentationLinks(schema.markdownDescription, filename);
|
|
77
|
+
for (const propertyName of requiredRichHelp[filename]) {
|
|
78
|
+
const property = schema.properties[propertyName];
|
|
79
|
+
assert.match(
|
|
80
|
+
property.markdownDescription,
|
|
81
|
+
/^```yaml\n/,
|
|
82
|
+
`${filename}.${propertyName} rich help must start with YAML syntax.`,
|
|
83
|
+
);
|
|
84
|
+
await assertDocumentationLinks(property.markdownDescription, `${filename}.${propertyName}`);
|
|
85
|
+
}
|
|
86
|
+
visitSchema(schema, filename);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const config = JSON.parse(await readFile(path.join(root, 'schemas', 'config.schema.json'), 'utf8'));
|
|
90
|
+
assert.equal(config.properties.language.default, 'en');
|
|
91
|
+
assert.deepEqual(config.properties.language.examples, ['en', 'sv', 'en-GB', 'sv-SE']);
|
|
92
|
+
assert.equal(config.properties.scrollBehavior.default, 'instant');
|
|
93
|
+
|
|
94
|
+
const sitewide = JSON.parse(await readFile(path.join(root, 'schemas', 'sitewide-content.schema.json'), 'utf8'));
|
|
95
|
+
assert.deepEqual(Object.keys(sitewide.properties.navigation.properties), ['label', 'logo']);
|
|
96
|
+
assert.deepEqual(Object.keys(sitewide.properties.navigation.properties.logo.properties), ['height']);
|
|
97
|
+
assert.match(sitewide.properties.navigation.properties.logo.markdownDescription, /```yaml\nnavigation:\n label: Example Site\n logo:\n height: 2rem\n```/);
|
|
98
|
+
assert.match(sitewide.properties.navigation.properties.logo.markdownDescription, /does not enable or select the file/);
|
|
99
|
+
assert.match(sitewide.properties.navigation.properties.logo.markdownDescription, /docs\/public-files\.md#navigation-logo/);
|
|
100
|
+
assert.equal(sitewide.properties.navigation.properties.logo.description.length < 100, true);
|
|
101
|
+
|
|
102
|
+
console.log('Schema metadata tests passed.');
|
|
@@ -37,19 +37,12 @@ try {
|
|
|
37
37
|
await mkdir(path.join(siteDir, 'images', 'work'), { recursive: true });
|
|
38
38
|
await mkdir(path.join(siteDir, 'public'), { recursive: true });
|
|
39
39
|
await writeFile(path.join(siteDir, 'public', 'robots.txt'), 'User-agent: *\nAllow: /\n');
|
|
40
|
-
await writeFile(path.join(siteDir, 'config.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
await writeFile(path.join(siteDir, 'theme.md'), `---
|
|
45
|
-
typography:
|
|
46
|
-
preset: quiet-gallery
|
|
47
|
-
presentation:
|
|
48
|
-
palette: dark
|
|
49
|
-
---
|
|
40
|
+
await writeFile(path.join(siteDir, 'config.yaml'), 'url: https://example.com/\n');
|
|
41
|
+
await writeFile(path.join(siteDir, 'theme.yaml'), `typography:
|
|
42
|
+
profile: restrained
|
|
43
|
+
palette: dark
|
|
50
44
|
`);
|
|
51
|
-
await writeFile(path.join(siteDir, 'sitewide-content.
|
|
52
|
-
banners:
|
|
45
|
+
await writeFile(path.join(siteDir, 'sitewide-content.yaml'), `banners:
|
|
53
46
|
- id: expired-banner
|
|
54
47
|
visible:
|
|
55
48
|
until: "2026-01-01"
|
|
@@ -61,7 +54,6 @@ banners:
|
|
|
61
54
|
until: "2026-12-31"
|
|
62
55
|
title: Active banner
|
|
63
56
|
text: Active banner text.
|
|
64
|
-
---
|
|
65
57
|
`);
|
|
66
58
|
await writeFile(path.join(siteDir, 'content.md'), `---
|
|
67
59
|
title: Temporary Visibility Test
|