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.
Files changed (65) hide show
  1. package/README.md +11 -0
  2. package/cli/build.js +13 -3
  3. package/compiler/directives.js +356 -0
  4. package/compiler/lexer.js +18 -3
  5. package/compiler/parser/core.js +6 -0
  6. package/compiler/parser/view.js +2 -6
  7. package/compiler/preprocessor.js +43 -23
  8. package/compiler/sourcemap.js +3 -1
  9. package/compiler/transformer/actions.js +329 -0
  10. package/compiler/transformer/export.js +7 -0
  11. package/compiler/transformer/expressions.js +85 -33
  12. package/compiler/transformer/imports.js +3 -0
  13. package/compiler/transformer/index.js +2 -0
  14. package/compiler/transformer/store.js +1 -1
  15. package/compiler/transformer/style.js +45 -16
  16. package/compiler/transformer/view.js +23 -2
  17. package/loader/rollup-plugin-server-components.js +391 -0
  18. package/loader/vite-plugin-server-components.js +420 -0
  19. package/loader/webpack-loader-server-components.js +356 -0
  20. package/package.json +124 -82
  21. package/runtime/async.js +4 -0
  22. package/runtime/context.js +16 -3
  23. package/runtime/dom-adapter.js +5 -3
  24. package/runtime/dom-virtual-list.js +2 -1
  25. package/runtime/form.js +8 -3
  26. package/runtime/graphql/cache.js +1 -1
  27. package/runtime/graphql/client.js +22 -0
  28. package/runtime/graphql/hooks.js +12 -6
  29. package/runtime/graphql/subscriptions.js +2 -0
  30. package/runtime/hmr.js +6 -3
  31. package/runtime/http.js +1 -0
  32. package/runtime/i18n.js +2 -0
  33. package/runtime/lru-cache.js +3 -1
  34. package/runtime/native.js +46 -20
  35. package/runtime/pulse.js +3 -0
  36. package/runtime/router/core.js +5 -1
  37. package/runtime/router/index.js +17 -1
  38. package/runtime/router/psc-integration.js +301 -0
  39. package/runtime/security.js +58 -29
  40. package/runtime/server-components/actions-server.js +798 -0
  41. package/runtime/server-components/actions.js +389 -0
  42. package/runtime/server-components/client.js +447 -0
  43. package/runtime/server-components/error-sanitizer.js +438 -0
  44. package/runtime/server-components/index.js +275 -0
  45. package/runtime/server-components/security-csrf.js +593 -0
  46. package/runtime/server-components/security-errors.js +227 -0
  47. package/runtime/server-components/security-ratelimit.js +733 -0
  48. package/runtime/server-components/security-validation.js +467 -0
  49. package/runtime/server-components/security.js +598 -0
  50. package/runtime/server-components/serializer.js +617 -0
  51. package/runtime/server-components/server.js +382 -0
  52. package/runtime/server-components/types.js +383 -0
  53. package/runtime/server-components/utils/mutex.js +60 -0
  54. package/runtime/server-components/utils/path-sanitizer.js +109 -0
  55. package/runtime/ssr.js +2 -1
  56. package/runtime/store.js +19 -10
  57. package/runtime/utils.js +12 -128
  58. package/types/animation.d.ts +300 -0
  59. package/types/i18n.d.ts +283 -0
  60. package/types/persistence.d.ts +267 -0
  61. package/types/sse.d.ts +248 -0
  62. package/types/sw.d.ts +150 -0
  63. package/runtime/a11y.js.original +0 -1844
  64. package/runtime/graphql.js.original +0 -1326
  65. 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
- result = result.replace(
195
- new RegExp(`\\b${stateVar}\\s*(\\+=|-=|\\*=|\\/=|&&=|\\|\\|=|\\?\\?=)\\s*`, 'g'),
196
- (_match, op) => {
197
- const baseOp = op.slice(0, -1); // Remove trailing '='
198
- return `${stateVar}.update(_v => _v ${baseOp} `;
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
- // Close the .update() call - find the end of the expression after the replacement
202
- // This is handled by the fact that the expression continues after the replacement text
203
- // and the closing paren is added by wrapping logic below.
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 negative lookbehind to skip compound assignments (already handled)
207
- // Use negative lookahead to skip == and ===
208
- result = result.replace(
209
- new RegExp(`\\b${stateVar}\\s*=(?!=)`, 'g'),
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
- // If we inserted .set( or .update(, we need to close the parenthesis
215
- // Find unclosed .set( and .update( calls and close them at end of expression
216
- if (result.includes('.set(') || result.includes('.update(_v =>')) {
217
- // For .update(_v => _v op expr), close with )
218
- result = result.replace(
219
- /\.update\(_v => _v [^\)]*$/,
220
- (m) => m + ')'
221
- );
222
- // For .set(expr), close with )
223
- result = result.replace(
224
- /\.set\(([^)]*$)/,
225
- (_m, expr) => `.set(${expr})`
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 === stringChar && prevCh !== '\\') {
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: '${storeBlock.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
- // Combine with existing wrapper if present
217
- const combinedWrapper = atRuleWrapper ? `${atRuleWrapper} { ${selector}` : selector;
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, combinedWrapper, false);
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
- lines.push(` ${atRuleWrapper} {`);
259
- lines.push(` ${scopedSelector} {`);
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(` ${prop.name}: ${prop.value};`);
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
- return selector.replace(
384
- /:(has|is|where|not)\(([^)]+)\)/g,
385
- (_match, pseudoClass, inner) => {
386
- // Recursively scope the inner selector
387
- const scopedInner = scopeStyleSelector(transformer, inner);
388
- return `:${pseudoClass}(${scopedInner})`;
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
- const templateStr = attr.expr.replace(/\{/g, '${');
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 = {};