pulse-js-framework 1.10.4 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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
package/README.md
CHANGED
|
@@ -354,10 +354,21 @@ const count: Pulse<number> = pulse(0);
|
|
|
354
354
|
| [Chat App](examples/chat) | Real-time messaging |
|
|
355
355
|
| [E-commerce](examples/ecommerce) | Shopping cart |
|
|
356
356
|
| [Dashboard](examples/dashboard) | Admin UI |
|
|
357
|
+
| [Weather App](examples/meteo) | API integration, async data fetching |
|
|
358
|
+
| [Sports News](examples/sports) | HTTP client with interceptors |
|
|
357
359
|
| [HMR Demo](examples/hmr) | Hot module replacement |
|
|
358
360
|
| [Router Demo](examples/router) | SPA routing |
|
|
359
361
|
| [Store Demo](examples/store) | State with undo/redo |
|
|
360
362
|
| [Electron App](examples/electron) | Desktop notes app |
|
|
363
|
+
| [Server Actions](examples/server-actions-ratelimit) | Rate limiting, CSRF protection |
|
|
364
|
+
| [LESS Example](examples/less-example) | LESS preprocessor support |
|
|
365
|
+
| [Stylus Example](examples/stylus-example) | Stylus preprocessor support |
|
|
366
|
+
| [Webpack](examples/webpack-example) | Webpack 5 loader integration |
|
|
367
|
+
| [Rollup](examples/rollup-example) | Rollup plugin with tree-shaking |
|
|
368
|
+
| [ESBuild](examples/esbuild-example) | ESBuild ultra-fast builds |
|
|
369
|
+
| [Parcel](examples/parcel-example) | Parcel zero-config bundling |
|
|
370
|
+
| [SASS Example](examples/sass-example) | SASS/SCSS preprocessor support |
|
|
371
|
+
| [Form Validation](examples/form-validation) | useForm, validators, file upload |
|
|
361
372
|
|
|
362
373
|
## Server-Side Rendering
|
|
363
374
|
|
package/cli/build.js
CHANGED
|
@@ -512,15 +512,25 @@ export async function previewBuild(args) {
|
|
|
512
512
|
}
|
|
513
513
|
});
|
|
514
514
|
|
|
515
|
-
|
|
516
|
-
|
|
515
|
+
return new Promise((resolve, reject) => {
|
|
516
|
+
server.on('error', (err) => {
|
|
517
|
+
if (err.code === 'EADDRINUSE') {
|
|
518
|
+
log.error(`Port ${port} is already in use. Try a different port.`);
|
|
519
|
+
}
|
|
520
|
+
reject(err);
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
server.listen(port, () => {
|
|
524
|
+
log.success(`
|
|
517
525
|
Pulse Preview Server running at:
|
|
518
526
|
|
|
519
527
|
Local: http://localhost:${port}/
|
|
520
528
|
|
|
521
529
|
Serving production build from: dist/
|
|
522
530
|
Press Ctrl+C to stop.
|
|
523
|
-
|
|
531
|
+
`);
|
|
532
|
+
resolve(server);
|
|
533
|
+
});
|
|
524
534
|
});
|
|
525
535
|
}
|
|
526
536
|
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse Compiler - Directive Detection
|
|
3
|
+
*
|
|
4
|
+
* Detects and handles file-level directives for Server Components:
|
|
5
|
+
* - 'use client' - Marks component as Client Component (runs on client only)
|
|
6
|
+
* - 'use server' - Marks component as Server Component (runs on server only)
|
|
7
|
+
*
|
|
8
|
+
* These are different from element-level @client/@server directives:
|
|
9
|
+
* - File-level: Affects entire component, used for bundle splitting
|
|
10
|
+
* - Element-level: Affects individual elements, used for SSR (ClientOnly/ServerOnly)
|
|
11
|
+
*
|
|
12
|
+
* @module pulse-js-framework/compiler/directives
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { TokenType } from './lexer.js';
|
|
16
|
+
import { CompileError } from '../runtime/errors.js';
|
|
17
|
+
|
|
18
|
+
// ============================================================================
|
|
19
|
+
// Directive Constants
|
|
20
|
+
// ============================================================================
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Supported file-level directives
|
|
24
|
+
*/
|
|
25
|
+
export const Directive = {
|
|
26
|
+
USE_CLIENT: 'use client',
|
|
27
|
+
USE_SERVER: 'use server'
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Check if a string value is a valid directive
|
|
32
|
+
* @param {string} value - String literal value
|
|
33
|
+
* @returns {string|null} Directive type or null
|
|
34
|
+
*/
|
|
35
|
+
export function isDirective(value) {
|
|
36
|
+
const normalized = value.trim().toLowerCase();
|
|
37
|
+
if (normalized === Directive.USE_CLIENT) return Directive.USE_CLIENT;
|
|
38
|
+
if (normalized === Directive.USE_SERVER) return Directive.USE_SERVER;
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Check if a token is a directive string literal
|
|
44
|
+
* @param {Object} token - Token to check
|
|
45
|
+
* @returns {string|null} Directive type or null
|
|
46
|
+
*/
|
|
47
|
+
export function isDirectiveToken(token) {
|
|
48
|
+
if (!token || token.type !== TokenType.STRING) return null;
|
|
49
|
+
return isDirective(token.value);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ============================================================================
|
|
53
|
+
// Directive Parsing
|
|
54
|
+
// ============================================================================
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Parse file-level directives at the beginning of a .pulse file
|
|
58
|
+
* Directives must appear before any other statements (imports, blocks, etc.)
|
|
59
|
+
*
|
|
60
|
+
* @param {Parser} parser - Parser instance
|
|
61
|
+
* @returns {string|null} Directive type ('use client' | 'use server' | null)
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* // .pulse file with directive:
|
|
65
|
+
* // 'use client';
|
|
66
|
+
* // import Button from './Button.pulse'
|
|
67
|
+
* // ...
|
|
68
|
+
*
|
|
69
|
+
* // Parsed as:
|
|
70
|
+
* // { directive: 'use client', imports: [...], ... }
|
|
71
|
+
*/
|
|
72
|
+
export function parseDirective(parser) {
|
|
73
|
+
// Check if first token is a string literal
|
|
74
|
+
const token = parser.current();
|
|
75
|
+
if (token?.type !== TokenType.STRING) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Check if it's a valid directive
|
|
80
|
+
const directive = isDirective(token.value);
|
|
81
|
+
if (!directive) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Consume the directive token
|
|
86
|
+
parser.advance();
|
|
87
|
+
|
|
88
|
+
// Optional semicolon after directive
|
|
89
|
+
if (parser.is(TokenType.SEMICOLON)) {
|
|
90
|
+
parser.advance();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return directive;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Validate directive usage rules
|
|
98
|
+
* @param {string} directive - Directive type
|
|
99
|
+
* @param {Object} program - Program AST node
|
|
100
|
+
* @throws {Error} If directive usage is invalid
|
|
101
|
+
*/
|
|
102
|
+
export function validateDirective(directive, program) {
|
|
103
|
+
if (!directive) return;
|
|
104
|
+
|
|
105
|
+
// Rule 1: Cannot have both 'use client' and 'use server'
|
|
106
|
+
if (directive === Directive.USE_CLIENT && program.serverDirective) {
|
|
107
|
+
throw new CompileError("Cannot use both 'use client' and 'use server' in the same file", { code: 'DIRECTIVE_CONFLICT', suggestion: 'Choose either \'use client\' or \'use server\' per file, not both' });
|
|
108
|
+
}
|
|
109
|
+
if (directive === Directive.USE_SERVER && program.clientDirective) {
|
|
110
|
+
throw new CompileError("Cannot use both 'use client' and 'use server' in the same file", { code: 'DIRECTIVE_CONFLICT', suggestion: 'Choose either \'use client\' or \'use server\' per file, not both' });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Rule 2: Server Components cannot have interactive features
|
|
114
|
+
// (This will be enforced in transformer/linter)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ============================================================================
|
|
118
|
+
// Component Type Detection
|
|
119
|
+
// ============================================================================
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Determine component type from directive
|
|
123
|
+
* @param {string|null} directive - File-level directive
|
|
124
|
+
* @returns {'client'|'server'|'shared'} Component type
|
|
125
|
+
*/
|
|
126
|
+
export function getComponentType(directive) {
|
|
127
|
+
if (directive === Directive.USE_CLIENT) return 'client';
|
|
128
|
+
if (directive === Directive.USE_SERVER) return 'server';
|
|
129
|
+
return 'shared'; // Default: can run on both server and client
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Check if component is a Client Component
|
|
134
|
+
* @param {string|null} directive - File-level directive
|
|
135
|
+
* @returns {boolean} True if Client Component
|
|
136
|
+
*/
|
|
137
|
+
export function isClientComponent(directive) {
|
|
138
|
+
return directive === Directive.USE_CLIENT;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Check if component is a Server Component
|
|
143
|
+
* @param {string|null} directive - File-level directive
|
|
144
|
+
* @returns {boolean} True if Server Component
|
|
145
|
+
*/
|
|
146
|
+
export function isServerComponent(directive) {
|
|
147
|
+
return directive === Directive.USE_SERVER;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Check if component is a Shared Component (runs on both)
|
|
152
|
+
* @param {string|null} directive - File-level directive
|
|
153
|
+
* @returns {boolean} True if Shared Component
|
|
154
|
+
*/
|
|
155
|
+
export function isSharedComponent(directive) {
|
|
156
|
+
return !directive || (directive !== Directive.USE_CLIENT && directive !== Directive.USE_SERVER);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ============================================================================
|
|
160
|
+
// JavaScript/TypeScript Source Code Parsing
|
|
161
|
+
// ============================================================================
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Parse directives from raw JavaScript/TypeScript source code
|
|
165
|
+
* Used by build tools to detect 'use client' and 'use server' in .js/.ts files
|
|
166
|
+
*
|
|
167
|
+
* @param {string} source - Source code
|
|
168
|
+
* @returns {{ useClient: boolean, useServer: boolean, line?: number }}
|
|
169
|
+
*
|
|
170
|
+
* @example
|
|
171
|
+
* parseDirectivesFromSource("'use client';\nimport foo from 'bar';")
|
|
172
|
+
* // { useClient: true, useServer: false, line: 1 }
|
|
173
|
+
*/
|
|
174
|
+
export function parseDirectivesFromSource(source) {
|
|
175
|
+
const result = {
|
|
176
|
+
useClient: false,
|
|
177
|
+
useServer: false,
|
|
178
|
+
line: undefined
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
if (!source || typeof source !== 'string') {
|
|
182
|
+
return result;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Split into lines for analysis
|
|
186
|
+
const lines = source.split('\n');
|
|
187
|
+
let seenCode = false;
|
|
188
|
+
|
|
189
|
+
for (let i = 0; i < lines.length; i++) {
|
|
190
|
+
const line = lines[i].trim();
|
|
191
|
+
|
|
192
|
+
// Skip empty lines
|
|
193
|
+
if (line === '') {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Skip comments
|
|
198
|
+
if (line.startsWith('//') || line.startsWith('/*') || line.startsWith('*')) {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Check for 'use client' directive
|
|
203
|
+
if (/^['"]use client['"];?$/.test(line)) {
|
|
204
|
+
if (seenCode) {
|
|
205
|
+
return result; // Directive after code - invalid
|
|
206
|
+
}
|
|
207
|
+
result.useClient = true;
|
|
208
|
+
result.line = i + 1;
|
|
209
|
+
return result;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Check for 'use server' directive
|
|
213
|
+
if (/^['"]use server['"];?$/.test(line)) {
|
|
214
|
+
if (seenCode) {
|
|
215
|
+
return result; // Directive after code - invalid
|
|
216
|
+
}
|
|
217
|
+
result.useServer = true;
|
|
218
|
+
result.line = i + 1;
|
|
219
|
+
return result;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Any other non-empty, non-comment line is "code"
|
|
223
|
+
seenCode = true;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return result;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Check if source code is a Client Component (has 'use client' directive)
|
|
231
|
+
*
|
|
232
|
+
* @param {string} source - Source code
|
|
233
|
+
* @returns {boolean}
|
|
234
|
+
*
|
|
235
|
+
* @example
|
|
236
|
+
* isClientComponentSource("'use client';\nexport function Button() {}") // true
|
|
237
|
+
*/
|
|
238
|
+
export function isClientComponentSource(source) {
|
|
239
|
+
const directives = parseDirectivesFromSource(source);
|
|
240
|
+
return directives.useClient;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Check if source code is a Server Component (has 'use server' directive)
|
|
245
|
+
*
|
|
246
|
+
* @param {string} source - Source code
|
|
247
|
+
* @returns {boolean}
|
|
248
|
+
*
|
|
249
|
+
* @example
|
|
250
|
+
* isServerComponentSource("'use server';\nexport async function createUser() {}") // true
|
|
251
|
+
*/
|
|
252
|
+
export function isServerComponentSource(source) {
|
|
253
|
+
const directives = parseDirectivesFromSource(source);
|
|
254
|
+
return directives.useServer;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Check if a file path matches *.server.js pattern
|
|
259
|
+
*
|
|
260
|
+
* @param {string} filePath - File path
|
|
261
|
+
* @returns {boolean}
|
|
262
|
+
*
|
|
263
|
+
* @example
|
|
264
|
+
* isServerFile('src/api/users.server.js') // true
|
|
265
|
+
* isServerFile('src/api/users.js') // false
|
|
266
|
+
*/
|
|
267
|
+
export function isServerFile(filePath) {
|
|
268
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
return /\.server\.(js|ts|jsx|tsx)$/.test(filePath);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Check if a file should be treated as a Server Component
|
|
276
|
+
* (either has 'use server' directive OR matches *.server.js pattern)
|
|
277
|
+
*
|
|
278
|
+
* @param {string} source - Source code
|
|
279
|
+
* @param {string} filePath - File path
|
|
280
|
+
* @returns {boolean}
|
|
281
|
+
*/
|
|
282
|
+
export function isServerModule(source, filePath) {
|
|
283
|
+
return isServerComponentSource(source) || isServerFile(filePath);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Check if a file should be treated as a Client Component
|
|
288
|
+
*
|
|
289
|
+
* @param {string} source - Source code
|
|
290
|
+
* @returns {boolean}
|
|
291
|
+
*/
|
|
292
|
+
export function isClientModule(source) {
|
|
293
|
+
return isClientComponentSource(source);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Get component type from source and file path
|
|
298
|
+
*
|
|
299
|
+
* @param {string} source - Source code
|
|
300
|
+
* @param {string} filePath - File path
|
|
301
|
+
* @returns {'client' | 'server' | 'shared'}
|
|
302
|
+
*
|
|
303
|
+
* @example
|
|
304
|
+
* getComponentTypeFromSource("'use client';", 'Button.js') // 'client'
|
|
305
|
+
* getComponentTypeFromSource("'use server';", 'api.js') // 'server'
|
|
306
|
+
* getComponentTypeFromSource("export const utils = {}", 'utils.js') // 'shared'
|
|
307
|
+
*/
|
|
308
|
+
export function getComponentTypeFromSource(source, filePath) {
|
|
309
|
+
if (isClientModule(source)) return 'client';
|
|
310
|
+
if (isServerModule(source, filePath)) return 'server';
|
|
311
|
+
return 'shared';
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Validate that source doesn't have both directives
|
|
316
|
+
*
|
|
317
|
+
* @param {string} source - Source code
|
|
318
|
+
* @returns {{ valid: boolean, error?: string }}
|
|
319
|
+
*/
|
|
320
|
+
export function validateDirectivesInSource(source) {
|
|
321
|
+
const directives = parseDirectivesFromSource(source);
|
|
322
|
+
|
|
323
|
+
if (directives.useClient && directives.useServer) {
|
|
324
|
+
return {
|
|
325
|
+
valid: false,
|
|
326
|
+
error: "Cannot have both 'use client' and 'use server' directives in the same file"
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return { valid: true };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// ============================================================================
|
|
334
|
+
// Exports
|
|
335
|
+
// ============================================================================
|
|
336
|
+
|
|
337
|
+
export default {
|
|
338
|
+
Directive,
|
|
339
|
+
isDirective,
|
|
340
|
+
isDirectiveToken,
|
|
341
|
+
parseDirective,
|
|
342
|
+
validateDirective,
|
|
343
|
+
getComponentType,
|
|
344
|
+
isClientComponent,
|
|
345
|
+
isServerComponent,
|
|
346
|
+
isSharedComponent,
|
|
347
|
+
// Source code parsing
|
|
348
|
+
parseDirectivesFromSource,
|
|
349
|
+
isClientComponentSource,
|
|
350
|
+
isServerComponentSource,
|
|
351
|
+
isServerFile,
|
|
352
|
+
isServerModule,
|
|
353
|
+
isClientModule,
|
|
354
|
+
getComponentTypeFromSource,
|
|
355
|
+
validateDirectivesInSource
|
|
356
|
+
};
|
package/compiler/lexer.js
CHANGED
|
@@ -97,6 +97,7 @@ export const TokenType = {
|
|
|
97
97
|
MINUS_ASSIGN: 'MINUS_ASSIGN', // -=
|
|
98
98
|
STAR_ASSIGN: 'STAR_ASSIGN', // *=
|
|
99
99
|
SLASH_ASSIGN: 'SLASH_ASSIGN', // /=
|
|
100
|
+
PIPE: 'PIPE', // | (bitwise OR)
|
|
100
101
|
|
|
101
102
|
// Literals
|
|
102
103
|
STRING: 'STRING',
|
|
@@ -495,8 +496,8 @@ export class Lexer {
|
|
|
495
496
|
// If not scientific notation, leave 'e' for the next token (e.g., 'em' unit)
|
|
496
497
|
}
|
|
497
498
|
|
|
498
|
-
// Check for BigInt suffix 'n'
|
|
499
|
-
if (this.current() === 'n') {
|
|
499
|
+
// Check for BigInt suffix 'n' (only valid on integers, not floats)
|
|
500
|
+
if (this.current() === 'n' && !value.includes('.') && !value.includes('e') && !value.includes('E')) {
|
|
500
501
|
rawValue += this.advance();
|
|
501
502
|
isBigInt = true;
|
|
502
503
|
}
|
|
@@ -822,6 +823,9 @@ export class Lexer {
|
|
|
822
823
|
} else {
|
|
823
824
|
this.tokens.push(new Token(TokenType.OR, '||', startLine, startColumn));
|
|
824
825
|
}
|
|
826
|
+
} else {
|
|
827
|
+
// Single | is bitwise OR
|
|
828
|
+
this.tokens.push(new Token(TokenType.PIPE, '|', startLine, startColumn));
|
|
825
829
|
}
|
|
826
830
|
continue;
|
|
827
831
|
}
|
|
@@ -985,9 +989,20 @@ export class Lexer {
|
|
|
985
989
|
lookahead++;
|
|
986
990
|
continue;
|
|
987
991
|
}
|
|
988
|
-
if (char === '.' || char === '#' || char === '[' || char === '{'
|
|
992
|
+
if (char === '.' || char === '#' || char === '[' || char === '{') {
|
|
989
993
|
return true;
|
|
990
994
|
}
|
|
995
|
+
// Space followed by { indicates element with children (selector context)
|
|
996
|
+
if (char === ' ') {
|
|
997
|
+
// Peek past whitespace to check for selector continuation
|
|
998
|
+
let spaceEnd = this.pos + lookahead + 1;
|
|
999
|
+
while (spaceEnd < this.source.length && this.source[spaceEnd] === ' ') spaceEnd++;
|
|
1000
|
+
const afterSpace = this.source[spaceEnd];
|
|
1001
|
+
if (afterSpace === '{' || afterSpace === '.' || afterSpace === '#' || afterSpace === '[') {
|
|
1002
|
+
return true;
|
|
1003
|
+
}
|
|
1004
|
+
return false;
|
|
1005
|
+
}
|
|
991
1006
|
break;
|
|
992
1007
|
}
|
|
993
1008
|
return false;
|
package/compiler/parser/core.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { TokenType } from '../lexer.js';
|
|
10
10
|
import { ParserError, SUGGESTIONS, getDocsUrl } from '../../runtime/errors.js';
|
|
11
|
+
import { parseDirective, validateDirective } from '../directives.js';
|
|
11
12
|
|
|
12
13
|
// ============================================================
|
|
13
14
|
// AST Node Types
|
|
@@ -192,6 +193,7 @@ export class Parser {
|
|
|
192
193
|
*/
|
|
193
194
|
parse() {
|
|
194
195
|
const program = new ASTNode(NodeType.Program, {
|
|
196
|
+
directive: null, // 'use client' | 'use server' | null
|
|
195
197
|
imports: [],
|
|
196
198
|
page: null,
|
|
197
199
|
route: null,
|
|
@@ -204,6 +206,10 @@ export class Parser {
|
|
|
204
206
|
store: null
|
|
205
207
|
});
|
|
206
208
|
|
|
209
|
+
// Parse file-level directive (must be first)
|
|
210
|
+
program.directive = parseDirective(this);
|
|
211
|
+
validateDirective(program.directive, program);
|
|
212
|
+
|
|
207
213
|
while (!this.is(TokenType.EOF)) {
|
|
208
214
|
// Import declarations (must come first)
|
|
209
215
|
if (this.is(TokenType.IMPORT)) {
|
package/compiler/parser/view.js
CHANGED
|
@@ -281,12 +281,8 @@ Parser.prototype.parseDirective = function() {
|
|
|
281
281
|
modifiers.push(this.advance().value);
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
-
if (
|
|
285
|
-
|
|
286
|
-
}
|
|
287
|
-
if (name === 'each' || name === 'for') {
|
|
288
|
-
return this.parseEachDirective();
|
|
289
|
-
}
|
|
284
|
+
// Note: 'if', 'each', 'for' are handled above as keyword tokens (IF, EACH, FOR)
|
|
285
|
+
// They will never reach here as IDENT tokens
|
|
290
286
|
|
|
291
287
|
// Accessibility directives
|
|
292
288
|
if (name === 'a11y') {
|
package/compiler/preprocessor.js
CHANGED
|
@@ -10,14 +10,18 @@
|
|
|
10
10
|
|
|
11
11
|
import { createRequire } from 'module';
|
|
12
12
|
|
|
13
|
-
// Cache for the sass module (null = not checked, false = not available, object =
|
|
13
|
+
// Cache for the sass module (null = not checked, false = not available, object = module)
|
|
14
|
+
// Separate sync/async caches so sync failure doesn't prevent async import from succeeding
|
|
14
15
|
let sassModule = null;
|
|
16
|
+
let sassModuleAsync = null;
|
|
15
17
|
|
|
16
|
-
// Cache for the less module
|
|
18
|
+
// Cache for the less module
|
|
17
19
|
let lessModule = null;
|
|
20
|
+
let lessModuleAsync = null;
|
|
18
21
|
|
|
19
|
-
// Cache for the stylus module
|
|
22
|
+
// Cache for the stylus module
|
|
20
23
|
let stylusModule = null;
|
|
24
|
+
let stylusModuleAsync = null;
|
|
21
25
|
|
|
22
26
|
/**
|
|
23
27
|
* Patterns that indicate SASS/SCSS syntax
|
|
@@ -62,9 +66,8 @@ const LESS_PATTERNS = [
|
|
|
62
66
|
* Patterns that indicate Stylus syntax
|
|
63
67
|
*/
|
|
64
68
|
const STYLUS_PATTERNS = [
|
|
65
|
-
/^[\w-]+\s
|
|
69
|
+
/^[\w-]+\s*=\s+/m, // Variables without $ or @: primary-color = #333 (require space after =)
|
|
66
70
|
/^\s*[\w-]+\([^)]*\)$/m, // Mixins without braces: button-style()
|
|
67
|
-
/^\s*[\w-]+$/m, // Mixin calls without parens or semicolon
|
|
68
71
|
/\{\s*\$[\w-]+\s*\}/, // Interpolation: {$variable}
|
|
69
72
|
/^\s*if\s+/m, // Conditionals: if condition
|
|
70
73
|
/^\s*unless\s+/m, // Unless: unless condition
|
|
@@ -72,7 +75,7 @@ const STYLUS_PATTERNS = [
|
|
|
72
75
|
/^\s*@css\s+\{/m, // Literal CSS blocks: @css { }
|
|
73
76
|
/^\s*@extends?\s+/m, // Extend: @extend or @extends
|
|
74
77
|
/^\s*\+[\w-]+/m, // Mixin calls with +: +button-style
|
|
75
|
-
|
|
78
|
+
/\barguments\b/, // Stylus arguments variable (word boundary)
|
|
76
79
|
/^[\w-]+\s*\?=/m, // Conditional assignment: var ?= value
|
|
77
80
|
];
|
|
78
81
|
|
|
@@ -188,18 +191,25 @@ export function tryLoadSassSync() {
|
|
|
188
191
|
* @returns {Promise<object|false>} The sass module or false if not available
|
|
189
192
|
*/
|
|
190
193
|
export async function tryLoadSass() {
|
|
191
|
-
// Return cached result
|
|
192
|
-
if (
|
|
194
|
+
// Return cached async result first, then sync cache
|
|
195
|
+
if (sassModuleAsync !== null) {
|
|
196
|
+
return sassModuleAsync;
|
|
197
|
+
}
|
|
198
|
+
if (sassModule !== null && sassModule !== false) {
|
|
193
199
|
return sassModule;
|
|
194
200
|
}
|
|
195
201
|
|
|
196
202
|
try {
|
|
197
203
|
// Try to import sass from the user's project
|
|
198
|
-
|
|
199
|
-
|
|
204
|
+
const mod = await import('sass');
|
|
205
|
+
sassModule = mod;
|
|
206
|
+
sassModuleAsync = mod;
|
|
207
|
+
return mod;
|
|
200
208
|
} catch {
|
|
201
209
|
// Fall back to sync require
|
|
202
|
-
|
|
210
|
+
const syncResult = tryLoadSassSync();
|
|
211
|
+
sassModuleAsync = syncResult || false;
|
|
212
|
+
return syncResult;
|
|
203
213
|
}
|
|
204
214
|
}
|
|
205
215
|
|
|
@@ -355,17 +365,22 @@ export function tryLoadLessSync() {
|
|
|
355
365
|
* @returns {Promise<object|false>} The less module or false if not available
|
|
356
366
|
*/
|
|
357
367
|
export async function tryLoadLess() {
|
|
358
|
-
|
|
359
|
-
|
|
368
|
+
if (lessModuleAsync !== null) {
|
|
369
|
+
return lessModuleAsync;
|
|
370
|
+
}
|
|
371
|
+
if (lessModule !== null && lessModule !== false) {
|
|
360
372
|
return lessModule;
|
|
361
373
|
}
|
|
362
374
|
|
|
363
375
|
try {
|
|
364
|
-
|
|
365
|
-
|
|
376
|
+
const mod = await import('less');
|
|
377
|
+
lessModule = mod;
|
|
378
|
+
lessModuleAsync = mod;
|
|
379
|
+
return mod;
|
|
366
380
|
} catch {
|
|
367
|
-
|
|
368
|
-
|
|
381
|
+
const syncResult = tryLoadLessSync();
|
|
382
|
+
lessModuleAsync = syncResult || false;
|
|
383
|
+
return syncResult;
|
|
369
384
|
}
|
|
370
385
|
}
|
|
371
386
|
|
|
@@ -498,17 +513,22 @@ export function tryLoadStylusSync() {
|
|
|
498
513
|
* @returns {Promise<object|false>} The stylus module or false if not available
|
|
499
514
|
*/
|
|
500
515
|
export async function tryLoadStylus() {
|
|
501
|
-
|
|
502
|
-
|
|
516
|
+
if (stylusModuleAsync !== null) {
|
|
517
|
+
return stylusModuleAsync;
|
|
518
|
+
}
|
|
519
|
+
if (stylusModule !== null && stylusModule !== false) {
|
|
503
520
|
return stylusModule;
|
|
504
521
|
}
|
|
505
522
|
|
|
506
523
|
try {
|
|
507
|
-
|
|
508
|
-
|
|
524
|
+
const mod = await import('stylus');
|
|
525
|
+
stylusModule = mod;
|
|
526
|
+
stylusModuleAsync = mod;
|
|
527
|
+
return mod;
|
|
509
528
|
} catch {
|
|
510
|
-
|
|
511
|
-
|
|
529
|
+
const syncResult = tryLoadStylusSync();
|
|
530
|
+
stylusModuleAsync = syncResult || false;
|
|
531
|
+
return syncResult;
|
|
512
532
|
}
|
|
513
533
|
}
|
|
514
534
|
|
package/compiler/sourcemap.js
CHANGED
|
@@ -21,7 +21,7 @@ const BASE64_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz012345
|
|
|
21
21
|
export function encodeVLQ(value) {
|
|
22
22
|
let encoded = '';
|
|
23
23
|
// Convert to unsigned and add sign bit
|
|
24
|
-
let vlq = value < 0 ? ((-value)
|
|
24
|
+
let vlq = value < 0 ? ((-value) * 2) + 1 : (value * 2);
|
|
25
25
|
|
|
26
26
|
do {
|
|
27
27
|
let digit = vlq & 0x1F; // 5 bits
|
|
@@ -350,6 +350,8 @@ export class SourceMapConsumer {
|
|
|
350
350
|
lineData.push(mapping);
|
|
351
351
|
}
|
|
352
352
|
|
|
353
|
+
// Sort by generatedColumn to ensure binary-search-safe ordering
|
|
354
|
+
lineData.sort((a, b) => a.generatedColumn - b.generatedColumn);
|
|
353
355
|
this._decodedMappings.push(lineData);
|
|
354
356
|
}
|
|
355
357
|
}
|