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.
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
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
- server.listen(port, () => {
516
- log.success(`
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 === '{' || 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;
@@ -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)) {
@@ -281,12 +281,8 @@ Parser.prototype.parseDirective = function() {
281
281
  modifiers.push(this.advance().value);
282
282
  }
283
283
 
284
- if (name === 'if') {
285
- return this.parseIfDirective();
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') {
@@ -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 = sass module)
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 (null = not checked, false = not available, object = 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 (null = not checked, false = not available, object = 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*=/m, // Variables without $ or @: primary-color = #333
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
- /arguments/, // Stylus arguments variable
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 if already checked
192
- if (sassModule !== null) {
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
- sassModule = await import('sass');
199
- return sassModule;
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
- return tryLoadSassSync();
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
- // Return cached result if already checked
359
- if (lessModule !== null) {
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
- lessModule = await import('less');
365
- return lessModule;
376
+ const mod = await import('less');
377
+ lessModule = mod;
378
+ lessModuleAsync = mod;
379
+ return mod;
366
380
  } catch {
367
- // Fall back to sync require
368
- return tryLoadLessSync();
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
- // Return cached result if already checked
502
- if (stylusModule !== null) {
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
- stylusModule = await import('stylus');
508
- return stylusModule;
524
+ const mod = await import('stylus');
525
+ stylusModule = mod;
526
+ stylusModuleAsync = mod;
527
+ return mod;
509
528
  } catch {
510
- // Fall back to sync require
511
- return tryLoadStylusSync();
529
+ const syncResult = tryLoadStylusSync();
530
+ stylusModuleAsync = syncResult || false;
531
+ return syncResult;
512
532
  }
513
533
  }
514
534
 
@@ -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) << 1) + 1 : (value << 1);
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
  }