pulse-js-framework 1.10.4 → 1.11.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/README.md +11 -0
- package/cli/build.js +13 -3
- package/compiler/directives.js +356 -0
- package/compiler/lexer.js +18 -3
- package/compiler/parser/core.js +6 -0
- package/compiler/parser/view.js +2 -6
- package/compiler/preprocessor.js +43 -23
- package/compiler/sourcemap.js +3 -1
- package/compiler/transformer/actions.js +329 -0
- package/compiler/transformer/export.js +7 -0
- package/compiler/transformer/expressions.js +85 -33
- package/compiler/transformer/imports.js +3 -0
- package/compiler/transformer/index.js +2 -0
- package/compiler/transformer/store.js +1 -1
- package/compiler/transformer/style.js +45 -16
- package/compiler/transformer/view.js +23 -2
- package/loader/rollup-plugin-server-components.js +391 -0
- package/loader/vite-plugin-server-components.js +420 -0
- package/loader/webpack-loader-server-components.js +356 -0
- package/package.json +124 -82
- package/runtime/async.js +4 -0
- package/runtime/context.js +16 -3
- package/runtime/dom-adapter.js +5 -3
- package/runtime/dom-virtual-list.js +2 -1
- package/runtime/form.js +8 -3
- package/runtime/graphql/cache.js +1 -1
- package/runtime/graphql/client.js +22 -0
- package/runtime/graphql/hooks.js +12 -6
- package/runtime/graphql/subscriptions.js +2 -0
- package/runtime/hmr.js +6 -3
- package/runtime/http.js +1 -0
- package/runtime/i18n.js +2 -0
- package/runtime/lru-cache.js +3 -1
- package/runtime/native.js +46 -20
- package/runtime/pulse.js +3 -0
- package/runtime/router/core.js +5 -1
- package/runtime/router/index.js +17 -1
- package/runtime/router/psc-integration.js +301 -0
- package/runtime/security.js +58 -29
- package/runtime/server-components/actions-server.js +798 -0
- package/runtime/server-components/actions.js +389 -0
- package/runtime/server-components/client.js +447 -0
- package/runtime/server-components/error-sanitizer.js +438 -0
- package/runtime/server-components/index.js +275 -0
- package/runtime/server-components/security-csrf.js +593 -0
- package/runtime/server-components/security-errors.js +227 -0
- package/runtime/server-components/security-ratelimit.js +733 -0
- package/runtime/server-components/security-validation.js +467 -0
- package/runtime/server-components/security.js +598 -0
- package/runtime/server-components/serializer.js +617 -0
- package/runtime/server-components/server.js +382 -0
- package/runtime/server-components/types.js +383 -0
- package/runtime/server-components/utils/mutex.js +60 -0
- package/runtime/server-components/utils/path-sanitizer.js +109 -0
- package/runtime/ssr.js +2 -1
- package/runtime/store.js +19 -10
- package/runtime/utils.js +12 -128
- package/types/animation.d.ts +300 -0
- package/types/i18n.d.ts +283 -0
- package/types/persistence.d.ts +267 -0
- package/types/sse.d.ts +248 -0
- package/types/sw.d.ts +150 -0
- package/runtime/a11y.js.original +0 -1844
- package/runtime/graphql.js.original +0 -1326
- package/runtime/router.js.original +0 -1605
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server Actions Compiler Support
|
|
3
|
+
*
|
|
4
|
+
* Detects and transforms Server Action functions marked with 'use server'.
|
|
5
|
+
* Generates client-side RPC stubs and server-side registration code.
|
|
6
|
+
*
|
|
7
|
+
* @module pulse-js-framework/compiler/transformer/actions
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// ============================================================
|
|
11
|
+
// Detection
|
|
12
|
+
// ============================================================
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Detect Server Action in function
|
|
16
|
+
* @param {Object} functionNode - Function AST node
|
|
17
|
+
* @returns {boolean} True if function is a Server Action
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* // Function with 'use server' directive
|
|
21
|
+
* async function createUser(data) {
|
|
22
|
+
* 'use server';
|
|
23
|
+
* return await db.users.create(data);
|
|
24
|
+
* }
|
|
25
|
+
* // isServerAction(functionNode) → true
|
|
26
|
+
*/
|
|
27
|
+
export function isServerAction(functionNode) {
|
|
28
|
+
// Look for 'use server' directive at top of function
|
|
29
|
+
if (!functionNode.body || !functionNode.body.length) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const firstStatement = functionNode.body[0];
|
|
34
|
+
if (firstStatement.type === 'StringLiteral' || firstStatement.type === 'ExpressionStatement') {
|
|
35
|
+
const value = firstStatement.value || firstStatement.expression?.value;
|
|
36
|
+
if (typeof value === 'string') {
|
|
37
|
+
const directive = value.trim().toLowerCase();
|
|
38
|
+
return directive === 'use server';
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Check if entire module has 'use server' directive
|
|
47
|
+
* @param {Object} ast - Module AST
|
|
48
|
+
* @returns {boolean} True if module-level 'use server'
|
|
49
|
+
*/
|
|
50
|
+
export function hasServerDirective(ast) {
|
|
51
|
+
if (!ast.body || !ast.body.length) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const firstStatement = ast.body[0];
|
|
56
|
+
if (firstStatement.type === 'StringLiteral' || firstStatement.type === 'ExpressionStatement') {
|
|
57
|
+
const value = firstStatement.value || firstStatement.expression?.value;
|
|
58
|
+
if (typeof value === 'string') {
|
|
59
|
+
const directive = value.trim().toLowerCase();
|
|
60
|
+
return directive === 'use server';
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ============================================================
|
|
68
|
+
// Transformation
|
|
69
|
+
// ============================================================
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Transform Server Action function to RPC stub
|
|
73
|
+
* @param {Object} functionNode - Function AST node
|
|
74
|
+
* @param {string} componentId - Component identifier
|
|
75
|
+
* @param {string} functionName - Function name
|
|
76
|
+
* @returns {string} Transformed JavaScript code
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* // Input:
|
|
80
|
+
* async function createUser(data) {
|
|
81
|
+
* 'use server';
|
|
82
|
+
* return await db.users.create(data);
|
|
83
|
+
* }
|
|
84
|
+
*
|
|
85
|
+
* // Output:
|
|
86
|
+
* import { createActionInvoker } from 'pulse-js-framework/runtime/server-components';
|
|
87
|
+
* const createUser = createActionInvoker('Component$createUser');
|
|
88
|
+
*/
|
|
89
|
+
export function transformServerAction(functionNode, componentId, functionName) {
|
|
90
|
+
const actionId = `${componentId}$${functionName}`;
|
|
91
|
+
|
|
92
|
+
// Generate client-side stub that calls server
|
|
93
|
+
return `
|
|
94
|
+
// Server Action: ${functionName}
|
|
95
|
+
import { createActionInvoker } from 'pulse-js-framework/runtime/server-components';
|
|
96
|
+
const ${functionName} = createActionInvoker('${actionId}');
|
|
97
|
+
`.trim();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Transform module with 'use server' directive
|
|
102
|
+
* @param {string} moduleId - Module identifier
|
|
103
|
+
* @param {Array<Object>} exportedFunctions - Exported function nodes
|
|
104
|
+
* @returns {string} Transformed module code
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* // Input module with 'use server':
|
|
108
|
+
* 'use server';
|
|
109
|
+
* export async function createUser(data) { ... }
|
|
110
|
+
* export async function deleteUser(id) { ... }
|
|
111
|
+
*
|
|
112
|
+
* // Output (client bundle):
|
|
113
|
+
* import { createActionInvoker } from 'pulse-js-framework/runtime/server-components';
|
|
114
|
+
* export const createUser = createActionInvoker('Module$createUser');
|
|
115
|
+
* export const deleteUser = createActionInvoker('Module$deleteUser');
|
|
116
|
+
*/
|
|
117
|
+
export function transformServerModule(moduleId, exportedFunctions) {
|
|
118
|
+
const imports = `import { createActionInvoker } from 'pulse-js-framework/runtime/server-components';`;
|
|
119
|
+
|
|
120
|
+
const stubs = exportedFunctions.map(fn => {
|
|
121
|
+
const actionId = `${moduleId}$${fn.name}`;
|
|
122
|
+
return `export const ${fn.name} = createActionInvoker('${actionId}');`;
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
return [imports, '', ...stubs].join('\n');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ============================================================
|
|
129
|
+
// Extraction
|
|
130
|
+
// ============================================================
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Extract Server Actions from actions block
|
|
134
|
+
* @param {Object} actionsBlock - Actions block AST node
|
|
135
|
+
* @param {string} componentId - Component identifier
|
|
136
|
+
* @returns {Array<{id, name, params}>} Server Actions metadata
|
|
137
|
+
*
|
|
138
|
+
* @example
|
|
139
|
+
* // .pulse file actions block:
|
|
140
|
+
* actions {
|
|
141
|
+
* async createUser(data) {
|
|
142
|
+
* 'use server';
|
|
143
|
+
* return await db.users.create(data);
|
|
144
|
+
* }
|
|
145
|
+
* async deleteUser(id) {
|
|
146
|
+
* 'use server';
|
|
147
|
+
* return await db.users.delete(id);
|
|
148
|
+
* }
|
|
149
|
+
* }
|
|
150
|
+
*
|
|
151
|
+
* // extractServerActions(actionsBlock, 'UserForm') →
|
|
152
|
+
* [
|
|
153
|
+
* { id: 'UserForm$createUser', name: 'createUser', params: ['data'] },
|
|
154
|
+
* { id: 'UserForm$deleteUser', name: 'deleteUser', params: ['id'] }
|
|
155
|
+
* ]
|
|
156
|
+
*/
|
|
157
|
+
export function extractServerActions(actionsBlock, componentId) {
|
|
158
|
+
const serverActions = [];
|
|
159
|
+
|
|
160
|
+
if (!actionsBlock || !actionsBlock.functions) {
|
|
161
|
+
return serverActions;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
for (const action of actionsBlock.functions) {
|
|
165
|
+
if (isServerAction(action)) {
|
|
166
|
+
serverActions.push({
|
|
167
|
+
id: `${componentId}$${action.name}`,
|
|
168
|
+
name: action.name,
|
|
169
|
+
params: action.params || []
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return serverActions;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Extract all exported functions from module
|
|
179
|
+
* @param {Object} ast - Module AST
|
|
180
|
+
* @returns {Array<{name, params, async}>} Exported functions
|
|
181
|
+
*/
|
|
182
|
+
export function extractExportedFunctions(ast) {
|
|
183
|
+
const functions = [];
|
|
184
|
+
|
|
185
|
+
if (!ast.body) {
|
|
186
|
+
return functions;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
for (const node of ast.body) {
|
|
190
|
+
// export function name() { }
|
|
191
|
+
if (node.type === 'ExportDeclaration' && node.declaration?.type === 'FunctionDeclaration') {
|
|
192
|
+
const fn = node.declaration;
|
|
193
|
+
functions.push({
|
|
194
|
+
name: fn.name,
|
|
195
|
+
params: fn.params || [],
|
|
196
|
+
async: fn.async || false
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// export { fn1, fn2 }
|
|
201
|
+
if (node.type === 'ExportNamedDeclaration' && node.specifiers) {
|
|
202
|
+
for (const specifier of node.specifiers) {
|
|
203
|
+
// Need to find the actual function declaration
|
|
204
|
+
const fnDecl = ast.body.find(n =>
|
|
205
|
+
n.type === 'FunctionDeclaration' && n.name === specifier.local
|
|
206
|
+
);
|
|
207
|
+
if (fnDecl) {
|
|
208
|
+
functions.push({
|
|
209
|
+
name: fnDecl.name,
|
|
210
|
+
params: fnDecl.params || [],
|
|
211
|
+
async: fnDecl.async || false
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return functions;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ============================================================
|
|
222
|
+
// Server-Side Registration
|
|
223
|
+
// ============================================================
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Generate server-side registration code
|
|
227
|
+
* @param {Array<{id, name}>} serverActions - Server Actions metadata
|
|
228
|
+
* @param {string} moduleId - Module identifier
|
|
229
|
+
* @returns {string} Registration code
|
|
230
|
+
*
|
|
231
|
+
* @example
|
|
232
|
+
* // generateServerRegistration([{ id: 'UserForm$createUser', name: 'createUser' }], 'UserForm')
|
|
233
|
+
* // →
|
|
234
|
+
* import { registerServerAction } from 'pulse-js-framework/runtime/server-components';
|
|
235
|
+
* import { createUser } from './UserForm.server.js';
|
|
236
|
+
* registerServerAction('UserForm$createUser', createUser);
|
|
237
|
+
*/
|
|
238
|
+
export function generateServerRegistration(serverActions, moduleId) {
|
|
239
|
+
if (!serverActions || serverActions.length === 0) {
|
|
240
|
+
return '';
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const imports = `import { registerServerAction } from 'pulse-js-framework/runtime/server-components';`;
|
|
244
|
+
const moduleImport = `import { ${serverActions.map(a => a.name).join(', ')} } from './${moduleId}.server.js';`;
|
|
245
|
+
|
|
246
|
+
const registrations = serverActions.map(action =>
|
|
247
|
+
`registerServerAction('${action.id}', ${action.name});`
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
return [imports, moduleImport, '', ...registrations].join('\n');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ============================================================
|
|
254
|
+
// Validation
|
|
255
|
+
// ============================================================
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Validate Server Action function
|
|
259
|
+
* @param {Object} functionNode - Function AST node
|
|
260
|
+
* @returns {{valid: boolean, errors: Array<string>}} Validation result
|
|
261
|
+
*
|
|
262
|
+
* @example
|
|
263
|
+
* validateServerAction(functionNode)
|
|
264
|
+
* // → { valid: false, errors: ['Server Action must be async'] }
|
|
265
|
+
*/
|
|
266
|
+
export function validateServerAction(functionNode) {
|
|
267
|
+
const errors = [];
|
|
268
|
+
|
|
269
|
+
// Must be async
|
|
270
|
+
if (!functionNode.async) {
|
|
271
|
+
errors.push('Server Action must be async');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Parameters must be serializable (no functions, classes)
|
|
275
|
+
if (functionNode.params) {
|
|
276
|
+
for (const param of functionNode.params) {
|
|
277
|
+
// Check for destructuring with functions (hard to validate at compile time)
|
|
278
|
+
// This is a basic check; runtime will validate actual arguments
|
|
279
|
+
if (param.type === 'FunctionExpression' || param.type === 'ArrowFunctionExpression') {
|
|
280
|
+
errors.push(`Server Action parameter '${param.name || 'anonymous'}' cannot be a function`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
valid: errors.length === 0,
|
|
287
|
+
errors
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Validate all Server Actions in module
|
|
293
|
+
* @param {Array<Object>} actions - Server Action nodes
|
|
294
|
+
* @returns {{valid: boolean, errors: Array<{action: string, errors: Array<string>}>}} Validation result
|
|
295
|
+
*/
|
|
296
|
+
export function validateServerActions(actions) {
|
|
297
|
+
const allErrors = [];
|
|
298
|
+
|
|
299
|
+
for (const action of actions) {
|
|
300
|
+
const result = validateServerAction(action);
|
|
301
|
+
if (!result.valid) {
|
|
302
|
+
allErrors.push({
|
|
303
|
+
action: action.name,
|
|
304
|
+
errors: result.errors
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
valid: allErrors.length === 0,
|
|
311
|
+
errors: allErrors
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ============================================================
|
|
316
|
+
// Exports
|
|
317
|
+
// ============================================================
|
|
318
|
+
|
|
319
|
+
export default {
|
|
320
|
+
isServerAction,
|
|
321
|
+
hasServerDirective,
|
|
322
|
+
transformServerAction,
|
|
323
|
+
transformServerModule,
|
|
324
|
+
extractServerActions,
|
|
325
|
+
extractExportedFunctions,
|
|
326
|
+
generateServerRegistration,
|
|
327
|
+
validateServerAction,
|
|
328
|
+
validateServerActions
|
|
329
|
+
};
|
|
@@ -13,6 +13,7 @@ export function generateExport(transformer) {
|
|
|
13
13
|
const pageName = transformer.ast.page?.name || 'Component';
|
|
14
14
|
const routePath = transformer.ast.route?.path || null;
|
|
15
15
|
const hasInit = transformer.actionNames.has('init');
|
|
16
|
+
const directive = transformer.directive;
|
|
16
17
|
|
|
17
18
|
const lines = ['// Export'];
|
|
18
19
|
lines.push(`export const ${pageName} = {`);
|
|
@@ -22,6 +23,12 @@ export function generateExport(transformer) {
|
|
|
22
23
|
lines.push(` route: ${JSON.stringify(routePath)},`);
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
// Add Server Component directive metadata
|
|
27
|
+
if (directive) {
|
|
28
|
+
lines.push(` __directive: ${JSON.stringify(directive)}, // 'use client' or 'use server'`);
|
|
29
|
+
lines.push(` __componentId: ${JSON.stringify(pageName)},`);
|
|
30
|
+
}
|
|
31
|
+
|
|
25
32
|
// Mount with reactive re-rendering (preserves focus)
|
|
26
33
|
lines.push(' mount: (target) => {');
|
|
27
34
|
lines.push(' const container = typeof target === "string" ? document.querySelector(target) : target;');
|
|
@@ -191,39 +191,83 @@ export function transformExpressionString(transformer, exprStr) {
|
|
|
191
191
|
// invalid code like stateVar.get() = expr (LHS of assignment is not a reference)
|
|
192
192
|
for (const stateVar of transformer.stateVars) {
|
|
193
193
|
// Compound assignment: stateVar += expr -> stateVar.update(_v => _v + expr)
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
194
|
+
// Use bracket-balancing to find the end of the RHS expression
|
|
195
|
+
const compoundPattern = new RegExp(`\\b${stateVar}\\s*(\\+=|-=|\\*=|\\/=|&&=|\\|\\|=|\\?\\?=)\\s*`, 'g');
|
|
196
|
+
let compoundMatch;
|
|
197
|
+
const compoundReplacements = [];
|
|
198
|
+
|
|
199
|
+
while ((compoundMatch = compoundPattern.exec(result)) !== null) {
|
|
200
|
+
const op = compoundMatch[1];
|
|
201
|
+
const baseOp = op.slice(0, -1);
|
|
202
|
+
const rhsStart = compoundMatch.index + compoundMatch[0].length;
|
|
203
|
+
|
|
204
|
+
// Find end of RHS expression using bracket balancing
|
|
205
|
+
let depth = 0;
|
|
206
|
+
let endIdx = rhsStart;
|
|
207
|
+
for (let i = rhsStart; i < result.length; i++) {
|
|
208
|
+
const ch = result[i];
|
|
209
|
+
if (ch === '(' || ch === '[' || ch === '{') { depth++; endIdx = i + 1; continue; }
|
|
210
|
+
if (ch === ')' || ch === ']' || ch === '}') {
|
|
211
|
+
if (depth > 0) { depth--; endIdx = i + 1; continue; }
|
|
212
|
+
break; // closing bracket at depth 0 = boundary
|
|
213
|
+
}
|
|
214
|
+
if (depth === 0 && (ch === ';' || ch === ',')) break;
|
|
215
|
+
endIdx = i + 1;
|
|
199
216
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
217
|
+
|
|
218
|
+
const rhs = result.slice(rhsStart, endIdx).trim();
|
|
219
|
+
if (rhs) {
|
|
220
|
+
compoundReplacements.push({
|
|
221
|
+
start: compoundMatch.index,
|
|
222
|
+
end: endIdx,
|
|
223
|
+
replacement: `${stateVar}.update(_v => _v ${baseOp} ${rhs})`
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Apply compound replacements in reverse order
|
|
229
|
+
for (let i = compoundReplacements.length - 1; i >= 0; i--) {
|
|
230
|
+
const r = compoundReplacements[i];
|
|
231
|
+
result = result.slice(0, r.start) + r.replacement + result.slice(r.end);
|
|
232
|
+
}
|
|
204
233
|
|
|
205
234
|
// Simple assignment: stateVar = expr -> stateVar.set(expr)
|
|
206
|
-
// Use
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
`${stateVar}.set(`
|
|
211
|
-
);
|
|
212
|
-
}
|
|
235
|
+
// Use bracket-balancing to find the end of the RHS expression
|
|
236
|
+
const simplePattern = new RegExp(`\\b${stateVar}\\s*=(?!=)\\s*`, 'g');
|
|
237
|
+
let simpleMatch;
|
|
238
|
+
const simpleReplacements = [];
|
|
213
239
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
240
|
+
while ((simpleMatch = simplePattern.exec(result)) !== null) {
|
|
241
|
+
const rhsStart = simpleMatch.index + simpleMatch[0].length;
|
|
242
|
+
|
|
243
|
+
let depth = 0;
|
|
244
|
+
let endIdx = rhsStart;
|
|
245
|
+
for (let i = rhsStart; i < result.length; i++) {
|
|
246
|
+
const ch = result[i];
|
|
247
|
+
if (ch === '(' || ch === '[' || ch === '{') { depth++; endIdx = i + 1; continue; }
|
|
248
|
+
if (ch === ')' || ch === ']' || ch === '}') {
|
|
249
|
+
if (depth > 0) { depth--; endIdx = i + 1; continue; }
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
if (depth === 0 && (ch === ';' || ch === ',')) break;
|
|
253
|
+
endIdx = i + 1;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const rhs = result.slice(rhsStart, endIdx).trim();
|
|
257
|
+
if (rhs) {
|
|
258
|
+
simpleReplacements.push({
|
|
259
|
+
start: simpleMatch.index,
|
|
260
|
+
end: endIdx,
|
|
261
|
+
replacement: `${stateVar}.set(${rhs})`
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Apply simple replacements in reverse order
|
|
267
|
+
for (let i = simpleReplacements.length - 1; i >= 0; i--) {
|
|
268
|
+
const r = simpleReplacements[i];
|
|
269
|
+
result = result.slice(0, r.start) + r.replacement + result.slice(r.end);
|
|
270
|
+
}
|
|
227
271
|
}
|
|
228
272
|
|
|
229
273
|
// Transform state var reads (not already transformed to .get/.set/.update)
|
|
@@ -238,13 +282,16 @@ export function transformExpressionString(transformer, exprStr) {
|
|
|
238
282
|
// Add optional chaining when followed by property access for nullable props
|
|
239
283
|
// Props commonly receive null values (e.g., notification: null)
|
|
240
284
|
for (const propVar of transformer.propVars) {
|
|
285
|
+
// Property access: propVar.x -> propVar.get()?.x
|
|
286
|
+
// Guard against already-transformed: skip if followed by .get( or .set(
|
|
241
287
|
result = result.replace(
|
|
242
|
-
new RegExp(`\\b${propVar}\\b(?=\\.)`, 'g'),
|
|
288
|
+
new RegExp(`\\b${propVar}\\b(?=\\.(?!get\\(|set\\())`, 'g'),
|
|
243
289
|
`${propVar}.get()?`
|
|
244
290
|
);
|
|
245
291
|
// Handle standalone prop var (not followed by property access)
|
|
292
|
+
// Guard: skip if already followed by .get or .set
|
|
246
293
|
result = result.replace(
|
|
247
|
-
new RegExp(`\\b${propVar}\\b(?!\\.)`, 'g'),
|
|
294
|
+
new RegExp(`\\b${propVar}\\b(?!\\.(?:get|set)\\()(?!\\.)`, 'g'),
|
|
248
295
|
`${propVar}.get()`
|
|
249
296
|
);
|
|
250
297
|
}
|
|
@@ -414,7 +461,6 @@ export function transformFunctionBody(transformer, tokens) {
|
|
|
414
461
|
|
|
415
462
|
for (let i = exprStart; i < code.length; i++) {
|
|
416
463
|
const ch = code[i];
|
|
417
|
-
const prevCh = i > 0 ? code[i-1] : '';
|
|
418
464
|
|
|
419
465
|
// Handle string literals
|
|
420
466
|
if (!inString && (ch === '"' || ch === "'" || ch === '`')) {
|
|
@@ -424,7 +470,13 @@ export function transformFunctionBody(transformer, tokens) {
|
|
|
424
470
|
continue;
|
|
425
471
|
}
|
|
426
472
|
if (inString) {
|
|
427
|
-
if (ch ===
|
|
473
|
+
if (ch === '\\') {
|
|
474
|
+
// Skip next character (escaped)
|
|
475
|
+
i++;
|
|
476
|
+
endIdx = i + 1;
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
if (ch === stringChar) {
|
|
428
480
|
inString = false;
|
|
429
481
|
}
|
|
430
482
|
endIdx = i + 1;
|
|
@@ -99,6 +99,9 @@ export function generateImports(transformer) {
|
|
|
99
99
|
}
|
|
100
100
|
if (namespaceSpec) {
|
|
101
101
|
importStr += `* as ${namespaceSpec.local}`;
|
|
102
|
+
if (namedSpecs.length > 0) {
|
|
103
|
+
importStr += ', ';
|
|
104
|
+
}
|
|
102
105
|
}
|
|
103
106
|
if (namedSpecs.length > 0) {
|
|
104
107
|
const named = namedSpecs.map(s =>
|
|
@@ -53,6 +53,8 @@ export class Transformer {
|
|
|
53
53
|
this.actionNames = new Set();
|
|
54
54
|
this.importedComponents = new Map();
|
|
55
55
|
this.scopeId = this.options.scopeStyles ? generateScopeId() : null;
|
|
56
|
+
this.directive = this.ast.directive || null; // 'use client' | 'use server' | null
|
|
57
|
+
this.componentId = this.ast.page?.name || 'Component';
|
|
56
58
|
|
|
57
59
|
// Track a11y feature usage for conditional imports
|
|
58
60
|
this.usesA11y = {
|
|
@@ -53,7 +53,7 @@ export function transformStore(transformer, storeBlock, transformValue) {
|
|
|
53
53
|
lines.push(stateProps);
|
|
54
54
|
lines.push('}, {');
|
|
55
55
|
lines.push(` persist: ${storeBlock.persist},`);
|
|
56
|
-
lines.push(` storageKey:
|
|
56
|
+
lines.push(` storageKey: ${JSON.stringify(storeBlock.storageKey)}`);
|
|
57
57
|
lines.push('});');
|
|
58
58
|
lines.push('');
|
|
59
59
|
}
|
|
@@ -213,11 +213,12 @@ export function flattenStyleRule(transformer, rule, parentSelector, output, atRu
|
|
|
213
213
|
// Conditional group @-rules (@media, @supports, @container) wrap their nested rules
|
|
214
214
|
// They can be nested inside each other
|
|
215
215
|
if (isConditionalGroup) {
|
|
216
|
-
//
|
|
217
|
-
|
|
216
|
+
// For nested conditional groups, build a properly structured wrapper array
|
|
217
|
+
// so closing braces are correctly generated
|
|
218
|
+
const wrappers = atRuleWrapper ? [atRuleWrapper, selector] : [selector];
|
|
218
219
|
|
|
219
220
|
for (const nested of rule.nestedRules) {
|
|
220
|
-
flattenStyleRule(transformer, nested, parentSelector, output,
|
|
221
|
+
flattenStyleRule(transformer, nested, parentSelector, output, wrappers, false);
|
|
221
222
|
}
|
|
222
223
|
return;
|
|
223
224
|
}
|
|
@@ -253,15 +254,23 @@ export function flattenStyleRule(transformer, rule, parentSelector, output, atRu
|
|
|
253
254
|
if (rule.properties.length > 0) {
|
|
254
255
|
const lines = [];
|
|
255
256
|
|
|
256
|
-
// If wrapped in an @-rule, output the wrapper
|
|
257
|
+
// If wrapped in an @-rule, output the wrapper(s)
|
|
257
258
|
if (atRuleWrapper) {
|
|
258
|
-
|
|
259
|
-
|
|
259
|
+
const wrappers = Array.isArray(atRuleWrapper) ? atRuleWrapper : [atRuleWrapper];
|
|
260
|
+
const baseIndent = ' ';
|
|
261
|
+
for (let w = 0; w < wrappers.length; w++) {
|
|
262
|
+
lines.push(`${baseIndent}${' '.repeat(w)}${wrappers[w]} {`);
|
|
263
|
+
}
|
|
264
|
+
const contentIndent = baseIndent + ' '.repeat(wrappers.length);
|
|
265
|
+
lines.push(`${contentIndent}${scopedSelector} {`);
|
|
260
266
|
for (const prop of rule.properties) {
|
|
261
|
-
lines.push(
|
|
267
|
+
lines.push(`${contentIndent} ${prop.name}: ${prop.value};`);
|
|
268
|
+
}
|
|
269
|
+
lines.push(`${contentIndent}}`);
|
|
270
|
+
// Close wrappers in reverse order
|
|
271
|
+
for (let w = wrappers.length - 1; w >= 0; w--) {
|
|
272
|
+
lines.push(`${baseIndent}${' '.repeat(w)}}`);
|
|
262
273
|
}
|
|
263
|
-
lines.push(' }');
|
|
264
|
-
lines.push(' }');
|
|
265
274
|
} else {
|
|
266
275
|
lines.push(` ${scopedSelector} {`);
|
|
267
276
|
for (const prop of rule.properties) {
|
|
@@ -380,12 +389,32 @@ export function scopeStyleSelector(transformer, selector) {
|
|
|
380
389
|
*/
|
|
381
390
|
function scopePseudoClassSelector(transformer, selector) {
|
|
382
391
|
// Match functional pseudo-classes: :has(), :is(), :where(), :not()
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
392
|
+
// Use balanced parenthesis matching to handle nested pseudo-classes like :not(:hover)
|
|
393
|
+
const pattern = /:(has|is|where|not)\(/g;
|
|
394
|
+
let result = '';
|
|
395
|
+
let lastIndex = 0;
|
|
396
|
+
let match;
|
|
397
|
+
|
|
398
|
+
while ((match = pattern.exec(selector)) !== null) {
|
|
399
|
+
result += selector.slice(lastIndex, match.index);
|
|
400
|
+
const pseudoClass = match[1];
|
|
401
|
+
const innerStart = match.index + match[0].length;
|
|
402
|
+
|
|
403
|
+
// Find matching closing paren with depth tracking
|
|
404
|
+
let depth = 1;
|
|
405
|
+
let i = innerStart;
|
|
406
|
+
for (; i < selector.length && depth > 0; i++) {
|
|
407
|
+
if (selector[i] === '(') depth++;
|
|
408
|
+
else if (selector[i] === ')') depth--;
|
|
389
409
|
}
|
|
390
|
-
|
|
410
|
+
|
|
411
|
+
const inner = selector.slice(innerStart, i - 1);
|
|
412
|
+
const scopedInner = scopeStyleSelector(transformer, inner);
|
|
413
|
+
result += `:${pseudoClass}(${scopedInner})`;
|
|
414
|
+
lastIndex = i;
|
|
415
|
+
pattern.lastIndex = i;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
result += selector.slice(lastIndex);
|
|
419
|
+
return result;
|
|
391
420
|
}
|
|
@@ -557,6 +557,9 @@ export function transformElement(transformer, node, indent) {
|
|
|
557
557
|
for (const child of node.children) {
|
|
558
558
|
content.push(transformViewNode(transformer, child, 0).trim());
|
|
559
559
|
}
|
|
560
|
+
if (content.length === 0) {
|
|
561
|
+
return `${pad}srOnly('')`;
|
|
562
|
+
}
|
|
560
563
|
const contentCode = content.length === 1 ? content[0] : `[${content.join(', ')}]`;
|
|
561
564
|
return `${pad}srOnly(${contentCode})`;
|
|
562
565
|
}
|
|
@@ -669,7 +672,24 @@ export function transformElement(transformer, node, indent) {
|
|
|
669
672
|
if (attr.isInterpolated) {
|
|
670
673
|
// String with interpolation: "display: {show ? 'block' : 'none'}"
|
|
671
674
|
// Convert to template literal: `display: ${show.get() ? 'block' : 'none'}`
|
|
672
|
-
|
|
675
|
+
// Only convert top-level interpolation braces, not nested braces (e.g., object literals)
|
|
676
|
+
let templateStr = '';
|
|
677
|
+
let depth = 0;
|
|
678
|
+
for (let ci = 0; ci < attr.expr.length; ci++) {
|
|
679
|
+
const ch = attr.expr[ci];
|
|
680
|
+
if (ch === '{' && depth === 0) {
|
|
681
|
+
templateStr += '${';
|
|
682
|
+
depth = 1;
|
|
683
|
+
} else if (ch === '{' && depth > 0) {
|
|
684
|
+
templateStr += ch;
|
|
685
|
+
depth++;
|
|
686
|
+
} else if (ch === '}' && depth > 0) {
|
|
687
|
+
depth--;
|
|
688
|
+
templateStr += ch;
|
|
689
|
+
} else {
|
|
690
|
+
templateStr += ch;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
673
693
|
exprCode = '`' + transformExpressionString(transformer, templateStr) + '`';
|
|
674
694
|
} else {
|
|
675
695
|
// Pure expression: {searchQuery}
|
|
@@ -688,6 +708,7 @@ export function transformElement(transformer, node, indent) {
|
|
|
688
708
|
* @returns {string} Scoped selector
|
|
689
709
|
*/
|
|
690
710
|
export function addScopeToSelector(transformer, selector) {
|
|
711
|
+
if (!selector) return `.${transformer.scopeId}`;
|
|
691
712
|
// If selector has classes, add scope class after the first class
|
|
692
713
|
// Otherwise add it at the end
|
|
693
714
|
if (selector.includes('.')) {
|
|
@@ -821,7 +842,7 @@ function expressionUsesState(transformer, node) {
|
|
|
821
842
|
export function transformComponentCall(transformer, node, indent) {
|
|
822
843
|
const pad = ' '.repeat(indent);
|
|
823
844
|
const selectorParts = node.selector.match(/^([a-zA-Z][a-zA-Z0-9]*)/);
|
|
824
|
-
const componentName = selectorParts[1];
|
|
845
|
+
const componentName = selectorParts ? selectorParts[1] : node.selector;
|
|
825
846
|
|
|
826
847
|
// Extract slots from children
|
|
827
848
|
const slots = {};
|