what-compiler 0.4.0 → 0.5.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.
- package/package.json +4 -3
- package/src/babel-plugin.js +13 -1
- package/src/file-router.js +320 -0
- package/src/index.js +1 -0
- package/src/vite-plugin.js +61 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "what-compiler",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "JSX compiler for What Framework - transforms JSX to optimized DOM operations",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
".": "./src/index.js",
|
|
9
9
|
"./babel": "./src/babel-plugin.js",
|
|
10
10
|
"./vite": "./src/vite-plugin.js",
|
|
11
|
-
"./runtime": "./src/runtime.js"
|
|
11
|
+
"./runtime": "./src/runtime.js",
|
|
12
|
+
"./file-router": "./src/file-router.js"
|
|
12
13
|
},
|
|
13
14
|
"keywords": [
|
|
14
15
|
"what",
|
|
@@ -22,7 +23,7 @@
|
|
|
22
23
|
"license": "MIT",
|
|
23
24
|
"peerDependencies": {
|
|
24
25
|
"@babel/core": "^7.0.0",
|
|
25
|
-
"what-core": "^0.
|
|
26
|
+
"what-core": "^0.5.0"
|
|
26
27
|
},
|
|
27
28
|
"files": [
|
|
28
29
|
"src"
|
package/src/babel-plugin.js
CHANGED
|
@@ -1101,7 +1101,19 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
1101
1101
|
}
|
|
1102
1102
|
}
|
|
1103
1103
|
|
|
1104
|
-
if (
|
|
1104
|
+
if (existingRenderImport) {
|
|
1105
|
+
const existingNames = new Set(
|
|
1106
|
+
existingRenderImport.specifiers
|
|
1107
|
+
.filter(s => t.isImportSpecifier(s))
|
|
1108
|
+
.map(s => s.imported.name)
|
|
1109
|
+
);
|
|
1110
|
+
|
|
1111
|
+
for (const spec of fgSpecifiers) {
|
|
1112
|
+
if (!existingNames.has(spec.imported.name)) {
|
|
1113
|
+
existingRenderImport.specifiers.push(spec);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
} else {
|
|
1105
1117
|
path.unshiftContainer('body',
|
|
1106
1118
|
t.importDeclaration(fgSpecifiers, t.stringLiteral('what-framework/render'))
|
|
1107
1119
|
);
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File-Based Router for What Framework
|
|
3
|
+
*
|
|
4
|
+
* Scans a pages directory and generates route configuration.
|
|
5
|
+
*
|
|
6
|
+
* File conventions:
|
|
7
|
+
* src/pages/index.jsx → /
|
|
8
|
+
* src/pages/about.jsx → /about
|
|
9
|
+
* src/pages/blog/index.jsx → /blog
|
|
10
|
+
* src/pages/blog/[slug].jsx → /blog/:slug
|
|
11
|
+
* src/pages/[...path].jsx → catch-all
|
|
12
|
+
* src/pages/_layout.jsx → layout for that directory
|
|
13
|
+
* src/pages/(auth)/login.jsx → /login (group doesn't affect URL)
|
|
14
|
+
* src/pages/api/users.js → API route: /api/users
|
|
15
|
+
*
|
|
16
|
+
* Page declarations (optional export in each page file):
|
|
17
|
+
* export const page = {
|
|
18
|
+
* mode: 'client', // default — SPA, JS required
|
|
19
|
+
* mode: 'server', // SSR on every request
|
|
20
|
+
* mode: 'static', // pre-rendered at build time
|
|
21
|
+
* mode: 'hybrid', // static HTML shell + interactive islands
|
|
22
|
+
* };
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import fs from 'fs';
|
|
26
|
+
import path from 'path';
|
|
27
|
+
|
|
28
|
+
const PAGE_EXTENSIONS = new Set(['.jsx', '.tsx', '.js', '.ts']);
|
|
29
|
+
const IGNORED_FILES = new Set(['_layout', '_error', '_loading', '_404']);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Scan a directory recursively and return all page files.
|
|
33
|
+
*/
|
|
34
|
+
export function scanPages(pagesDir) {
|
|
35
|
+
const pages = [];
|
|
36
|
+
const layouts = [];
|
|
37
|
+
const apiRoutes = [];
|
|
38
|
+
|
|
39
|
+
function walk(dir, urlPrefix = '') {
|
|
40
|
+
if (!fs.existsSync(dir)) return;
|
|
41
|
+
|
|
42
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
43
|
+
|
|
44
|
+
for (const entry of entries) {
|
|
45
|
+
const fullPath = path.join(dir, entry.name);
|
|
46
|
+
|
|
47
|
+
if (entry.isDirectory()) {
|
|
48
|
+
// Route groups: (name)/ — strip from URL
|
|
49
|
+
const groupMatch = entry.name.match(/^\((.+)\)$/);
|
|
50
|
+
if (groupMatch) {
|
|
51
|
+
walk(fullPath, urlPrefix); // Same URL prefix
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// API directory
|
|
56
|
+
if (entry.name === 'api' && urlPrefix === '') {
|
|
57
|
+
walkApi(fullPath, '/api');
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
walk(fullPath, urlPrefix + '/' + fileNameToSegment(entry.name));
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Only process page extensions
|
|
66
|
+
const ext = path.extname(entry.name);
|
|
67
|
+
if (!PAGE_EXTENSIONS.has(ext)) continue;
|
|
68
|
+
|
|
69
|
+
const baseName = path.basename(entry.name, ext);
|
|
70
|
+
|
|
71
|
+
// Layout files
|
|
72
|
+
if (baseName === '_layout') {
|
|
73
|
+
layouts.push({
|
|
74
|
+
filePath: fullPath,
|
|
75
|
+
urlPrefix: urlPrefix || '/',
|
|
76
|
+
});
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Error/loading/404 boundaries (reserved names)
|
|
81
|
+
if (IGNORED_FILES.has(baseName)) continue;
|
|
82
|
+
|
|
83
|
+
// Convert file name to URL segment
|
|
84
|
+
const urlSegment = fileNameToSegment(baseName);
|
|
85
|
+
const routePath = baseName === 'index'
|
|
86
|
+
? (urlPrefix || '/')
|
|
87
|
+
: urlPrefix + '/' + urlSegment;
|
|
88
|
+
|
|
89
|
+
pages.push({
|
|
90
|
+
filePath: fullPath,
|
|
91
|
+
routePath: normalizePath(routePath),
|
|
92
|
+
isDynamic: routePath.includes(':') || routePath.includes('*'),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function walkApi(dir, urlPrefix) {
|
|
98
|
+
if (!fs.existsSync(dir)) return;
|
|
99
|
+
|
|
100
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
101
|
+
|
|
102
|
+
for (const entry of entries) {
|
|
103
|
+
const fullPath = path.join(dir, entry.name);
|
|
104
|
+
|
|
105
|
+
if (entry.isDirectory()) {
|
|
106
|
+
walkApi(fullPath, urlPrefix + '/' + fileNameToSegment(entry.name));
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const ext = path.extname(entry.name);
|
|
111
|
+
if (!PAGE_EXTENSIONS.has(ext)) continue;
|
|
112
|
+
|
|
113
|
+
const baseName = path.basename(entry.name, ext);
|
|
114
|
+
const segment = fileNameToSegment(baseName);
|
|
115
|
+
const routePath = baseName === 'index'
|
|
116
|
+
? urlPrefix
|
|
117
|
+
: urlPrefix + '/' + segment;
|
|
118
|
+
|
|
119
|
+
apiRoutes.push({
|
|
120
|
+
filePath: fullPath,
|
|
121
|
+
routePath: normalizePath(routePath),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
walk(pagesDir);
|
|
127
|
+
|
|
128
|
+
// Sort: static routes first, then dynamic, then catch-all
|
|
129
|
+
pages.sort((a, b) => {
|
|
130
|
+
const aWeight = routeWeight(a.routePath);
|
|
131
|
+
const bWeight = routeWeight(b.routePath);
|
|
132
|
+
return aWeight - bWeight;
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
return { pages, layouts, apiRoutes };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Convert a file name to a URL segment.
|
|
140
|
+
* [slug] → :slug
|
|
141
|
+
* [...path] → *path (catch-all)
|
|
142
|
+
* about → about
|
|
143
|
+
*/
|
|
144
|
+
function fileNameToSegment(name) {
|
|
145
|
+
// Catch-all: [...param]
|
|
146
|
+
const catchAll = name.match(/^\[\.\.\.(\w+)\]$/);
|
|
147
|
+
if (catchAll) return '*' + catchAll[1];
|
|
148
|
+
|
|
149
|
+
// Dynamic: [param]
|
|
150
|
+
const dynamic = name.match(/^\[(\w+)\]$/);
|
|
151
|
+
if (dynamic) return ':' + dynamic[1];
|
|
152
|
+
|
|
153
|
+
return name;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Normalize a route path.
|
|
158
|
+
*/
|
|
159
|
+
function normalizePath(p) {
|
|
160
|
+
// Remove double slashes
|
|
161
|
+
let result = p.replace(/\/+/g, '/');
|
|
162
|
+
// Remove trailing slash (except root)
|
|
163
|
+
if (result.length > 1 && result.endsWith('/')) {
|
|
164
|
+
result = result.slice(0, -1);
|
|
165
|
+
}
|
|
166
|
+
return result || '/';
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Route weight for sorting — static routes first.
|
|
171
|
+
*/
|
|
172
|
+
function routeWeight(path) {
|
|
173
|
+
if (path.includes('*')) return 100; // Catch-all last
|
|
174
|
+
if (path.includes(':')) return 10; // Dynamic middle
|
|
175
|
+
return 0; // Static first
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Extract `export const page = { ... }` from a file's source code.
|
|
180
|
+
* Uses simple regex — doesn't need a full parser for this.
|
|
181
|
+
*/
|
|
182
|
+
export function extractPageConfig(source) {
|
|
183
|
+
// Match: export const page = { ... }
|
|
184
|
+
// Handles single-line and simple multi-line objects
|
|
185
|
+
const match = source.match(
|
|
186
|
+
/export\s+const\s+page\s*=\s*(\{[^}]*\})/s
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
if (!match) {
|
|
190
|
+
return { mode: 'client' }; // Default
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
// Simple evaluation of the object literal
|
|
195
|
+
// Only supports string/boolean/number literals for safety
|
|
196
|
+
const obj = match[1]
|
|
197
|
+
.replace(/'/g, '"')
|
|
198
|
+
.replace(/(\w+)\s*:/g, '"$1":')
|
|
199
|
+
.replace(/,\s*}/g, '}')
|
|
200
|
+
.replace(/\/\/[^\n]*/g, ''); // Strip comments
|
|
201
|
+
|
|
202
|
+
return { mode: 'client', ...JSON.parse(obj) };
|
|
203
|
+
} catch {
|
|
204
|
+
return { mode: 'client' };
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Generate the virtual routes module source code.
|
|
210
|
+
* This is what gets imported as 'virtual:what-routes'.
|
|
211
|
+
*/
|
|
212
|
+
export function generateRoutesModule(pagesDir, rootDir) {
|
|
213
|
+
const { pages, layouts, apiRoutes } = scanPages(pagesDir);
|
|
214
|
+
|
|
215
|
+
const imports = [];
|
|
216
|
+
const routeEntries = [];
|
|
217
|
+
|
|
218
|
+
// Generate layout imports
|
|
219
|
+
const layoutMap = new Map();
|
|
220
|
+
layouts.forEach((layout, i) => {
|
|
221
|
+
const varName = `_layout${i}`;
|
|
222
|
+
const relPath = toImportPath(layout.filePath, rootDir);
|
|
223
|
+
imports.push(`import ${varName} from '${relPath}';`);
|
|
224
|
+
layoutMap.set(layout.urlPrefix, varName);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// Generate page imports and route entries
|
|
228
|
+
pages.forEach((page, i) => {
|
|
229
|
+
const varName = `_page${i}`;
|
|
230
|
+
const relPath = toImportPath(page.filePath, rootDir);
|
|
231
|
+
imports.push(`import ${varName} from '${relPath}';`);
|
|
232
|
+
|
|
233
|
+
// Read file to extract page config
|
|
234
|
+
let pageConfig = { mode: 'client' };
|
|
235
|
+
try {
|
|
236
|
+
const source = fs.readFileSync(page.filePath, 'utf-8');
|
|
237
|
+
pageConfig = extractPageConfig(source);
|
|
238
|
+
} catch {}
|
|
239
|
+
|
|
240
|
+
// Find matching layout (closest parent)
|
|
241
|
+
const layoutVar = findLayout(page.routePath, layoutMap);
|
|
242
|
+
|
|
243
|
+
const entry = {
|
|
244
|
+
path: page.routePath,
|
|
245
|
+
component: varName,
|
|
246
|
+
mode: pageConfig.mode || 'client',
|
|
247
|
+
layout: layoutVar || null,
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
routeEntries.push(entry);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// Generate API route entries
|
|
254
|
+
const apiEntries = [];
|
|
255
|
+
apiRoutes.forEach((route, i) => {
|
|
256
|
+
const varName = `_api${i}`;
|
|
257
|
+
const relPath = toImportPath(route.filePath, rootDir);
|
|
258
|
+
imports.push(`import * as ${varName} from '${relPath}';`);
|
|
259
|
+
apiEntries.push({
|
|
260
|
+
path: route.routePath,
|
|
261
|
+
handlers: varName,
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// Build the module
|
|
266
|
+
const lines = [
|
|
267
|
+
'// Auto-generated by What Framework file router',
|
|
268
|
+
'// Do not edit — changes will be overwritten',
|
|
269
|
+
'',
|
|
270
|
+
...imports,
|
|
271
|
+
'',
|
|
272
|
+
'export const routes = [',
|
|
273
|
+
...routeEntries.map(r =>
|
|
274
|
+
` { path: '${r.path}', component: ${r.component}, mode: '${r.mode}'${r.layout ? `, layout: ${r.layout}` : ''} },`
|
|
275
|
+
),
|
|
276
|
+
'];',
|
|
277
|
+
'',
|
|
278
|
+
`export const apiRoutes = [`,
|
|
279
|
+
...apiEntries.map(r =>
|
|
280
|
+
` { path: '${r.path}', handlers: ${r.handlers} },`
|
|
281
|
+
),
|
|
282
|
+
'];',
|
|
283
|
+
'',
|
|
284
|
+
// Export page modes for the build system
|
|
285
|
+
'export const pageModes = {',
|
|
286
|
+
...routeEntries.map(r =>
|
|
287
|
+
` '${r.path}': '${r.mode}',`
|
|
288
|
+
),
|
|
289
|
+
'};',
|
|
290
|
+
];
|
|
291
|
+
|
|
292
|
+
return lines.join('\n');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Convert absolute file path to a root-relative import path.
|
|
297
|
+
*/
|
|
298
|
+
function toImportPath(filePath, rootDir) {
|
|
299
|
+
const rel = path.relative(rootDir, filePath);
|
|
300
|
+
// Ensure forward slashes and starts with /
|
|
301
|
+
return '/' + rel.split(path.sep).join('/');
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Find the closest layout for a given route path.
|
|
306
|
+
*/
|
|
307
|
+
function findLayout(routePath, layoutMap) {
|
|
308
|
+
// Walk up from the route path to find the nearest layout
|
|
309
|
+
const segments = routePath.split('/').filter(Boolean);
|
|
310
|
+
|
|
311
|
+
while (segments.length > 0) {
|
|
312
|
+
const prefix = '/' + segments.join('/');
|
|
313
|
+
if (layoutMap.has(prefix)) return layoutMap.get(prefix);
|
|
314
|
+
segments.pop();
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Check root layout
|
|
318
|
+
if (layoutMap.has('/')) return layoutMap.get('/');
|
|
319
|
+
return null;
|
|
320
|
+
}
|
package/src/index.js
CHANGED
package/src/vite-plugin.js
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* What Framework Vite Plugin
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
*
|
|
4
|
+
* 1. Transforms JSX via the What babel plugin
|
|
5
|
+
* 2. Provides file-based routing via virtual:what-routes
|
|
6
|
+
* 3. Watches pages directory for HMR
|
|
5
7
|
*/
|
|
6
8
|
|
|
9
|
+
import path from 'path';
|
|
7
10
|
import { transformSync } from '@babel/core';
|
|
8
11
|
import whatBabelPlugin from './babel-plugin.js';
|
|
12
|
+
import { generateRoutesModule, scanPages } from './file-router.js';
|
|
13
|
+
|
|
14
|
+
const VIRTUAL_ROUTES_ID = 'virtual:what-routes';
|
|
15
|
+
const RESOLVED_VIRTUAL_ID = '\0' + VIRTUAL_ROUTES_ID;
|
|
9
16
|
|
|
10
17
|
export default function whatVitePlugin(options = {}) {
|
|
11
18
|
const {
|
|
@@ -16,12 +23,63 @@ export default function whatVitePlugin(options = {}) {
|
|
|
16
23
|
// Enable source maps
|
|
17
24
|
sourceMaps = true,
|
|
18
25
|
// Production optimizations
|
|
19
|
-
production = process.env.NODE_ENV === 'production'
|
|
26
|
+
production = process.env.NODE_ENV === 'production',
|
|
27
|
+
// Pages directory (relative to project root)
|
|
28
|
+
pages = 'src/pages',
|
|
20
29
|
} = options;
|
|
21
30
|
|
|
31
|
+
let rootDir = '';
|
|
32
|
+
let pagesDir = '';
|
|
33
|
+
let server = null;
|
|
34
|
+
|
|
22
35
|
return {
|
|
23
36
|
name: 'vite-plugin-what',
|
|
24
37
|
|
|
38
|
+
configResolved(config) {
|
|
39
|
+
rootDir = config.root;
|
|
40
|
+
pagesDir = path.resolve(rootDir, pages);
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
configureServer(devServer) {
|
|
44
|
+
server = devServer;
|
|
45
|
+
|
|
46
|
+
// Watch the pages directory for file additions/removals
|
|
47
|
+
devServer.watcher.on('add', (file) => {
|
|
48
|
+
if (file.startsWith(pagesDir)) {
|
|
49
|
+
// Invalidate the virtual routes module
|
|
50
|
+
const mod = devServer.moduleGraph.getModuleById(RESOLVED_VIRTUAL_ID);
|
|
51
|
+
if (mod) {
|
|
52
|
+
devServer.moduleGraph.invalidateModule(mod);
|
|
53
|
+
devServer.ws.send({ type: 'full-reload' });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
devServer.watcher.on('unlink', (file) => {
|
|
59
|
+
if (file.startsWith(pagesDir)) {
|
|
60
|
+
const mod = devServer.moduleGraph.getModuleById(RESOLVED_VIRTUAL_ID);
|
|
61
|
+
if (mod) {
|
|
62
|
+
devServer.moduleGraph.invalidateModule(mod);
|
|
63
|
+
devServer.ws.send({ type: 'full-reload' });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
// Resolve virtual module
|
|
70
|
+
resolveId(id) {
|
|
71
|
+
if (id === VIRTUAL_ROUTES_ID) {
|
|
72
|
+
return RESOLVED_VIRTUAL_ID;
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
// Generate the routes module
|
|
77
|
+
load(id) {
|
|
78
|
+
if (id === RESOLVED_VIRTUAL_ID) {
|
|
79
|
+
return generateRoutesModule(pagesDir, rootDir);
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
|
|
25
83
|
// Transform JSX files
|
|
26
84
|
transform(code, id) {
|
|
27
85
|
// Check if we should process this file
|