@mauroandre/velojs 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.
- package/LICENSE +21 -0
- package/README.md +1049 -0
- package/bin/velojs.js +15 -0
- package/package.json +120 -0
- package/src/cli.ts +83 -0
- package/src/client.tsx +79 -0
- package/src/components.tsx +155 -0
- package/src/config.ts +29 -0
- package/src/cookie.ts +7 -0
- package/src/factory.ts +1 -0
- package/src/hooks.tsx +266 -0
- package/src/index.ts +19 -0
- package/src/init.ts +177 -0
- package/src/server.tsx +347 -0
- package/src/types.ts +39 -0
- package/src/vite.ts +937 -0
package/src/vite.ts
ADDED
|
@@ -0,0 +1,937 @@
|
|
|
1
|
+
import type { Plugin, PluginOption, UserConfig } from "vite";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import type { VeloConfig } from "./config.js";
|
|
5
|
+
|
|
6
|
+
// External plugins
|
|
7
|
+
import preact from "@preact/preset-vite";
|
|
8
|
+
import devServer from "@hono/vite-dev-server";
|
|
9
|
+
|
|
10
|
+
// Babel imports
|
|
11
|
+
import { parse } from "@babel/parser";
|
|
12
|
+
import _traverse from "@babel/traverse";
|
|
13
|
+
import _generate from "@babel/generator";
|
|
14
|
+
import * as t from "@babel/types";
|
|
15
|
+
|
|
16
|
+
// Workaround para ESM — handle both CJS-wrapped and direct ESM exports
|
|
17
|
+
const traverse = typeof _traverse === "function"
|
|
18
|
+
? _traverse
|
|
19
|
+
: (_traverse as unknown as { default: typeof _traverse }).default;
|
|
20
|
+
const generate = typeof _generate === "function"
|
|
21
|
+
? _generate
|
|
22
|
+
: (_generate as unknown as { default: typeof _generate }).default;
|
|
23
|
+
|
|
24
|
+
// ============================================
|
|
25
|
+
// TRANSFORMAÇÃO 1: Injetar metadata (moduleId + fullPath)
|
|
26
|
+
// ============================================
|
|
27
|
+
|
|
28
|
+
export function injectMetadata(
|
|
29
|
+
code: string,
|
|
30
|
+
moduleId: string,
|
|
31
|
+
fullPath?: string,
|
|
32
|
+
path?: string
|
|
33
|
+
): string {
|
|
34
|
+
const ast = parse(code, {
|
|
35
|
+
sourceType: "module",
|
|
36
|
+
plugins: ["typescript", "jsx"],
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
let metadataFound = false;
|
|
40
|
+
|
|
41
|
+
traverse(ast, {
|
|
42
|
+
// Procura: export const metadata = { ... }
|
|
43
|
+
ExportNamedDeclaration(nodePath) {
|
|
44
|
+
const declaration = nodePath.node.declaration;
|
|
45
|
+
|
|
46
|
+
if (
|
|
47
|
+
t.isVariableDeclaration(declaration) &&
|
|
48
|
+
declaration.declarations.length === 1
|
|
49
|
+
) {
|
|
50
|
+
const declarator = declaration.declarations[0];
|
|
51
|
+
|
|
52
|
+
if (
|
|
53
|
+
declarator &&
|
|
54
|
+
t.isIdentifier(declarator.id, { name: "metadata" }) &&
|
|
55
|
+
t.isObjectExpression(declarator.init)
|
|
56
|
+
) {
|
|
57
|
+
metadataFound = true;
|
|
58
|
+
const properties = declarator.init.properties;
|
|
59
|
+
|
|
60
|
+
// Remove moduleId, fullPath e path existentes se houver
|
|
61
|
+
const filteredProps = properties.filter((prop) => {
|
|
62
|
+
if (
|
|
63
|
+
t.isObjectProperty(prop) &&
|
|
64
|
+
t.isIdentifier(prop.key)
|
|
65
|
+
) {
|
|
66
|
+
return (
|
|
67
|
+
prop.key.name !== "moduleId" &&
|
|
68
|
+
prop.key.name !== "fullPath" &&
|
|
69
|
+
prop.key.name !== "path"
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
return true;
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Adiciona moduleId
|
|
76
|
+
const newProps: t.ObjectProperty[] = [
|
|
77
|
+
t.objectProperty(
|
|
78
|
+
t.identifier("moduleId"),
|
|
79
|
+
t.stringLiteral(moduleId)
|
|
80
|
+
),
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
// Adiciona fullPath se disponível
|
|
84
|
+
if (fullPath !== undefined) {
|
|
85
|
+
newProps.push(
|
|
86
|
+
t.objectProperty(
|
|
87
|
+
t.identifier("fullPath"),
|
|
88
|
+
t.stringLiteral(fullPath)
|
|
89
|
+
)
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Adiciona path se disponível
|
|
94
|
+
if (path !== undefined) {
|
|
95
|
+
newProps.push(
|
|
96
|
+
t.objectProperty(
|
|
97
|
+
t.identifier("path"),
|
|
98
|
+
t.stringLiteral(path)
|
|
99
|
+
)
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
declarator.init.properties = [...newProps, ...filteredProps];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Se não encontrou metadata, adiciona no início
|
|
110
|
+
if (!metadataFound) {
|
|
111
|
+
const props: t.ObjectProperty[] = [
|
|
112
|
+
t.objectProperty(
|
|
113
|
+
t.identifier("moduleId"),
|
|
114
|
+
t.stringLiteral(moduleId)
|
|
115
|
+
),
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
if (fullPath !== undefined) {
|
|
119
|
+
props.push(
|
|
120
|
+
t.objectProperty(
|
|
121
|
+
t.identifier("fullPath"),
|
|
122
|
+
t.stringLiteral(fullPath)
|
|
123
|
+
)
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (path !== undefined) {
|
|
128
|
+
props.push(
|
|
129
|
+
t.objectProperty(
|
|
130
|
+
t.identifier("path"),
|
|
131
|
+
t.stringLiteral(path)
|
|
132
|
+
)
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const metadataExport = t.exportNamedDeclaration(
|
|
137
|
+
t.variableDeclaration("const", [
|
|
138
|
+
t.variableDeclarator(
|
|
139
|
+
t.identifier("metadata"),
|
|
140
|
+
t.objectExpression(props)
|
|
141
|
+
),
|
|
142
|
+
])
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
ast.program.body.unshift(metadataExport);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const output = generate(ast, { retainLines: true });
|
|
149
|
+
return output.code;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ============================================
|
|
153
|
+
// TRANSFORMAÇÃO 2: Injetar moduleId no Loader e useLoader
|
|
154
|
+
// ============================================
|
|
155
|
+
|
|
156
|
+
export function transformLoaderFunctions(code: string, moduleId: string): string {
|
|
157
|
+
const ast = parse(code, {
|
|
158
|
+
sourceType: "module",
|
|
159
|
+
plugins: ["typescript", "jsx"],
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
traverse(ast, {
|
|
163
|
+
CallExpression(nodePath) {
|
|
164
|
+
const callee = nodePath.node.callee;
|
|
165
|
+
|
|
166
|
+
// Verifica se é chamada de Loader ou useLoader (com ou sem type params)
|
|
167
|
+
const isLoader =
|
|
168
|
+
t.isIdentifier(callee, { name: "Loader" }) ||
|
|
169
|
+
(t.isTSInstantiationExpression(callee) &&
|
|
170
|
+
t.isIdentifier(callee.expression, { name: "Loader" }));
|
|
171
|
+
|
|
172
|
+
const isUseLoader =
|
|
173
|
+
t.isIdentifier(callee, { name: "useLoader" }) ||
|
|
174
|
+
(t.isTSInstantiationExpression(callee) &&
|
|
175
|
+
t.isIdentifier(callee.expression, { name: "useLoader" }));
|
|
176
|
+
|
|
177
|
+
if (!isLoader && !isUseLoader) return;
|
|
178
|
+
|
|
179
|
+
const args = nodePath.node.arguments;
|
|
180
|
+
const moduleIdArg = t.stringLiteral(moduleId);
|
|
181
|
+
|
|
182
|
+
// Se já tem moduleId (string com "/" como primeiro arg), não faz nada
|
|
183
|
+
const firstArg = args[0];
|
|
184
|
+
if (t.isStringLiteral(firstArg) && firstArg.value.includes("/")) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Prepende moduleId como primeiro argumento, mantendo os existentes
|
|
189
|
+
// useLoader() → useLoader("moduleId")
|
|
190
|
+
// useLoader([deps]) → useLoader("moduleId", [deps])
|
|
191
|
+
nodePath.node.arguments = [moduleIdArg, ...args];
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const output = generate(ast, { retainLines: true });
|
|
196
|
+
return output.code;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ============================================
|
|
200
|
+
// TRANSFORMAÇÃO 3: Actions → Fetch stubs (client only)
|
|
201
|
+
// ============================================
|
|
202
|
+
|
|
203
|
+
export function transformActionsForClient(code: string, moduleId: string): string {
|
|
204
|
+
const ast = parse(code, {
|
|
205
|
+
sourceType: "module",
|
|
206
|
+
plugins: ["typescript", "jsx"],
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
traverse(ast, {
|
|
210
|
+
ExportNamedDeclaration(nodePath) {
|
|
211
|
+
const declaration = nodePath.node.declaration;
|
|
212
|
+
|
|
213
|
+
// Procura: export const action_xxx = async (...) => { ... }
|
|
214
|
+
if (!t.isVariableDeclaration(declaration)) return;
|
|
215
|
+
|
|
216
|
+
const declarator = declaration.declarations[0];
|
|
217
|
+
if (!declarator || !t.isIdentifier(declarator.id)) return;
|
|
218
|
+
|
|
219
|
+
const name = declarator.id.name;
|
|
220
|
+
if (!name.startsWith("action_")) return;
|
|
221
|
+
|
|
222
|
+
const actionName = name.replace("action_", "");
|
|
223
|
+
|
|
224
|
+
// Verifica se é arrow function async
|
|
225
|
+
const init = declarator.init;
|
|
226
|
+
if (!t.isArrowFunctionExpression(init) || !init.async) return;
|
|
227
|
+
|
|
228
|
+
const params = init.params;
|
|
229
|
+
|
|
230
|
+
// Cria o corpo do fetch stub
|
|
231
|
+
const fetchCall = createFetchStub(moduleId, actionName, params);
|
|
232
|
+
|
|
233
|
+
// Substitui o corpo da função
|
|
234
|
+
init.body = t.blockStatement([t.returnStatement(fetchCall)]);
|
|
235
|
+
|
|
236
|
+
// Ajusta os parâmetros para o client
|
|
237
|
+
adjustParamsForClient(init, params);
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const output = generate(ast, { retainLines: true });
|
|
242
|
+
return output.code;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Cria a expressão fetch para o stub
|
|
247
|
+
*/
|
|
248
|
+
function createFetchStub(
|
|
249
|
+
moduleId: string,
|
|
250
|
+
actionName: string,
|
|
251
|
+
params: t.ArrowFunctionExpression["params"]
|
|
252
|
+
): t.Expression {
|
|
253
|
+
const url = `/_action/${moduleId}/${actionName}`;
|
|
254
|
+
|
|
255
|
+
// Se não tem parâmetros, fetch simples sem body
|
|
256
|
+
if (params.length === 0) {
|
|
257
|
+
return t.callExpression(
|
|
258
|
+
t.memberExpression(
|
|
259
|
+
t.callExpression(t.identifier("fetch"), [
|
|
260
|
+
t.stringLiteral(url),
|
|
261
|
+
t.objectExpression([
|
|
262
|
+
t.objectProperty(
|
|
263
|
+
t.identifier("method"),
|
|
264
|
+
t.stringLiteral("POST")
|
|
265
|
+
),
|
|
266
|
+
]),
|
|
267
|
+
]),
|
|
268
|
+
t.identifier("then")
|
|
269
|
+
),
|
|
270
|
+
[
|
|
271
|
+
t.arrowFunctionExpression(
|
|
272
|
+
[t.identifier("r")],
|
|
273
|
+
t.callExpression(
|
|
274
|
+
t.memberExpression(
|
|
275
|
+
t.identifier("r"),
|
|
276
|
+
t.identifier("json")
|
|
277
|
+
),
|
|
278
|
+
[]
|
|
279
|
+
)
|
|
280
|
+
),
|
|
281
|
+
]
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Com parâmetros - precisa enviar body
|
|
286
|
+
return t.callExpression(
|
|
287
|
+
t.memberExpression(
|
|
288
|
+
t.callExpression(t.identifier("fetch"), [
|
|
289
|
+
t.stringLiteral(url),
|
|
290
|
+
t.objectExpression([
|
|
291
|
+
t.objectProperty(
|
|
292
|
+
t.identifier("method"),
|
|
293
|
+
t.stringLiteral("POST")
|
|
294
|
+
),
|
|
295
|
+
t.objectProperty(
|
|
296
|
+
t.identifier("headers"),
|
|
297
|
+
t.objectExpression([
|
|
298
|
+
t.objectProperty(
|
|
299
|
+
t.stringLiteral("Content-Type"),
|
|
300
|
+
t.stringLiteral("application/json")
|
|
301
|
+
),
|
|
302
|
+
])
|
|
303
|
+
),
|
|
304
|
+
t.objectProperty(
|
|
305
|
+
t.identifier("body"),
|
|
306
|
+
t.callExpression(
|
|
307
|
+
t.memberExpression(
|
|
308
|
+
t.identifier("JSON"),
|
|
309
|
+
t.identifier("stringify")
|
|
310
|
+
),
|
|
311
|
+
[t.identifier("body")]
|
|
312
|
+
)
|
|
313
|
+
),
|
|
314
|
+
]),
|
|
315
|
+
]),
|
|
316
|
+
t.identifier("then")
|
|
317
|
+
),
|
|
318
|
+
[
|
|
319
|
+
t.arrowFunctionExpression(
|
|
320
|
+
[t.identifier("r")],
|
|
321
|
+
t.callExpression(
|
|
322
|
+
t.memberExpression(t.identifier("r"), t.identifier("json")),
|
|
323
|
+
[]
|
|
324
|
+
)
|
|
325
|
+
),
|
|
326
|
+
]
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Ajusta os parâmetros da action para o client
|
|
332
|
+
* Ex: ({ body, c }: ActionArgs<LoginBody>) → ({ body }: { body: LoginBody })
|
|
333
|
+
*/
|
|
334
|
+
function adjustParamsForClient(
|
|
335
|
+
fn: t.ArrowFunctionExpression,
|
|
336
|
+
params: t.ArrowFunctionExpression["params"]
|
|
337
|
+
): void {
|
|
338
|
+
if (params.length === 0) return;
|
|
339
|
+
|
|
340
|
+
const firstParam = params[0];
|
|
341
|
+
|
|
342
|
+
// Se é ObjectPattern (desestruturação), mantém só { body }
|
|
343
|
+
if (t.isObjectPattern(firstParam)) {
|
|
344
|
+
// Extrai o tipo do body se tiver ActionArgs<T>
|
|
345
|
+
let bodyType: t.TSType | null = null;
|
|
346
|
+
|
|
347
|
+
if (
|
|
348
|
+
t.isTSTypeAnnotation(firstParam.typeAnnotation) &&
|
|
349
|
+
t.isTSTypeReference(firstParam.typeAnnotation.typeAnnotation)
|
|
350
|
+
) {
|
|
351
|
+
const typeRef = firstParam.typeAnnotation.typeAnnotation;
|
|
352
|
+
|
|
353
|
+
// Verifica se é ActionArgs<T>
|
|
354
|
+
if (
|
|
355
|
+
t.isIdentifier(typeRef.typeName, { name: "ActionArgs" }) &&
|
|
356
|
+
typeRef.typeParameters?.params[0]
|
|
357
|
+
) {
|
|
358
|
+
bodyType = typeRef.typeParameters.params[0];
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Cria novo parâmetro: { body }: { body: T }
|
|
363
|
+
const bodyProp = t.objectProperty(
|
|
364
|
+
t.identifier("body"),
|
|
365
|
+
t.identifier("body"),
|
|
366
|
+
false,
|
|
367
|
+
true // shorthand
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
const newParam = t.objectPattern([bodyProp]);
|
|
371
|
+
|
|
372
|
+
if (bodyType) {
|
|
373
|
+
newParam.typeAnnotation = t.tsTypeAnnotation(
|
|
374
|
+
t.tsTypeLiteral([
|
|
375
|
+
t.tsPropertySignature(
|
|
376
|
+
t.identifier("body"),
|
|
377
|
+
t.tsTypeAnnotation(bodyType)
|
|
378
|
+
),
|
|
379
|
+
])
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
fn.params = [newParam];
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// ============================================
|
|
388
|
+
// TRANSFORMAÇÃO 4: Remover loaders (client only)
|
|
389
|
+
// ============================================
|
|
390
|
+
|
|
391
|
+
export function removeLoaders(code: string): string {
|
|
392
|
+
const ast = parse(code, {
|
|
393
|
+
sourceType: "module",
|
|
394
|
+
plugins: ["typescript", "jsx"],
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
traverse(ast, {
|
|
398
|
+
ExportNamedDeclaration(nodePath) {
|
|
399
|
+
const declaration = nodePath.node.declaration;
|
|
400
|
+
|
|
401
|
+
// Procura: export const loader = ...
|
|
402
|
+
if (!t.isVariableDeclaration(declaration)) return;
|
|
403
|
+
|
|
404
|
+
const declarator = declaration.declarations[0];
|
|
405
|
+
if (!declarator || !t.isIdentifier(declarator.id)) return;
|
|
406
|
+
|
|
407
|
+
if (declarator.id.name === "loader") {
|
|
408
|
+
// Remove o export inteiro
|
|
409
|
+
nodePath.remove();
|
|
410
|
+
}
|
|
411
|
+
},
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
const output = generate(ast, { retainLines: true });
|
|
415
|
+
return output.code;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// ============================================
|
|
419
|
+
// TRANSFORMAÇÃO 5: Remover middlewares e imports (client only)
|
|
420
|
+
// ============================================
|
|
421
|
+
|
|
422
|
+
export function removeMiddlewares(code: string): string {
|
|
423
|
+
const ast = parse(code, {
|
|
424
|
+
sourceType: "module",
|
|
425
|
+
plugins: ["typescript", "jsx"],
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
// Fase 1: Coleta identificadores usados em middlewares: [...]
|
|
429
|
+
const middlewareIdentifiers = new Set<string>();
|
|
430
|
+
|
|
431
|
+
traverse(ast, {
|
|
432
|
+
ObjectProperty(nodePath) {
|
|
433
|
+
// Procura: middlewares: [...]
|
|
434
|
+
if (
|
|
435
|
+
t.isIdentifier(nodePath.node.key, { name: "middlewares" }) &&
|
|
436
|
+
t.isArrayExpression(nodePath.node.value)
|
|
437
|
+
) {
|
|
438
|
+
// Coleta todos os identificadores no array
|
|
439
|
+
for (const element of nodePath.node.value.elements) {
|
|
440
|
+
if (t.isIdentifier(element)) {
|
|
441
|
+
middlewareIdentifiers.add(element.name);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Remove a propriedade middlewares
|
|
446
|
+
nodePath.remove();
|
|
447
|
+
}
|
|
448
|
+
},
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
// Se não encontrou middlewares, retorna código original
|
|
452
|
+
if (middlewareIdentifiers.size === 0) {
|
|
453
|
+
return code;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Fase 2: Remove imports dos identificadores de middleware
|
|
457
|
+
traverse(ast, {
|
|
458
|
+
ImportDeclaration(nodePath) {
|
|
459
|
+
const specifiers = nodePath.node.specifiers;
|
|
460
|
+
|
|
461
|
+
// Filtra os specifiers, removendo os que são middlewares
|
|
462
|
+
const remainingSpecifiers = specifiers.filter((specifier) => {
|
|
463
|
+
if (t.isImportSpecifier(specifier)) {
|
|
464
|
+
const localName = specifier.local.name;
|
|
465
|
+
return !middlewareIdentifiers.has(localName);
|
|
466
|
+
}
|
|
467
|
+
// Mantém default imports e namespace imports
|
|
468
|
+
return true;
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
if (remainingSpecifiers.length === 0) {
|
|
472
|
+
// Remove o import inteiro se não sobrou nenhum specifier
|
|
473
|
+
nodePath.remove();
|
|
474
|
+
} else if (remainingSpecifiers.length !== specifiers.length) {
|
|
475
|
+
// Atualiza os specifiers se alguns foram removidos
|
|
476
|
+
nodePath.node.specifiers = remainingSpecifiers;
|
|
477
|
+
}
|
|
478
|
+
},
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
const output = generate(ast, { retainLines: true });
|
|
482
|
+
return output.code;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// ============================================
|
|
486
|
+
// TRANSFORMAÇÃO 6: Extrair e injetar fullPaths
|
|
487
|
+
// ============================================
|
|
488
|
+
|
|
489
|
+
export interface PathInfo {
|
|
490
|
+
fullPath: string;
|
|
491
|
+
path: string;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Parseia routes.tsx e retorna Map de moduleId → { fullPath, path }
|
|
496
|
+
* moduleId aqui é derivado do import source (ex: "./auth/Login.js" → "auth/Login")
|
|
497
|
+
*/
|
|
498
|
+
export function buildFullPathMap(code: string): Map<string, PathInfo> {
|
|
499
|
+
const ast = parse(code, {
|
|
500
|
+
sourceType: "module",
|
|
501
|
+
plugins: ["typescript", "jsx"],
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
// Fase 1: Coletar imports - localName → source
|
|
505
|
+
const imports = new Map<string, string>();
|
|
506
|
+
|
|
507
|
+
traverse(ast, {
|
|
508
|
+
ImportDeclaration(nodePath) {
|
|
509
|
+
const source = nodePath.node.source.value;
|
|
510
|
+
|
|
511
|
+
for (const specifier of nodePath.node.specifiers) {
|
|
512
|
+
if (t.isImportNamespaceSpecifier(specifier)) {
|
|
513
|
+
// import * as Name from "./path"
|
|
514
|
+
imports.set(specifier.local.name, source);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
},
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
// Fase 2: Percorrer rotas e coletar moduleName → { fullPath, path }
|
|
521
|
+
const moduleNameToPaths = new Map<string, PathInfo>();
|
|
522
|
+
|
|
523
|
+
traverse(ast, {
|
|
524
|
+
ExportDefaultDeclaration(nodePath) {
|
|
525
|
+
const declaration = nodePath.node.declaration;
|
|
526
|
+
|
|
527
|
+
let arrayNode: t.ArrayExpression | null = null;
|
|
528
|
+
|
|
529
|
+
if (t.isArrayExpression(declaration)) {
|
|
530
|
+
arrayNode = declaration;
|
|
531
|
+
} else if (
|
|
532
|
+
t.isTSSatisfiesExpression(declaration) &&
|
|
533
|
+
t.isArrayExpression(declaration.expression)
|
|
534
|
+
) {
|
|
535
|
+
arrayNode = declaration.expression;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (arrayNode) {
|
|
539
|
+
collectFullPaths(arrayNode.elements, "", moduleNameToPaths);
|
|
540
|
+
}
|
|
541
|
+
},
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
// Fase 3: Converter moduleName → moduleId baseado no import source
|
|
545
|
+
const result = new Map<string, PathInfo>();
|
|
546
|
+
|
|
547
|
+
for (const [moduleName, pathInfo] of moduleNameToPaths) {
|
|
548
|
+
const source = imports.get(moduleName);
|
|
549
|
+
if (source) {
|
|
550
|
+
// "./auth/Login.js" → "auth/Login"
|
|
551
|
+
const moduleId = source
|
|
552
|
+
.replace(/^\.\//, "")
|
|
553
|
+
.replace(/\.(tsx?|jsx?|js)$/, "");
|
|
554
|
+
result.set(moduleId, pathInfo);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
return result;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Percorre recursivamente a árvore de rotas e coleta moduleName → { fullPath, path }
|
|
563
|
+
*/
|
|
564
|
+
export function collectFullPaths(
|
|
565
|
+
elements: (t.Expression | t.SpreadElement | null)[],
|
|
566
|
+
parentPath: string,
|
|
567
|
+
result: Map<string, PathInfo>
|
|
568
|
+
): void {
|
|
569
|
+
for (const element of elements) {
|
|
570
|
+
if (!t.isObjectExpression(element)) continue;
|
|
571
|
+
|
|
572
|
+
let nodePath = "";
|
|
573
|
+
let currentPath = parentPath;
|
|
574
|
+
let moduleName: string | null = null;
|
|
575
|
+
let childrenNode: t.ArrayExpression | null = null;
|
|
576
|
+
|
|
577
|
+
for (const prop of element.properties) {
|
|
578
|
+
if (!t.isObjectProperty(prop)) continue;
|
|
579
|
+
|
|
580
|
+
const key = prop.key;
|
|
581
|
+
const keyName = t.isIdentifier(key) ? key.name : null;
|
|
582
|
+
|
|
583
|
+
if (keyName === "path" && t.isStringLiteral(prop.value)) {
|
|
584
|
+
nodePath = prop.value.value;
|
|
585
|
+
// Adiciona / entre parentPath e nodePath se necessário
|
|
586
|
+
if (nodePath && !nodePath.startsWith("/")) {
|
|
587
|
+
currentPath = parentPath + "/" + nodePath;
|
|
588
|
+
} else {
|
|
589
|
+
currentPath = parentPath + nodePath;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if (keyName === "module" && t.isIdentifier(prop.value)) {
|
|
594
|
+
moduleName = prop.value.name;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (keyName === "children" && t.isArrayExpression(prop.value)) {
|
|
598
|
+
childrenNode = prop.value;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Adiciona o módulo com seu fullPath e path
|
|
603
|
+
if (moduleName) {
|
|
604
|
+
// Se é folha (sem children) e path é "/", fullPath = parentPath (sem trailing slash)
|
|
605
|
+
// Isso permite que wouter use "/" para index routes, mas servidor registra sem trailing slash
|
|
606
|
+
const isLeafWithSlash = !childrenNode && nodePath === "/";
|
|
607
|
+
const effectiveFullPath = isLeafWithSlash ? (parentPath || "/") : currentPath;
|
|
608
|
+
|
|
609
|
+
result.set(moduleName, {
|
|
610
|
+
fullPath: effectiveFullPath,
|
|
611
|
+
path: nodePath,
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// Recursivamente processa os filhos
|
|
616
|
+
if (childrenNode) {
|
|
617
|
+
collectFullPaths(childrenNode.elements, currentPath, result);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// ============================================
|
|
623
|
+
// VIRTUAL MODULE IDs
|
|
624
|
+
// ============================================
|
|
625
|
+
|
|
626
|
+
const VIRTUAL_SERVER_ENTRY = "virtual:velo/server-entry";
|
|
627
|
+
const VIRTUAL_CLIENT_ENTRY = "virtual:velo/client-entry";
|
|
628
|
+
const RESOLVED_VIRTUAL_SERVER = "\0" + VIRTUAL_SERVER_ENTRY;
|
|
629
|
+
const RESOLVED_VIRTUAL_CLIENT = "\0" + VIRTUAL_CLIENT_ENTRY;
|
|
630
|
+
|
|
631
|
+
// ============================================
|
|
632
|
+
// VITE PLUGIN - TRANSFORM (internal)
|
|
633
|
+
// ============================================
|
|
634
|
+
|
|
635
|
+
function veloTransformPlugin(veloConfig: VeloConfig, appDirectory: string): Plugin {
|
|
636
|
+
// Initialize with cwd as fallback, will be updated in configResolved
|
|
637
|
+
let appDir: string = path.resolve(process.cwd(), appDirectory);
|
|
638
|
+
const routesFile = veloConfig.routesFile ?? "routes.tsx";
|
|
639
|
+
|
|
640
|
+
// Map de moduleId → { fullPath, path } (populado no buildStart)
|
|
641
|
+
const pathInfoMap = new Map<string, PathInfo>();
|
|
642
|
+
|
|
643
|
+
return {
|
|
644
|
+
name: "velo:transform",
|
|
645
|
+
enforce: "pre",
|
|
646
|
+
|
|
647
|
+
configResolved(resolvedConfig) {
|
|
648
|
+
appDir = path.resolve(resolvedConfig.root, appDirectory);
|
|
649
|
+
},
|
|
650
|
+
|
|
651
|
+
buildStart() {
|
|
652
|
+
// Lê routes.tsx e popula o Map de paths
|
|
653
|
+
const routesFilePath = path.join(appDir, routesFile);
|
|
654
|
+
if (fs.existsSync(routesFilePath)) {
|
|
655
|
+
const routesCode = fs.readFileSync(routesFilePath, "utf-8");
|
|
656
|
+
const paths = buildFullPathMap(routesCode);
|
|
657
|
+
pathInfoMap.clear();
|
|
658
|
+
for (const [moduleId, pathInfo] of paths) {
|
|
659
|
+
pathInfoMap.set(moduleId, pathInfo);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
},
|
|
663
|
+
|
|
664
|
+
handleHotUpdate({ file, server }) {
|
|
665
|
+
// Reconstrói o Map quando routes.tsx muda
|
|
666
|
+
const routesFilePath = path.join(appDir, routesFile);
|
|
667
|
+
if (file === routesFilePath) {
|
|
668
|
+
const routesCode = fs.readFileSync(routesFilePath, "utf-8");
|
|
669
|
+
const paths = buildFullPathMap(routesCode);
|
|
670
|
+
pathInfoMap.clear();
|
|
671
|
+
for (const [moduleId, pathInfo] of paths) {
|
|
672
|
+
pathInfoMap.set(moduleId, pathInfo);
|
|
673
|
+
}
|
|
674
|
+
// Força reload completo para aplicar novas rotas
|
|
675
|
+
server.ws.send({ type: "full-reload" });
|
|
676
|
+
return [];
|
|
677
|
+
}
|
|
678
|
+
},
|
|
679
|
+
|
|
680
|
+
resolveId(id) {
|
|
681
|
+
// Handle server entry (with or without project root prefix)
|
|
682
|
+
if (id === VIRTUAL_SERVER_ENTRY || id.endsWith(VIRTUAL_SERVER_ENTRY)) {
|
|
683
|
+
return RESOLVED_VIRTUAL_SERVER;
|
|
684
|
+
}
|
|
685
|
+
// Handle client entry (with or without project root prefix)
|
|
686
|
+
if (
|
|
687
|
+
id === VIRTUAL_CLIENT_ENTRY ||
|
|
688
|
+
id.endsWith(VIRTUAL_CLIENT_ENTRY) ||
|
|
689
|
+
id === "/__velo_client.js"
|
|
690
|
+
) {
|
|
691
|
+
return RESOLVED_VIRTUAL_CLIENT;
|
|
692
|
+
}
|
|
693
|
+
return null;
|
|
694
|
+
},
|
|
695
|
+
|
|
696
|
+
load(id) {
|
|
697
|
+
const routesFile = veloConfig.routesFile ?? "routes.tsx";
|
|
698
|
+
const serverInit = veloConfig.serverInit ?? "server.tsx";
|
|
699
|
+
const clientInit = veloConfig.clientInit ?? "client.tsx";
|
|
700
|
+
|
|
701
|
+
// Paths relativos ao appDir
|
|
702
|
+
const routesPath = path.join(appDir, routesFile).replace(/\.tsx?$/, ".js");
|
|
703
|
+
const serverInitPath = path.join(appDir, serverInit).replace(/\.tsx?$/, ".js");
|
|
704
|
+
const clientInitPath = path.join(appDir, clientInit).replace(/\.tsx?$/, ".js");
|
|
705
|
+
|
|
706
|
+
if (id === RESOLVED_VIRTUAL_SERVER) {
|
|
707
|
+
return `
|
|
708
|
+
import "${serverInitPath}";
|
|
709
|
+
import routes from "${routesPath}";
|
|
710
|
+
import { startServer } from "@mauroandre/velojs/server";
|
|
711
|
+
|
|
712
|
+
export default await startServer({ routes });
|
|
713
|
+
`;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
if (id === RESOLVED_VIRTUAL_CLIENT) {
|
|
717
|
+
return `
|
|
718
|
+
import "${clientInitPath}";
|
|
719
|
+
import routes from "${routesPath}";
|
|
720
|
+
import { startClient } from "@mauroandre/velojs/client";
|
|
721
|
+
|
|
722
|
+
startClient({ routes });
|
|
723
|
+
`;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
return null;
|
|
727
|
+
},
|
|
728
|
+
|
|
729
|
+
transform(code, id, transformOptions) {
|
|
730
|
+
const isSSR = transformOptions?.ssr === true;
|
|
731
|
+
|
|
732
|
+
// Ignora virtual modules
|
|
733
|
+
if (id.startsWith("\0")) return null;
|
|
734
|
+
|
|
735
|
+
// Ignora arquivos dentro de .velojs/
|
|
736
|
+
if (id.includes("/.velojs/")) return null;
|
|
737
|
+
|
|
738
|
+
// Ignora arquivos que não são tsx/ts
|
|
739
|
+
if (!id.endsWith(".tsx") && !id.endsWith(".ts")) return null;
|
|
740
|
+
|
|
741
|
+
// Ignora arquivos fora do diretório da aplicação
|
|
742
|
+
if (!id.startsWith(appDir)) return null;
|
|
743
|
+
|
|
744
|
+
let transformedCode = code;
|
|
745
|
+
let hasTransformations = false;
|
|
746
|
+
|
|
747
|
+
// Verifica padrões no código
|
|
748
|
+
const hasMiddlewares = /middlewares:\s*\[/.test(code);
|
|
749
|
+
const hasComponent = /export\s+(const|function)\s+Component/.test(
|
|
750
|
+
code
|
|
751
|
+
);
|
|
752
|
+
const hasLoader = /export\s+(const|function)\s+loader/.test(code);
|
|
753
|
+
const hasAction = /export\s+const\s+action_\w+/.test(code);
|
|
754
|
+
const hasLoaderCall = /\bLoader\s*</.test(code) || /\bLoader\s*\(/.test(code);
|
|
755
|
+
const hasUseLoaderCall = /\buseLoader\s*</.test(code) || /\buseLoader\s*\(/.test(code);
|
|
756
|
+
|
|
757
|
+
// 5. Remover middlewares e imports (client only)
|
|
758
|
+
if (!isSSR && hasMiddlewares) {
|
|
759
|
+
transformedCode = removeMiddlewares(transformedCode);
|
|
760
|
+
hasTransformations = true;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// Se tem Component, loader, action, ou chamadas de Loader/useLoader, aplica transformações
|
|
764
|
+
if (hasComponent || hasLoader || hasAction || hasLoaderCall || hasUseLoaderCall) {
|
|
765
|
+
const moduleId = path
|
|
766
|
+
.relative(appDir, id)
|
|
767
|
+
.replace(/\.(tsx?|jsx?)$/, "")
|
|
768
|
+
.replace(/\\/g, "/");
|
|
769
|
+
|
|
770
|
+
// Busca pathInfo no Map
|
|
771
|
+
const pathInfo = pathInfoMap.get(moduleId);
|
|
772
|
+
|
|
773
|
+
// 1. Injeta metadata.moduleId, metadata.fullPath e metadata.path
|
|
774
|
+
transformedCode = injectMetadata(transformedCode, moduleId, pathInfo?.fullPath, pathInfo?.path);
|
|
775
|
+
|
|
776
|
+
// 2. Transformar Loader e useLoader
|
|
777
|
+
transformedCode = transformLoaderFunctions(transformedCode, moduleId);
|
|
778
|
+
|
|
779
|
+
// 3. Transformar actions em fetch stubs (client only)
|
|
780
|
+
if (!isSSR) {
|
|
781
|
+
transformedCode = transformActionsForClient(
|
|
782
|
+
transformedCode,
|
|
783
|
+
moduleId
|
|
784
|
+
);
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// 4. Remover loaders (client only)
|
|
788
|
+
if (!isSSR) {
|
|
789
|
+
transformedCode = removeLoaders(transformedCode);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
hasTransformations = true;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
if (!hasTransformations) return null;
|
|
796
|
+
|
|
797
|
+
return {
|
|
798
|
+
code: transformedCode,
|
|
799
|
+
map: null,
|
|
800
|
+
};
|
|
801
|
+
},
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// ============================================
|
|
806
|
+
// VITE PLUGIN - CONFIG (internal)
|
|
807
|
+
// ============================================
|
|
808
|
+
|
|
809
|
+
function veloConfigPlugin(): Plugin {
|
|
810
|
+
return {
|
|
811
|
+
name: "velo:config",
|
|
812
|
+
|
|
813
|
+
config(userConfig, { mode }) {
|
|
814
|
+
const isServer = mode === "server";
|
|
815
|
+
const isDev = mode === "development";
|
|
816
|
+
|
|
817
|
+
const config: UserConfig = {
|
|
818
|
+
define: {
|
|
819
|
+
"process.env.NODE_ENV": JSON.stringify(isDev ? "development" : "production"),
|
|
820
|
+
"process.env.STATIC_BASE_URL": JSON.stringify(process.env.STATIC_BASE_URL || ""),
|
|
821
|
+
},
|
|
822
|
+
resolve: {
|
|
823
|
+
alias: {
|
|
824
|
+
react: "preact/compat",
|
|
825
|
+
"react-dom": "preact/compat",
|
|
826
|
+
},
|
|
827
|
+
},
|
|
828
|
+
};
|
|
829
|
+
|
|
830
|
+
if (isServer) {
|
|
831
|
+
config.build = {
|
|
832
|
+
ssr: VIRTUAL_SERVER_ENTRY,
|
|
833
|
+
outDir: "dist",
|
|
834
|
+
emptyOutDir: false,
|
|
835
|
+
copyPublicDir: false,
|
|
836
|
+
rollupOptions: {
|
|
837
|
+
output: {
|
|
838
|
+
entryFileNames: "server.js",
|
|
839
|
+
},
|
|
840
|
+
},
|
|
841
|
+
};
|
|
842
|
+
} else if (!isDev) {
|
|
843
|
+
// Client production build
|
|
844
|
+
config.build = {
|
|
845
|
+
manifest: true,
|
|
846
|
+
outDir: "dist/client",
|
|
847
|
+
rollupOptions: {
|
|
848
|
+
input: VIRTUAL_CLIENT_ENTRY,
|
|
849
|
+
output: {
|
|
850
|
+
entryFileNames: "client.js",
|
|
851
|
+
assetFileNames: (assetInfo) => {
|
|
852
|
+
if (assetInfo.names?.[0]?.endsWith(".css")) {
|
|
853
|
+
return "client.css";
|
|
854
|
+
}
|
|
855
|
+
return "[name][extname]";
|
|
856
|
+
},
|
|
857
|
+
},
|
|
858
|
+
},
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
return config;
|
|
863
|
+
},
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// ============================================
|
|
868
|
+
// VITE PLUGIN - STATIC URL REWRITE (internal)
|
|
869
|
+
// ============================================
|
|
870
|
+
|
|
871
|
+
/**
|
|
872
|
+
* Rewrites root-relative url() paths in CSS at build time.
|
|
873
|
+
* url(/img/foo.png) → url(STATIC_BASE_URL/img/foo.png)
|
|
874
|
+
*
|
|
875
|
+
* This allows users to write normal CSS paths and have them
|
|
876
|
+
* automatically point to a bucket/CDN when STATIC_BASE_URL is set.
|
|
877
|
+
* Only runs during build — in dev, paths stay as-is.
|
|
878
|
+
*/
|
|
879
|
+
function veloStaticUrlPlugin(): Plugin {
|
|
880
|
+
return {
|
|
881
|
+
name: "velo:static-url",
|
|
882
|
+
apply: "build",
|
|
883
|
+
enforce: "post",
|
|
884
|
+
|
|
885
|
+
generateBundle(_, bundle) {
|
|
886
|
+
const staticBase = process.env.STATIC_BASE_URL || "";
|
|
887
|
+
if (!staticBase) return;
|
|
888
|
+
|
|
889
|
+
for (const chunk of Object.values(bundle)) {
|
|
890
|
+
if (
|
|
891
|
+
chunk.type === "asset" &&
|
|
892
|
+
typeof chunk.source === "string" &&
|
|
893
|
+
chunk.fileName.endsWith(".css")
|
|
894
|
+
) {
|
|
895
|
+
// Rewrite url(/...) but not url(//...) (protocol-relative)
|
|
896
|
+
chunk.source = chunk.source.replace(
|
|
897
|
+
/url\(\s*(['"]?)\/(?!\/)/g,
|
|
898
|
+
`url($1${staticBase}/`
|
|
899
|
+
);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
},
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// ============================================
|
|
907
|
+
// MAIN EXPORT
|
|
908
|
+
// ============================================
|
|
909
|
+
|
|
910
|
+
export function veloPlugin(config?: VeloConfig): PluginOption[] {
|
|
911
|
+
const veloConfig: VeloConfig = config ?? {};
|
|
912
|
+
const appDirectory = veloConfig.appDirectory ?? "./app";
|
|
913
|
+
|
|
914
|
+
return [
|
|
915
|
+
veloConfigPlugin(),
|
|
916
|
+
veloTransformPlugin(veloConfig, appDirectory),
|
|
917
|
+
veloStaticUrlPlugin(),
|
|
918
|
+
preact(),
|
|
919
|
+
devServer({
|
|
920
|
+
entry: VIRTUAL_SERVER_ENTRY,
|
|
921
|
+
}),
|
|
922
|
+
veloWsBridgePlugin(),
|
|
923
|
+
];
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
/**
|
|
927
|
+
* Exposes Vite's HTTP server via globalThis so the app can attach
|
|
928
|
+
* WebSocket upgrade handlers in dev mode.
|
|
929
|
+
*/
|
|
930
|
+
function veloWsBridgePlugin(): Plugin {
|
|
931
|
+
return {
|
|
932
|
+
name: "velo:ws-bridge",
|
|
933
|
+
configureServer(viteServer) {
|
|
934
|
+
(globalThis as any).__veloDevServer = viteServer.httpServer;
|
|
935
|
+
},
|
|
936
|
+
};
|
|
937
|
+
}
|