@janga/norna 0.7.0 → 0.7.2

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 (63) hide show
  1. package/README.md +18 -4
  2. package/astro.config.mjs +2 -0
  3. package/bin/norna-cli.mjs +170 -0
  4. package/bin/norna.mjs +149 -150
  5. package/docs/README.md +36 -16
  6. package/docs/commands.md +18 -5
  7. package/docs/configuration.md +37 -2
  8. package/docs/content.md +98 -257
  9. package/docs/{command-organization.md → design/command-organization.md} +9 -5
  10. package/docs/{site-examples-structure-note.md → design/site-examples-structure.md} +17 -22
  11. package/docs/engine-development.md +33 -5
  12. package/docs/getting-started.md +40 -4
  13. package/docs/local-development.md +13 -0
  14. package/docs/publishing.md +23 -0
  15. package/docs/routes.md +90 -0
  16. package/docs/site-structure.md +15 -8
  17. package/docs/theme.md +150 -0
  18. package/docs/typography.md +125 -0
  19. package/examples/dog-gallery/site/.norna/generated-images.json +226 -0
  20. package/examples/dog-gallery/site/config.mjs +97 -0
  21. package/examples/dog-gallery/site/content.md +146 -0
  22. package/examples/dog-gallery/site/images/black-dogs/black-puppy-meadow.png +0 -0
  23. package/examples/dog-gallery/site/images/black-dogs/photo-of-a-black-dog.jpg +0 -0
  24. package/examples/dog-gallery/site/images/brown-dogs/brown-dog.jpg +0 -0
  25. package/examples/dog-gallery/site/images/brown-dogs/dog-accompanies-master.jpg +0 -0
  26. package/examples/dog-gallery/site/images/golden-dogs/golden-retriever.jpg +0 -0
  27. package/examples/dog-gallery/site/images/golden-dogs/toller-puppy.jpg +0 -0
  28. package/examples/dog-gallery/site/images/white-dogs/white-cute-dog.jpg +0 -0
  29. package/examples/dog-gallery/site/images/white-dogs/white-puppy-garden.png +0 -0
  30. package/examples/dog-gallery/site/public/favicon.svg +7 -0
  31. package/examples/dog-gallery/site/public/robots.txt +2 -0
  32. package/examples/dog-gallery/site/routes/dog-care/route-content.md +35 -0
  33. package/examples/dog-gallery/site/theme.md +55 -0
  34. package/fixtures/basic/site/config.mjs +1 -0
  35. package/package.json +8 -7
  36. package/scripts/check-config.mjs +1 -0
  37. package/scripts/dev-local.mjs +64 -15
  38. package/scripts/init-site.mjs +1 -1
  39. package/scripts/lib/project-config.mjs +22 -0
  40. package/scripts/lib/site-content.mjs +2 -1
  41. package/scripts/lib/site-paths.mjs +18 -3
  42. package/scripts/lib/typography.mjs +5 -5
  43. package/scripts/show-typography.mjs +252 -29
  44. package/scripts/test-cli-discovery.mjs +124 -0
  45. package/scripts/test-engine-commands.mjs +18 -4
  46. package/scripts/test-navigation.mjs +10 -6
  47. package/scripts/test-package-check.mjs +32 -4
  48. package/src/components/SiteNavigation.astro +7 -5
  49. package/src/components/SitePage.astro +3 -0
  50. package/src/components/SiteSection.astro +24 -24
  51. package/src/content.config.ts +4 -0
  52. package/src/layouts/BaseLayout.astro +2 -1
  53. package/src/lib/basePath.ts +21 -0
  54. package/src/lib/generatedImages.ts +8 -4
  55. package/src/lib/sectionContent.ts +6 -1
  56. package/src/lib/sitePublicAssets.ts +8 -1
  57. package/src/styles/global.css +8 -8
  58. package/starters/basic/.github/workflows/deploy.yml +3 -3
  59. package/starters/basic/README.md +21 -0
  60. package/starters/basic/package.json +1 -1
  61. package/starters/basic/site/config.mjs +1 -0
  62. package/starters/basic/site/content.md +5 -3
  63. package/starters/basic/site/theme.md +4 -0
package/README.md CHANGED
@@ -22,7 +22,8 @@ A `norna` site is file-driven:
22
22
 
23
23
  1. A site repository depends on this package.
24
24
  2. The site keeps technical settings, including page width, gallery width, and
25
- the global font family, in `site/config.mjs`.
25
+ the global font family, public URL, and optional URL base path in
26
+ `site/config.mjs`.
26
27
  3. The site can keep site-wide visual theme defaults in `site/theme.md`.
27
28
  4. The site keeps homepage content, section order, gallery rows, alt text, and
28
29
  captions in `site/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
 
@@ -73,14 +77,17 @@ Actions use the same engine version.
73
77
  - Understand required site files: [Site Structure](docs/site-structure.md)
74
78
  - Edit sections and galleries: [Content](docs/content.md)
75
79
  - Configure a site: [Configuration](docs/configuration.md)
76
- - Configure site-wide theme: [Content](docs/content.md#site-theme)
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)
77
84
  - Set page width: [`layout.pageWidth`](docs/configuration.md#layoutpagewidth)
78
85
  - Set side gutters: [`layout.gutter`](docs/configuration.md#layoutgutter)
79
86
  - Set gallery width: [`gallery.width`](docs/configuration.md#gallerywidth)
80
87
  - Keep images within viewport height:
81
88
  [`gallery.maxAvailableHeightPercent`](docs/configuration.md#gallerymaxavailableheightpercent)
82
89
  - Set the site font: [`typography.fontFamily`](docs/configuration.md#typographyfontfamily)
83
- - Choose typography presets and overrides: [Content](docs/content.md#typography-presets)
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,170 @@
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 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;
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
+
90
+ if (command === '-h' || command === '--help' || command === 'help') {
91
+ console.log(usage);
92
+ process.exit(0);
93
+ }
94
+
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]);
164
+ } else {
165
+ throw new Error(`Unknown command: ${command}\n${usage}`);
166
+ }
167
+ } catch (error) {
168
+ console.error(error instanceof Error ? error.message : String(error));
169
+ process.exit(1);
170
+ }
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
  }
package/docs/README.md CHANGED
@@ -11,18 +11,27 @@ For a site maintainer:
11
11
  2. [Site Structure](site-structure.md)
12
12
  3. [Configuration](configuration.md)
13
13
  4. [Content](content.md)
14
- 5. [Images And Metadata](images-and-metadata.md)
15
- 6. [Local Development](local-development.md)
16
- 7. [Publishing](publishing.md)
14
+ 5. [Theme](theme.md)
15
+ 6. [Typography](typography.md)
16
+ 7. [Routes](routes.md)
17
+ 8. [Images And Metadata](images-and-metadata.md)
18
+ 9. [Local Development](local-development.md)
19
+ 10. [Publishing](publishing.md)
17
20
 
18
21
  For a developer integrating or updating the engine:
19
22
 
20
23
  1. [Commands](commands.md)
21
- 2. [Command Organization](command-organization.md)
22
- 3. [Site Examples Structure Note](site-examples-structure-note.md)
23
- 4. [Configuration](configuration.md)
24
- 5. [Content](content.md)
25
- 6. [Engine Development](engine-development.md)
24
+ 2. [Configuration](configuration.md)
25
+ 3. [Content](content.md)
26
+ 4. [Theme](theme.md)
27
+ 5. [Typography](typography.md)
28
+ 6. [Routes](routes.md)
29
+ 7. [Engine Development](engine-development.md)
30
+
31
+ For design and naming principles:
32
+
33
+ 1. [Command Organization](design/command-organization.md)
34
+ 2. [Site Examples Structure](design/site-examples-structure.md)
26
35
 
27
36
  For a quick reference:
28
37
 
@@ -30,19 +39,30 @@ For a quick reference:
30
39
  `site/config.mjs` field, including `layout.pageWidth`, `layout.gutter`,
31
40
  gallery viewport limits, `typography.fontFamily`, validation rule, and
32
41
  default.
33
- - [Content](content.md) describes page and route frontmatter, section
34
- frontmatter, typography presets, site themes, inline styles, temporary
35
- sections, Markdown section matching, and content validation.
42
+ - [Content](content.md) describes page frontmatter, section frontmatter,
43
+ galleries, carousels, temporary sections, Markdown section matching, and
44
+ content validation.
45
+ - [Theme](theme.md) describes site-wide presentation, page and section
46
+ presentation overrides, frame colors, and inline styles.
47
+ - [Typography](typography.md) describes presets, roles, overrides,
48
+ inheritance, and inspection commands.
49
+ - [Routes](routes.md) describes route files, slugs, route navigation, and
50
+ route image directories.
36
51
  - [Commands](commands.md) lists the public CLI surface and starter npm scripts.
37
- - [Command Organization](command-organization.md) defines command namespaces
38
- for pure gallery projects, mixed projects, and engine development.
39
52
  - [Site Structure](site-structure.md) separates versioned source files from
40
53
  generated build output.
41
- - [Site Examples Structure Note](site-examples-structure-note.md) defines the
42
- intended vocabulary for starters, examples, docs sites, reference docs, and
43
- fixtures before any repository reorganization.
54
+
55
+ Design documents are intentionally separate from the user reference:
56
+
57
+ - [Command Organization](design/command-organization.md) defines command
58
+ namespaces for pure gallery projects, mixed projects, and engine development.
59
+ - [Site Examples Structure](design/site-examples-structure.md) defines the
60
+ intended vocabulary for starters, examples, documentation sites, reference
61
+ docs, and fixtures.
44
62
 
45
63
  Site repositories should document only their site-specific choices and link here
46
64
  for generic `norna` behavior.
47
65
 
66
+ `norna` is licensed under [GNU GPL v3](../LICENSE).
67
+
48
68
  Planning and future work are tracked in [../BACKLOG.md](../BACKLOG.md).
package/docs/commands.md CHANGED
@@ -1,7 +1,17 @@
1
1
  # Commands
2
2
 
3
- The `norna` binary is the stable command surface. The starter's npm
4
- scripts are thin aliases around these commands.
3
+ The `norna` binary is the stable command surface. Site repositories normally
4
+ use starter npm scripts such as `npm run norna:content:check`; those scripts
5
+ call the local `norna` binary from `node_modules/.bin`.
6
+
7
+ Npm installation exposes the command as `norna` on macOS, Linux, and Windows
8
+ through the package `bin` field. When a globally installed `norna` is started
9
+ inside a project that declares and has installed its own `@janga/norna`
10
+ dependency, the launcher delegates to that project-local version. If no
11
+ project-local install is found, the version that was started continues running.
12
+
13
+ Use direct `norna ...` commands only when the binary is on your shell `PATH`,
14
+ inside an npm script, or through `npm exec -- norna ...`.
5
15
 
6
16
  ## CLI Commands
7
17
 
@@ -97,8 +107,9 @@ should normally mean the repository's complete publishable artifact, while
97
107
  image files after confirmation.
98
108
  - `typography:presets`: prints the exact built-in values for every typography
99
109
  preset.
100
- - `typography:show`: prints the selected site's resolved typography after
101
- applying theme, page, and section presentation.
110
+ - `typography:show`: prints the selected site's resolved typography for the
111
+ theme, every page route, and every section. Each value includes its source,
112
+ and inherited page or section values are marked with `inherited: true`.
102
113
  - `site:public`: copies `site/public/` into `site/.norna/public/` and
103
114
  removes stale copied static files.
104
115
  - `images`: generates WebP variants and writes
@@ -120,7 +131,9 @@ should normally mean the repository's complete publishable artifact, while
120
131
  - `build`: runs config check, content check, public sync, image generation, and
121
132
  Astro build.
122
133
  - `build:local`: runs `build` and restarts `dev:local`.
123
- - `dev:local`: starts Astro dev in background mode on `localhost:4321`.
134
+ - `dev:local`: starts Astro dev in background mode on `localhost:4321`. Pass
135
+ `--kill` to stop processes that are blocking the standard port before
136
+ starting.
124
137
  - `dev:lan`: starts the same server on all local network interfaces and prints
125
138
  the IPv4 URL to open from another device on the same network. Stop it after
126
139
  testing because it is accessible to that local network.