pulse-js-framework 1.10.0 → 1.10.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 (37) hide show
  1. package/compiler/parser/_extract.js +393 -0
  2. package/compiler/parser/blocks.js +361 -0
  3. package/compiler/parser/core.js +306 -0
  4. package/compiler/parser/expressions.js +386 -0
  5. package/compiler/parser/imports.js +108 -0
  6. package/compiler/parser/index.js +47 -0
  7. package/compiler/parser/state.js +155 -0
  8. package/compiler/parser/style.js +445 -0
  9. package/compiler/parser/view.js +632 -0
  10. package/compiler/parser.js +15 -2372
  11. package/compiler/parser.js.original +2376 -0
  12. package/package.json +2 -1
  13. package/runtime/a11y/announcements.js +213 -0
  14. package/runtime/a11y/contrast.js +125 -0
  15. package/runtime/a11y/focus.js +412 -0
  16. package/runtime/a11y/index.js +35 -0
  17. package/runtime/a11y/preferences.js +121 -0
  18. package/runtime/a11y/utils.js +164 -0
  19. package/runtime/a11y/validation.js +258 -0
  20. package/runtime/a11y/widgets.js +545 -0
  21. package/runtime/a11y.js +15 -1840
  22. package/runtime/a11y.js.original +1844 -0
  23. package/runtime/graphql/cache.js +69 -0
  24. package/runtime/graphql/client.js +563 -0
  25. package/runtime/graphql/hooks.js +492 -0
  26. package/runtime/graphql/index.js +62 -0
  27. package/runtime/graphql/subscriptions.js +241 -0
  28. package/runtime/graphql.js +12 -1322
  29. package/runtime/graphql.js.original +1326 -0
  30. package/runtime/router/core.js +956 -0
  31. package/runtime/router/guards.js +90 -0
  32. package/runtime/router/history.js +204 -0
  33. package/runtime/router/index.js +36 -0
  34. package/runtime/router/lazy.js +180 -0
  35. package/runtime/router/utils.js +226 -0
  36. package/runtime/router.js +12 -1600
  37. package/runtime/router.js.original +1605 -0
@@ -0,0 +1,361 @@
1
+ /**
2
+ * Pulse Parser - Block Parsing
3
+ *
4
+ * Actions, router, store blocks, and function/guard parsing
5
+ *
6
+ * @module compiler/parser/blocks
7
+ */
8
+
9
+ import { TokenType } from '../lexer.js';
10
+ import { NodeType, ASTNode, Parser } from './core.js';
11
+
12
+ // ============================================================
13
+ // Actions Block Parsing
14
+ // ============================================================
15
+
16
+ /**
17
+ * Parse actions block
18
+ */
19
+ Parser.prototype.parseActionsBlock = function() {
20
+ this.expect(TokenType.ACTIONS);
21
+ this.expect(TokenType.LBRACE);
22
+
23
+ const functions = [];
24
+ while (!this.is(TokenType.RBRACE) && !this.is(TokenType.EOF)) {
25
+ functions.push(this.parseFunctionDeclaration());
26
+ }
27
+
28
+ this.expect(TokenType.RBRACE);
29
+ return new ASTNode(NodeType.ActionsBlock, { functions });
30
+ };
31
+
32
+ /**
33
+ * Parse function declaration
34
+ */
35
+ Parser.prototype.parseFunctionDeclaration = function() {
36
+ let async = false;
37
+ if (this.is(TokenType.IDENT) && this.current().value === 'async') {
38
+ this.advance();
39
+ async = true;
40
+ }
41
+
42
+ const name = this.expect(TokenType.IDENT).value;
43
+ this.expect(TokenType.LPAREN);
44
+
45
+ const params = [];
46
+ while (!this.is(TokenType.RPAREN) && !this.is(TokenType.EOF)) {
47
+ // Accept IDENT or keyword tokens that can be used as parameter names
48
+ const paramToken = this.current();
49
+ if (this.is(TokenType.IDENT) || this.is(TokenType.PAGE) ||
50
+ this.is(TokenType.ROUTE) || this.is(TokenType.FROM) ||
51
+ this.is(TokenType.STATE) || this.is(TokenType.VIEW) ||
52
+ this.is(TokenType.STORE) || this.is(TokenType.ROUTER)) {
53
+ params.push(this.advance().value);
54
+ } else {
55
+ throw this.createError(`Expected parameter name but got ${paramToken?.type}`);
56
+ }
57
+ if (this.is(TokenType.COMMA)) {
58
+ this.advance();
59
+ }
60
+ }
61
+ this.expect(TokenType.RPAREN);
62
+
63
+ // Parse function body as raw JS
64
+ this.expect(TokenType.LBRACE);
65
+ const body = this.parseFunctionBody();
66
+ this.expect(TokenType.RBRACE);
67
+
68
+ return new ASTNode(NodeType.FunctionDeclaration, { name, params, body, async });
69
+ };
70
+
71
+ /**
72
+ * Parse function body (raw content between braces)
73
+ */
74
+ Parser.prototype.parseFunctionBody = function() {
75
+ // Simplified: collect all tokens until matching }
76
+ const statements = [];
77
+ let braceCount = 1;
78
+
79
+ while (!this.is(TokenType.EOF)) {
80
+ if (this.is(TokenType.LBRACE)) {
81
+ braceCount++;
82
+ } else if (this.is(TokenType.RBRACE)) {
83
+ braceCount--;
84
+ if (braceCount === 0) break;
85
+ }
86
+
87
+ // Collect raw token for reconstruction
88
+ statements.push(this.current());
89
+ this.advance();
90
+ }
91
+
92
+ return statements;
93
+ };
94
+
95
+ // ============================================================
96
+ // Router Block Parsing
97
+ // ============================================================
98
+
99
+ /**
100
+ * Parse router block
101
+ */
102
+ Parser.prototype.parseRouterBlock = function() {
103
+ this.expect(TokenType.ROUTER);
104
+ this.expect(TokenType.LBRACE);
105
+
106
+ const config = {
107
+ mode: 'history',
108
+ base: '',
109
+ routes: [],
110
+ beforeEach: null,
111
+ afterEach: null
112
+ };
113
+
114
+ while (!this.is(TokenType.RBRACE) && !this.is(TokenType.EOF)) {
115
+ // mode: "hash"
116
+ if (this.is(TokenType.MODE)) {
117
+ this.advance();
118
+ this.expect(TokenType.COLON);
119
+ config.mode = this.expect(TokenType.STRING).value;
120
+ }
121
+ // base: "/app"
122
+ else if (this.is(TokenType.BASE)) {
123
+ this.advance();
124
+ this.expect(TokenType.COLON);
125
+ config.base = this.expect(TokenType.STRING).value;
126
+ }
127
+ // routes { ... }
128
+ else if (this.is(TokenType.ROUTES)) {
129
+ config.routes = this.parseRoutesBlock();
130
+ }
131
+ // beforeEach(to, from) { ... }
132
+ else if (this.is(TokenType.BEFORE_EACH)) {
133
+ config.beforeEach = this.parseGuardHook('beforeEach');
134
+ }
135
+ // afterEach(to) { ... }
136
+ else if (this.is(TokenType.AFTER_EACH)) {
137
+ config.afterEach = this.parseGuardHook('afterEach');
138
+ }
139
+ else {
140
+ throw this.createError(
141
+ `Unexpected token '${this.current()?.value}' in router block. ` +
142
+ `Expected: mode, base, routes, beforeEach, or afterEach`
143
+ );
144
+ }
145
+ }
146
+
147
+ this.expect(TokenType.RBRACE);
148
+ return new ASTNode(NodeType.RouterBlock, config);
149
+ };
150
+
151
+ /**
152
+ * Parse routes block
153
+ */
154
+ Parser.prototype.parseRoutesBlock = function() {
155
+ this.expect(TokenType.ROUTES);
156
+ this.expect(TokenType.LBRACE);
157
+
158
+ const routes = [];
159
+ while (!this.is(TokenType.RBRACE) && !this.is(TokenType.EOF)) {
160
+ const path = this.expect(TokenType.STRING).value;
161
+ this.expect(TokenType.COLON);
162
+ const handler = this.expect(TokenType.IDENT).value;
163
+ routes.push(new ASTNode(NodeType.RouteDefinition, { path, handler }));
164
+ }
165
+
166
+ this.expect(TokenType.RBRACE);
167
+ return routes;
168
+ };
169
+
170
+ /**
171
+ * Parse guard hook: beforeEach(to, from) { ... }
172
+ */
173
+ Parser.prototype.parseGuardHook = function(name) {
174
+ this.advance(); // skip keyword
175
+ this.expect(TokenType.LPAREN);
176
+ const params = [];
177
+ while (!this.is(TokenType.RPAREN) && !this.is(TokenType.EOF)) {
178
+ // Accept IDENT or FROM (since 'from' is a keyword but valid as parameter name)
179
+ if (this.is(TokenType.IDENT)) {
180
+ params.push(this.advance().value);
181
+ } else if (this.is(TokenType.FROM)) {
182
+ params.push(this.advance().value);
183
+ } else {
184
+ throw this.createError(`Expected parameter name but got ${this.current()?.type}`);
185
+ }
186
+ if (this.is(TokenType.COMMA)) this.advance();
187
+ }
188
+ this.expect(TokenType.RPAREN);
189
+ this.expect(TokenType.LBRACE);
190
+ const body = this.parseFunctionBody();
191
+ this.expect(TokenType.RBRACE);
192
+
193
+ return new ASTNode(NodeType.GuardHook, { name, params, body });
194
+ };
195
+
196
+ // ============================================================
197
+ // Store Block Parsing
198
+ // ============================================================
199
+
200
+ /**
201
+ * Parse store block
202
+ */
203
+ Parser.prototype.parseStoreBlock = function() {
204
+ this.expect(TokenType.STORE);
205
+ this.expect(TokenType.LBRACE);
206
+
207
+ const config = {
208
+ state: null,
209
+ getters: null,
210
+ actions: null,
211
+ persist: false,
212
+ storageKey: 'pulse-store',
213
+ plugins: []
214
+ };
215
+
216
+ while (!this.is(TokenType.RBRACE) && !this.is(TokenType.EOF)) {
217
+ // state { ... }
218
+ if (this.is(TokenType.STATE)) {
219
+ config.state = this.parseStateBlock();
220
+ }
221
+ // getters { ... }
222
+ else if (this.is(TokenType.GETTERS)) {
223
+ config.getters = this.parseGettersBlock();
224
+ }
225
+ // actions { ... }
226
+ else if (this.is(TokenType.ACTIONS)) {
227
+ config.actions = this.parseActionsBlock();
228
+ }
229
+ // persist: true
230
+ else if (this.is(TokenType.PERSIST)) {
231
+ this.advance();
232
+ this.expect(TokenType.COLON);
233
+ if (this.is(TokenType.TRUE)) {
234
+ this.advance();
235
+ config.persist = true;
236
+ } else if (this.is(TokenType.FALSE)) {
237
+ this.advance();
238
+ config.persist = false;
239
+ } else {
240
+ throw this.createError('Expected true or false for persist');
241
+ }
242
+ }
243
+ // storageKey: "my-store"
244
+ else if (this.is(TokenType.STORAGE_KEY)) {
245
+ this.advance();
246
+ this.expect(TokenType.COLON);
247
+ config.storageKey = this.expect(TokenType.STRING).value;
248
+ }
249
+ // plugins: [historyPlugin, loggerPlugin]
250
+ else if (this.is(TokenType.PLUGINS)) {
251
+ this.advance();
252
+ this.expect(TokenType.COLON);
253
+ config.plugins = this.parseArrayLiteral();
254
+ }
255
+ else {
256
+ throw this.createError(
257
+ `Unexpected token '${this.current()?.value}' in store block. ` +
258
+ `Expected: state, getters, actions, persist, storageKey, or plugins`
259
+ );
260
+ }
261
+ }
262
+
263
+ this.expect(TokenType.RBRACE);
264
+ return new ASTNode(NodeType.StoreBlock, config);
265
+ };
266
+
267
+ /**
268
+ * Parse getters block
269
+ */
270
+ Parser.prototype.parseGettersBlock = function() {
271
+ this.expect(TokenType.GETTERS);
272
+ this.expect(TokenType.LBRACE);
273
+
274
+ const getters = [];
275
+ while (!this.is(TokenType.RBRACE) && !this.is(TokenType.EOF)) {
276
+ getters.push(this.parseGetterDeclaration());
277
+ }
278
+
279
+ this.expect(TokenType.RBRACE);
280
+ return new ASTNode(NodeType.GettersBlock, { getters });
281
+ };
282
+
283
+ /**
284
+ * Parse getter declaration: name() { return ... }
285
+ */
286
+ Parser.prototype.parseGetterDeclaration = function() {
287
+ const name = this.expect(TokenType.IDENT).value;
288
+ this.expect(TokenType.LPAREN);
289
+ this.expect(TokenType.RPAREN);
290
+ this.expect(TokenType.LBRACE);
291
+ const body = this.parseFunctionBody();
292
+ this.expect(TokenType.RBRACE);
293
+
294
+ return new ASTNode(NodeType.GetterDeclaration, { name, body });
295
+ };
296
+
297
+ // ============================================================
298
+ // Router View Directives
299
+ // ============================================================
300
+
301
+ /**
302
+ * Parse @link directive: @link("/path") "text"
303
+ */
304
+ Parser.prototype.parseLinkDirective = function() {
305
+ this.expect(TokenType.LPAREN);
306
+ const path = this.parseExpression();
307
+
308
+ let options = null;
309
+ if (this.is(TokenType.COMMA)) {
310
+ this.advance();
311
+ options = this.parseObjectLiteralExpr();
312
+ }
313
+ this.expect(TokenType.RPAREN);
314
+
315
+ // Parse link content (text or children)
316
+ let content = null;
317
+ if (this.is(TokenType.STRING)) {
318
+ content = this.parseTextNode();
319
+ } else if (this.is(TokenType.LBRACE)) {
320
+ this.advance();
321
+ content = [];
322
+ while (!this.is(TokenType.RBRACE) && !this.is(TokenType.EOF)) {
323
+ content.push(this.parseViewChild());
324
+ }
325
+ this.expect(TokenType.RBRACE);
326
+ }
327
+
328
+ return new ASTNode(NodeType.LinkDirective, { path, options, content });
329
+ };
330
+
331
+ /**
332
+ * Parse @outlet directive
333
+ */
334
+ Parser.prototype.parseOutletDirective = function() {
335
+ let container = null;
336
+ if (this.is(TokenType.LPAREN)) {
337
+ this.advance();
338
+ if (this.is(TokenType.STRING)) {
339
+ container = this.expect(TokenType.STRING).value;
340
+ }
341
+ this.expect(TokenType.RPAREN);
342
+ }
343
+ return new ASTNode(NodeType.OutletDirective, { container });
344
+ };
345
+
346
+ /**
347
+ * Parse @navigate directive
348
+ */
349
+ Parser.prototype.parseNavigateDirective = function() {
350
+ this.expect(TokenType.LPAREN);
351
+ const path = this.parseExpression();
352
+
353
+ let options = null;
354
+ if (this.is(TokenType.COMMA)) {
355
+ this.advance();
356
+ options = this.parseObjectLiteralExpr();
357
+ }
358
+ this.expect(TokenType.RPAREN);
359
+
360
+ return new ASTNode(NodeType.NavigateDirective, { path, options });
361
+ };
@@ -0,0 +1,306 @@
1
+ /**
2
+ * Pulse Parser Core - Base parser infrastructure
3
+ *
4
+ * Contains NodeType constants, ASTNode class, and base Parser class with utility methods
5
+ *
6
+ * @module compiler/parser/core
7
+ */
8
+
9
+ import { TokenType } from '../lexer.js';
10
+ import { ParserError, SUGGESTIONS, getDocsUrl } from '../../runtime/errors.js';
11
+
12
+ // ============================================================
13
+ // AST Node Types
14
+ // ============================================================
15
+
16
+ export const NodeType = {
17
+ Program: 'Program',
18
+ ImportDeclaration: 'ImportDeclaration',
19
+ ImportSpecifier: 'ImportSpecifier',
20
+ PageDeclaration: 'PageDeclaration',
21
+ RouteDeclaration: 'RouteDeclaration',
22
+ PropsBlock: 'PropsBlock',
23
+ StateBlock: 'StateBlock',
24
+ ViewBlock: 'ViewBlock',
25
+ ActionsBlock: 'ActionsBlock',
26
+ StyleBlock: 'StyleBlock',
27
+ SlotElement: 'SlotElement',
28
+ Element: 'Element',
29
+ TextNode: 'TextNode',
30
+ Interpolation: 'Interpolation',
31
+ Directive: 'Directive',
32
+ IfDirective: 'IfDirective',
33
+ EachDirective: 'EachDirective',
34
+ EventDirective: 'EventDirective',
35
+ ModelDirective: 'ModelDirective',
36
+
37
+ // Accessibility directives
38
+ A11yDirective: 'A11yDirective',
39
+ LiveDirective: 'LiveDirective',
40
+ FocusTrapDirective: 'FocusTrapDirective',
41
+
42
+ // SSR directives
43
+ ClientDirective: 'ClientDirective',
44
+ ServerDirective: 'ServerDirective',
45
+
46
+ Property: 'Property',
47
+ ObjectLiteral: 'ObjectLiteral',
48
+ ArrayLiteral: 'ArrayLiteral',
49
+ Identifier: 'Identifier',
50
+ MemberExpression: 'MemberExpression',
51
+ CallExpression: 'CallExpression',
52
+ BinaryExpression: 'BinaryExpression',
53
+ UnaryExpression: 'UnaryExpression',
54
+ UpdateExpression: 'UpdateExpression',
55
+ Literal: 'Literal',
56
+ TemplateLiteral: 'TemplateLiteral',
57
+ ConditionalExpression: 'ConditionalExpression',
58
+ ArrowFunction: 'ArrowFunction',
59
+ SpreadElement: 'SpreadElement',
60
+ AssignmentExpression: 'AssignmentExpression',
61
+ FunctionDeclaration: 'FunctionDeclaration',
62
+ StyleRule: 'StyleRule',
63
+ StyleProperty: 'StyleProperty',
64
+
65
+ // Router nodes
66
+ RouterBlock: 'RouterBlock',
67
+ RoutesBlock: 'RoutesBlock',
68
+ RouteDefinition: 'RouteDefinition',
69
+ GuardHook: 'GuardHook',
70
+
71
+ // Store nodes
72
+ StoreBlock: 'StoreBlock',
73
+ GettersBlock: 'GettersBlock',
74
+ GetterDeclaration: 'GetterDeclaration',
75
+
76
+ // View directives for router
77
+ LinkDirective: 'LinkDirective',
78
+ OutletDirective: 'OutletDirective',
79
+ NavigateDirective: 'NavigateDirective'
80
+ };
81
+
82
+ // ============================================================
83
+ // AST Node Class
84
+ // ============================================================
85
+
86
+ /**
87
+ * AST Node class
88
+ */
89
+ export class ASTNode {
90
+ constructor(type, props = {}) {
91
+ this.type = type;
92
+ Object.assign(this, props);
93
+ }
94
+ }
95
+
96
+ // ============================================================
97
+ // Base Parser Class
98
+ // ============================================================
99
+
100
+ /**
101
+ * Parser class with base utility methods
102
+ * Parse methods are added via modules (imports, state, view, style, expressions, blocks)
103
+ */
104
+ export class Parser {
105
+ constructor(tokens) {
106
+ this.tokens = tokens;
107
+ this.pos = 0;
108
+ }
109
+
110
+ /**
111
+ * Get current token
112
+ */
113
+ current() {
114
+ return this.tokens[this.pos];
115
+ }
116
+
117
+ /**
118
+ * Peek at token at offset
119
+ */
120
+ peek(offset = 1) {
121
+ const index = this.pos + offset;
122
+ if (index < 0 || index >= this.tokens.length) return undefined;
123
+ return this.tokens[index];
124
+ }
125
+
126
+ /**
127
+ * Check if current token matches type
128
+ */
129
+ is(type) {
130
+ return this.current()?.type === type;
131
+ }
132
+
133
+ /**
134
+ * Check if current token matches any of types
135
+ */
136
+ isAny(...types) {
137
+ return types.includes(this.current()?.type);
138
+ }
139
+
140
+ /**
141
+ * Advance to next token and return current
142
+ */
143
+ advance() {
144
+ return this.tokens[this.pos++];
145
+ }
146
+
147
+ /**
148
+ * Expect a specific token type
149
+ */
150
+ expect(type, message = null) {
151
+ if (!this.is(type)) {
152
+ const token = this.current();
153
+ throw this.createError(
154
+ message || `Expected ${type} but got ${token?.type}`,
155
+ token,
156
+ { suggestion: SUGGESTIONS['unexpected-token']?.(type, token?.type) }
157
+ );
158
+ }
159
+ return this.advance();
160
+ }
161
+
162
+ /**
163
+ * Create a parse error with detailed information
164
+ * @param {string} message - Error message
165
+ * @param {Object} [token] - Token where error occurred
166
+ * @param {Object} [options] - Additional options (suggestion, code)
167
+ * @returns {ParserError} The parser error
168
+ */
169
+ createError(message, token = null, options = {}) {
170
+ const t = token || this.current();
171
+ const code = options.code || 'PARSER_ERROR';
172
+
173
+ // Build enhanced message with docs link
174
+ let enhancedMessage = message;
175
+ if (options.suggestion) {
176
+ enhancedMessage += `\n → ${options.suggestion}`;
177
+ }
178
+ enhancedMessage += `\n See: ${getDocsUrl(code)}`;
179
+
180
+ return new ParserError(enhancedMessage, {
181
+ line: t?.line || 1,
182
+ column: t?.column || 1,
183
+ token: t,
184
+ code,
185
+ ...options
186
+ });
187
+ }
188
+
189
+ /**
190
+ * Parse the entire program
191
+ * This method delegates to specialized parse methods from other modules
192
+ */
193
+ parse() {
194
+ const program = new ASTNode(NodeType.Program, {
195
+ imports: [],
196
+ page: null,
197
+ route: null,
198
+ props: null,
199
+ state: null,
200
+ view: null,
201
+ actions: null,
202
+ style: null,
203
+ router: null,
204
+ store: null
205
+ });
206
+
207
+ while (!this.is(TokenType.EOF)) {
208
+ // Import declarations (must come first)
209
+ if (this.is(TokenType.IMPORT)) {
210
+ program.imports.push(this.parseImportDeclaration());
211
+ }
212
+ // Page/Route declarations
213
+ else if (this.is(TokenType.AT)) {
214
+ this.advance();
215
+ if (this.is(TokenType.PAGE)) {
216
+ program.page = this.parsePageDeclaration();
217
+ } else if (this.is(TokenType.ROUTE)) {
218
+ program.route = this.parseRouteDeclaration();
219
+ } else {
220
+ throw this.createError(
221
+ `Expected 'page' or 'route' after '@', got '${this.current()?.value}'`
222
+ );
223
+ }
224
+ }
225
+ // Props block
226
+ else if (this.is(TokenType.PROPS)) {
227
+ if (program.props) {
228
+ throw this.createError('Duplicate props block - only one props block allowed per file', null, {
229
+ code: 'DUPLICATE_BLOCK',
230
+ suggestion: SUGGESTIONS['duplicate-declaration']?.('props')
231
+ });
232
+ }
233
+ program.props = this.parsePropsBlock();
234
+ }
235
+ // State block
236
+ else if (this.is(TokenType.STATE)) {
237
+ if (program.state) {
238
+ throw this.createError('Duplicate state block - only one state block allowed per file', null, {
239
+ code: 'DUPLICATE_BLOCK',
240
+ suggestion: SUGGESTIONS['duplicate-declaration']?.('state')
241
+ });
242
+ }
243
+ program.state = this.parseStateBlock();
244
+ }
245
+ // View block
246
+ else if (this.is(TokenType.VIEW)) {
247
+ if (program.view) {
248
+ throw this.createError('Duplicate view block - only one view block allowed per file', null, {
249
+ code: 'DUPLICATE_BLOCK',
250
+ suggestion: SUGGESTIONS['duplicate-declaration']?.('view')
251
+ });
252
+ }
253
+ program.view = this.parseViewBlock();
254
+ }
255
+ // Actions block
256
+ else if (this.is(TokenType.ACTIONS)) {
257
+ if (program.actions) {
258
+ throw this.createError('Duplicate actions block - only one actions block allowed per file', null, {
259
+ code: 'DUPLICATE_BLOCK',
260
+ suggestion: SUGGESTIONS['duplicate-declaration']?.('actions')
261
+ });
262
+ }
263
+ program.actions = this.parseActionsBlock();
264
+ }
265
+ // Style block
266
+ else if (this.is(TokenType.STYLE)) {
267
+ if (program.style) {
268
+ throw this.createError('Duplicate style block - only one style block allowed per file', null, {
269
+ code: 'DUPLICATE_BLOCK',
270
+ suggestion: SUGGESTIONS['duplicate-declaration']?.('style')
271
+ });
272
+ }
273
+ program.style = this.parseStyleBlock();
274
+ }
275
+ // Router block
276
+ else if (this.is(TokenType.ROUTER)) {
277
+ if (program.router) {
278
+ throw this.createError('Duplicate router block - only one router block allowed per file', null, {
279
+ code: 'DUPLICATE_BLOCK',
280
+ suggestion: SUGGESTIONS['duplicate-declaration']?.('router')
281
+ });
282
+ }
283
+ program.router = this.parseRouterBlock();
284
+ }
285
+ // Store block
286
+ else if (this.is(TokenType.STORE)) {
287
+ if (program.store) {
288
+ throw this.createError('Duplicate store block - only one store block allowed per file', null, {
289
+ code: 'DUPLICATE_BLOCK',
290
+ suggestion: SUGGESTIONS['duplicate-declaration']?.('store')
291
+ });
292
+ }
293
+ program.store = this.parseStoreBlock();
294
+ }
295
+ else {
296
+ const token = this.current();
297
+ throw this.createError(
298
+ `Unexpected token '${token?.value || token?.type}' at line ${token?.line}:${token?.column}. ` +
299
+ `Expected: import, @page, @route, props, state, view, actions, style, router, or store`
300
+ );
301
+ }
302
+ }
303
+
304
+ return program;
305
+ }
306
+ }