@vistagenic/vista 0.3.8 → 0.3.9

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.
@@ -75,20 +75,22 @@ function collect() {
75
75
  const pages = [];
76
76
  const layouts = [];
77
77
  let root = null;
78
+ let rootLayout = null;
78
79
  for (const appRel of ['app', 'src/app']) {
79
80
  const dir = path.join(modulesDir, appRel);
80
81
  if (!fs.existsSync(dir)) continue;
81
82
  for (const file of walk(dir)) {
82
83
  const relative = path.relative(dir, file).replaceAll('\\', '/');
83
84
  const name = path.basename(relative);
84
- const segments = relative.split('/').slice(0, -1);
85
- const url = moduleUrl(appRel, relative);
86
- if (!root && name.startsWith('root.') && segments.length === 0) root = url.split('?')[0];
87
- if (name.startsWith('page.') || name.startsWith('index.')) pages.push({ module: url.split('?')[0], segments });
88
- else if (name.startsWith('layout.')) layouts.push({ module: url.split('?')[0], segments });
85
+ const segments = relative.split('/').slice(0, -1).filter(Boolean);
86
+ const url = moduleUrl(appRel, relative).split('?')[0];
87
+ if (!root && name.startsWith('root.') && segments.length === 0) root = url;
88
+ if (!rootLayout && name.startsWith('layout.') && segments.length === 0) rootLayout = url;
89
+ if (name.startsWith('page.') || name.startsWith('index.')) pages.push({ module: url, segments });
90
+ else if (name.startsWith('layout.') && segments.length > 0) layouts.push({ module: url, segments });
89
91
  }
90
92
  }
91
- return { root: root || '/_flashpack/modules/app/root.tsx.js', pages, layouts };
93
+ return { root: root || rootLayout || '/_flashpack/modules/app/root.tsx.js', pages, layouts };
92
94
  }
93
95
 
94
96
  function layoutApplies(segments, parts) {
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { createRequire, register } from 'node:module';
4
- import { pathToFileURL } from 'node:url';
4
+ import { fileURLToPath, pathToFileURL } from 'node:url';
5
5
 
6
6
  function readArg(name) {
7
7
  const index = process.argv.indexOf(name);
@@ -13,6 +13,8 @@ const cwd = path.resolve(readArg('--cwd') || process.cwd());
13
13
  const requestPath = readArg('--path') || '/';
14
14
  const modulesDir = path.join(cwd, '.flash', 'dev', 'modules');
15
15
  const shimsDir = path.join(cwd, '.flash', 'dev', 'ssr');
16
+ const selfDir = path.dirname(fileURLToPath(import.meta.url));
17
+ const vistaPkgRoot = path.resolve(selfDir, '..');
16
18
 
17
19
  const registered = register(new URL('./flashpack-ssr-loader.mjs', import.meta.url), {
18
20
  parentURL: import.meta.url,
@@ -22,60 +24,179 @@ if (registered && typeof registered.then === 'function') {
22
24
  await registered;
23
25
  }
24
26
 
25
- function moduleFile(relativePath) {
26
- const file = path.join(modulesDir, ...relativePath.split('/'));
27
- return fs.existsSync(file) ? file : null;
27
+ function resolveFrom(roots, request) {
28
+ const errors = [];
29
+ for (const root of roots) {
30
+ try {
31
+ return createRequire(path.join(root, 'package.json'))(request);
32
+ } catch (error) {
33
+ errors.push(`${root}: ${error && error.message ? error.message : error}`);
34
+ }
35
+ }
36
+ throw new Error(`Cannot resolve '${request}' for flashpack SSR\n${errors.join('\n')}`);
28
37
  }
29
38
 
30
- const appRequire = createRequire(path.join(cwd, 'package.json'));
31
- const { createElement } = appRequire('react');
32
- const { renderToReadableStream } = appRequire('react-dom/server');
39
+ const react = resolveFrom([cwd, vistaPkgRoot, path.resolve(cwd, '../..'), path.resolve(cwd, '../../..')], 'react');
40
+ const reactDomServer = resolveFrom(
41
+ [cwd, vistaPkgRoot, path.resolve(cwd, '../..'), path.resolve(cwd, '../../..')],
42
+ 'react-dom/server'
43
+ );
44
+ const { createElement } = react;
45
+ const { renderToReadableStream } = reactDomServer;
33
46
 
34
- const rootFile = moduleFile('app/root.tsx.js') || moduleFile('app/root.jsx.js');
35
- if (!rootFile) {
36
- throw new Error('flashpack ssr: app/root was not compiled');
47
+ function walk(dir, found = []) {
48
+ if (!fs.existsSync(dir)) return found;
49
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
50
+ const full = path.join(dir, entry.name);
51
+ if (entry.isDirectory()) walk(full, found);
52
+ else if (entry.name.endsWith('.js')) found.push(full);
53
+ }
54
+ return found;
37
55
  }
38
56
 
39
- const pathname = requestPath.replace(/\/$/, '') || '/';
40
- let pageFile = null;
41
- let pageProps = {};
42
- let layoutFile = null;
43
-
44
- if (pathname === '/docs' || pathname.startsWith('/docs/')) {
45
- layoutFile = moduleFile('app/docs/layout.tsx.js');
46
- if (pathname === '/docs') {
47
- pageFile = moduleFile('app/docs/page.tsx.js');
48
- } else {
49
- pageFile = moduleFile('app/docs/[...slug]/page.tsx.js');
50
- pageProps = { params: { slug: pathname.split('/').filter(Boolean).slice(1) } };
57
+ function partsOf(pathname) {
58
+ return String(pathname || '/')
59
+ .split('?')[0]
60
+ .split('#')[0]
61
+ .split('/')
62
+ .filter(Boolean);
63
+ }
64
+
65
+ function matchSegments(segments, parts) {
66
+ const params = {};
67
+ let pi = 0;
68
+ let staticCount = 0;
69
+ let dynamicCount = 0;
70
+ for (const seg of segments) {
71
+ if (seg.startsWith('(') && seg.endsWith(')')) continue;
72
+ if (seg.startsWith('[...') && seg.endsWith(']')) {
73
+ params[seg.slice(4, -1)] = parts.slice(pi);
74
+ dynamicCount += 1;
75
+ pi = parts.length;
76
+ continue;
77
+ }
78
+ if (seg.startsWith('[') && seg.endsWith(']')) {
79
+ if (pi >= parts.length) return null;
80
+ params[seg.slice(1, -1)] = parts[pi];
81
+ dynamicCount += 1;
82
+ pi += 1;
83
+ continue;
84
+ }
85
+ if (pi >= parts.length || parts[pi] !== seg) return null;
86
+ staticCount += 1;
87
+ pi += 1;
88
+ }
89
+ if (pi !== parts.length) return null;
90
+ return { params, staticCount, dynamicCount };
91
+ }
92
+
93
+ function layoutApplies(segments, parts) {
94
+ let pi = 0;
95
+ for (const seg of segments || []) {
96
+ if (seg.startsWith('(') && seg.endsWith(')')) continue;
97
+ if (seg.startsWith('[...')) return true;
98
+ if (seg.startsWith('[') && seg.endsWith(']')) {
99
+ if (pi >= parts.length) return false;
100
+ pi += 1;
101
+ continue;
102
+ }
103
+ if (parts[pi] !== seg) return false;
104
+ pi += 1;
105
+ }
106
+ return true;
107
+ }
108
+
109
+ function collectRoutes() {
110
+ const pages = [];
111
+ const layouts = [];
112
+ let rootFile = null;
113
+ let rootLayoutFile = null;
114
+
115
+ for (const appRel of ['app', 'src/app']) {
116
+ const dir = path.join(modulesDir, appRel);
117
+ if (!fs.existsSync(dir)) continue;
118
+ for (const file of walk(dir)) {
119
+ const relative = path.relative(dir, file).replaceAll('\\', '/');
120
+ const name = path.basename(relative);
121
+ const segments = relative.split('/').slice(0, -1).filter(Boolean);
122
+ if (!rootFile && name.startsWith('root.') && segments.length === 0) {
123
+ rootFile = file;
124
+ }
125
+ if (!rootLayoutFile && name.startsWith('layout.') && segments.length === 0) {
126
+ rootLayoutFile = file;
127
+ }
128
+ if (name.startsWith('page.') || name.startsWith('index.')) {
129
+ pages.push({ file, segments });
130
+ } else if (name.startsWith('layout.') && segments.length > 0) {
131
+ // Root layout is used as the document shell, not a nested layout.
132
+ layouts.push({ file, segments });
133
+ }
134
+ }
135
+ }
136
+
137
+ return {
138
+ rootFile: rootFile || rootLayoutFile,
139
+ pages,
140
+ layouts,
141
+ };
142
+ }
143
+
144
+ function pickPage(pages, parts) {
145
+ let best = null;
146
+ for (const page of pages) {
147
+ const matched = matchSegments(page.segments || [], parts);
148
+ if (!matched) continue;
149
+ const candidate = { page, ...matched };
150
+ if (
151
+ !best ||
152
+ candidate.staticCount > best.staticCount ||
153
+ (candidate.staticCount === best.staticCount && candidate.dynamicCount < best.dynamicCount)
154
+ ) {
155
+ best = candidate;
156
+ }
51
157
  }
158
+ return best;
52
159
  }
53
160
 
54
- if (!pageFile) {
55
- pageFile =
56
- moduleFile('app/index.tsx.js') ||
57
- moduleFile('app/index.jsx.js') ||
58
- moduleFile('app/page.tsx.js') ||
59
- moduleFile('app/page.jsx.js');
161
+ async function renderNode(Component, props) {
162
+ if (
163
+ typeof Component === 'function' &&
164
+ Component.constructor &&
165
+ Component.constructor.name === 'AsyncFunction'
166
+ ) {
167
+ return await Component(props);
168
+ }
169
+ return createElement(Component, props);
60
170
  }
61
171
 
62
- if (!pageFile) {
172
+ const table = collectRoutes();
173
+ if (!table.rootFile) {
174
+ throw new Error('flashpack ssr: app/root or app/layout was not compiled');
175
+ }
176
+
177
+ const pathname = requestPath.replace(/\/$/, '') || '/';
178
+ const parts = partsOf(pathname);
179
+ const match = pickPage(table.pages, parts);
180
+ if (!match) {
63
181
  throw new Error(`flashpack ssr: no page module for ${pathname}`);
64
182
  }
65
183
 
66
- const rootMod = await import(pathToFileURL(rootFile).href);
67
- const pageMod = await import(pathToFileURL(pageFile).href);
184
+ const rootMod = await import(pathToFileURL(table.rootFile).href);
185
+ const pageMod = await import(pathToFileURL(match.page.file).href);
68
186
  const Root = rootMod.default;
69
187
  const Page = pageMod.default;
70
- let page = createElement(Page, pageProps);
71
- if (page && typeof page.then === 'function') {
72
- page = await page;
73
- }
74
- if (layoutFile) {
75
- const layoutMod = await import(pathToFileURL(layoutFile).href);
76
- page = createElement(layoutMod.default, { children: page });
188
+
189
+ let tree = await renderNode(Page, { params: match.params });
190
+ const nested = table.layouts
191
+ .filter((layout) => layoutApplies(layout.segments, parts))
192
+ .sort((a, b) => (a.segments || []).length - (b.segments || []).length);
193
+ for (const layout of nested) {
194
+ const layoutMod = await import(pathToFileURL(layout.file).href);
195
+ if (typeof layoutMod.default !== 'function') continue;
196
+ tree = await renderNode(layoutMod.default, { params: match.params, children: tree });
77
197
  }
198
+ tree = createElement(Root, { children: tree });
78
199
 
79
- const stream = await renderToReadableStream(createElement(Root, { children: page }));
200
+ const stream = await renderToReadableStream(tree);
80
201
  await stream.allReady;
81
202
  process.stdout.write(await new Response(stream).text());
@@ -25,10 +25,10 @@ function vistaVersion() {
25
25
  try {
26
26
  const require = (0, module_1.createRequire)(__filename);
27
27
  const pkg = require('../../package.json');
28
- return pkg.version || '0.3.8';
28
+ return pkg.version || '0.3.9';
29
29
  }
30
30
  catch {
31
- return '0.3.8';
31
+ return '0.3.9';
32
32
  }
33
33
  }
34
34
  function installDevClient(cwd) {
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vistagenic/vista",
3
- "version": "0.3.8",
3
+ "version": "0.3.9",
4
4
  "description": "The React Framework for Visionaries - Rust-powered SSR with Server Components",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",