@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,322 @@
1
+ import { pathToFileURL } from 'node:url';
2
+ import { siteConfigLabel, siteConfigPath } from './site-paths.mjs';
3
+
4
+ const { default: siteConfig } = await import(/* @vite-ignore */ pathToFileURL(siteConfigPath).href);
5
+
6
+ const cssLengthPattern = String.raw`(?:\d+|\d*\.\d+)(?:px|rem|em|vw|vh|vmin|vmax|ch|%)`;
7
+ const simpleCssLengthPattern = new RegExp(`^${cssLengthPattern}$`);
8
+ const clampCssLengthPattern = new RegExp(`^clamp\\(\\s*${cssLengthPattern}\\s*,\\s*${cssLengthPattern}\\s*,\\s*${cssLengthPattern}\\s*\\)$`);
9
+
10
+ const assertObject = (value, path) => {
11
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
12
+ throw new Error(`${path} must be an object in ${siteConfigLabel}.`);
13
+ }
14
+
15
+ return value;
16
+ };
17
+
18
+ const readString = (object, key, path) => {
19
+ const value = object[key];
20
+
21
+ if (typeof value !== 'string' || value.trim() === '') {
22
+ throw new Error(`${path}.${key} must be a non-empty string in ${siteConfigLabel}.`);
23
+ }
24
+
25
+ return value.trim();
26
+ };
27
+
28
+ const readOptionalString = (object, key, path) => {
29
+ const value = object[key];
30
+
31
+ if (value === undefined || value === null || value === '') {
32
+ return null;
33
+ }
34
+
35
+ if (typeof value !== 'string' || value.trim() === '') {
36
+ throw new Error(`${path}.${key} must be a non-empty string when set in ${siteConfigLabel}.`);
37
+ }
38
+
39
+ return value.trim();
40
+ };
41
+
42
+ const readFontFamily = (object, key, path, fallback) => {
43
+ const value = object[key] ?? fallback;
44
+
45
+ if (typeof value !== 'string' || value.trim() === '') {
46
+ throw new Error(`${path}.${key} must be a non-empty CSS font-family value in ${siteConfigLabel}.`);
47
+ }
48
+
49
+ const normalizedValue = value.trim();
50
+
51
+ if (/[\n\r;{}]/.test(normalizedValue)) {
52
+ throw new Error(`${path}.${key} must not contain semicolons, braces, or line breaks in ${siteConfigLabel}.`);
53
+ }
54
+
55
+ return normalizedValue;
56
+ };
57
+
58
+ const readCssLength = (object, key, path, fallback) => {
59
+ const value = object[key] ?? fallback;
60
+
61
+ if (typeof value !== 'string' || value.trim() === '') {
62
+ throw new Error(`${path}.${key} must be a non-empty CSS length in ${siteConfigLabel}.`);
63
+ }
64
+
65
+ const normalizedValue = value.trim();
66
+
67
+ if (!simpleCssLengthPattern.test(normalizedValue) || parseFloat(normalizedValue) <= 0) {
68
+ throw new Error(`${path}.${key} must be a CSS length such as "900px", "56rem", or "90%" in ${siteConfigLabel}.`);
69
+ }
70
+
71
+ return normalizedValue;
72
+ };
73
+
74
+ const readCssLengthValue = (value, path) => {
75
+ if (typeof value !== 'string' || value.trim() === '') {
76
+ throw new Error(`${path} must be a non-empty CSS length in ${siteConfigLabel}.`);
77
+ }
78
+
79
+ const normalizedValue = value.trim();
80
+
81
+ if (
82
+ (!simpleCssLengthPattern.test(normalizedValue) && !clampCssLengthPattern.test(normalizedValue))
83
+ || parseFloat(normalizedValue) <= 0
84
+ ) {
85
+ throw new Error(`${path} must be a CSS length such as "48px", "3rem", "4vw", or a clamp() of those lengths in ${siteConfigLabel}.`);
86
+ }
87
+
88
+ return normalizedValue;
89
+ };
90
+
91
+ const readResponsiveCssLength = (object, key, path, fallback) => {
92
+ const value = object[key] ?? fallback;
93
+
94
+ if (typeof value === 'string') {
95
+ const length = readCssLengthValue(value, `${path}.${key}`);
96
+
97
+ return Object.freeze({
98
+ desktop: length,
99
+ mobile: length,
100
+ });
101
+ }
102
+
103
+ const responsiveValue = assertObject(value, `${path}.${key}`);
104
+
105
+ return Object.freeze({
106
+ desktop: readCssLengthValue(responsiveValue.desktop ?? fallback.desktop, `${path}.${key}.desktop`),
107
+ mobile: readCssLengthValue(responsiveValue.mobile ?? fallback.mobile, `${path}.${key}.mobile`),
108
+ });
109
+ };
110
+
111
+ const readPercentValue = (value, path) => {
112
+ if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0 || value > 100) {
113
+ throw new Error(`${path} must be a number greater than 0 and less than or equal to 100 in ${siteConfigLabel}.`);
114
+ }
115
+
116
+ return value;
117
+ };
118
+
119
+ const readResponsivePercent = (object, key, path, fallback) => {
120
+ const value = object[key] ?? fallback;
121
+
122
+ if (typeof value === 'number') {
123
+ const percent = readPercentValue(value, `${path}.${key}`);
124
+
125
+ return Object.freeze({
126
+ desktop: percent,
127
+ mobile: percent,
128
+ });
129
+ }
130
+
131
+ const responsiveValue = assertObject(value, `${path}.${key}`);
132
+
133
+ return Object.freeze({
134
+ desktop: readPercentValue(responsiveValue.desktop ?? fallback.desktop, `${path}.${key}.desktop`),
135
+ mobile: readPercentValue(responsiveValue.mobile ?? fallback.mobile, `${path}.${key}.mobile`),
136
+ });
137
+ };
138
+
139
+ const readBoolean = (object, key, path, fallback) => {
140
+ const value = object[key] ?? fallback;
141
+
142
+ if (typeof value !== 'boolean') {
143
+ throw new Error(`${path}.${key} must be a boolean in ${siteConfigLabel}.`);
144
+ }
145
+
146
+ return value;
147
+ };
148
+
149
+ const readPositiveInteger = (object, key, path, fallback) => {
150
+ const value = object[key] ?? fallback;
151
+
152
+ if (!Number.isInteger(value) || value <= 0) {
153
+ throw new Error(`${path}.${key} must be a positive integer in ${siteConfigLabel}.`);
154
+ }
155
+
156
+ return value;
157
+ };
158
+
159
+ const readPositiveNumber = (object, key, path, fallback) => {
160
+ const value = object[key] ?? fallback;
161
+
162
+ if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
163
+ throw new Error(`${path}.${key} must be a positive number in ${siteConfigLabel}.`);
164
+ }
165
+
166
+ return value;
167
+ };
168
+
169
+ const readUrl = (object, key, path) => {
170
+ const value = readString(object, key, path);
171
+
172
+ try {
173
+ return new URL(value).href;
174
+ } catch {
175
+ throw new Error(`${path}.${key} must be an absolute URL in ${siteConfigLabel}.`);
176
+ }
177
+ };
178
+
179
+ const readSmoothScroll = (navigation) => {
180
+ const rawSmoothScroll = assertObject(navigation.smoothScroll ?? {}, 'navigation.smoothScroll');
181
+ const minimumDurationMs = readPositiveInteger(rawSmoothScroll, 'minimumDurationMs', 'navigation.smoothScroll', 2_000);
182
+ const maximumDurationMs = readPositiveInteger(rawSmoothScroll, 'maximumDurationMs', 'navigation.smoothScroll', 4_000);
183
+
184
+ if (maximumDurationMs < minimumDurationMs) {
185
+ throw new Error(`navigation.smoothScroll.maximumDurationMs must be greater than or equal to minimumDurationMs in ${siteConfigLabel}.`);
186
+ }
187
+
188
+ return Object.freeze({
189
+ durationPerPixelMs: readPositiveNumber(rawSmoothScroll, 'durationPerPixelMs', 'navigation.smoothScroll', 0.22),
190
+ enabled: readBoolean(rawSmoothScroll, 'enabled', 'navigation.smoothScroll', true),
191
+ maximumDurationMs,
192
+ minimumDurationMs,
193
+ });
194
+ };
195
+
196
+ const readLocale = (rawLocale) => {
197
+ const locale = assertObject(rawLocale ?? {}, 'locale');
198
+ const labels = assertObject(locale.labels ?? {}, 'locale.labels');
199
+ const lang = locale.lang ?? 'en';
200
+
201
+ if (typeof lang !== 'string' || !/^[a-zA-Z]{2,3}(?:-[a-zA-Z0-9]+)*$/.test(lang.trim())) {
202
+ throw new Error(`locale.lang must be a valid language tag such as "en" or "sv" in ${siteConfigLabel}.`);
203
+ }
204
+
205
+ return Object.freeze({
206
+ lang: lang.trim(),
207
+ labels: Object.freeze({
208
+ closeMenu: readString({ closeMenu: labels.closeMenu ?? 'Close menu' }, 'closeMenu', 'locale.labels'),
209
+ skipToContent: readString({ skipToContent: labels.skipToContent ?? 'Skip to content' }, 'skipToContent', 'locale.labels'),
210
+ sectionNavigation: readString({ sectionNavigation: labels.sectionNavigation ?? 'Sections' }, 'sectionNavigation', 'locale.labels'),
211
+ gallery: readString({ gallery: labels.gallery ?? 'Gallery' }, 'gallery', 'locale.labels'),
212
+ menu: readString({ menu: labels.menu ?? 'Menu' }, 'menu', 'locale.labels'),
213
+ pageNavigation: readString({ pageNavigation: labels.pageNavigation ?? 'On this page' }, 'pageNavigation', 'locale.labels'),
214
+ siteNavigation: readString({ siteNavigation: labels.siteNavigation ?? 'Pages' }, 'siteNavigation', 'locale.labels'),
215
+ }),
216
+ });
217
+ };
218
+
219
+ const readDateTimeFormat = (object, path) => {
220
+ const dateTimeFormat = assertObject(object, path);
221
+ const locale = readString(dateTimeFormat, 'locale', path);
222
+ const timeZone = readString(dateTimeFormat, 'timeZone', path);
223
+ const dateStyle = readString(dateTimeFormat, 'dateStyle', path);
224
+ const timeStyle = readString(dateTimeFormat, 'timeStyle', path);
225
+
226
+ try {
227
+ new Intl.DateTimeFormat(locale, {
228
+ dateStyle,
229
+ timeStyle,
230
+ timeZone,
231
+ });
232
+ } catch (error) {
233
+ const message = error instanceof Error ? error.message : String(error);
234
+ throw new Error(`${path} must be a valid Intl.DateTimeFormat configuration in ${siteConfigLabel}: ${message}`);
235
+ }
236
+
237
+ return Object.freeze({
238
+ dateStyle,
239
+ locale,
240
+ timeStyle,
241
+ timeZone,
242
+ });
243
+ };
244
+
245
+ const readBuildInfo = (footer) => {
246
+ const value = footer.buildInfo;
247
+
248
+ if (value === undefined || value === null || value === false) {
249
+ return null;
250
+ }
251
+
252
+ const buildInfo = assertObject(value, 'footer.buildInfo');
253
+
254
+ return Object.freeze({
255
+ enabled: readBoolean(buildInfo, 'enabled', 'footer.buildInfo', true),
256
+ text: readString(buildInfo, 'text', 'footer.buildInfo'),
257
+ dateTimeFormat: readDateTimeFormat(buildInfo.dateTimeFormat, 'footer.buildInfo.dateTimeFormat'),
258
+ });
259
+ };
260
+
261
+ const rawConfig = assertObject(siteConfig, 'default export');
262
+ const rawSite = assertObject(rawConfig.site, 'site');
263
+ const rawLayout = assertObject(rawConfig.layout ?? {}, 'layout');
264
+ const rawGallery = assertObject(rawConfig.gallery ?? {}, 'gallery');
265
+ const rawTypography = assertObject(rawConfig.typography ?? {}, 'typography');
266
+ const rawNavigation = assertObject(rawConfig.navigation ?? {}, 'navigation');
267
+ const rawLocale = rawConfig.locale ?? {};
268
+ const rawFooter = assertObject(rawConfig.footer ?? {}, 'footer');
269
+ const rawGithub = assertObject(rawConfig.github, 'github');
270
+ const rawDeploy = assertObject(rawConfig.deploy ?? {}, 'deploy');
271
+ const rawDeployWatch = assertObject(rawDeploy.watch ?? {}, 'deploy.watch');
272
+
273
+ const defaultFontFamily = "Arial, 'Helvetica Neue', Helvetica, sans-serif";
274
+
275
+ export const projectConfig = Object.freeze({
276
+ site: Object.freeze({
277
+ url: readUrl(rawSite, 'url', 'site'),
278
+ }),
279
+ layout: Object.freeze({
280
+ gutter: readResponsiveCssLength(rawLayout, 'gutter', 'layout', Object.freeze({
281
+ desktop: 'clamp(1.25rem, 4vw, 3rem)',
282
+ mobile: '1rem',
283
+ })),
284
+ pageWidth: readCssLength(rawLayout, 'pageWidth', 'layout', '1180px'),
285
+ }),
286
+ gallery: Object.freeze({
287
+ maxAvailableHeightPercent: readResponsivePercent(rawGallery, 'maxAvailableHeightPercent', 'gallery', Object.freeze({
288
+ desktop: 74,
289
+ mobile: 68,
290
+ })),
291
+ maxAvailableWidthPercent: readResponsivePercent(rawGallery, 'maxAvailableWidthPercent', 'gallery', Object.freeze({
292
+ desktop: 100,
293
+ mobile: 100,
294
+ })),
295
+ width: readCssLength(rawGallery, 'width', 'gallery', '900px'),
296
+ }),
297
+ typography: Object.freeze({
298
+ fontFamily: readFontFamily(rawTypography, 'fontFamily', 'typography', defaultFontFamily),
299
+ }),
300
+ navigation: Object.freeze({
301
+ smoothScroll: readSmoothScroll(rawNavigation),
302
+ }),
303
+ locale: readLocale(rawLocale),
304
+ footer: Object.freeze({
305
+ buildInfo: readBuildInfo(rawFooter),
306
+ copyrightMessage: readOptionalString(rawFooter, 'copyrightMessage', 'footer'),
307
+ }),
308
+ github: Object.freeze({
309
+ repo: readString(rawGithub, 'repo', 'github'),
310
+ branch: readString(rawGithub, 'branch', 'github'),
311
+ pagesWorkflow: readString(rawGithub, 'pagesWorkflow', 'github'),
312
+ }),
313
+ deploy: Object.freeze({
314
+ watch: Object.freeze({
315
+ intervalMs: readPositiveInteger(rawDeployWatch, 'intervalMs', 'deploy.watch', 10_000),
316
+ timeoutMs: readPositiveInteger(rawDeployWatch, 'timeoutMs', 'deploy.watch', 15 * 60_000),
317
+ runLimit: readPositiveInteger(rawDeployWatch, 'runLimit', 'deploy.watch', 10),
318
+ }),
319
+ }),
320
+ });
321
+
322
+ export default projectConfig;
@@ -0,0 +1,21 @@
1
+ import { spawn } from 'node:child_process';
2
+
3
+ export const runInherit = (command, args, options = {}) => new Promise((resolve, reject) => {
4
+ const child = spawn(command, args, {
5
+ stdio: 'inherit',
6
+ ...options,
7
+ });
8
+
9
+ child.once('error', reject);
10
+ child.once('exit', (code, signal) => {
11
+ if (code === 0) {
12
+ resolve();
13
+ return;
14
+ }
15
+
16
+ const commandText = [command, ...args].join(' ');
17
+ reject(new Error(signal
18
+ ? `${commandText} exited with signal ${signal}.`
19
+ : `${commandText} exited with code ${code}.`));
20
+ });
21
+ });