@jk2908/solas 0.1.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 (105) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +333 -0
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +219 -0
  5. package/dist/error-boundary.d.ts +1 -0
  6. package/dist/error-boundary.js +1 -0
  7. package/dist/index.d.ts +7 -0
  8. package/dist/index.js +235 -0
  9. package/dist/internal/build.d.ts +104 -0
  10. package/dist/internal/build.js +633 -0
  11. package/dist/internal/codegen/config.d.ts +5 -0
  12. package/dist/internal/codegen/config.js +19 -0
  13. package/dist/internal/codegen/environments.d.ts +12 -0
  14. package/dist/internal/codegen/environments.js +42 -0
  15. package/dist/internal/codegen/manifest.d.ts +5 -0
  16. package/dist/internal/codegen/manifest.js +15 -0
  17. package/dist/internal/codegen/maps.d.ts +5 -0
  18. package/dist/internal/codegen/maps.js +75 -0
  19. package/dist/internal/codegen/utils.d.ts +1 -0
  20. package/dist/internal/codegen/utils.js +2 -0
  21. package/dist/internal/env/browser.d.ts +4 -0
  22. package/dist/internal/env/browser.js +58 -0
  23. package/dist/internal/env/request-context.d.ts +19 -0
  24. package/dist/internal/env/request-context.js +2 -0
  25. package/dist/internal/env/rsc.d.ts +39 -0
  26. package/dist/internal/env/rsc.js +368 -0
  27. package/dist/internal/env/ssr.d.ts +42 -0
  28. package/dist/internal/env/ssr.js +149 -0
  29. package/dist/internal/env/utils.d.ts +2 -0
  30. package/dist/internal/env/utils.js +28 -0
  31. package/dist/internal/metadata.d.ts +81 -0
  32. package/dist/internal/metadata.js +185 -0
  33. package/dist/internal/navigation/http-exception-boundary.d.ts +12 -0
  34. package/dist/internal/navigation/http-exception-boundary.js +48 -0
  35. package/dist/internal/navigation/http-exception.d.ts +33 -0
  36. package/dist/internal/navigation/http-exception.js +45 -0
  37. package/dist/internal/navigation/link.d.ts +13 -0
  38. package/dist/internal/navigation/link.js +63 -0
  39. package/dist/internal/navigation/redirect-boundary.d.ts +12 -0
  40. package/dist/internal/navigation/redirect-boundary.js +39 -0
  41. package/dist/internal/navigation/redirect.d.ts +21 -0
  42. package/dist/internal/navigation/redirect.js +63 -0
  43. package/dist/internal/navigation/use-search-params.d.ts +1 -0
  44. package/dist/internal/navigation/use-search-params.js +13 -0
  45. package/dist/internal/prerender.d.ts +151 -0
  46. package/dist/internal/prerender.js +422 -0
  47. package/dist/internal/render/head.d.ts +4 -0
  48. package/dist/internal/render/head.js +38 -0
  49. package/dist/internal/render/tree.d.ts +47 -0
  50. package/dist/internal/render/tree.js +108 -0
  51. package/dist/internal/router/create-router.d.ts +6 -0
  52. package/dist/internal/router/create-router.js +95 -0
  53. package/dist/internal/router/pattern.d.ts +8 -0
  54. package/dist/internal/router/pattern.js +31 -0
  55. package/dist/internal/router/prefetcher.d.ts +47 -0
  56. package/dist/internal/router/prefetcher.js +90 -0
  57. package/dist/internal/router/resolver.d.ts +174 -0
  58. package/dist/internal/router/resolver.js +356 -0
  59. package/dist/internal/router/router-context.d.ts +11 -0
  60. package/dist/internal/router/router-context.js +7 -0
  61. package/dist/internal/router/router-provider.d.ts +6 -0
  62. package/dist/internal/router/router-provider.js +131 -0
  63. package/dist/internal/router/router.d.ts +79 -0
  64. package/dist/internal/router/router.js +417 -0
  65. package/dist/internal/router/use-router.d.ts +5 -0
  66. package/dist/internal/router/use-router.js +5 -0
  67. package/dist/internal/server/cookies.d.ts +6 -0
  68. package/dist/internal/server/cookies.js +17 -0
  69. package/dist/internal/server/dynamic.d.ts +9 -0
  70. package/dist/internal/server/dynamic.js +22 -0
  71. package/dist/internal/server/headers.d.ts +5 -0
  72. package/dist/internal/server/headers.js +19 -0
  73. package/dist/internal/server/url.d.ts +5 -0
  74. package/dist/internal/server/url.js +16 -0
  75. package/dist/internal/ui/defaults/error.d.ts +4 -0
  76. package/dist/internal/ui/defaults/error.js +6 -0
  77. package/dist/internal/ui/error-boundary.d.ts +26 -0
  78. package/dist/internal/ui/error-boundary.js +41 -0
  79. package/dist/navigation.d.ts +6 -0
  80. package/dist/navigation.js +6 -0
  81. package/dist/prerender.d.ts +1 -0
  82. package/dist/prerender.js +1 -0
  83. package/dist/router.d.ts +4 -0
  84. package/dist/router.js +4 -0
  85. package/dist/server.d.ts +4 -0
  86. package/dist/server.js +4 -0
  87. package/dist/solas.d.ts +32 -0
  88. package/dist/solas.js +125 -0
  89. package/dist/types.d.ts +93 -0
  90. package/dist/types.js +1 -0
  91. package/dist/utils/compress.d.ts +11 -0
  92. package/dist/utils/compress.js +76 -0
  93. package/dist/utils/context.d.ts +6 -0
  94. package/dist/utils/context.js +25 -0
  95. package/dist/utils/cookies.d.ts +3 -0
  96. package/dist/utils/cookies.js +35 -0
  97. package/dist/utils/export-reader.d.ts +29 -0
  98. package/dist/utils/export-reader.js +117 -0
  99. package/dist/utils/format.d.ts +6 -0
  100. package/dist/utils/format.js +72 -0
  101. package/dist/utils/logger.d.ts +52 -0
  102. package/dist/utils/logger.js +105 -0
  103. package/dist/utils/time.d.ts +4 -0
  104. package/dist/utils/time.js +29 -0
  105. package/package.json +111 -0
package/dist/index.js ADDED
@@ -0,0 +1,235 @@
1
+ import fsSync from 'node:fs';
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
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';
15
+ const DEFAULT_CONFIG = {
16
+ precompress: true,
17
+ prerender: false,
18
+ trailingSlash: false,
19
+ };
20
+ function solas(c) {
21
+ const config = Solas.Config.validate({
22
+ ...DEFAULT_CONFIG,
23
+ ...c,
24
+ url: c.url ?? process.env.VITE_APP_URL?.toString() ?? process.env.APP_URL?.toString(),
25
+ });
26
+ if (config.logger?.level)
27
+ Logger.defaultLevel = config.logger.level;
28
+ const logger = new Logger();
29
+ const exportReader = new ExportReader();
30
+ const buildContext = {
31
+ prerenderedRoutes: new Set(),
32
+ exportReader,
33
+ };
34
+ // cache for file contents to avoid unnecessary readFile invocations
35
+ const fileCache = new Map();
36
+ async function maybeWrite(filePath, content) {
37
+ try {
38
+ const cached = fileCache.get(filePath);
39
+ if (cached === content) {
40
+ // if content is unchanged and file exists, skip write
41
+ if (await Bun.file(filePath).exists())
42
+ return null;
43
+ // else, file is missing but cached content is the same as
44
+ // last time we saw it, write it
45
+ await Bun.write(filePath, content);
46
+ fileCache.set(filePath, content);
47
+ return path.relative(process.cwd(), filePath);
48
+ }
49
+ const curr = cached ?? (await fs.readFile(filePath, 'utf-8'));
50
+ fileCache.set(filePath, curr);
51
+ // no change, bail
52
+ if (curr === content)
53
+ return null;
54
+ try {
55
+ await Bun.write(filePath, content);
56
+ fileCache.set(filePath, content);
57
+ return path.relative(process.cwd(), filePath);
58
+ }
59
+ catch (err) {
60
+ logger.error(`[maybeWrite] Failed to write file: ${filePath}`, err);
61
+ return null;
62
+ }
63
+ }
64
+ catch (err) {
65
+ // file doesn't exist, write it
66
+ if (err instanceof Error && 'code' in err && err.code === 'ENOENT') {
67
+ try {
68
+ await Bun.write(filePath, content);
69
+ fileCache.set(filePath, content);
70
+ return path.relative(process.cwd(), filePath);
71
+ }
72
+ catch (err) {
73
+ logger.error(`[maybeWrite] Failed to write file: ${filePath}`, err);
74
+ return null;
75
+ }
76
+ }
77
+ logger.error(`[maybeWrite] Failed to read file: ${filePath}`, err);
78
+ return null;
79
+ }
80
+ }
81
+ async function build() {
82
+ const cwd = process.cwd();
83
+ const routesDir = path.join(cwd, Solas.Config.APP_DIR);
84
+ const generatedDir = path.join(cwd, Solas.Config.GENERATED_DIR);
85
+ await Promise.all([
86
+ fs.mkdir(routesDir, { recursive: true }),
87
+ fs.mkdir(generatedDir, { recursive: true }),
88
+ ]);
89
+ const processor = new Build.Finder(buildContext, config);
90
+ const { manifest, prerenderedRoutes, imports, modules } = await processor.run();
91
+ // set prerenderable routes in context for use in closeBundle
92
+ buildContext.prerenderedRoutes = prerenderedRoutes;
93
+ const files = [
94
+ ['config.ts', writeConfig(config)],
95
+ ['manifest.ts', writeManifest(manifest)],
96
+ ['maps.ts', writeMaps(imports, modules)],
97
+ [Solas.Config.ENTRY_RSC, writeRSCEntry()],
98
+ [Solas.Config.ENTRY_SSR, writeSSREntry()],
99
+ [Solas.Config.ENTRY_BROWSER, writeBrowserEntry()],
100
+ ];
101
+ const writes = await Promise.all(files.map(([file, content]) => maybeWrite(path.join(generatedDir, file), content)));
102
+ const changed = writes.filter(n => n !== null);
103
+ // early return if nothing has changed
104
+ if (!changed.length)
105
+ return;
106
+ await Promise.all(changed.map(filePath => Format.run(filePath).catch(() => {
107
+ logger.error(`[build] Failed to format file: ${filePath}`);
108
+ })));
109
+ return changed;
110
+ }
111
+ let rebuildRunning = false;
112
+ let rebuildQueued = false;
113
+ let rebuildReason = 'change';
114
+ // normalise all watcher paths to forward slashes so path checks behave the
115
+ // same on Windows and POSIX
116
+ const WATCH_CWD = process.cwd().replace(/\\/g, '/');
117
+ const WATCH_APP_ROOT = `${WATCH_CWD}/${Solas.Config.APP_DIR}/`;
118
+ // convert watcher paths to a consistent slash format before comparing them
119
+ const normaliseWatchPath = (p) => p.replace(/\\/g, '/');
120
+ // resolve relative watcher paths against the project root so prefix checks are reliable
121
+ const toAbsoluteWatchPath = (p) => normaliseWatchPath(path.isAbsolute(p) ? p : path.join(WATCH_CWD, p));
122
+ // only route changes inside the app directory should trigger a rebuild
123
+ const inAppDir = (p) => toAbsoluteWatchPath(p).startsWith(WATCH_APP_ROOT);
124
+ // route graph rebuilds only care about framework route files, with endpoint
125
+ // edits needing special treatment because verb exports can change in-place
126
+ const routeFile = /\/\+(layout|page|401|403|404|500|loading|middleware|endpoint)\.(t|j)sx?$/;
127
+ const endpointFile = /\/\+endpoint\.(t|j)sx?$/;
128
+ const rebuild = Time.debounce((event, p) => {
129
+ const queue = () => {
130
+ void (async () => {
131
+ // collapse bursts of file events into one active rebuild plus a single
132
+ // queued rerun when changes land mid-build
133
+ if (rebuildRunning) {
134
+ rebuildQueued = true;
135
+ return;
136
+ }
137
+ rebuildRunning = true;
138
+ do {
139
+ rebuildQueued = false;
140
+ try {
141
+ const changed = await build();
142
+ if (changed)
143
+ logger.info('[watch]', `route graph rebuilt (${rebuildReason})`);
144
+ }
145
+ catch (err) {
146
+ logger.error('[watch] route rebuild failed', err);
147
+ }
148
+ } while (rebuildQueued);
149
+ rebuildRunning = false;
150
+ })();
151
+ };
152
+ // ignore anything outside the app dir
153
+ if (!inAppDir(p))
154
+ return;
155
+ const file = toAbsoluteWatchPath(p);
156
+ // directory adds/removals can change route structure immediately
157
+ if (event === 'addDir' || event === 'unlinkDir') {
158
+ rebuildReason = `${event}: ${path.relative(WATCH_CWD, file)}`;
159
+ queue();
160
+ return;
161
+ }
162
+ // non-route files do not affect generated route artifacts
163
+ if (!routeFile.test(file))
164
+ return;
165
+ // content changes only matter for route graph when endpoint verbs change
166
+ if (event === 'change' && !endpointFile.test(file))
167
+ return;
168
+ rebuildReason = `${event}: ${path.relative(WATCH_CWD, file)}`;
169
+ queue();
170
+ }, 75);
171
+ const plugin = {
172
+ name: Solas.Config.NAME,
173
+ enforce: 'pre',
174
+ async config(viteConfig) {
175
+ await build();
176
+ const pkg = JSON.parse(fsSync.readFileSync(new URL('../package.json', import.meta.url), 'utf-8'));
177
+ if (typeof pkg.version !== 'string' || pkg.version.length === 0) {
178
+ throw new Error(`Missing ${Solas.Config.NAME} package version`);
179
+ }
180
+ viteConfig.build ??= {};
181
+ viteConfig.build.outDir = Solas.Config.OUT_DIR;
182
+ viteConfig.build.emptyOutDir = true;
183
+ viteConfig.server ??= {};
184
+ viteConfig.server.port = config.port ?? viteConfig.server.port ?? 8787;
185
+ viteConfig.define ??= {};
186
+ viteConfig.define['import.meta.env.APP_URL'] = JSON.stringify(process.env.APP_URL);
187
+ viteConfig.define['import.meta.env.VITE_APP_URL'] = JSON.stringify(process.env.VITE_APP_URL);
188
+ viteConfig.define['import.meta.env.SOLAS_VERSION'] = JSON.stringify(pkg.version);
189
+ viteConfig.resolve ??= {};
190
+ viteConfig.resolve.alias = {
191
+ ...(viteConfig.resolve.alias ?? {}),
192
+ '.solas': path.resolve(process.cwd(), Solas.Config.GENERATED_DIR),
193
+ };
194
+ viteConfig.optimizeDeps ??= {};
195
+ viteConfig.optimizeDeps.exclude = [
196
+ ...(Array.isArray(viteConfig.optimizeDeps.exclude)
197
+ ? viteConfig.optimizeDeps.exclude
198
+ : []),
199
+ 'react-dom/client',
200
+ ];
201
+ },
202
+ configureServer(server) {
203
+ logger.info('[configureServer]', `Watching for changes in ./${Solas.Config.APP_DIR}...`);
204
+ server.watcher
205
+ .on('add', (p) => rebuild('add', p))
206
+ .on('change', (p) => rebuild('change', p))
207
+ .on('unlink', (p) => rebuild('unlink', p))
208
+ .on('addDir', (p) => rebuild('addDir', p))
209
+ .on('unlinkDir', (p) => rebuild('unlinkDir', p));
210
+ },
211
+ async closeBundle() {
212
+ if (process.env.NODE_ENV === 'development')
213
+ return;
214
+ // write build manifest
215
+ const generatedDir = path.join(process.cwd(), Solas.Config.GENERATED_DIR);
216
+ await Bun.write(path.join(generatedDir, 'build.json'), JSON.stringify({
217
+ prerenderedRoutes: Array.from(buildContext.prerenderedRoutes),
218
+ precompress: config.precompress,
219
+ }));
220
+ logger.info('[closeBundle]', 'vite build complete');
221
+ },
222
+ };
223
+ return [
224
+ plugin,
225
+ rsc({
226
+ entries: {
227
+ rsc: `./${Solas.Config.GENERATED_DIR}/${Solas.Config.ENTRY_RSC}`,
228
+ ssr: `./${Solas.Config.GENERATED_DIR}/${Solas.Config.ENTRY_SSR}`,
229
+ client: `./${Solas.Config.GENERATED_DIR}/${Solas.Config.ENTRY_BROWSER}`,
230
+ },
231
+ }),
232
+ ];
233
+ }
234
+ export default solas;
235
+ export { Solas } from './solas';
@@ -0,0 +1,104 @@
1
+ import type { BuildContext, Endpoint, PluginConfig, Segment } from '../types';
2
+ export declare namespace Build {
3
+ type ScanResult = {
4
+ segments: {
5
+ dir: string;
6
+ page?: string;
7
+ layouts: (string | null)[];
8
+ shell: string;
9
+ '401s': (string | null)[];
10
+ '403s': (string | null)[];
11
+ '404s': (string | null)[];
12
+ '500s': (string | null)[];
13
+ loaders: (string | null)[];
14
+ middlewares: (string | null)[];
15
+ }[];
16
+ endpoints: {
17
+ file: string;
18
+ middlewares: (string | null)[];
19
+ }[];
20
+ };
21
+ type Imports = {
22
+ endpoints: {
23
+ static: Map<string, string>;
24
+ };
25
+ components: {
26
+ static: Map<string, string>;
27
+ dynamic: Map<string, string>;
28
+ };
29
+ middlewares: {
30
+ static: Map<string, string>;
31
+ };
32
+ };
33
+ type Modules = Record<string, {
34
+ shellId?: string;
35
+ layoutIds?: (string | null)[];
36
+ pageId?: string;
37
+ '401Ids'?: (string | null)[];
38
+ '403Ids'?: (string | null)[];
39
+ '404Ids'?: (string | null)[];
40
+ '500Ids'?: (string | null)[];
41
+ loadingIds?: (string | null)[];
42
+ middlewareIds?: (string | null)[];
43
+ endpointId?: string;
44
+ }>;
45
+ const EntryKind: {
46
+ readonly SHELL: "$S";
47
+ readonly LAYOUT: "$L";
48
+ readonly PAGE: "$P";
49
+ readonly 401: "$401";
50
+ readonly 403: "$403";
51
+ readonly 404: "$404";
52
+ readonly 500: "$500";
53
+ readonly LOADING: "$LOAD";
54
+ readonly MIDDLEWARE: "$MW";
55
+ readonly ENDPOINT: "$E";
56
+ };
57
+ /**
58
+ * Finder class to process application routes
59
+ */
60
+ class Finder {
61
+ #private;
62
+ readonly buildContext: BuildContext;
63
+ readonly config: PluginConfig;
64
+ constructor(buildContext: BuildContext, config: PluginConfig);
65
+ /**
66
+ * Extracts dynamic parameter names from a file path
67
+ */
68
+ static getParams(file: string): string[];
69
+ /**
70
+ * Get the depth of a route based on slashes
71
+ */
72
+ static getDepth(route: string): number;
73
+ /**
74
+ * Convert a file path to a canonical route.
75
+ */
76
+ static toCanonicalRoute(file: string): string;
77
+ /**
78
+ * Get the import path for a file
79
+ * This finds the relative path from the generated
80
+ * directory to the file, removes the extension and
81
+ * replaces backslashes with forward slashes.
82
+ */
83
+ static getImportPath(file: string): string;
84
+ /**
85
+ * Run the Finder to get the app route and associated data
86
+ * needed for codegen
87
+ */
88
+ run(): Promise<{
89
+ manifest: Record<string, (Endpoint | Segment)[] | Endpoint | Segment>;
90
+ imports: Imports;
91
+ modules: Modules;
92
+ prerenderedRoutes: Set<string>;
93
+ }>;
94
+ /**
95
+ * Process the scanned route data
96
+ */
97
+ process(res: ScanResult): Promise<{
98
+ manifest: Record<string, (Endpoint | Segment)[] | Endpoint | Segment>;
99
+ imports: Imports;
100
+ modules: Modules;
101
+ prerenderedRoutes: Set<string>;
102
+ }>;
103
+ }
104
+ }