@janga/norna 0.7.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.
Files changed (77) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +109 -0
  3. package/astro.config.mjs +17 -0
  4. package/bin/norna.mjs +170 -0
  5. package/docs/README.md +48 -0
  6. package/docs/command-organization.md +402 -0
  7. package/docs/commands.md +152 -0
  8. package/docs/configuration.md +376 -0
  9. package/docs/content.md +384 -0
  10. package/docs/engine-development.md +164 -0
  11. package/docs/getting-started.md +96 -0
  12. package/docs/images-and-metadata.md +88 -0
  13. package/docs/local-development.md +61 -0
  14. package/docs/publishing.md +81 -0
  15. package/docs/site-examples-structure-note.md +105 -0
  16. package/docs/site-structure.md +81 -0
  17. package/fixtures/basic/site/.norna/generated-images.json +1 -0
  18. package/fixtures/basic/site/config.mjs +59 -0
  19. package/fixtures/basic/site/content.md +17 -0
  20. package/fixtures/basic/site/images/work/.gitkeep +1 -0
  21. package/fixtures/basic/site/public/robots.txt +2 -0
  22. package/fixtures/basic/site/theme.md +7 -0
  23. package/package.json +90 -0
  24. package/scripts/build-site.mjs +16 -0
  25. package/scripts/check-config.mjs +37 -0
  26. package/scripts/deploy-site.mjs +389 -0
  27. package/scripts/dev-local.mjs +313 -0
  28. package/scripts/doctor.mjs +38 -0
  29. package/scripts/engine-version.mjs +137 -0
  30. package/scripts/generate-images.mjs +369 -0
  31. package/scripts/init-site.mjs +249 -0
  32. package/scripts/lib/astro-command.mjs +34 -0
  33. package/scripts/lib/ci-lockfile.mjs +34 -0
  34. package/scripts/lib/image-dimensions.mjs +78 -0
  35. package/scripts/lib/presentation.mjs +72 -0
  36. package/scripts/lib/project-config.mjs +322 -0
  37. package/scripts/lib/run-command.mjs +21 -0
  38. package/scripts/lib/site-content.mjs +392 -0
  39. package/scripts/lib/site-paths.mjs +127 -0
  40. package/scripts/lib/typography.mjs +166 -0
  41. package/scripts/release.mjs +77 -0
  42. package/scripts/show-typography.mjs +210 -0
  43. package/scripts/sync-content-sections.mjs +610 -0
  44. package/scripts/sync-site-public.mjs +42 -0
  45. package/scripts/test-ci-lockfile.mjs +65 -0
  46. package/scripts/test-content-check.mjs +364 -0
  47. package/scripts/test-engine-commands.mjs +128 -0
  48. package/scripts/test-navigation-preview.mjs +108 -0
  49. package/scripts/test-navigation.mjs +116 -0
  50. package/scripts/test-package-check.mjs +394 -0
  51. package/scripts/test-site-public.mjs +85 -0
  52. package/scripts/test-temporary-visibility.mjs +99 -0
  53. package/scripts/update-engine.mjs +127 -0
  54. package/scripts/watch-pages-deploy.mjs +430 -0
  55. package/src/components/GalleryGrid.astro +221 -0
  56. package/src/components/SiteNavigation.astro +410 -0
  57. package/src/components/SitePage.astro +69 -0
  58. package/src/components/SiteSection.astro +174 -0
  59. package/src/content.config.ts +171 -0
  60. package/src/layouts/BaseLayout.astro +90 -0
  61. package/src/lib/generatedImages.ts +63 -0
  62. package/src/lib/sectionContent.ts +125 -0
  63. package/src/lib/sitePages.ts +80 -0
  64. package/src/lib/sitePublicAssets.ts +39 -0
  65. package/src/lib/visibility.ts +35 -0
  66. package/src/pages/[slug].astro +31 -0
  67. package/src/pages/index.astro +16 -0
  68. package/src/styles/global.css +872 -0
  69. package/starters/basic/.github/workflows/deploy.yml +65 -0
  70. package/starters/basic/README.md +55 -0
  71. package/starters/basic/package.json +35 -0
  72. package/starters/basic/site/config.mjs +60 -0
  73. package/starters/basic/site/content.md +21 -0
  74. package/starters/basic/site/images/work/.gitkeep +1 -0
  75. package/starters/basic/site/public/robots.txt +2 -0
  76. package/starters/basic/site/theme.md +53 -0
  77. package/tsconfig.json +5 -0
@@ -0,0 +1,77 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { promisify } from 'node:util';
5
+ import { execFile } from 'node:child_process';
6
+ import { runInherit } from './lib/run-command.mjs';
7
+
8
+ const execFileAsync = promisify(execFile);
9
+ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
10
+ const npmBin = process.platform === 'win32' ? 'npm.cmd' : 'npm';
11
+ const npmRegistry = 'https://registry.npmjs.org/';
12
+ const npmCachePath = '/private/tmp/norna-npm-cache';
13
+ const releaseTypes = new Set(['patch', 'minor', 'major']);
14
+ const releaseArguments = process.argv.slice(2);
15
+ const [releaseType] = releaseArguments;
16
+ const showHelp = releaseArguments.includes('--help') || releaseArguments.includes('-h');
17
+
18
+ const printUsage = () => {
19
+ console.log('Usage: node scripts/release.mjs <patch|minor|major>');
20
+ console.log('Checks npm authentication, runs npm test, creates the version commit and tag, publishes to npm, then pushes the commit and tag.');
21
+ };
22
+
23
+ const run = (command, args) => runInherit(command, args, { cwd: repoRoot });
24
+
25
+ const readPackageVersion = async () => {
26
+ const packageJson = JSON.parse(await readFile(path.join(repoRoot, 'package.json'), 'utf8'));
27
+ return packageJson.version;
28
+ };
29
+
30
+ const assertCleanWorktree = async (stage) => {
31
+ const { stdout } = await execFileAsync('git', ['status', '--short'], { cwd: repoRoot });
32
+
33
+ if (stdout.trim()) {
34
+ throw new Error(`Working tree must be clean ${stage}. Commit, stash, or discard the listed changes first.\n${stdout.trim()}`);
35
+ }
36
+ };
37
+
38
+ const assertNpmAuthenticated = async () => {
39
+ try {
40
+ const { stdout } = await execFileAsync(npmBin, [
41
+ 'whoami',
42
+ `--registry=${npmRegistry}`,
43
+ '--cache',
44
+ npmCachePath,
45
+ ], { cwd: repoRoot });
46
+ console.log(`npm registry authentication: ${stdout.trim()}`);
47
+ } catch {
48
+ throw new Error([
49
+ 'Cannot publish because npm is not authenticated for the registry/cache used by release:publish.',
50
+ 'Run this command, complete the browser login, then start the release again:',
51
+ `npm login --registry=${npmRegistry} --auth-type=web --cache ${npmCachePath}`,
52
+ ].join('\n'));
53
+ }
54
+ };
55
+
56
+ if (showHelp || !releaseType) {
57
+ printUsage();
58
+ process.exitCode = showHelp ? 0 : 1;
59
+ } else if (!releaseTypes.has(releaseType)) {
60
+ printUsage();
61
+ throw new Error(`Unsupported release type: ${releaseType}`);
62
+ } else {
63
+ const previousVersion = await readPackageVersion();
64
+
65
+ await assertCleanWorktree('before the release checks');
66
+ await assertNpmAuthenticated();
67
+ console.log(`Preparing a ${releaseType} release from ${previousVersion}.`);
68
+ await run(npmBin, ['test']);
69
+ await assertCleanWorktree('after the release checks');
70
+ await run(npmBin, ['version', releaseType, '--message', 'Release %s']);
71
+
72
+ const publishedVersion = await readPackageVersion();
73
+ await run(npmBin, ['run', 'release:publish']);
74
+ await run('git', ['push', '--follow-tags']);
75
+
76
+ console.log(`Released @janga/norna@${publishedVersion}.`);
77
+ }
@@ -0,0 +1,210 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import {
3
+ defaultTypography,
4
+ resolveTypographyOverride,
5
+ resolveTypographyConfig,
6
+ toYamlLines,
7
+ typographyPresets,
8
+ } from './lib/typography.mjs';
9
+ import {
10
+ splitSiteFile,
11
+ validateContentFrontmatterStructure,
12
+ validateFrontmatterIndentation,
13
+ validateThemeFrontmatterStructure,
14
+ } from './lib/site-content.mjs';
15
+ import {
16
+ siteContentLabel,
17
+ siteContentPath,
18
+ siteThemeLabel,
19
+ siteThemePath,
20
+ } from './lib/site-paths.mjs';
21
+
22
+ const mode = process.argv[2] ?? 'show';
23
+
24
+ const countIndent = (line) => line.match(/^\s*/)?.[0].length ?? 0;
25
+
26
+ const parseScalar = (rawValue) => {
27
+ const value = rawValue.trim();
28
+
29
+ if (/^-?\d+(?:\.\d+)?$/.test(value)) return Number(value);
30
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
31
+ return value.slice(1, -1);
32
+ }
33
+
34
+ return value;
35
+ };
36
+
37
+ const parseMapping = (lines, startIndex, baseIndent) => {
38
+ const value = {};
39
+ let index = startIndex;
40
+
41
+ while (index < lines.length) {
42
+ const line = lines[index];
43
+ if (!line.trim() || line.trim().startsWith('#')) {
44
+ index += 1;
45
+ continue;
46
+ }
47
+
48
+ const indent = countIndent(line);
49
+ if (indent <= baseIndent) break;
50
+ if (line.trim().startsWith('- ')) break;
51
+
52
+ const match = line.trim().match(/^([a-zA-Z][a-zA-Z0-9-]*):(?:\s+(.*))?$/);
53
+ if (!match) break;
54
+
55
+ const [, key, rawValue] = match;
56
+ if (rawValue === undefined) {
57
+ const parsed = parseMapping(lines, index + 1, indent);
58
+ value[key] = parsed.value;
59
+ index = parsed.nextIndex;
60
+ } else {
61
+ value[key] = parseScalar(rawValue);
62
+ index += 1;
63
+ }
64
+ }
65
+
66
+ return { value, nextIndex: index };
67
+ };
68
+
69
+ const findMap = (lines, label, parentStart = 0, parentEnd = lines.length, requiredIndent = null) => {
70
+ for (let index = parentStart; index < parentEnd; index += 1) {
71
+ const line = lines[index];
72
+ const match = line.match(/^(\s*)([a-zA-Z][a-zA-Z0-9-]*):\s*$/);
73
+ if (!match || match[2] !== label) continue;
74
+ if (requiredIndent !== null && match[1].length !== requiredIndent) continue;
75
+
76
+ return {
77
+ index,
78
+ indent: match[1].length,
79
+ ...parseMapping(lines, index + 1, match[1].length),
80
+ };
81
+ }
82
+
83
+ return null;
84
+ };
85
+
86
+ const getSectionBlocks = (lines) => {
87
+ const sectionsMap = findMap(lines, 'sections');
88
+ if (!sectionsMap) return [];
89
+
90
+ const sections = [];
91
+ let current = null;
92
+
93
+ for (let index = sectionsMap.index + 1; index < lines.length; index += 1) {
94
+ const line = lines[index];
95
+ if (!line.trim()) continue;
96
+
97
+ const indent = countIndent(line);
98
+ if (indent <= sectionsMap.indent) break;
99
+
100
+ const sectionMatch = line.match(/^\s{2}-\s+id:\s*([a-z0-9-]+)\s*$/);
101
+ if (sectionMatch) {
102
+ if (current) current.end = index;
103
+ current = { id: sectionMatch[1], start: index, end: lines.length };
104
+ sections.push(current);
105
+ }
106
+ }
107
+
108
+ const finalSection = sections.at(-1);
109
+ if (finalSection) {
110
+ const afterSections = lines.findIndex((line, index) => (
111
+ index > finalSection.start &&
112
+ line.trim() &&
113
+ countIndent(line) <= sectionsMap.indent
114
+ ));
115
+ finalSection.end = afterSections === -1 ? lines.length : afterSections;
116
+ }
117
+
118
+ return sections;
119
+ };
120
+
121
+ const readSiteTypography = async () => {
122
+ const { frontmatter, frontmatterBody } = splitSiteFile(await readFile(siteContentPath, 'utf8'));
123
+ const themeFile = await readFile(siteThemePath, 'utf8').catch((error) => {
124
+ if (error?.code === 'ENOENT') {
125
+ return '---\n---\n';
126
+ }
127
+
128
+ throw error;
129
+ });
130
+ const { frontmatter: themeFrontmatter, frontmatterBody: themeFrontmatterBody } = splitSiteFile(themeFile, siteThemeLabel);
131
+ const indentationIssues = [];
132
+ validateFrontmatterIndentation(frontmatter, (issue) => indentationIssues.push(issue));
133
+ validateContentFrontmatterStructure(frontmatter, (issue) => indentationIssues.push(issue));
134
+ validateFrontmatterIndentation(themeFrontmatter, (issue) => indentationIssues.push(issue));
135
+ validateThemeFrontmatterStructure(themeFrontmatter, (issue) => indentationIssues.push(issue));
136
+ if (indentationIssues.length > 0) {
137
+ throw new Error([
138
+ `Cannot inspect typography because ${siteContentLabel} or ${siteThemeLabel} has invalid frontmatter.`,
139
+ ...indentationIssues.map((issue) => `- ${issue.message}`),
140
+ ].join('\n'));
141
+ }
142
+
143
+ const lines = frontmatterBody.split(/\r?\n/);
144
+ const themeLines = themeFrontmatterBody.split(/\r?\n/);
145
+ const themePresentation = findMap(themeLines, 'presentation', 0, themeLines.length, 0);
146
+ const themeTypographyConfig = themePresentation
147
+ ? findMap(themeLines, 'typography', themePresentation.index + 1, themePresentation.nextIndex)?.value
148
+ : null;
149
+ const pagePresentation = findMap(lines, 'presentation', 0, lines.length, 0);
150
+ const pageTypographyConfig = pagePresentation
151
+ ? findMap(lines, 'typography', pagePresentation.index + 1, pagePresentation.nextIndex)?.value
152
+ : null;
153
+ const themeTypography = resolveTypographyConfig(themeTypographyConfig ?? defaultTypography);
154
+ const pageTypography = resolveTypographyOverride(themeTypography, pageTypographyConfig ?? undefined);
155
+ const sections = getSectionBlocks(lines).map((section) => {
156
+ const presentation = findMap(lines, 'presentation', section.start + 1, section.end);
157
+ const typography = presentation
158
+ ? findMap(lines, 'typography', presentation.index + 1, presentation.nextIndex)?.value
159
+ : null;
160
+
161
+ return {
162
+ id: section.id,
163
+ typography,
164
+ resolved: resolveTypographyOverride(pageTypography, typography ?? undefined),
165
+ };
166
+ });
167
+
168
+ return {
169
+ themeTypography,
170
+ pageTypography,
171
+ sections,
172
+ };
173
+ };
174
+
175
+ if (mode === 'presets') {
176
+ console.log(toYamlLines(typographyPresets).join('\n'));
177
+ } else if (mode === 'show') {
178
+ const siteTypography = await readSiteTypography();
179
+ const output = {
180
+ source: siteContentLabel,
181
+ theme: {
182
+ source: siteThemeLabel,
183
+ presentation: {
184
+ typography: {
185
+ preset: siteTypography.themeTypography.preset,
186
+ resolved: siteTypography.themeTypography.values,
187
+ },
188
+ },
189
+ },
190
+ page: {
191
+ typography: {
192
+ preset: siteTypography.pageTypography.preset,
193
+ resolved: siteTypography.pageTypography.values,
194
+ },
195
+ },
196
+ sections: Object.fromEntries(siteTypography.sections.map((section) => [
197
+ section.id,
198
+ {
199
+ typography: {
200
+ preset: section.resolved.preset,
201
+ resolved: section.resolved.values,
202
+ },
203
+ },
204
+ ])),
205
+ };
206
+
207
+ console.log(toYamlLines(output).join('\n'));
208
+ } else {
209
+ throw new Error('Usage: norna typography:presets|typography:show');
210
+ }