@janga/norna 0.7.1 → 0.7.3

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 (168) hide show
  1. package/README.md +32 -18
  2. package/astro.config.mjs +2 -0
  3. package/bin/norna-cli.mjs +175 -0
  4. package/bin/norna.mjs +149 -150
  5. package/docs/README.md +41 -22
  6. package/docs/commands.md +25 -12
  7. package/docs/configuration.md +38 -135
  8. package/docs/content.md +108 -265
  9. package/docs/{command-organization.md → design/command-organization.md} +64 -60
  10. package/docs/{site-examples-structure-note.md → design/site-examples-structure.md} +19 -24
  11. package/docs/engine-development.md +35 -7
  12. package/docs/getting-started.md +55 -26
  13. package/docs/images-and-metadata.md +1 -1
  14. package/docs/local-development.md +14 -1
  15. package/docs/publishing.md +24 -1
  16. package/docs/routes.md +90 -0
  17. package/docs/site-structure.md +18 -10
  18. package/docs/theme.md +292 -0
  19. package/docs/typography.md +144 -0
  20. package/examples/dog-gallery/.astro/collections/site.schema.json +802 -0
  21. package/examples/dog-gallery/.astro/collections/theme.schema.json +714 -0
  22. package/examples/dog-gallery/.astro/content-assets.mjs +4 -0
  23. package/examples/dog-gallery/.astro/content-modules.mjs +1 -0
  24. package/examples/dog-gallery/.astro/content.d.ts +175 -0
  25. package/examples/dog-gallery/.astro/data-store.json +1 -0
  26. package/examples/dog-gallery/.astro/dev-local.json +8 -0
  27. package/examples/dog-gallery/.astro/dev.json +13 -0
  28. package/examples/dog-gallery/.astro/dev.log +49 -0
  29. package/examples/dog-gallery/.astro/settings.json +5 -0
  30. package/examples/dog-gallery/.astro/types.d.ts +2 -0
  31. package/examples/dog-gallery/dist/_astro/GalleryGrid.astro_astro_type_script_index_0_lang.B0kxDyGz.js +1 -0
  32. package/examples/dog-gallery/dist/_astro/SitePage.BFHZm5UG.css +1 -0
  33. package/examples/dog-gallery/dist/dog-care/index.html +15 -0
  34. package/examples/dog-gallery/dist/favicon.svg +7 -0
  35. package/examples/dog-gallery/dist/images/generated/black-dogs/black-puppy-meadow-7255e3d9-1080.webp +0 -0
  36. package/examples/dog-gallery/dist/images/generated/black-dogs/black-puppy-meadow-7255e3d9-1440.webp +0 -0
  37. package/examples/dog-gallery/dist/images/generated/black-dogs/black-puppy-meadow-7255e3d9-1536.webp +0 -0
  38. package/examples/dog-gallery/dist/images/generated/black-dogs/black-puppy-meadow-7255e3d9-480.webp +0 -0
  39. package/examples/dog-gallery/dist/images/generated/black-dogs/black-puppy-meadow-7255e3d9-768.webp +0 -0
  40. package/examples/dog-gallery/dist/images/generated/black-dogs/photo-of-a-black-dog-5cf614d2-480.webp +0 -0
  41. package/examples/dog-gallery/dist/images/generated/black-dogs/photo-of-a-black-dog-5cf614d2-768.webp +0 -0
  42. package/examples/dog-gallery/dist/images/generated/black-dogs/photo-of-a-black-dog-5cf614d2-960.webp +0 -0
  43. package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-1080.webp +0 -0
  44. package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-1440.webp +0 -0
  45. package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-1920.webp +0 -0
  46. package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-3939.webp +0 -0
  47. package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-480.webp +0 -0
  48. package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-768.webp +0 -0
  49. package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-1080.webp +0 -0
  50. package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-1440.webp +0 -0
  51. package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-1920.webp +0 -0
  52. package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-2400.webp +0 -0
  53. package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-480.webp +0 -0
  54. package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-768.webp +0 -0
  55. package/examples/dog-gallery/dist/images/generated/golden-dogs/golden-retriever-f797fd88-1080.webp +0 -0
  56. package/examples/dog-gallery/dist/images/generated/golden-dogs/golden-retriever-f797fd88-1440.webp +0 -0
  57. package/examples/dog-gallery/dist/images/generated/golden-dogs/golden-retriever-f797fd88-1600.webp +0 -0
  58. package/examples/dog-gallery/dist/images/generated/golden-dogs/golden-retriever-f797fd88-480.webp +0 -0
  59. package/examples/dog-gallery/dist/images/generated/golden-dogs/golden-retriever-f797fd88-768.webp +0 -0
  60. package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-1080.webp +0 -0
  61. package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-1440.webp +0 -0
  62. package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-1920.webp +0 -0
  63. package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-2576.webp +0 -0
  64. package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-480.webp +0 -0
  65. package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-768.webp +0 -0
  66. package/examples/dog-gallery/dist/images/generated/white-dogs/white-cute-dog-077a0689-1080.webp +0 -0
  67. package/examples/dog-gallery/dist/images/generated/white-dogs/white-cute-dog-077a0689-1277.webp +0 -0
  68. package/examples/dog-gallery/dist/images/generated/white-dogs/white-cute-dog-077a0689-480.webp +0 -0
  69. package/examples/dog-gallery/dist/images/generated/white-dogs/white-cute-dog-077a0689-768.webp +0 -0
  70. package/examples/dog-gallery/dist/images/generated/white-dogs/white-puppy-garden-d3ab2ca0-1080.webp +0 -0
  71. package/examples/dog-gallery/dist/images/generated/white-dogs/white-puppy-garden-d3ab2ca0-1440.webp +0 -0
  72. package/examples/dog-gallery/dist/images/generated/white-dogs/white-puppy-garden-d3ab2ca0-1536.webp +0 -0
  73. package/examples/dog-gallery/dist/images/generated/white-dogs/white-puppy-garden-d3ab2ca0-480.webp +0 -0
  74. package/examples/dog-gallery/dist/images/generated/white-dogs/white-puppy-garden-d3ab2ca0-768.webp +0 -0
  75. package/examples/dog-gallery/dist/index.html +43 -0
  76. package/examples/dog-gallery/dist/robots.txt +2 -0
  77. package/examples/dog-gallery/node_modules/.vite/deps/_metadata.json +74 -0
  78. package/examples/dog-gallery/node_modules/.vite/deps/astro-D_GIpJIE.js +434 -0
  79. package/examples/dog-gallery/node_modules/.vite/deps/astro-D_GIpJIE.js.map +1 -0
  80. package/examples/dog-gallery/node_modules/.vite/deps/astro_n_aria-query.js +6390 -0
  81. package/examples/dog-gallery/node_modules/.vite/deps/astro_n_aria-query.js.map +1 -0
  82. package/examples/dog-gallery/node_modules/.vite/deps/astro_n_axobject-query.js +2702 -0
  83. package/examples/dog-gallery/node_modules/.vite/deps/astro_n_axobject-query.js.map +1 -0
  84. package/examples/dog-gallery/node_modules/.vite/deps/astro_n_html-escaper.js +65 -0
  85. package/examples/dog-gallery/node_modules/.vite/deps/astro_n_html-escaper.js.map +1 -0
  86. package/examples/dog-gallery/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +455 -0
  87. package/examples/dog-gallery/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +1 -0
  88. package/examples/dog-gallery/node_modules/.vite/deps/audit-DM9Hkl5B.js +1534 -0
  89. package/examples/dog-gallery/node_modules/.vite/deps/audit-DM9Hkl5B.js.map +1 -0
  90. package/examples/dog-gallery/node_modules/.vite/deps/highlight-DnZ1ULOL.js +48 -0
  91. package/examples/dog-gallery/node_modules/.vite/deps/highlight-DnZ1ULOL.js.map +1 -0
  92. package/examples/dog-gallery/node_modules/.vite/deps/icons-RyebKi45.js +42 -0
  93. package/examples/dog-gallery/node_modules/.vite/deps/icons-RyebKi45.js.map +1 -0
  94. package/examples/dog-gallery/node_modules/.vite/deps/package.json +3 -0
  95. package/examples/dog-gallery/node_modules/.vite/deps/rolldown-runtime-BvCyGRYZ.js +4 -0
  96. package/examples/dog-gallery/node_modules/.vite/deps/settings-DaU-gNnK.js +190 -0
  97. package/examples/dog-gallery/node_modules/.vite/deps/settings-DaU-gNnK.js.map +1 -0
  98. package/examples/dog-gallery/node_modules/.vite/deps/toolbar-63PA619H.js +458 -0
  99. package/examples/dog-gallery/node_modules/.vite/deps/toolbar-63PA619H.js.map +1 -0
  100. package/examples/dog-gallery/node_modules/.vite/deps/ui-library--bkcNSYz.js +1016 -0
  101. package/examples/dog-gallery/node_modules/.vite/deps/ui-library--bkcNSYz.js.map +1 -0
  102. package/examples/dog-gallery/node_modules/.vite/deps/window-jPkpKVcy.js +32 -0
  103. package/examples/dog-gallery/node_modules/.vite/deps/window-jPkpKVcy.js.map +1 -0
  104. package/examples/dog-gallery/node_modules/.vite/deps/xray-BXCHpEJP.js +111 -0
  105. package/examples/dog-gallery/node_modules/.vite/deps/xray-BXCHpEJP.js.map +1 -0
  106. package/examples/dog-gallery/site/.norna/generated-images.json +226 -0
  107. package/examples/dog-gallery/site/config.mjs +67 -0
  108. package/examples/dog-gallery/site/content.md +168 -0
  109. package/examples/dog-gallery/site/images/black-dogs/black-puppy-meadow.png +0 -0
  110. package/examples/dog-gallery/site/images/black-dogs/photo-of-a-black-dog.jpg +0 -0
  111. package/examples/dog-gallery/site/images/brown-dogs/brown-dog.jpg +0 -0
  112. package/examples/dog-gallery/site/images/brown-dogs/dog-accompanies-master.jpg +0 -0
  113. package/examples/dog-gallery/site/images/golden-dogs/golden-retriever.jpg +0 -0
  114. package/examples/dog-gallery/site/images/golden-dogs/toller-puppy.jpg +0 -0
  115. package/examples/dog-gallery/site/images/white-dogs/white-cute-dog.jpg +0 -0
  116. package/examples/dog-gallery/site/images/white-dogs/white-puppy-garden.png +0 -0
  117. package/examples/dog-gallery/site/public/favicon.svg +7 -0
  118. package/examples/dog-gallery/site/public/robots.txt +2 -0
  119. package/examples/dog-gallery/site/routes/dog-care/route-content.md +49 -0
  120. package/examples/dog-gallery/site/theme.md +114 -0
  121. package/fixtures/basic/site/config.mjs +3 -22
  122. package/fixtures/basic/site/content.md +1 -1
  123. package/fixtures/basic/site/theme.md +7 -3
  124. package/package.json +10 -9
  125. package/scripts/check-config.mjs +4 -3
  126. package/scripts/deploy-site.mjs +3 -3
  127. package/scripts/dev-local.mjs +64 -15
  128. package/scripts/init-site.mjs +11 -9
  129. package/scripts/lib/frontmatter-yaml.mjs +85 -0
  130. package/scripts/lib/presentation.mjs +10 -11
  131. package/scripts/lib/project-config.mjs +113 -45
  132. package/scripts/lib/site-content.mjs +21 -2
  133. package/scripts/lib/site-paths.mjs +18 -3
  134. package/scripts/lib/theme-config.mjs +34 -0
  135. package/scripts/lib/typography.mjs +103 -23
  136. package/scripts/show-typography.mjs +202 -75
  137. package/scripts/sync-content-sections.mjs +6 -6
  138. package/scripts/test-cli-discovery.mjs +124 -0
  139. package/scripts/test-content-check.mjs +3 -4
  140. package/scripts/test-engine-commands.mjs +20 -6
  141. package/scripts/test-navigation.mjs +10 -6
  142. package/scripts/test-package-check.mjs +40 -11
  143. package/scripts/test-temporary-visibility.mjs +2 -3
  144. package/src/components/SiteNavigation.astro +7 -5
  145. package/src/components/SitePage.astro +5 -3
  146. package/src/components/SiteSection.astro +63 -29
  147. package/src/content.config.ts +64 -2
  148. package/src/layouts/BaseLayout.astro +19 -1
  149. package/src/lib/basePath.ts +21 -0
  150. package/src/lib/generatedImages.ts +8 -4
  151. package/src/lib/sectionContent.ts +6 -1
  152. package/src/lib/sitePublicAssets.ts +8 -1
  153. package/src/styles/global.css +59 -55
  154. package/starters/basic/.github/workflows/deploy.yml +3 -3
  155. package/starters/basic/README.md +30 -14
  156. package/starters/basic/package.json +3 -3
  157. package/starters/basic/site/config.mjs +4 -24
  158. package/starters/basic/site/content.md +44 -9
  159. package/starters/basic/site/theme.md +113 -16
  160. package/starters/project/.github/workflows/deploy.yml +65 -0
  161. package/starters/project/README.md +72 -0
  162. package/starters/project/package-lock.json +4296 -0
  163. package/starters/project/package.json +35 -0
  164. package/starters/project/site/config.mjs +43 -0
  165. package/starters/project/site/content.md +88 -0
  166. package/starters/project/site/public/robots.txt +2 -0
  167. package/starters/project/site/routes/guide/route-content.md +58 -0
  168. package/starters/project/site/theme.md +15 -0
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # norna
2
2
 
3
- `norna` is a reusable command-line toolchain for small static gallery
4
- sites. It provides the CLI, Astro renderer, validation scripts, image pipeline,
3
+ `norna` is a reusable command-line toolchain for small static websites.
4
+ It provides the CLI, Astro renderer, validation scripts, image pipeline,
5
5
  starter project, fixtures, and deploy helpers used by site repositories such as
6
6
  `www.walde.se`.
7
7
 
@@ -21,10 +21,11 @@ belong in each site repository. For example, Karin Walde's site lives in
21
21
  A `norna` site is file-driven:
22
22
 
23
23
  1. A site repository depends on this package.
24
- 2. The site keeps technical settings, including page width, gallery width, and
25
- the global font family, in `site/config.mjs`.
26
- 3. The site can keep site-wide visual theme defaults in `site/theme.md`.
27
- 4. The site keeps homepage content, section order, gallery rows, alt text, and
24
+ 2. The site keeps technical settings, including public URL and optional URL
25
+ base path, in `site/config.mjs`.
26
+ 3. The site keeps site-wide visual theme defaults, including layout, image
27
+ sizing, font, typography, colors, and frame colors, in `site/theme.md`.
28
+ 4. The site keeps homepage content, section order, image rows, alt text, and
28
29
  captions in `site/content.md`.
29
30
  5. Optional route pages live under
30
31
  `site/routes/<route-folder>/route-content.md`.
@@ -35,7 +36,10 @@ A `norna` site is file-driven:
35
36
  Astro pages, and can help publish the committed branch through GitHub Pages.
36
37
 
37
38
  The default site directory is `site/`. Commands can also use another directory
38
- with `NORNA_SITE_DIR` or `norna --site-dir <path>`.
39
+ with `NORNA_SITE_DIR` or `norna --site-dir <path>`. Without an explicit site
40
+ directory, commands first accept the current directory when it contains
41
+ `config.mjs` and `content.md`; otherwise they walk upward looking for a
42
+ `site/` directory with those files.
39
43
 
40
44
  ## Quick Start
41
45
 
@@ -52,10 +56,10 @@ For a new site repository:
52
56
 
53
57
  ```sh
54
58
  cd ../
55
- npx @janga/norna@latest init my-gallery
56
- cd my-gallery
59
+ npx @janga/norna@latest init my-site
60
+ cd my-site
57
61
  npm install
58
- npm run norna:dev
62
+ npm run dev
59
63
  ```
60
64
 
61
65
  Run `init` before `npm install`. A new site directory is not a Node project
@@ -71,16 +75,19 @@ Actions use the same engine version.
71
75
 
72
76
  - Create a site: [Getting Started](docs/getting-started.md)
73
77
  - Understand required site files: [Site Structure](docs/site-structure.md)
74
- - Edit sections and galleries: [Content](docs/content.md)
78
+ - Edit sections and image rows: [Content](docs/content.md)
75
79
  - Configure a site: [Configuration](docs/configuration.md)
76
- - Configure site-wide theme: [Content](docs/content.md#site-theme)
77
- - Set page width: [`layout.pageWidth`](docs/configuration.md#layoutpagewidth)
78
- - Set side gutters: [`layout.gutter`](docs/configuration.md#layoutgutter)
79
- - Set gallery width: [`gallery.width`](docs/configuration.md#gallerywidth)
80
+ - Publish under a GitHub Pages project path:
81
+ [`site.basePath`](docs/configuration.md#sitebasepath)
82
+ - Configure site-wide theme: [Theme](docs/theme.md)
83
+ - Add route pages: [Routes](docs/routes.md)
84
+ - Set page width: [`layout.pageWidth`](docs/theme.md#layout)
85
+ - Set side gutters: [`layout.gutter`](docs/theme.md#layout)
86
+ - Set image area width: [`gallery.width`](docs/theme.md#image-sizing)
80
87
  - Keep images within viewport height:
81
- [`gallery.maxAvailableHeightPercent`](docs/configuration.md#gallerymaxavailableheightpercent)
82
- - Set the site font: [`typography.fontFamily`](docs/configuration.md#typographyfontfamily)
83
- - Choose typography presets and overrides: [Content](docs/content.md#typography-presets)
88
+ [`gallery.maxAvailableHeightPercent`](docs/theme.md#image-sizing)
89
+ - Set the site font: [`typography.fontFamily`](docs/theme.md#typography)
90
+ - Choose typography presets and overrides: [Typography](docs/typography.md)
84
91
  - Look up CLI and npm scripts: [Commands](docs/commands.md)
85
92
  - Understand generated images: [Images And Metadata](docs/images-and-metadata.md)
86
93
  - Run local preview: [Local Development](docs/local-development.md)
@@ -98,8 +105,15 @@ Actions use the same engine version.
98
105
  GitHub Pages workflows created from the starter install the image tools during
99
106
  deployment.
100
107
 
108
+ ## License
109
+
110
+ `norna` is licensed under [GNU GPL v3](LICENSE).
111
+
101
112
  ## Documentation
102
113
 
114
+ The Norna-built introduction site lives in [`site/`](site/) and is configured
115
+ for GitHub Pages at <https://janga.github.io/norna/>.
116
+
103
117
  Start with [docs/README.md](docs/README.md) for the documentation map and
104
118
  recommended reading order.
105
119
 
package/astro.config.mjs CHANGED
@@ -7,9 +7,11 @@ import {
7
7
  astroPublicDir,
8
8
  engineRoot,
9
9
  } from './scripts/lib/site-paths.mjs';
10
+ import projectConfig from './scripts/lib/project-config.mjs';
10
11
 
11
12
  // https://astro.build/config
12
13
  export default defineConfig({
14
+ base: projectConfig.site.basePath,
13
15
  cacheDir: astroCacheDir,
14
16
  outDir: astroDistDir,
15
17
  publicDir: astroPublicDir,
@@ -0,0 +1,175 @@
1
+ #!/usr/bin/env node
2
+ import path from 'node:path';
3
+
4
+ const args = process.argv.slice(2);
5
+ process.env.NORNA_INVOCATION_ROOT ??= process.cwd();
6
+
7
+ const usage = `
8
+ Usage: norna <command> [options]
9
+
10
+ Commands:
11
+ dev:local Start local Astro dev server
12
+ dev:lan Start dev server for this local network
13
+ dev:restart Restart local Astro dev server
14
+ dev:status Show local dev server status
15
+ dev:logs Show local dev server logs
16
+ dev:stop Stop local dev server
17
+ config:check Validate site/config.mjs
18
+ content:check Validate site/content.md and image references
19
+ content:sync Rewrite section order and move misplaced images
20
+ typography presets Show built-in typography preset values
21
+ typography show Show resolved typography for the selected site
22
+ site:public Sync site/public/ to public/
23
+ images Generate optimized image variants
24
+ engine:update Update @janga/norna in a site repository
25
+ engine:version Show installed engine and Astro versions
26
+ init Create a new site project from the starter
27
+ build Build the selected site
28
+ build:local Build and restart local dev server
29
+ deploy Build and deploy committed branch
30
+ deploy:commit Build, commit allowed changes, push, and check Pages
31
+ deploy:watch Watch GitHub Pages workflow
32
+ preview Preview dist/
33
+ astro Run Astro with norna config
34
+ doctor Print resolved paths
35
+
36
+ Global options:
37
+ --site-dir <path> Use a specific site source directory
38
+ -h, --help Show this help
39
+ `.trim();
40
+
41
+ const parseArgs = (rawArgs) => {
42
+ const commandArgs = [];
43
+ let siteDir = null;
44
+
45
+ for (let index = 0; index < rawArgs.length; index += 1) {
46
+ const arg = rawArgs[index];
47
+
48
+ if (arg === '--site-dir') {
49
+ const value = rawArgs[index + 1];
50
+ if (!value || value.startsWith('-')) {
51
+ throw new Error('--site-dir requires a path.');
52
+ }
53
+
54
+ siteDir = value;
55
+ index += 1;
56
+ continue;
57
+ }
58
+
59
+ if (arg.startsWith('--site-dir=')) {
60
+ siteDir = arg.slice('--site-dir='.length);
61
+ if (!siteDir) {
62
+ throw new Error('--site-dir requires a path.');
63
+ }
64
+ continue;
65
+ }
66
+
67
+ commandArgs.push(arg);
68
+ }
69
+
70
+ return { commandArgs, siteDir };
71
+ };
72
+
73
+ let parsedArgs;
74
+
75
+ try {
76
+ parsedArgs = parseArgs(args);
77
+ } catch (error) {
78
+ console.error(error instanceof Error ? error.message : String(error));
79
+ process.exit(1);
80
+ }
81
+
82
+ const { commandArgs, siteDir } = parsedArgs;
83
+
84
+ if (siteDir) {
85
+ process.env.NORNA_SITE_DIR = siteDir;
86
+ }
87
+
88
+ const [command = 'help', ...rest] = commandArgs;
89
+ const [subcommand, ...subcommandRest] = rest;
90
+
91
+ if (command === '-h' || command === '--help' || command === 'help') {
92
+ console.log(usage);
93
+ process.exit(0);
94
+ }
95
+
96
+ const [
97
+ { runAstroInherit },
98
+ { engineRoot, siteProjectRoot },
99
+ { runInherit },
100
+ ] = await Promise.all([
101
+ import('../scripts/lib/astro-command.mjs'),
102
+ import('../scripts/lib/site-paths.mjs'),
103
+ import('../scripts/lib/run-command.mjs'),
104
+ ]);
105
+
106
+ const runScript = (relativePath, scriptArgs = []) => runInherit(
107
+ process.execPath,
108
+ [path.join(engineRoot, relativePath), ...scriptArgs],
109
+ { cwd: siteProjectRoot },
110
+ );
111
+
112
+ const runBuild = () => runScript('scripts/build-site.mjs');
113
+
114
+ try {
115
+ if (command === 'dev' || command === 'dev:local') {
116
+ await runScript('scripts/dev-local.mjs', rest);
117
+ } else if (command === 'dev:lan') {
118
+ await runScript('scripts/dev-local.mjs', ['lan', ...rest]);
119
+ } else if (command === 'dev:restart') {
120
+ await runScript('scripts/dev-local.mjs', ['restart', ...rest]);
121
+ } else if (command === 'dev:status') {
122
+ await runScript('scripts/dev-local.mjs', ['status', ...rest]);
123
+ } else if (command === 'dev:logs') {
124
+ await runScript('scripts/dev-local.mjs', ['logs', ...rest]);
125
+ } else if (command === 'dev:stop') {
126
+ await runScript('scripts/dev-local.mjs', ['stop', ...rest]);
127
+ } else if (command === 'config:check') {
128
+ await runScript('scripts/check-config.mjs', rest);
129
+ } else if (command === 'content:check') {
130
+ await runScript('scripts/sync-content-sections.mjs', ['--check', ...rest]);
131
+ await runAstroInherit(['sync']);
132
+ } else if (command === 'content:sync') {
133
+ await runScript('scripts/sync-content-sections.mjs', ['--write', ...rest]);
134
+ } else if (command === 'typography:presets' || (command === 'typography' && subcommand === 'presets')) {
135
+ const scriptArgs = command === 'typography' ? subcommandRest : rest;
136
+ await runScript('scripts/show-typography.mjs', ['presets', ...scriptArgs]);
137
+ } else if (command === 'typography:show' || (command === 'typography' && subcommand === 'show')) {
138
+ const scriptArgs = command === 'typography' ? subcommandRest : rest;
139
+ await runScript('scripts/show-typography.mjs', ['show', ...scriptArgs]);
140
+ } else if (command === 'typography') {
141
+ throw new Error(`Unknown typography command: ${subcommand ?? ''}\n${usage}`);
142
+ } else if (command === 'site:public') {
143
+ await runScript('scripts/sync-site-public.mjs', rest);
144
+ } else if (command === 'images') {
145
+ await runScript('scripts/generate-images.mjs', rest);
146
+ } else if (command === 'engine:update') {
147
+ await runScript('scripts/update-engine.mjs', rest);
148
+ } else if (command === 'engine:version') {
149
+ await runScript('scripts/engine-version.mjs', rest);
150
+ } else if (command === 'init') {
151
+ await runScript('scripts/init-site.mjs', rest);
152
+ } else if (command === 'build') {
153
+ await runBuild();
154
+ } else if (command === 'build:local') {
155
+ await runBuild();
156
+ await runScript('scripts/dev-local.mjs', ['restart']);
157
+ } else if (command === 'deploy') {
158
+ await runScript('scripts/deploy-site.mjs', rest);
159
+ } else if (command === 'deploy:commit') {
160
+ await runScript('scripts/deploy-site.mjs', ['commit', ...rest]);
161
+ } else if (command === 'deploy:watch') {
162
+ await runScript('scripts/watch-pages-deploy.mjs', rest);
163
+ } else if (command === 'doctor') {
164
+ await runScript('scripts/doctor.mjs', rest);
165
+ } else if (command === 'astro') {
166
+ await runAstroInherit(rest);
167
+ } else if (command === 'preview') {
168
+ await runAstroInherit(['preview', ...rest]);
169
+ } else {
170
+ throw new Error(`Unknown command: ${command}\n${usage}`);
171
+ }
172
+ } catch (error) {
173
+ console.error(error instanceof Error ? error.message : String(error));
174
+ process.exit(1);
175
+ }
package/bin/norna.mjs CHANGED
@@ -1,170 +1,169 @@
1
1
  #!/usr/bin/env node
2
+ import { spawn } from 'node:child_process';
3
+ import { existsSync } from 'node:fs';
4
+ import { readFile, realpath } from 'node:fs/promises';
5
+ import { createRequire } from 'node:module';
2
6
  import path from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
3
8
 
4
- const args = process.argv.slice(2);
5
- process.env.NORNA_INVOCATION_ROOT ??= process.cwd();
6
-
7
- const usage = `
8
- Usage: norna <command> [options]
9
-
10
- Commands:
11
- dev:local Start local Astro dev server
12
- dev:lan Start dev server for this local network
13
- dev:restart Restart local Astro dev server
14
- dev:status Show local dev server status
15
- dev:logs Show local dev server logs
16
- dev:stop Stop local dev server
17
- config:check Validate site/config.mjs
18
- content:check Validate site/content.md and gallery references
19
- content:sync Rewrite section order and move misplaced images
20
- typography:presets Show built-in typography preset values
21
- typography:show Show resolved typography for the selected site
22
- site:public Sync site/public/ to public/
23
- images Generate optimized image variants
24
- engine:update Update @janga/norna in a site repository
25
- engine:version Show installed engine and Astro versions
26
- init Create a new site project from the starter
27
- build Build the selected site
28
- build:local Build and restart local dev server
29
- deploy Build and deploy committed branch
30
- deploy:commit Build, commit allowed changes, push, and check Pages
31
- deploy:watch Watch GitHub Pages workflow
32
- preview Preview dist/
33
- astro Run Astro with norna config
34
- doctor Print resolved paths
35
-
36
- Global options:
37
- --site-dir <path> Use a specific site source directory
38
- -h, --help Show this help
39
- `.trim();
40
-
41
- const parseArgs = (rawArgs) => {
42
- const commandArgs = [];
43
- let siteDir = null;
44
-
45
- for (let index = 0; index < rawArgs.length; index += 1) {
46
- const arg = rawArgs[index];
47
-
48
- if (arg === '--site-dir') {
49
- const value = rawArgs[index + 1];
50
- if (!value || value.startsWith('-')) {
51
- throw new Error('--site-dir requires a path.');
52
- }
53
-
54
- siteDir = value;
55
- index += 1;
56
- continue;
9
+ const packageName = '@janga/norna';
10
+ const currentEntrypoint = fileURLToPath(import.meta.url);
11
+ const implementationEntrypoint = new URL('./norna-cli.mjs', import.meta.url);
12
+
13
+ const readJson = async (filePath) => JSON.parse(await readFile(filePath, 'utf8'));
14
+
15
+ const readJsonOptional = async (filePath) => {
16
+ try {
17
+ return await readJson(filePath);
18
+ } catch (error) {
19
+ if (error.code === 'ENOENT') {
20
+ return null;
57
21
  }
58
22
 
59
- if (arg.startsWith('--site-dir=')) {
60
- siteDir = arg.slice('--site-dir='.length);
61
- if (!siteDir) {
62
- throw new Error('--site-dir requires a path.');
63
- }
64
- continue;
23
+ throw error;
24
+ }
25
+ };
26
+
27
+ const realpathOptional = async (filePath) => {
28
+ try {
29
+ return await realpath(filePath);
30
+ } catch (error) {
31
+ if (error.code === 'ENOENT') {
32
+ return null;
65
33
  }
66
34
 
67
- commandArgs.push(arg);
35
+ throw error;
68
36
  }
37
+ };
38
+
39
+ const findNearestProjectRoot = (startDirectory) => {
40
+ let current = path.resolve(startDirectory);
41
+
42
+ while (true) {
43
+ if (existsSync(path.join(current, 'package.json'))) {
44
+ return current;
45
+ }
46
+
47
+ const parent = path.dirname(current);
48
+ if (parent === current) {
49
+ return null;
50
+ }
69
51
 
70
- return { commandArgs, siteDir };
52
+ current = parent;
53
+ }
71
54
  };
72
55
 
73
- let parsedArgs;
56
+ const isWithinDirectory = (parentDirectory, childDirectory) => {
57
+ const relativePath = path.relative(parentDirectory, childDirectory);
58
+ return relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath));
59
+ };
74
60
 
75
- try {
76
- parsedArgs = parseArgs(args);
77
- } catch (error) {
78
- console.error(error instanceof Error ? error.message : String(error));
79
- process.exit(1);
80
- }
61
+ const declaresNornaDependency = (packageJson) => [
62
+ 'dependencies',
63
+ 'devDependencies',
64
+ 'optionalDependencies',
65
+ 'peerDependencies',
66
+ ].some((field) => Object.hasOwn(packageJson[field] ?? {}, packageName));
81
67
 
82
- const { commandArgs, siteDir } = parsedArgs;
68
+ const getPackageBinEntrypoint = (packageRoot, packageJson) => {
69
+ const bin = packageJson.bin;
70
+ const binPath = typeof bin === 'string' ? bin : bin?.norna;
83
71
 
84
- if (siteDir) {
85
- process.env.NORNA_SITE_DIR = siteDir;
86
- }
72
+ if (!binPath) {
73
+ throw new Error(`${packageName} does not define a norna bin entrypoint.`);
74
+ }
87
75
 
88
- const [command = 'help', ...rest] = commandArgs;
76
+ return path.resolve(packageRoot, binPath);
77
+ };
89
78
 
90
- if (command === '-h' || command === '--help' || command === 'help') {
91
- console.log(usage);
92
- process.exit(0);
93
- }
79
+ const resolveLocalEntrypoint = async () => {
80
+ const currentPackageRoot = findNearestProjectRoot(path.dirname(currentEntrypoint));
81
+ const currentPackageJson = currentPackageRoot
82
+ ? await readJsonOptional(path.join(currentPackageRoot, 'package.json'))
83
+ : null;
84
+
85
+ if (
86
+ currentPackageJson?.name === packageName
87
+ && isWithinDirectory(currentPackageRoot, process.cwd())
88
+ ) {
89
+ return null;
90
+ }
91
+
92
+ const projectRoot = findNearestProjectRoot(process.cwd());
93
+ if (!projectRoot) {
94
+ return null;
95
+ }
96
+
97
+ const projectPackageJson = await readJsonOptional(path.join(projectRoot, 'package.json'));
98
+ if (!projectPackageJson || projectPackageJson.name === packageName || !declaresNornaDependency(projectPackageJson)) {
99
+ return null;
100
+ }
101
+
102
+ let localPackageJsonPath;
103
+ try {
104
+ const requireFromProject = createRequire(path.join(projectRoot, 'package.json'));
105
+ localPackageJsonPath = requireFromProject.resolve(`${packageName}/package.json`);
106
+ } catch (error) {
107
+ if (error.code === 'MODULE_NOT_FOUND') {
108
+ return null;
109
+ }
110
+
111
+ throw error;
112
+ }
113
+
114
+ const localPackageJson = await readJson(localPackageJsonPath);
115
+ const localPackageRoot = path.dirname(localPackageJsonPath);
116
+ return getPackageBinEntrypoint(localPackageRoot, localPackageJson);
117
+ };
118
+
119
+ const runLocalEntrypoint = (entrypoint) => {
120
+ const child = spawn(process.execPath, [entrypoint, ...process.argv.slice(2)], {
121
+ cwd: process.cwd(),
122
+ env: process.env,
123
+ stdio: 'inherit',
124
+ });
125
+
126
+ const forwardSignal = (signal) => {
127
+ if (!child.killed) {
128
+ child.kill(signal);
129
+ }
130
+ };
131
+ const signals = ['SIGINT', 'SIGTERM'];
132
+ for (const signal of signals) {
133
+ process.once(signal, forwardSignal);
134
+ }
135
+
136
+ child.once('exit', (code, signal) => {
137
+ for (const forwardedSignal of signals) {
138
+ process.removeListener(forwardedSignal, forwardSignal);
139
+ }
140
+
141
+ if (signal) {
142
+ process.kill(process.pid, signal);
143
+ return;
144
+ }
145
+
146
+ process.exit(code ?? 1);
147
+ });
148
+
149
+ child.once('error', (error) => {
150
+ console.error(error instanceof Error ? error.message : String(error));
151
+ process.exit(1);
152
+ });
153
+ };
154
+
155
+ const localEntrypoint = await resolveLocalEntrypoint();
156
+ if (localEntrypoint) {
157
+ const [currentRealpath, localRealpath] = await Promise.all([
158
+ realpathOptional(currentEntrypoint),
159
+ realpathOptional(localEntrypoint),
160
+ ]);
94
161
 
95
- const [
96
- { runAstroInherit },
97
- { engineRoot, siteProjectRoot },
98
- { runInherit },
99
- ] = await Promise.all([
100
- import('../scripts/lib/astro-command.mjs'),
101
- import('../scripts/lib/site-paths.mjs'),
102
- import('../scripts/lib/run-command.mjs'),
103
- ]);
104
-
105
- const runScript = (relativePath, scriptArgs = []) => runInherit(
106
- process.execPath,
107
- [path.join(engineRoot, relativePath), ...scriptArgs],
108
- { cwd: siteProjectRoot },
109
- );
110
-
111
- const runBuild = () => runScript('scripts/build-site.mjs');
112
-
113
- try {
114
- if (command === 'dev' || command === 'dev:local') {
115
- await runScript('scripts/dev-local.mjs', rest);
116
- } else if (command === 'dev:lan') {
117
- await runScript('scripts/dev-local.mjs', ['lan', ...rest]);
118
- } else if (command === 'dev:restart') {
119
- await runScript('scripts/dev-local.mjs', ['restart', ...rest]);
120
- } else if (command === 'dev:status') {
121
- await runScript('scripts/dev-local.mjs', ['status', ...rest]);
122
- } else if (command === 'dev:logs') {
123
- await runScript('scripts/dev-local.mjs', ['logs', ...rest]);
124
- } else if (command === 'dev:stop') {
125
- await runScript('scripts/dev-local.mjs', ['stop', ...rest]);
126
- } else if (command === 'config:check') {
127
- await runScript('scripts/check-config.mjs', rest);
128
- } else if (command === 'content:check') {
129
- await runScript('scripts/sync-content-sections.mjs', ['--check', ...rest]);
130
- await runAstroInherit(['sync']);
131
- } else if (command === 'content:sync') {
132
- await runScript('scripts/sync-content-sections.mjs', ['--write', ...rest]);
133
- } else if (command === 'typography:presets') {
134
- await runScript('scripts/show-typography.mjs', ['presets', ...rest]);
135
- } else if (command === 'typography:show') {
136
- await runScript('scripts/show-typography.mjs', ['show', ...rest]);
137
- } else if (command === 'site:public') {
138
- await runScript('scripts/sync-site-public.mjs', rest);
139
- } else if (command === 'images') {
140
- await runScript('scripts/generate-images.mjs', rest);
141
- } else if (command === 'engine:update') {
142
- await runScript('scripts/update-engine.mjs', rest);
143
- } else if (command === 'engine:version') {
144
- await runScript('scripts/engine-version.mjs', rest);
145
- } else if (command === 'init') {
146
- await runScript('scripts/init-site.mjs', rest);
147
- } else if (command === 'build') {
148
- await runBuild();
149
- } else if (command === 'build:local') {
150
- await runBuild();
151
- await runScript('scripts/dev-local.mjs', ['restart']);
152
- } else if (command === 'deploy') {
153
- await runScript('scripts/deploy-site.mjs', rest);
154
- } else if (command === 'deploy:commit') {
155
- await runScript('scripts/deploy-site.mjs', ['commit', ...rest]);
156
- } else if (command === 'deploy:watch') {
157
- await runScript('scripts/watch-pages-deploy.mjs', rest);
158
- } else if (command === 'doctor') {
159
- await runScript('scripts/doctor.mjs', rest);
160
- } else if (command === 'astro') {
161
- await runAstroInherit(rest);
162
- } else if (command === 'preview') {
163
- await runAstroInherit(['preview', ...rest]);
162
+ if (localRealpath && currentRealpath !== localRealpath) {
163
+ runLocalEntrypoint(localEntrypoint);
164
164
  } else {
165
- throw new Error(`Unknown command: ${command}\n${usage}`);
165
+ await import(implementationEntrypoint.href);
166
166
  }
167
- } catch (error) {
168
- console.error(error instanceof Error ? error.message : String(error));
169
- process.exit(1);
167
+ } else {
168
+ await import(implementationEntrypoint.href);
170
169
  }