vorzelajs 0.0.1

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 (96) hide show
  1. package/README.md +188 -0
  2. package/bin/vorzelajs.mjs +2 -0
  3. package/dist/analytics.d.ts +132 -0
  4. package/dist/analytics.d.ts.map +1 -0
  5. package/dist/analytics.js +690 -0
  6. package/dist/cli/build.d.ts +2 -0
  7. package/dist/cli/build.d.ts.map +1 -0
  8. package/dist/cli/build.js +22 -0
  9. package/dist/cli/dev.d.ts +2 -0
  10. package/dist/cli/dev.d.ts.map +1 -0
  11. package/dist/cli/dev.js +93 -0
  12. package/dist/cli/index.d.ts +3 -0
  13. package/dist/cli/index.d.ts.map +1 -0
  14. package/dist/cli/index.js +29 -0
  15. package/dist/cli/serve.d.ts +2 -0
  16. package/dist/cli/serve.d.ts.map +1 -0
  17. package/dist/cli/serve.js +43 -0
  18. package/dist/cookie.d.ts +33 -0
  19. package/dist/cookie.d.ts.map +1 -0
  20. package/dist/cookie.js +198 -0
  21. package/dist/debug/error-stack.d.ts +14 -0
  22. package/dist/debug/error-stack.d.ts.map +1 -0
  23. package/dist/debug/error-stack.js +52 -0
  24. package/dist/internal/document.d.ts +12 -0
  25. package/dist/internal/document.d.ts.map +1 -0
  26. package/dist/internal/document.jsx +56 -0
  27. package/dist/internal/entry-client.d.ts +2 -0
  28. package/dist/internal/entry-client.d.ts.map +1 -0
  29. package/dist/internal/entry-client.jsx +8 -0
  30. package/dist/internal/entry-server.d.ts +14 -0
  31. package/dist/internal/entry-server.d.ts.map +1 -0
  32. package/dist/internal/entry-server.jsx +71 -0
  33. package/dist/runtime/create-route.d.ts +8 -0
  34. package/dist/runtime/create-route.d.ts.map +1 -0
  35. package/dist/runtime/create-route.js +18 -0
  36. package/dist/runtime/head.d.ts +10 -0
  37. package/dist/runtime/head.d.ts.map +1 -0
  38. package/dist/runtime/head.js +111 -0
  39. package/dist/runtime/index.d.ts +6 -0
  40. package/dist/runtime/index.d.ts.map +1 -0
  41. package/dist/runtime/index.jsx +4 -0
  42. package/dist/runtime/navigation.d.ts +36 -0
  43. package/dist/runtime/navigation.d.ts.map +1 -0
  44. package/dist/runtime/navigation.js +70 -0
  45. package/dist/runtime/path.d.ts +11 -0
  46. package/dist/runtime/path.d.ts.map +1 -0
  47. package/dist/runtime/path.js +80 -0
  48. package/dist/runtime/resolve.d.ts +11 -0
  49. package/dist/runtime/resolve.d.ts.map +1 -0
  50. package/dist/runtime/resolve.js +449 -0
  51. package/dist/runtime/runtime.d.ts +40 -0
  52. package/dist/runtime/runtime.d.ts.map +1 -0
  53. package/dist/runtime/runtime.jsx +779 -0
  54. package/dist/runtime/search.d.ts +23 -0
  55. package/dist/runtime/search.d.ts.map +1 -0
  56. package/dist/runtime/search.js +178 -0
  57. package/dist/runtime/server.d.ts +10 -0
  58. package/dist/runtime/server.d.ts.map +1 -0
  59. package/dist/runtime/server.js +5 -0
  60. package/dist/runtime/types.d.ts +248 -0
  61. package/dist/runtime/types.d.ts.map +1 -0
  62. package/dist/runtime/types.js +1 -0
  63. package/dist/seo.d.ts +16 -0
  64. package/dist/seo.d.ts.map +1 -0
  65. package/dist/seo.js +69 -0
  66. package/dist/server/index.d.ts +53 -0
  67. package/dist/server/index.d.ts.map +1 -0
  68. package/dist/server/index.js +268 -0
  69. package/dist/session.d.ts +23 -0
  70. package/dist/session.d.ts.map +1 -0
  71. package/dist/session.js +58 -0
  72. package/dist/vite/index.d.ts +13 -0
  73. package/dist/vite/index.d.ts.map +1 -0
  74. package/dist/vite/index.js +174 -0
  75. package/dist/vite/routes-plugin.d.ts +4 -0
  76. package/dist/vite/routes-plugin.d.ts.map +1 -0
  77. package/dist/vite/routes-plugin.js +345 -0
  78. package/dist/vite/server-only.d.ts +3 -0
  79. package/dist/vite/server-only.d.ts.map +1 -0
  80. package/dist/vite/server-only.js +342 -0
  81. package/package.json +76 -0
  82. package/templates/bare/README.md +22 -0
  83. package/templates/bare/src/components/counter-card.tsx +19 -0
  84. package/templates/bare/src/routes/__root.tsx +24 -0
  85. package/templates/bare/src/routes/index.tsx +36 -0
  86. package/templates/base/gitignore +4 -0
  87. package/templates/base/public/favicon.svg +5 -0
  88. package/templates/base/tsconfig.json +18 -0
  89. package/templates/modern/README.md +28 -0
  90. package/templates/modern/src/components/counter-card.tsx +19 -0
  91. package/templates/modern/src/routes/__root.tsx +42 -0
  92. package/templates/modern/src/routes/about.tsx +55 -0
  93. package/templates/modern/src/routes/index.tsx +51 -0
  94. package/templates/styling/css/styles.css +269 -0
  95. package/templates/styling/css-modules/styles.css +269 -0
  96. package/templates/styling/tailwind/styles.css +271 -0
@@ -0,0 +1,345 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ const ROUTES_DIR = path.join('src', 'routes');
4
+ const OUTPUT_FILE = path.join('src', 'routeTree.gen.ts');
5
+ const OUTPUT_HYDRATION_FILE = path.join('src', 'routeHydration.gen.ts');
6
+ const ROUTE_FILE_PATTERN = /\.(ts|tsx)$/u;
7
+ const SERVER_ONLY_ROUTE_FILE_PATTERN = /\.server\.(ts|tsx)$/u;
8
+ const SERVER_ONLY_DIR_PATTERN = /[\\/]\.server[\\/]/u;
9
+ const SERVER_ONLY_SPECIFIER_PATTERN = /(?:^|[\\/])\.server(?:[\\/]|$)|\.server(?:$|\.)/u;
10
+ const CLIENT_EVENT_HANDLER_PATTERN = /\bon[A-Z][A-Za-z0-9]*\s*=/u;
11
+ const CLIENT_ROUTER_HOOK_PATTERN = /\b(useNavigate|useSetSearch|Route\.useSetSearch)\b/u;
12
+ const CLIENT_SOLID_HOOK_PATTERN = /\b(createSignal|createEffect|createRenderEffect|createResource|onMount|onCleanup)\b/u;
13
+ const CLIENT_BROWSER_GLOBAL_PATTERN = /\b(window|document|navigator|localStorage|sessionStorage)\s*\./u;
14
+ const CLIENT_BROWSER_FUNCTION_PATTERN = /\b(requestAnimationFrame|matchMedia)\s*\(/u;
15
+ const SOURCE_FILE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx'];
16
+ const IMPORT_EXPORT_PATTERN = /\b(?:import|export)\s+(?!type\b)[\w\s{},*]+?\s+from\s+['"]([^'"]+)['"]/gu;
17
+ const SIDE_EFFECT_IMPORT_PATTERN = /\bimport\s+['"]([^'"]+)['"]/gu;
18
+ const DYNAMIC_IMPORT_PATTERN = /\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)/gu;
19
+ function toPosix(value) {
20
+ return value.split(path.sep).join('/');
21
+ }
22
+ function isPathlessSegment(segment) {
23
+ return segment.startsWith('_') && segment !== '__root';
24
+ }
25
+ function isServerOnlyRouteFile(filePath) {
26
+ return SERVER_ONLY_ROUTE_FILE_PATTERN.test(filePath) || SERVER_ONLY_DIR_PATTERN.test(filePath);
27
+ }
28
+ function isServerOnlyModuleSpecifier(specifier) {
29
+ return SERVER_ONLY_SPECIFIER_PATTERN.test(specifier);
30
+ }
31
+ async function walkFiles(dirPath) {
32
+ try {
33
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
34
+ const nestedFiles = await Promise.all(entries.map(async (entry) => {
35
+ const fullPath = path.join(dirPath, entry.name);
36
+ if (entry.isDirectory()) {
37
+ return walkFiles(fullPath);
38
+ }
39
+ return entry.isFile() ? [fullPath] : [];
40
+ }));
41
+ return nestedFiles.flat();
42
+ }
43
+ catch (error) {
44
+ if (error.code === 'ENOENT') {
45
+ return [];
46
+ }
47
+ throw error;
48
+ }
49
+ }
50
+ function getRouteInfo(filePath, routesDir) {
51
+ const relativePath = toPosix(path.relative(routesDir, filePath));
52
+ const withoutExtension = relativePath.replace(ROUTE_FILE_PATTERN, '');
53
+ const importPath = `./routes/${withoutExtension}`;
54
+ if (withoutExtension === '__root') {
55
+ return {
56
+ filePath,
57
+ fullPath: '/',
58
+ id: '__root__',
59
+ importPath,
60
+ matchPath: null,
61
+ parentId: null,
62
+ sourcePath: withoutExtension,
63
+ to: '/',
64
+ isPathlessFile: false,
65
+ };
66
+ }
67
+ const segments = withoutExtension.split('/');
68
+ const isIndex = segments[segments.length - 1] === 'index';
69
+ const fileSegment = segments[segments.length - 1];
70
+ const directorySegments = segments.slice(0, -1);
71
+ const publicDirectorySegments = directorySegments.filter((segment) => !isPathlessSegment(segment));
72
+ const isPathlessFile = isPathlessSegment(fileSegment);
73
+ const publicSegments = isIndex || isPathlessFile
74
+ ? publicDirectorySegments
75
+ : [...publicDirectorySegments, fileSegment];
76
+ const fullPath = publicSegments.length > 0 ? `/${publicSegments.join('/')}` : '/';
77
+ const id = withoutExtension === 'index'
78
+ ? '/'
79
+ : isIndex
80
+ ? `/${directorySegments.join('/')}/`
81
+ : `/${withoutExtension}`;
82
+ return {
83
+ filePath,
84
+ fullPath,
85
+ id,
86
+ importPath,
87
+ matchPath: isPathlessFile ? null : fullPath,
88
+ parentId: '__root__',
89
+ sourcePath: withoutExtension,
90
+ to: fullPath,
91
+ isPathlessFile,
92
+ };
93
+ }
94
+ function findParentRouteId(route, routesBySourcePath) {
95
+ if (route.id === '__root__') {
96
+ return null;
97
+ }
98
+ const sourceSegments = route.sourcePath.split('/');
99
+ const directorySegments = sourceSegments.slice(0, -1);
100
+ for (let depth = directorySegments.length; depth > 0; depth -= 1) {
101
+ const candidateSourcePath = directorySegments.slice(0, depth).join('/');
102
+ const candidateRoute = routesBySourcePath.get(candidateSourcePath);
103
+ if (candidateRoute?.isPathlessFile) {
104
+ return candidateRoute.id;
105
+ }
106
+ }
107
+ return '__root__';
108
+ }
109
+ function assertNoDuplicateMatchPaths(routes) {
110
+ const seen = new Map();
111
+ routes.forEach((route) => {
112
+ if (!route.matchPath) {
113
+ return;
114
+ }
115
+ const existing = seen.get(route.matchPath);
116
+ if (existing) {
117
+ throw new Error(`Duplicate routable path '${route.matchPath}' detected for '${existing}' and '${route.sourcePath}'`);
118
+ }
119
+ seen.set(route.matchPath, route.sourcePath);
120
+ });
121
+ }
122
+ function createGeneratedFile(routes) {
123
+ const sortedRoutes = [...routes].sort((left, right) => {
124
+ if (left.id === '__root__')
125
+ return -1;
126
+ if (right.id === '__root__')
127
+ return 1;
128
+ return left.id.localeCompare(right.id);
129
+ });
130
+ const routesById = sortedRoutes
131
+ .map((route) => ` '${route.id}': typeof import('${route.importPath}')['Route']`)
132
+ .join('\n');
133
+ const routesByFullPath = sortedRoutes
134
+ .filter((route) => route.id !== '__root__' && route.matchPath !== null)
135
+ .map((route) => ` '${route.fullPath}': typeof import('${route.importPath}')['Route']`)
136
+ .join('\n');
137
+ const routesByTo = sortedRoutes
138
+ .filter((route) => route.id !== '__root__' && route.matchPath !== null)
139
+ .map((route) => ` '${route.to}': typeof import('${route.importPath}')['Route']`)
140
+ .join('\n');
141
+ const manifestEntries = sortedRoutes.map((route) => ` {
142
+ id: '${route.id}',
143
+ fullPath: '${route.fullPath}',
144
+ matchPath: ${route.matchPath ? `'${route.matchPath}'` : 'null'},
145
+ to: '${route.to}',
146
+ parentId: ${route.parentId ? `'${route.parentId}'` : 'null'},
147
+ importPath: '${route.importPath}',
148
+ loadRoute: () => import('${route.importPath}'),
149
+ }`).join(',\n');
150
+ return `/* eslint-disable */
151
+
152
+ // @ts-nocheck
153
+
154
+ // This file was automatically generated by VorzelaJs.
155
+ // Do not edit this file manually.
156
+
157
+ import type { GeneratedRouteRecord } from 'vorzelajs'
158
+
159
+ export const routeManifest = [
160
+ ${manifestEntries}
161
+ ] as const satisfies readonly GeneratedRouteRecord[]
162
+
163
+ export interface FileRoutesById {
164
+ ${routesById}
165
+ }
166
+
167
+ export interface FileRoutesByFullPath {
168
+ ${routesByFullPath}
169
+ }
170
+
171
+ export interface FileRoutesByTo {
172
+ ${routesByTo}
173
+ }
174
+
175
+ export type RouteId = keyof FileRoutesById
176
+ export type RouteFullPath = keyof FileRoutesByFullPath
177
+ export type RouteTo = keyof FileRoutesByTo
178
+ `;
179
+ }
180
+ function detectRouteHydration(source) {
181
+ return CLIENT_EVENT_HANDLER_PATTERN.test(source)
182
+ || CLIENT_ROUTER_HOOK_PATTERN.test(source)
183
+ || CLIENT_SOLID_HOOK_PATTERN.test(source)
184
+ || CLIENT_BROWSER_GLOBAL_PATTERN.test(source)
185
+ || CLIENT_BROWSER_FUNCTION_PATTERN.test(source)
186
+ ? 'client'
187
+ : 'static';
188
+ }
189
+ function isLocalModuleSpecifier(specifier) {
190
+ return specifier.startsWith('./') || specifier.startsWith('../');
191
+ }
192
+ function extractLocalImportSpecifiers(source) {
193
+ const specifiers = new Set();
194
+ for (const pattern of [IMPORT_EXPORT_PATTERN, SIDE_EFFECT_IMPORT_PATTERN, DYNAMIC_IMPORT_PATTERN]) {
195
+ pattern.lastIndex = 0;
196
+ for (const match of source.matchAll(pattern)) {
197
+ const specifier = match[1];
198
+ if (specifier && isLocalModuleSpecifier(specifier) && !isServerOnlyModuleSpecifier(specifier)) {
199
+ specifiers.add(specifier);
200
+ }
201
+ }
202
+ }
203
+ return [...specifiers];
204
+ }
205
+ async function pathExists(filePath) {
206
+ try {
207
+ await fs.access(filePath);
208
+ return true;
209
+ }
210
+ catch {
211
+ return false;
212
+ }
213
+ }
214
+ async function resolveLocalModulePath(specifier, importerPath) {
215
+ const basePath = path.resolve(path.dirname(importerPath), specifier);
216
+ const candidates = path.extname(basePath)
217
+ ? [basePath]
218
+ : [
219
+ ...SOURCE_FILE_EXTENSIONS.map((extension) => `${basePath}${extension}`),
220
+ ...SOURCE_FILE_EXTENSIONS.map((extension) => path.join(basePath, `index${extension}`)),
221
+ ];
222
+ for (const candidate of candidates) {
223
+ if (await pathExists(candidate)) {
224
+ return candidate;
225
+ }
226
+ }
227
+ return null;
228
+ }
229
+ async function detectRouteHydrationFromFile(filePath, cache) {
230
+ const resolvedPath = path.resolve(filePath);
231
+ const cached = cache.get(resolvedPath);
232
+ if (cached) {
233
+ return cached;
234
+ }
235
+ const pending = (async () => {
236
+ const source = await fs.readFile(resolvedPath, 'utf-8');
237
+ if (detectRouteHydration(source) === 'client') {
238
+ return 'client';
239
+ }
240
+ for (const specifier of extractLocalImportSpecifiers(source)) {
241
+ const dependencyPath = await resolveLocalModulePath(specifier, resolvedPath);
242
+ if (!dependencyPath) {
243
+ continue;
244
+ }
245
+ if (await detectRouteHydrationFromFile(dependencyPath, cache) === 'client') {
246
+ return 'client';
247
+ }
248
+ }
249
+ return 'static';
250
+ })();
251
+ cache.set(resolvedPath, pending);
252
+ return pending;
253
+ }
254
+ async function createGeneratedHydrationFile(routes) {
255
+ const sortedRoutes = [...routes].sort((left, right) => {
256
+ if (left.id === '__root__')
257
+ return -1;
258
+ if (right.id === '__root__')
259
+ return 1;
260
+ return left.id.localeCompare(right.id);
261
+ });
262
+ const hydrationCache = new Map();
263
+ const hydrationEntries = await Promise.all(sortedRoutes.map(async (route) => {
264
+ const detected = await detectRouteHydrationFromFile(route.filePath, hydrationCache);
265
+ return ` '${route.id}': { detected: '${detected}' }`;
266
+ }));
267
+ return `/* eslint-disable */
268
+
269
+ // This file was automatically generated by VorzelaJs.
270
+ // Do not edit this file manually.
271
+
272
+ import type { GeneratedRouteHydrationRecord } from 'vorzelajs'
273
+
274
+ export const routeHydrationManifest = {
275
+ ${hydrationEntries.join(',\n')}
276
+ } as const satisfies Readonly<Record<string, GeneratedRouteHydrationRecord>>
277
+ `;
278
+ }
279
+ async function writeIfChanged(filePath, content) {
280
+ const current = await fs.readFile(filePath, 'utf-8').catch(() => null);
281
+ if (current !== content) {
282
+ await fs.writeFile(filePath, content, 'utf-8');
283
+ }
284
+ }
285
+ export async function generateRoutes(projectRoot = process.cwd()) {
286
+ const routesDir = path.resolve(projectRoot, ROUTES_DIR);
287
+ const outputPath = path.resolve(projectRoot, OUTPUT_FILE);
288
+ const hydrationOutputPath = path.resolve(projectRoot, OUTPUT_HYDRATION_FILE);
289
+ const routeFiles = (await walkFiles(routesDir))
290
+ .filter((filePath) => ROUTE_FILE_PATTERN.test(filePath))
291
+ .filter((filePath) => !isServerOnlyRouteFile(filePath))
292
+ .filter((filePath) => !filePath.endsWith('.d.ts'));
293
+ const routes = routeFiles.map((filePath) => getRouteInfo(filePath, routesDir));
294
+ if (!routes.some((route) => route.id === '__root__')) {
295
+ throw new Error(`Missing root route file at ${path.join(routesDir, '__root.tsx')}`);
296
+ }
297
+ const routesBySourcePath = new Map(routes.map((route) => [route.sourcePath, route]));
298
+ const routesWithParents = routes.map((route) => ({
299
+ ...route,
300
+ parentId: findParentRouteId(route, routesBySourcePath),
301
+ }));
302
+ assertNoDuplicateMatchPaths(routesWithParents);
303
+ await Promise.all([
304
+ writeIfChanged(outputPath, createGeneratedFile(routesWithParents)),
305
+ writeIfChanged(hydrationOutputPath, await createGeneratedHydrationFile(routesWithParents)),
306
+ ]);
307
+ }
308
+ export function vorzelaRoutesPlugin() {
309
+ let projectRoot = process.cwd();
310
+ return {
311
+ name: 'vorzelajs-routes',
312
+ enforce: 'pre',
313
+ configResolved(config) {
314
+ projectRoot = config.root;
315
+ },
316
+ async buildStart() {
317
+ await generateRoutes(projectRoot);
318
+ },
319
+ configureServer(server) {
320
+ const sourceRoot = path.resolve(projectRoot, 'src');
321
+ const generatedRouteTreePath = path.resolve(projectRoot, OUTPUT_FILE);
322
+ const generatedHydrationPath = path.resolve(projectRoot, OUTPUT_HYDRATION_FILE);
323
+ const handleRouteChange = async (filePath) => {
324
+ const absolutePath = path.resolve(filePath);
325
+ if (absolutePath === generatedRouteTreePath
326
+ || absolutePath === generatedHydrationPath
327
+ || !absolutePath.startsWith(sourceRoot)
328
+ || !ROUTE_FILE_PATTERN.test(absolutePath)
329
+ || isServerOnlyRouteFile(absolutePath)) {
330
+ return;
331
+ }
332
+ try {
333
+ await generateRoutes(projectRoot);
334
+ server.ws.send({ type: 'full-reload' });
335
+ }
336
+ catch (error) {
337
+ server.config.logger.error(`[vorzelajs-routes] ${error.message}`);
338
+ }
339
+ };
340
+ server.watcher.on('add', handleRouteChange);
341
+ server.watcher.on('change', handleRouteChange);
342
+ server.watcher.on('unlink', handleRouteChange);
343
+ },
344
+ };
345
+ }
@@ -0,0 +1,3 @@
1
+ import type { Plugin } from 'vite';
2
+ export declare function vorzelaServerOnlyPlugin(): Plugin;
3
+ //# sourceMappingURL=server-only.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server-only.d.ts","sourceRoot":"","sources":["../../src/vite/server-only.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAA;AAsblC,wBAAgB,uBAAuB,IAAI,MAAM,CA2BhD"}
@@ -0,0 +1,342 @@
1
+ import path from 'node:path';
2
+ import ts from 'typescript';
3
+ const ROUTE_FILE_PATTERN = /src[\\/]routes[\\/].*\.(ts|tsx)$/u;
4
+ const SERVER_ONLY_MODULE_PATTERN = /(?:^|[\\/])\.server(?:[\\/]|$)|\.server(?:$|\.)/u;
5
+ const ROUTE_SERVER_PROPERTY_NAMES = new Set(['loader', 'beforeLoad', 'validateSearch']);
6
+ function stripQuerySuffix(value) {
7
+ const queryIndex = value.indexOf('?');
8
+ return queryIndex === -1 ? value : value.slice(0, queryIndex);
9
+ }
10
+ function isRouteFileId(id) {
11
+ return ROUTE_FILE_PATTERN.test(stripQuerySuffix(id));
12
+ }
13
+ function isServerOnlyModuleSpecifier(specifier) {
14
+ return SERVER_ONLY_MODULE_PATTERN.test(specifier);
15
+ }
16
+ function formatRelativeId(id, root) {
17
+ return path.relative(root, stripQuerySuffix(id)) || stripQuerySuffix(id);
18
+ }
19
+ function getObjectLiteralElementName(property) {
20
+ if (!('name' in property)) {
21
+ return null;
22
+ }
23
+ return getPropertyNameText(property.name);
24
+ }
25
+ function getPropertyNameText(name) {
26
+ if (!name) {
27
+ return null;
28
+ }
29
+ if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) {
30
+ return name.text;
31
+ }
32
+ return null;
33
+ }
34
+ function getBindingNames(name, names = []) {
35
+ if (ts.isIdentifier(name)) {
36
+ names.push(name.text);
37
+ return names;
38
+ }
39
+ for (const element of name.elements) {
40
+ if (!ts.isOmittedExpression(element)) {
41
+ getBindingNames(element.name, names);
42
+ }
43
+ }
44
+ return names;
45
+ }
46
+ function getTopLevelStatementBindingNames(statement) {
47
+ if (ts.isFunctionDeclaration(statement) && statement.name) {
48
+ return [statement.name.text];
49
+ }
50
+ if (ts.isVariableStatement(statement)) {
51
+ return statement.declarationList.declarations.flatMap((declaration) => getBindingNames(declaration.name));
52
+ }
53
+ return [];
54
+ }
55
+ function isNodeWithinRanges(node, sourceFile, ranges) {
56
+ const start = node.getStart(sourceFile);
57
+ return ranges.some((range) => start >= range.start && node.end <= range.end);
58
+ }
59
+ function isIdentifierReference(node) {
60
+ const parent = node.parent;
61
+ if ((ts.isPropertyAssignment(parent) && parent.name === node && parent.initializer !== node)
62
+ || (ts.isShorthandPropertyAssignment(parent) && parent.name === node && parent.objectAssignmentInitializer !== undefined)
63
+ || (ts.isMethodDeclaration(parent) && parent.name === node)
64
+ || (ts.isPropertyDeclaration(parent) && parent.name === node)
65
+ || (ts.isPropertySignature(parent) && parent.name === node)
66
+ || (ts.isPropertyAccessExpression(parent) && parent.name === node)
67
+ || (ts.isQualifiedName(parent) && parent.right === node)
68
+ || (ts.isImportClause(parent) && parent.name === node)
69
+ || (ts.isImportSpecifier(parent) && (parent.name === node || parent.propertyName === node))
70
+ || (ts.isNamespaceImport(parent) && parent.name === node)
71
+ || (ts.isBindingElement(parent) && (parent.name === node || parent.propertyName === node))
72
+ || (ts.isVariableDeclaration(parent) && parent.name === node)
73
+ || (ts.isFunctionDeclaration(parent) && parent.name === node)
74
+ || (ts.isParameter(parent) && parent.name === node)
75
+ || ts.isTypeReferenceNode(parent)
76
+ || ts.isExpressionWithTypeArguments(parent)
77
+ || ts.isImportTypeNode(parent)
78
+ || ts.isTypeQueryNode(parent)) {
79
+ return false;
80
+ }
81
+ return true;
82
+ }
83
+ function isRouteOptionsObject(node) {
84
+ const propertyNames = new Set(node.properties
85
+ .map((property) => getObjectLiteralElementName(property))
86
+ .filter((name) => name !== null));
87
+ return propertyNames.has('component');
88
+ }
89
+ function collectRouteServerOnlyContext(sourceFile) {
90
+ const ranges = [];
91
+ const topLevelBindingNames = new Set();
92
+ const visit = (node) => {
93
+ if (ts.isObjectLiteralExpression(node) && isRouteOptionsObject(node)) {
94
+ for (const property of node.properties) {
95
+ const name = getObjectLiteralElementName(property);
96
+ if (!name || !ROUTE_SERVER_PROPERTY_NAMES.has(name)) {
97
+ continue;
98
+ }
99
+ ranges.push({
100
+ end: property.end,
101
+ start: property.getStart(sourceFile),
102
+ });
103
+ if (ts.isPropertyAssignment(property) && ts.isIdentifier(property.initializer)) {
104
+ topLevelBindingNames.add(property.initializer.text);
105
+ }
106
+ if (ts.isShorthandPropertyAssignment(property)) {
107
+ topLevelBindingNames.add(property.name.text);
108
+ }
109
+ }
110
+ }
111
+ ts.forEachChild(node, visit);
112
+ };
113
+ visit(sourceFile);
114
+ for (const statement of sourceFile.statements) {
115
+ if (getTopLevelStatementBindingNames(statement).some((name) => topLevelBindingNames.has(name))) {
116
+ ranges.push({
117
+ end: statement.end,
118
+ start: statement.getStart(sourceFile),
119
+ });
120
+ }
121
+ }
122
+ return {
123
+ ranges,
124
+ topLevelBindingNames,
125
+ };
126
+ }
127
+ function getImportValueBindingNames(importClause) {
128
+ if (!importClause || importClause.isTypeOnly) {
129
+ return [];
130
+ }
131
+ const names = [];
132
+ if (importClause.name) {
133
+ names.push(importClause.name.text);
134
+ }
135
+ if (!importClause.namedBindings) {
136
+ return names;
137
+ }
138
+ if (ts.isNamespaceImport(importClause.namedBindings)) {
139
+ names.push(importClause.namedBindings.name.text);
140
+ return names;
141
+ }
142
+ for (const element of importClause.namedBindings.elements) {
143
+ if (!element.isTypeOnly) {
144
+ names.push(element.name.text);
145
+ }
146
+ }
147
+ return names;
148
+ }
149
+ function getServerOnlyImportInfo(sourceFile, serverOnlyRanges, id, root) {
150
+ const removableImports = new Set();
151
+ const valueImports = new Map();
152
+ const throwClientBoundaryError = (specifier, reason) => {
153
+ throw new Error(`[vorzelajs-server-only] ${formatRelativeId(id, root)} ${reason} "${specifier}". `
154
+ + 'Move that usage into loader/beforeLoad/validateSearch, or keep it behind a .server import that is only referenced there.');
155
+ };
156
+ const visitForRouteViolations = (node) => {
157
+ if (ts.isExportDeclaration(node)
158
+ && node.moduleSpecifier
159
+ && ts.isStringLiteral(node.moduleSpecifier)
160
+ && isServerOnlyModuleSpecifier(node.moduleSpecifier.text)) {
161
+ throwClientBoundaryError(node.moduleSpecifier.text, 're-exports a .server module from client-visible route code:');
162
+ }
163
+ if (ts.isCallExpression(node)
164
+ && node.expression.kind === ts.SyntaxKind.ImportKeyword
165
+ && node.arguments.length === 1
166
+ && ts.isStringLiteral(node.arguments[0])
167
+ && isServerOnlyModuleSpecifier(node.arguments[0].text)
168
+ && !isNodeWithinRanges(node, sourceFile, serverOnlyRanges)) {
169
+ throwClientBoundaryError(node.arguments[0].text, 'uses a .server dynamic import outside server-only route code:');
170
+ }
171
+ ts.forEachChild(node, visitForRouteViolations);
172
+ };
173
+ visitForRouteViolations(sourceFile);
174
+ for (const statement of sourceFile.statements) {
175
+ if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) {
176
+ continue;
177
+ }
178
+ const specifier = statement.moduleSpecifier.text;
179
+ if (!isServerOnlyModuleSpecifier(specifier)) {
180
+ continue;
181
+ }
182
+ if (!statement.importClause) {
183
+ throwClientBoundaryError(specifier, 'contains a side-effect .server import in client-visible route code:');
184
+ }
185
+ const bindingNames = getImportValueBindingNames(statement.importClause);
186
+ if (bindingNames.length === 0) {
187
+ continue;
188
+ }
189
+ removableImports.add(statement);
190
+ for (const bindingName of bindingNames) {
191
+ valueImports.set(bindingName, specifier);
192
+ }
193
+ }
194
+ if (valueImports.size === 0) {
195
+ return removableImports;
196
+ }
197
+ const visitForBindingUsage = (node) => {
198
+ if (ts.isImportDeclaration(node)) {
199
+ return;
200
+ }
201
+ if (ts.isIdentifier(node) && isIdentifierReference(node)) {
202
+ const specifier = valueImports.get(node.text);
203
+ if (specifier && !isNodeWithinRanges(node, sourceFile, serverOnlyRanges)) {
204
+ throwClientBoundaryError(specifier, `references .server binding "${node.text}" outside server-only route code from`);
205
+ }
206
+ }
207
+ ts.forEachChild(node, visitForBindingUsage);
208
+ };
209
+ visitForBindingUsage(sourceFile);
210
+ return removableImports;
211
+ }
212
+ function stripTopLevelServerOnlyStatement(statement, bindingNames) {
213
+ if (ts.isFunctionDeclaration(statement) && statement.name && bindingNames.has(statement.name.text)) {
214
+ return null;
215
+ }
216
+ if (ts.isVariableStatement(statement)) {
217
+ const keptDeclarations = statement.declarationList.declarations.filter((declaration) => {
218
+ return !getBindingNames(declaration.name).some((name) => bindingNames.has(name));
219
+ });
220
+ if (keptDeclarations.length === 0) {
221
+ return null;
222
+ }
223
+ if (keptDeclarations.length !== statement.declarationList.declarations.length) {
224
+ return ts.factory.updateVariableStatement(statement, statement.modifiers, ts.factory.updateVariableDeclarationList(statement.declarationList, keptDeclarations));
225
+ }
226
+ }
227
+ return statement;
228
+ }
229
+ function stripRouteServerProperties(code, id, root) {
230
+ const normalizedId = stripQuerySuffix(id);
231
+ const scriptKind = normalizedId.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
232
+ const sourceFile = ts.createSourceFile(normalizedId, code, ts.ScriptTarget.Latest, true, scriptKind);
233
+ const { ranges: serverOnlyRanges, topLevelBindingNames } = collectRouteServerOnlyContext(sourceFile);
234
+ const removableImports = getServerOnlyImportInfo(sourceFile, serverOnlyRanges, normalizedId, root);
235
+ let changed = false;
236
+ const transformer = (context) => {
237
+ const visitor = (node) => {
238
+ if (ts.isObjectLiteralExpression(node) && isRouteOptionsObject(node)) {
239
+ const filteredProperties = node.properties.filter((property) => {
240
+ const name = getObjectLiteralElementName(property);
241
+ const shouldKeep = !name || !ROUTE_SERVER_PROPERTY_NAMES.has(name);
242
+ if (!shouldKeep) {
243
+ changed = true;
244
+ }
245
+ return shouldKeep;
246
+ });
247
+ if (filteredProperties.length !== node.properties.length) {
248
+ return ts.factory.updateObjectLiteralExpression(node, filteredProperties);
249
+ }
250
+ }
251
+ return ts.visitEachChild(node, visitor, context);
252
+ };
253
+ return (rootNode) => {
254
+ const nextStatements = [];
255
+ for (const statement of rootNode.statements) {
256
+ if (ts.isImportDeclaration(statement) && removableImports.has(statement)) {
257
+ changed = true;
258
+ continue;
259
+ }
260
+ const strippedStatement = stripTopLevelServerOnlyStatement(statement, topLevelBindingNames);
261
+ if (!strippedStatement) {
262
+ changed = true;
263
+ continue;
264
+ }
265
+ nextStatements.push(ts.visitNode(strippedStatement, visitor));
266
+ }
267
+ if (!changed) {
268
+ return rootNode;
269
+ }
270
+ return ts.factory.updateSourceFile(rootNode, nextStatements);
271
+ };
272
+ };
273
+ const result = ts.transform(sourceFile, [transformer]);
274
+ try {
275
+ if (!changed) {
276
+ return null;
277
+ }
278
+ const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
279
+ return printer.printFile(result.transformed[0]);
280
+ }
281
+ finally {
282
+ result.dispose();
283
+ }
284
+ }
285
+ function assertNoServerOnlyImportsInClientFile(code, id, sourceRoot, root) {
286
+ const normalizedId = stripQuerySuffix(id);
287
+ if (!normalizedId.startsWith(`${sourceRoot}${path.sep}`) || isRouteFileId(normalizedId)) {
288
+ return;
289
+ }
290
+ const scriptKind = normalizedId.endsWith('.tsx')
291
+ ? ts.ScriptKind.TSX
292
+ : normalizedId.endsWith('.jsx')
293
+ ? ts.ScriptKind.JSX
294
+ : normalizedId.endsWith('.js')
295
+ ? ts.ScriptKind.JS
296
+ : ts.ScriptKind.TS;
297
+ const sourceFile = ts.createSourceFile(normalizedId, code, ts.ScriptTarget.Latest, true, scriptKind);
298
+ const throwClientBoundaryError = (specifier) => {
299
+ throw new Error(`[vorzelajs-server-only] ${formatRelativeId(id, root)} imports .server module "${specifier}". `
300
+ + 'Only route loader/beforeLoad/validateSearch code may reference .server modules in the client build.');
301
+ };
302
+ const visit = (node) => {
303
+ if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node))
304
+ && node.moduleSpecifier
305
+ && ts.isStringLiteral(node.moduleSpecifier)
306
+ && isServerOnlyModuleSpecifier(node.moduleSpecifier.text)) {
307
+ throwClientBoundaryError(node.moduleSpecifier.text);
308
+ }
309
+ if (ts.isCallExpression(node)
310
+ && node.expression.kind === ts.SyntaxKind.ImportKeyword
311
+ && node.arguments.length === 1
312
+ && ts.isStringLiteral(node.arguments[0])
313
+ && isServerOnlyModuleSpecifier(node.arguments[0].text)) {
314
+ throwClientBoundaryError(node.arguments[0].text);
315
+ }
316
+ ts.forEachChild(node, visit);
317
+ };
318
+ visit(sourceFile);
319
+ }
320
+ export function vorzelaServerOnlyPlugin() {
321
+ let root = process.cwd();
322
+ let sourceRoot = path.resolve(root, 'src');
323
+ return {
324
+ name: 'vorzelajs-server-only-support',
325
+ enforce: 'pre',
326
+ configResolved(config) {
327
+ root = config.root;
328
+ sourceRoot = path.resolve(root, 'src');
329
+ },
330
+ transform(code, id) {
331
+ assertNoServerOnlyImportsInClientFile(code, id, sourceRoot, root);
332
+ if (!isRouteFileId(id)) {
333
+ return null;
334
+ }
335
+ const modified = stripRouteServerProperties(code, id, root);
336
+ if (!modified) {
337
+ return null;
338
+ }
339
+ return { code: modified, map: null };
340
+ },
341
+ };
342
+ }