@jk2908/solas 0.2.2 → 0.3.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 (82) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +5 -3
  3. package/dist/cli.js +77 -83
  4. package/dist/error-boundary.d.ts +1 -1
  5. package/dist/error-boundary.js +1 -1
  6. package/dist/index.d.ts +3 -3
  7. package/dist/index.js +10 -14
  8. package/dist/internal/build.d.ts +1 -1
  9. package/dist/internal/build.js +4 -4
  10. package/dist/internal/codegen/config.d.ts +1 -1
  11. package/dist/internal/codegen/config.js +10 -10
  12. package/dist/internal/codegen/environments.js +22 -18
  13. package/dist/internal/codegen/manifest.d.ts +1 -1
  14. package/dist/internal/codegen/manifest.js +6 -7
  15. package/dist/internal/codegen/maps.d.ts +1 -1
  16. package/dist/internal/codegen/maps.js +38 -27
  17. package/dist/internal/codegen/utils.d.ts +20 -0
  18. package/dist/internal/codegen/utils.js +140 -1
  19. package/dist/internal/env/browser.js +20 -16
  20. package/dist/internal/env/request-context.d.ts +2 -2
  21. package/dist/internal/env/request-context.js +1 -1
  22. package/dist/internal/env/rsc.d.ts +8 -22
  23. package/dist/internal/env/rsc.js +38 -117
  24. package/dist/internal/env/ssr.js +9 -9
  25. package/dist/internal/env/utils.js +2 -2
  26. package/dist/internal/metadata.d.ts +2 -2
  27. package/dist/internal/metadata.js +18 -6
  28. package/dist/internal/navigation/http-exception-boundary.d.ts +2 -2
  29. package/dist/internal/navigation/http-exception-boundary.js +1 -1
  30. package/dist/internal/navigation/link.js +1 -1
  31. package/dist/internal/navigation/redirect-boundary.d.ts +1 -1
  32. package/dist/internal/navigation/redirect-boundary.js +1 -1
  33. package/dist/internal/navigation/redirect.js +1 -1
  34. package/dist/internal/navigation/use-search-params.js +4 -2
  35. package/dist/internal/prerender.d.ts +10 -1
  36. package/dist/internal/prerender.js +55 -5
  37. package/dist/internal/render/head.d.ts +4 -1
  38. package/dist/internal/render/head.js +37 -18
  39. package/dist/internal/render/tree.d.ts +1 -1
  40. package/dist/internal/render/tree.js +3 -3
  41. package/dist/internal/router/create-router.d.ts +2 -2
  42. package/dist/internal/router/create-router.js +1 -1
  43. package/dist/internal/router/prefetcher.d.ts +1 -1
  44. package/dist/internal/router/prefetcher.js +8 -3
  45. package/dist/internal/router/resolver.d.ts +29 -29
  46. package/dist/internal/router/resolver.js +4 -4
  47. package/dist/internal/router/router-context.d.ts +4 -0
  48. package/dist/internal/router/router-context.js +1 -0
  49. package/dist/internal/router/router-provider.d.ts +6 -2
  50. package/dist/internal/router/router-provider.js +38 -22
  51. package/dist/internal/router/router.d.ts +1 -1
  52. package/dist/internal/router/router.js +4 -4
  53. package/dist/internal/router/use-router.d.ts +5 -1
  54. package/dist/internal/router/use-router.js +1 -1
  55. package/dist/internal/router/utils.d.ts +1 -1
  56. package/dist/internal/server/actions.d.ts +30 -0
  57. package/dist/internal/server/actions.js +107 -0
  58. package/dist/internal/server/cookies.d.ts +1 -1
  59. package/dist/internal/server/cookies.js +3 -3
  60. package/dist/internal/server/dynamic.js +2 -2
  61. package/dist/internal/server/headers.js +2 -2
  62. package/dist/internal/server/url.js +14 -3
  63. package/dist/internal/ui/defaults/error.d.ts +1 -1
  64. package/dist/internal/ui/error-boundary.d.ts +1 -1
  65. package/dist/internal/ui/error-boundary.js +1 -1
  66. package/dist/navigation.d.ts +6 -6
  67. package/dist/navigation.js +6 -6
  68. package/dist/prerender.d.ts +1 -1
  69. package/dist/prerender.js +1 -1
  70. package/dist/router.d.ts +4 -4
  71. package/dist/router.js +4 -4
  72. package/dist/server.d.ts +4 -4
  73. package/dist/server.js +4 -4
  74. package/dist/solas.d.ts +1 -1
  75. package/dist/solas.js +1 -0
  76. package/dist/types.d.ts +6 -6
  77. package/dist/types.js +1 -1
  78. package/dist/utils/context.js +1 -1
  79. package/dist/utils/logger.js +2 -2
  80. package/package.json +3 -1
  81. package/dist/utils/format.d.ts +0 -6
  82. package/dist/utils/format.js +0 -72
package/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ # Changelog
2
+
3
+ ## 0.3.0 - 2026-04-07
4
+
5
+ - Fixed `useSearchParams()` hydration so query-driven ui uses the initial request url on first render.
6
+ - Switched internal runtime and generated imports to explicit `.js` specifiers, and corrected the router action import path.
7
+ - Simplified generated config, manifest, and route map output to emit source literals directly.
8
+ - Removed the generated-file formatting pass and deleted the internal `Format` helper.
9
+ - Documented that the Solas cli currently requires Bun 1.2+ on `PATH`.
10
+
11
+ ## 0.2.3 - 2026-04-02
12
+
13
+ - Previous release.
package/README.md CHANGED
@@ -4,6 +4,8 @@ Solas is a minimal React meta-framework powered by Vite, created for experimenti
4
4
 
5
5
  It has not been rigorously tested yet (there are currently no automated tests) ... and broken behaviour should be expected.
6
6
 
7
+ Solas currently requires Bun 1.2+ on your `PATH`. You can still manage dependencies with `npm`, `pnpm`, or `yarn`, but the Solas CLI and Vite plugin runtime use Bun APIs directly.
8
+
7
9
  ## Install
8
10
 
9
11
  ```sh
@@ -315,9 +317,9 @@ export default defineConfig(({ mode }) => ({
315
317
  solas({
316
318
  url: mode === 'production' ? 'https://example.com' : 'http://localhost:8787',
317
319
  sitemap: {
318
- async routes(discovered) {
319
- const posts = await fetchPostSlugs()
320
- return [...discovered, ...posts.map(s => `/blog/${s}`)]
320
+ async routes(existing) {
321
+ const posts = await getPosts()
322
+ return [...existing, ...posts.map(p => `/blog/${p.slug}`)]
321
323
  },
322
324
  },
323
325
  }),
package/dist/cli.js CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env bun
2
2
  import fs from 'node:fs/promises';
3
3
  import path from 'node:path';
4
- import { Solas } from './solas';
5
- import { Compress } from './utils/compress';
6
- import { Logger } from './utils/logger';
7
- import { Prerender } from './internal/prerender';
4
+ import { Solas } from './solas.js';
5
+ import { Compress } from './utils/compress.js';
6
+ import { Logger } from './utils/logger.js';
7
+ import { Prerender } from './internal/prerender.js';
8
8
  const logger = new Logger();
9
9
  const DEFAULT_PREVIEW_PORT = 4173;
10
10
  /**
@@ -52,10 +52,21 @@ async function build() {
52
52
  const concurrency = Prerender.Build.getConcurrency();
53
53
  // track the extra prerender files we write for preview
54
54
  const artifactManifestRoutes = {};
55
+ // keep in-flight artifact writes bounded so result handling does not block on one route at a time
56
+ const pendingWrites = new Set();
55
57
  logger.info('[prerender]', `prerendering ${manifest.prerenderRoutes.length} routes (timeout: ${timeout}ms, concurrency: ${concurrency})...`);
56
58
  // load the built server entry and render each prerendered route through it
57
59
  const rscEntry = path.join(rscDir, 'index.js');
58
60
  const { default: app } = await import(/* @vite-ignore */ rscEntry);
61
+ async function enqueueWrite(task) {
62
+ const write = task().finally(() => {
63
+ pendingWrites.delete(write);
64
+ });
65
+ pendingWrites.add(write);
66
+ if (pendingWrites.size >= concurrency) {
67
+ await Promise.race(pendingWrites);
68
+ }
69
+ }
59
70
  // run prerender through the built app so build output uses the same path as preview
60
71
  for await (const result of Prerender.Build.run(app, manifest.prerenderRoutes, {
61
72
  timeout,
@@ -63,98 +74,81 @@ async function build() {
63
74
  origin: manifest.url,
64
75
  })) {
65
76
  const route = result.route;
66
- try {
67
- // store prerender metadata for this route under the framework folder
68
- const artifactDir = Prerender.Artifact.getPath(outDir, route);
69
- if ('error' in result)
70
- throw result.error;
71
- if ('status' in result) {
72
- logger.warn('[prerender]', `skipped ${route}: ${result.status}`);
73
- continue;
74
- }
75
- const artifact = result.artifact;
76
- if (artifact.mode === 'ppr') {
77
- // for ppr save the shell now and keep the postponed state for later
77
+ if ('error' in result) {
78
+ logger.error(`[prerender]: Failed ${route}: ${result.error}. This often means unresolved async work (for example external fetches or dynamic rendering in full mode)`);
79
+ continue;
80
+ }
81
+ if ('status' in result) {
82
+ logger.warn(`[prerender]: Skipped ${route}: ${result.status}`);
83
+ continue;
84
+ }
85
+ const artifact = result.artifact;
86
+ const artifactDir = Prerender.Artifact.getPath(outDir, route);
87
+ await enqueueWrite(async () => {
88
+ try {
89
+ if (artifact.mode === 'ppr') {
90
+ // for ppr save the shell now and keep the postponed state for later
91
+ await fs.mkdir(artifactDir, { recursive: true });
92
+ const writes = [
93
+ Bun.write(path.join(artifactDir, 'prelude.html'), artifact.html),
94
+ Bun.write(path.join(artifactDir, 'metadata.json'), JSON.stringify({
95
+ schema: artifact.schema,
96
+ route: artifact.route,
97
+ createdAt: artifact.createdAt,
98
+ mode: artifact.mode,
99
+ })),
100
+ ];
101
+ if (artifact.postponed !== undefined) {
102
+ writes.push(Bun.write(path.join(artifactDir, 'postponed.json'), JSON.stringify(artifact.postponed)));
103
+ }
104
+ await Promise.all(writes);
105
+ artifactManifestRoutes[route] = {
106
+ mode: artifact.mode,
107
+ createdAt: artifact.createdAt,
108
+ files: artifact.postponed !== undefined
109
+ ? ['metadata', 'prelude', 'postponed']
110
+ : ['metadata', 'prelude'],
111
+ };
112
+ logger.info('[prerender:artifacts]', JSON.stringify({
113
+ route,
114
+ prelude: artifact.html,
115
+ postponed: artifact.postponed ?? null,
116
+ metadata: {
117
+ schema: artifact.schema,
118
+ route: artifact.route,
119
+ createdAt: artifact.createdAt,
120
+ mode: artifact.mode,
121
+ },
122
+ }));
123
+ logger.info('[prerender]', `${route} (ppr)`);
124
+ return;
125
+ }
126
+ // full prerender still keeps metadata so preview knows to serve saved html
78
127
  await fs.mkdir(artifactDir, { recursive: true });
79
- const writes = [
80
- Bun.write(path.join(artifactDir, 'prelude.html'), artifact.html),
128
+ const fullPrerenderFilename = Prerender.Artifact.getFullHtmlFileName(artifact.html);
129
+ await Promise.all([
81
130
  Bun.write(path.join(artifactDir, 'metadata.json'), JSON.stringify({
82
131
  schema: artifact.schema,
83
132
  route: artifact.route,
84
133
  createdAt: artifact.createdAt,
85
134
  mode: artifact.mode,
86
135
  })),
87
- ];
88
- if (artifact.postponed !== undefined) {
89
- writes.push(Bun.write(path.join(artifactDir, 'postponed.json'), JSON.stringify(artifact.postponed)));
90
- }
91
- await Promise.all(writes);
136
+ Bun.write(Prerender.Artifact.getFilePath(outDir, route, fullPrerenderFilename), artifact.html),
137
+ ]);
92
138
  artifactManifestRoutes[route] = {
93
139
  mode: artifact.mode,
94
140
  createdAt: artifact.createdAt,
95
- files: artifact.postponed !== undefined
96
- ? ['metadata', 'prelude', 'postponed']
97
- : ['metadata', 'prelude'],
141
+ files: ['metadata', 'html'],
142
+ fullPrerenderFilename,
98
143
  };
99
- logger.info('[prerender:artifacts]', JSON.stringify({
100
- route,
101
- prelude: artifact.html,
102
- postponed: artifact.postponed ?? null,
103
- metadata: {
104
- schema: artifact.schema,
105
- route: artifact.route,
106
- createdAt: artifact.createdAt,
107
- mode: artifact.mode,
108
- },
109
- }));
110
- logger.info('[prerender]', `${route} (ppr)`);
111
- continue;
144
+ logger.info(`[prerender]: ${route} (full)`);
112
145
  }
113
- // @todo: hash files
114
- // full prerender still keeps metadata so preview knows to serve saved html
115
- await fs.mkdir(artifactDir, { recursive: true });
116
- await Bun.write(path.join(artifactDir, 'metadata.json'), JSON.stringify({
117
- schema: artifact.schema,
118
- route: artifact.route,
119
- createdAt: artifact.createdAt,
120
- mode: artifact.mode,
121
- }));
122
- const routePath = route.replace(/^\//, '').replace(/\/$/, '');
123
- const outPath = route === '/'
124
- ? path.join(outDir, 'index.html')
125
- : manifest.trailingSlash === 'always'
126
- ? path.join(outDir, routePath, 'index.html')
127
- : path.join(outDir, `${routePath}.html`);
128
- // remove the old file shape for this route so switching trailingSlash mode does not leave
129
- // both variants behind. we have to do this before writing the new file so that if the
130
- // route shape changes, we still remove the old one instead of leaving in the output
131
- const alternateOutPath = route === '/'
132
- ? null
133
- : manifest.trailingSlash === 'always'
134
- ? path.join(outDir, `${routePath}.html`)
135
- : path.join(outDir, routePath, 'index.html');
136
- if (alternateOutPath) {
137
- // remove the old file shape so switching trailingSlash mode
138
- // does not leave both variants behind
139
- await Promise.all([
140
- fs.rm(alternateOutPath, { force: true }),
141
- fs.rm(`${alternateOutPath}.br`, { force: true }),
142
- ]);
143
- await fs.rmdir(path.dirname(alternateOutPath)).catch(() => { });
146
+ catch (err) {
147
+ logger.error(`[prerender]: Failed ${route}: ${err}. This often means unresolved async work (for example external fetches or dynamic rendering in full mode).`);
144
148
  }
145
- await fs.mkdir(path.dirname(outPath), { recursive: true });
146
- await Bun.write(outPath, artifact.html);
147
- artifactManifestRoutes[route] = {
148
- mode: artifact.mode,
149
- createdAt: artifact.createdAt,
150
- files: ['metadata', 'html'],
151
- };
152
- logger.info('[prerender]', `${route} (full)`);
153
- }
154
- catch (err) {
155
- logger.error('[prerender]', `failed ${route}: ${err}. This often means unresolved async work (for example external fetches or dynamic rendering in full mode).`);
156
- }
149
+ });
157
150
  }
151
+ await Promise.all(pendingWrites);
158
152
  // write one manifest for the saved prerender files after all routes finish
159
153
  await fs.mkdir(artifactRoot, { recursive: true });
160
154
  await Bun.write(Prerender.Artifact.getManifestPath(outDir), JSON.stringify({
@@ -224,7 +218,7 @@ async function preview() {
224
218
  await fs.access(rscEntry);
225
219
  }
226
220
  catch (err) {
227
- logger.error(`[preview] missing ${path.relative(cwd, rscEntry)} - run \`${Solas.Config.SLUG} build\` first`, err);
221
+ logger.error(`[preview] missing ${path.relative(cwd, rscEntry)} - run \`${Solas.Config.SLUG} build\` from this project directory first`, err);
228
222
  process.exit(1);
229
223
  }
230
224
  const { default: app } = await import(/* @vite-ignore */ rscEntry);
@@ -1 +1 @@
1
- export { ErrorBoundary } from './internal/ui/error-boundary';
1
+ export { ErrorBoundary } from './internal/ui/error-boundary.js';
@@ -1 +1 @@
1
- export { ErrorBoundary } from './internal/ui/error-boundary';
1
+ export { ErrorBoundary } from './internal/ui/error-boundary.js';
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { PluginOption } from 'vite';
2
- import type { PluginConfig } from './types';
2
+ import type { PluginConfig } from './types.js';
3
3
  declare function solas(c: PluginConfig): PluginOption[];
4
4
  export default solas;
5
- export { Solas } from './solas';
6
5
  export type * from './solas.d.ts';
7
- export type * from './types';
6
+ export { Solas } from './solas.js';
7
+ export type * from './types.js';
package/dist/index.js CHANGED
@@ -2,16 +2,15 @@ import fsSync from 'node:fs';
2
2
  import fs from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
  import rsc from '@vitejs/plugin-rsc';
5
- import { Solas } from './solas';
6
- import { ExportReader } from './utils/export-reader';
7
- import { Format } from './utils/format';
8
- import { Logger } from './utils/logger';
9
- import { Time } from './utils/time';
10
- import { Build } from './internal/build';
11
- import { writeConfig } from './internal/codegen/config';
12
- import { writeBrowserEntry, writeRSCEntry, writeSSREntry, } from './internal/codegen/environments';
13
- import { writeManifest } from './internal/codegen/manifest';
14
- import { writeMaps } from './internal/codegen/maps';
5
+ import { ExportReader } from './utils/export-reader.js';
6
+ import { Logger } from './utils/logger.js';
7
+ import { Time } from './utils/time.js';
8
+ import { Build } from './internal/build.js';
9
+ import { writeConfig } from './internal/codegen/config.js';
10
+ import { writeBrowserEntry, writeRSCEntry, writeSSREntry, } from './internal/codegen/environments.js';
11
+ import { writeManifest } from './internal/codegen/manifest.js';
12
+ import { writeMaps } from './internal/codegen/maps.js';
13
+ import { Solas } from './solas.js';
15
14
  const DEFAULT_CONFIG = {
16
15
  precompress: true,
17
16
  prerender: false,
@@ -104,9 +103,6 @@ function solas(c) {
104
103
  // early return if nothing has changed
105
104
  if (!changed.length)
106
105
  return;
107
- await Promise.all(changed.map(filePath => Format.run(filePath).catch(err => {
108
- logger.error(`[build] Failed to format file: ${filePath}`, err);
109
- })));
110
106
  return changed;
111
107
  }
112
108
  let rebuildRunning = false;
@@ -248,4 +244,4 @@ function solas(c) {
248
244
  ];
249
245
  }
250
246
  export default solas;
251
- export { Solas } from './solas';
247
+ export { Solas } from './solas.js';
@@ -1,4 +1,4 @@
1
- import type { BuildContext, Endpoint, PluginConfig, Segment } from '../types';
1
+ import type { BuildContext, Endpoint, PluginConfig, Segment } from '../types.js';
2
2
  /**
3
3
  * Types, constants, and the Finder class for route discovery and manifest generation.
4
4
  * The Finder walks the app directory, builds inheritance chains, and transforms
@@ -1,9 +1,9 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
- import { Solas } from '../solas';
4
- import { Logger } from '../utils/logger';
5
- import { normalisePathname } from './router/utils';
6
- import { Prerender } from './prerender';
3
+ import { Solas } from '../solas.js';
4
+ import { Logger } from '../utils/logger.js';
5
+ import { normalisePathname } from './router/utils.js';
6
+ import { Prerender } from './prerender.js';
7
7
  export { Build };
8
8
  /**
9
9
  * Types, constants, and the Finder class for route discovery and manifest generation.
@@ -1,4 +1,4 @@
1
- import type { PluginConfig } from '../../types';
1
+ import type { PluginConfig } from '../../types.js';
2
2
  /**
3
3
  * Generates the code to create an exported config object
4
4
  */
@@ -1,19 +1,19 @@
1
- import { Solas } from '../../solas';
2
- import { AUTOGEN_MSG } from './utils';
1
+ import { Solas } from '../../solas.js';
2
+ import { AUTOGEN_MSG, toSourceLiteral } from './utils.js';
3
3
  /**
4
4
  * Generates the code to create an exported config object
5
5
  */
6
6
  export function writeConfig(config) {
7
7
  return `
8
- ${AUTOGEN_MSG}
8
+ ${AUTOGEN_MSG}
9
9
 
10
- import type { PluginConfig } from '${Solas.Config.PKG_NAME}'
11
- import { Logger } from '${Solas.Config.PKG_NAME}/utils/logger'
10
+ import type { PluginConfig } from '${Solas.Config.PKG_NAME}'
11
+ import { Logger } from '${Solas.Config.PKG_NAME}/utils/logger'
12
12
 
13
- const config = ${JSON.stringify(config, null, 2)} as const satisfies PluginConfig
13
+ const config = ${toSourceLiteral(config)} as const satisfies PluginConfig
14
14
 
15
- if (config.logger?.level) Logger.defaultLevel = config.logger.level
16
-
17
- export { config }
18
- `.trim();
15
+ if (config.logger?.level) Logger.defaultLevel = config.logger.level
16
+
17
+ export { config }
18
+ `.trim();
19
19
  }
@@ -1,42 +1,46 @@
1
- import { Solas } from '../../solas';
2
- import { AUTOGEN_MSG } from './utils';
1
+ import { Solas } from '../../solas.js';
2
+ import { AUTOGEN_MSG } from './utils.js';
3
3
  /**
4
4
  * Generates the RSC entry code
5
5
  */
6
6
  export function writeRSCEntry() {
7
7
  return `
8
- ${AUTOGEN_MSG}
8
+ ${AUTOGEN_MSG}
9
9
 
10
- import { createHandler } from '${Solas.Config.PKG_NAME}/env/rsc'
10
+ import { createHandler } from '${Solas.Config.PKG_NAME}/env/rsc'
11
+ import { Prerender } from '${Solas.Config.PKG_NAME}/prerender'
12
+ import { Solas } from '${Solas.Config.PKG_NAME}'
11
13
 
12
- import { manifest } from './manifest'
13
- import { importMap } from './maps'
14
- import { config } from './config'
14
+ import { manifest } from './manifest.js'
15
+ import { importMap } from './maps.js'
16
+ import { config } from './config.js'
15
17
 
16
- export default createHandler(config, manifest, importMap)
18
+ const artifactManifest = await Prerender.Artifact.loadManifest(Solas.Config.OUT_DIR)
17
19
 
18
- import.meta.hot?.accept()
19
- `.trim();
20
+ export default createHandler(config, manifest, importMap, artifactManifest)
21
+
22
+ import.meta.hot?.accept()
23
+ `.trim();
20
24
  }
21
25
  /**
22
26
  * Generates the SSR entry code
23
27
  */
24
28
  export function writeSSREntry() {
25
29
  return `
26
- ${AUTOGEN_MSG}
27
-
28
- export { prerender, resume, ssr } from '${Solas.Config.PKG_NAME}/env/ssr'
29
- `.trim();
30
+ ${AUTOGEN_MSG}
31
+
32
+ export { prerender, resume, ssr } from '${Solas.Config.PKG_NAME}/env/ssr'
33
+ `.trim();
30
34
  }
31
35
  /**
32
36
  * Generates the browser entry code
33
37
  */
34
38
  export function writeBrowserEntry() {
35
39
  return `
36
- ${AUTOGEN_MSG}
40
+ ${AUTOGEN_MSG}
37
41
 
38
- import { browser } from '${Solas.Config.PKG_NAME}/env/browser'
42
+ import { browser } from '${Solas.Config.PKG_NAME}/env/browser'
39
43
 
40
- browser()
41
- `.trim();
44
+ browser()
45
+ `.trim();
42
46
  }
@@ -1,4 +1,4 @@
1
- import type { Manifest } from '../../types';
1
+ import type { Manifest } from '../../types.js';
2
2
  /**
3
3
  * Generates the code to create an exported manifest object
4
4
  */
@@ -1,15 +1,14 @@
1
- import { Solas } from '../../solas';
2
- import { AUTOGEN_MSG } from './utils';
1
+ import { Solas } from '../../solas.js';
2
+ import { AUTOGEN_MSG, toSourceLiteral } from './utils.js';
3
3
  /**
4
4
  * Generates the code to create an exported manifest object
5
5
  */
6
6
  export function writeManifest(manifest) {
7
7
  return `
8
- ${AUTOGEN_MSG}
8
+ ${AUTOGEN_MSG}
9
9
 
10
- import type { Manifest } from '${Solas.Config.PKG_NAME}'
10
+ import type { Manifest } from '${Solas.Config.PKG_NAME}'
11
11
 
12
- export const manifest =
13
- ${JSON.stringify(manifest, null, 2)} as const satisfies Manifest
14
- `.trim();
12
+ export const manifest = ${toSourceLiteral(manifest)} as const satisfies Manifest
13
+ `.trim();
15
14
  }
@@ -1,4 +1,4 @@
1
- import type { Build } from '../build';
1
+ import type { Build } from '../build.js';
2
2
  /**
3
3
  * Generates the import map for all route components, endpoints, layouts, shells, and middlewares
4
4
  */
@@ -1,5 +1,5 @@
1
- import { Solas } from '../../solas';
2
- import { AUTOGEN_MSG } from './utils';
1
+ import { Solas } from '../../solas.js';
2
+ import { AUTOGEN_MSG, toIdentifier, toIdentifierList, toRelativeModuleSpecifier, toStringLiteral, } from './utils.js';
3
3
  /**
4
4
  * Generates the import map for all route components, endpoints, layouts, shells, and middlewares
5
5
  */
@@ -7,69 +7,80 @@ export function writeMaps(imports, modules) {
7
7
  const statics = [
8
8
  ...imports.endpoints.static.entries().map(([k, v]) => {
9
9
  const [, method] = k.split('_');
10
- return `import { ${method.toUpperCase()} as ${k}} from ${JSON.stringify(v)}`.trim();
10
+ return `import { ${toIdentifier(method.toUpperCase(), `endpoint export for ${k}`)} as ${toIdentifier(k, `endpoint alias for ${v}`)} } from ${toRelativeModuleSpecifier(v, `endpoint import for ${k}`)}`;
11
11
  }),
12
12
  ...imports.components.static
13
13
  .entries()
14
- .map(([k, v]) => `import * as ${k} from ${JSON.stringify(v)}`.trim()),
14
+ .map(([k, v]) => `import * as ${toIdentifier(k, `component alias for ${v}`)} from ${toRelativeModuleSpecifier(v, `component import for ${k}`)}`),
15
15
  ...imports.middlewares.static
16
16
  .entries()
17
- .map(([k, v]) => `import { middleware as ${k} } from ${JSON.stringify(v)}`.trim()),
17
+ .map(([k, v]) => `import { middleware as ${toIdentifier(k, `middleware alias for ${v}`)} } from ${toRelativeModuleSpecifier(v, `middleware import for ${k}`)}`),
18
18
  ];
19
19
  const dynamics = [
20
20
  ...imports.components.dynamic
21
21
  .entries()
22
- .map(([k, v]) => `export const ${k} = () => import(${JSON.stringify(v)})`.trim()),
22
+ .map(([k, v]) => `export const ${toIdentifier(k, `dynamic component alias for ${v}`)} = () => import(${toRelativeModuleSpecifier(v, `dynamic import for ${k}`)})`),
23
23
  ];
24
- const map = Object.entries(modules).map(([id, m]) => {
24
+ const map = Object.entries(modules).map(([moduleId, m]) => {
25
25
  const parts = [];
26
- if (m.shellId)
27
- parts.push(`shell: ${m.shellId}`);
26
+ if (m.shellId) {
27
+ parts.push(`shell: ${toIdentifier(m.shellId, `shell id for ${moduleId}`)}`);
28
+ }
28
29
  if (m.layoutIds?.length) {
29
- const layouts = m.layoutIds.map(id => (id === null ? 'null' : id)).join(', ');
30
+ const layouts = toIdentifierList(m.layoutIds, `layouts for ${moduleId}`);
30
31
  parts.push(`layouts: [${layouts}]`);
31
32
  }
32
- if (m.pageId)
33
- parts.push(`page: ${m.pageId}`);
34
- if (m.endpointId)
35
- parts.push(`endpoint: ${m.endpointId}`);
33
+ if (m.pageId) {
34
+ parts.push(`page: ${toIdentifier(m.pageId, `page id for ${moduleId}`)}`);
35
+ }
36
+ if (m.endpointId) {
37
+ parts.push(`endpoint: ${toIdentifier(m.endpointId, `endpoint id for ${moduleId}`)}`);
38
+ }
36
39
  if (m['401Ids']?.length) {
37
- const unauthorized = m['401Ids'].map(id => (id === null ? 'null' : id)).join(', ');
40
+ const unauthorized = toIdentifierList(m['401Ids'], `401s for ${moduleId}`);
38
41
  parts.push(`'401s': [${unauthorized}]`);
39
42
  }
40
43
  if (m['403Ids']?.length) {
41
- const forbidden = m['403Ids'].map(id => (id === null ? 'null' : id)).join(', ');
44
+ const forbidden = toIdentifierList(m['403Ids'], `403s for ${moduleId}`);
42
45
  parts.push(`'403s': [${forbidden}]`);
43
46
  }
44
47
  if (m['404Ids']?.length) {
45
- const notFounds = m['404Ids'].map(id => (id === null ? 'null' : id)).join(', ');
48
+ const notFounds = toIdentifierList(m['404Ids'], `404s for ${moduleId}`);
46
49
  parts.push(`'404s': [${notFounds}]`);
47
50
  }
48
51
  if (m['500Ids']?.length) {
49
- const serverErrors = m['500Ids'].map(id => (id === null ? 'null' : id)).join(', ');
52
+ const serverErrors = toIdentifierList(m['500Ids'], `500s for ${moduleId}`);
50
53
  parts.push(`'500s': [${serverErrors}]`);
51
54
  }
52
55
  if (m.loadingIds?.length) {
53
- const loaders = m.loadingIds.map(id => (id === null ? 'null' : id)).join(', ');
56
+ const loaders = toIdentifierList(m.loadingIds, `loaders for ${moduleId}`);
54
57
  parts.push(`loaders: [${loaders}]`);
55
58
  }
56
59
  if (m.middlewareIds?.length) {
57
- const middleware = m.middlewareIds.map(id => (id === null ? 'null' : id)).join(', ');
60
+ const middleware = toIdentifierList(m.middlewareIds, `middlewares for ${moduleId}`);
58
61
  parts.push(`middlewares: [${middleware}]`);
59
62
  }
60
- return `${JSON.stringify(id)}: { ${parts.join(', ')} }`;
63
+ if (parts.length === 0)
64
+ return `\t${toStringLiteral(moduleId)}: {}`;
65
+ return `\t${toStringLiteral(moduleId)}: {\n\t\t${parts.join(',\n\t\t')}\n\t}`;
61
66
  });
62
67
  return `
63
- ${AUTOGEN_MSG}
68
+ ${AUTOGEN_MSG}
64
69
 
65
70
  import type { ImportMap } from '${Solas.Config.PKG_NAME}'
66
71
 
67
- ${statics.join('\n')}
68
-
69
- ${dynamics.join('\n')}
72
+ ${statics.length
73
+ ? `${statics.join('\n')}
74
+
75
+ `
76
+ : ''}${dynamics.length
77
+ ? `${dynamics.join('\n')}
78
+
79
+ `
80
+ : ''}
70
81
 
71
82
  export const importMap = {
72
- ${map.join(',\n')}
83
+ ${map.join(',\n')}
73
84
  } as const satisfies ImportMap
74
- `;
85
+ `.trim();
75
86
  }
@@ -1 +1,21 @@
1
1
  export declare const AUTOGEN_MSG = "// auto-generated by Solas";
2
+ /**
3
+ * Validate an identifier before writing it into generated source unquoted
4
+ */
5
+ export declare function toIdentifier(value: string, label: string): string;
6
+ /**
7
+ * Validate and quote a relative specifier before embedding it in generated imports
8
+ */
9
+ export declare function toRelativeModuleSpecifier(value: string, label: string): string;
10
+ /**
11
+ * Validate a nullable identifier list whilst preserving explicit null holes
12
+ */
13
+ export declare function toIdentifierList(values: readonly (string | null)[], label: string): string;
14
+ /**
15
+ * Escape text into a safe string literal for generated source
16
+ */
17
+ export declare function toStringLiteral(value: string, quoteStyle?: "'" | '"'): string;
18
+ /**
19
+ * Emit readable ts source for generated config and manifest data
20
+ */
21
+ export declare function toSourceLiteral(value: unknown, level?: number): string;