pulse-js-framework 1.11.3 → 1.11.4
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/cli/analyze.js +21 -8
- package/cli/build.js +83 -56
- package/cli/dev.js +108 -94
- package/cli/docs-test.js +52 -33
- package/cli/index.js +81 -51
- package/cli/mobile.js +92 -40
- package/cli/release.js +64 -46
- package/cli/scaffold.js +14 -13
- package/compiler/lexer.js +55 -54
- package/compiler/parser/core.js +1 -0
- package/compiler/parser/state.js +6 -12
- package/compiler/parser/style.js +17 -20
- package/compiler/parser/view.js +1 -3
- package/compiler/preprocessor.js +124 -262
- package/compiler/sourcemap.js +10 -4
- package/compiler/transformer/expressions.js +122 -106
- package/compiler/transformer/index.js +2 -4
- package/compiler/transformer/style.js +74 -7
- package/compiler/transformer/view.js +86 -36
- package/loader/esbuild-plugin-server-components.js +209 -0
- package/loader/esbuild-plugin.js +41 -93
- package/loader/parcel-plugin.js +37 -97
- package/loader/rollup-plugin-server-components.js +30 -169
- package/loader/rollup-plugin.js +27 -78
- package/loader/shared.js +362 -0
- package/loader/swc-plugin.js +65 -82
- package/loader/vite-plugin-server-components.js +30 -171
- package/loader/vite-plugin.js +25 -10
- package/loader/webpack-loader-server-components.js +21 -134
- package/loader/webpack-loader.js +25 -80
- package/package.json +52 -12
- package/runtime/dom-selector.js +2 -1
- package/runtime/form.js +4 -3
- package/runtime/http.js +6 -1
- package/runtime/logger.js +44 -24
- package/runtime/router/utils.js +14 -7
- package/runtime/security.js +13 -1
- package/runtime/server-components/actions-server.js +23 -19
- package/runtime/server-components/error-sanitizer.js +18 -18
- package/runtime/server-components/security.js +41 -24
- package/runtime/ssr-preload.js +5 -3
- package/runtime/testing.js +759 -0
- package/runtime/utils.js +3 -2
- package/server/utils.js +15 -9
- package/sw/index.js +2 -0
- package/types/loaders.d.ts +1043 -0
- package/compiler/parser/_extract.js +0 -393
- package/loader/README.md +0 -509
package/cli/release.js
CHANGED
|
@@ -10,11 +10,11 @@
|
|
|
10
10
|
* - Push to remote
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { readFileSync, writeFileSync,
|
|
13
|
+
import { readFileSync, writeFileSync, unlinkSync, openSync, closeSync, ftruncateSync } from 'fs';
|
|
14
14
|
import { join, dirname } from 'path';
|
|
15
15
|
import { fileURLToPath } from 'url';
|
|
16
16
|
import { execSync } from 'child_process';
|
|
17
|
-
import {
|
|
17
|
+
import { randomBytes } from 'crypto';
|
|
18
18
|
import { createInterface } from 'readline';
|
|
19
19
|
import https from 'https';
|
|
20
20
|
import { log } from './logger.js';
|
|
@@ -23,6 +23,30 @@ import { runDocsTest } from './docs-test.js';
|
|
|
23
23
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
24
24
|
const root = join(__dirname, '..');
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Atomically read-modify-write a file using a file descriptor to prevent TOCTOU races.
|
|
28
|
+
* Opens the file with 'r+' flag, reads content, applies transform, truncates, and writes.
|
|
29
|
+
* Returns false if the file doesn't exist (ENOENT), true on success.
|
|
30
|
+
*/
|
|
31
|
+
function atomicReadModifyWrite(filePath, transformFn) {
|
|
32
|
+
let fd;
|
|
33
|
+
try {
|
|
34
|
+
fd = openSync(filePath, 'r+');
|
|
35
|
+
} catch (err) {
|
|
36
|
+
if (err.code === 'ENOENT') return false;
|
|
37
|
+
throw err;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const content = readFileSync(fd, 'utf-8');
|
|
41
|
+
const newContent = transformFn(content);
|
|
42
|
+
ftruncateSync(fd, 0);
|
|
43
|
+
writeFileSync(fd, newContent, { encoding: 'utf-8' });
|
|
44
|
+
} finally {
|
|
45
|
+
closeSync(fd);
|
|
46
|
+
}
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
26
50
|
/**
|
|
27
51
|
* Prompt user for input
|
|
28
52
|
*/
|
|
@@ -211,17 +235,15 @@ function updatePackageJson(newVersion) {
|
|
|
211
235
|
*/
|
|
212
236
|
function updateDocsState(newVersion) {
|
|
213
237
|
const statePath = join(root, 'docs/src/state.js');
|
|
214
|
-
if (!existsSync(statePath)) {
|
|
215
|
-
log.warn(' docs/src/state.js not found, skipping');
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
238
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
239
|
+
const updated = atomicReadModifyWrite(statePath, (content) => {
|
|
240
|
+
return content.replace(
|
|
241
|
+
/export const version = '[^']+'/,
|
|
242
|
+
`export const version = '${newVersion}'`
|
|
243
|
+
);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
if (!updated) { log.warn(' docs/src/state.js not found, skipping'); return; }
|
|
225
247
|
log.info(` Updated docs/src/state.js to v${newVersion}`);
|
|
226
248
|
}
|
|
227
249
|
|
|
@@ -230,12 +252,6 @@ function updateDocsState(newVersion) {
|
|
|
230
252
|
*/
|
|
231
253
|
function updateChangelog(newVersion, title, changes) {
|
|
232
254
|
const changelogPath = join(root, 'CHANGELOG.md');
|
|
233
|
-
if (!existsSync(changelogPath)) {
|
|
234
|
-
log.warn(' CHANGELOG.md not found, skipping');
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
let content = readFileSync(changelogPath, 'utf-8');
|
|
239
255
|
|
|
240
256
|
// Build changelog entry
|
|
241
257
|
const date = getCurrentDate();
|
|
@@ -277,18 +293,22 @@ function updateChangelog(newVersion, title, changes) {
|
|
|
277
293
|
entry += '\n';
|
|
278
294
|
}
|
|
279
295
|
|
|
280
|
-
//
|
|
281
|
-
const
|
|
282
|
-
|
|
296
|
+
// Atomically read-modify-write to prevent TOCTOU race (CWE-367)
|
|
297
|
+
const updated = atomicReadModifyWrite(changelogPath, (content) => {
|
|
298
|
+
const lines = content.split('\n');
|
|
299
|
+
const insertIndex = lines.findIndex(line => line.startsWith('## ['));
|
|
283
300
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
301
|
+
if (insertIndex !== -1) {
|
|
302
|
+
lines.splice(insertIndex, 0, entry);
|
|
303
|
+
} else {
|
|
304
|
+
// No existing version entries, add after header
|
|
305
|
+
lines.push(entry);
|
|
306
|
+
}
|
|
290
307
|
|
|
291
|
-
|
|
308
|
+
return lines.join('\n');
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
if (!updated) { log.warn(' CHANGELOG.md not found, skipping'); return; }
|
|
292
312
|
log.info(` Updated CHANGELOG.md with v${newVersion}`);
|
|
293
313
|
}
|
|
294
314
|
|
|
@@ -297,12 +317,7 @@ function updateChangelog(newVersion, title, changes) {
|
|
|
297
317
|
*/
|
|
298
318
|
function updateDocsChangelog(newVersion, title, changes) {
|
|
299
319
|
const changelogPagePath = join(root, 'docs/src/pages/ChangelogPage.js');
|
|
300
|
-
if (!existsSync(changelogPagePath)) {
|
|
301
|
-
log.warn(' docs/src/pages/ChangelogPage.js not found, skipping');
|
|
302
|
-
return;
|
|
303
|
-
}
|
|
304
320
|
|
|
305
|
-
let content = readFileSync(changelogPagePath, 'utf-8');
|
|
306
321
|
const monthYear = getCurrentMonthYear();
|
|
307
322
|
|
|
308
323
|
// Build HTML changelog section
|
|
@@ -337,15 +352,18 @@ function updateDocsChangelog(newVersion, title, changes) {
|
|
|
337
352
|
</section>
|
|
338
353
|
`;
|
|
339
354
|
|
|
340
|
-
//
|
|
341
|
-
const
|
|
342
|
-
|
|
355
|
+
// Atomically read-modify-write to prevent TOCTOU race (CWE-367)
|
|
356
|
+
const updated = atomicReadModifyWrite(changelogPagePath, (content) => {
|
|
357
|
+
const insertMarker = '<section class="doc-section changelog-section">';
|
|
358
|
+
const insertIndex = content.indexOf(insertMarker);
|
|
343
359
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
360
|
+
if (insertIndex !== -1) {
|
|
361
|
+
return content.slice(0, insertIndex) + section + content.slice(insertIndex);
|
|
362
|
+
}
|
|
363
|
+
return content;
|
|
364
|
+
});
|
|
347
365
|
|
|
348
|
-
|
|
366
|
+
if (!updated) { log.warn(' docs/src/pages/ChangelogPage.js not found, skipping'); return; }
|
|
349
367
|
log.info(` Updated docs/src/pages/ChangelogPage.js with v${newVersion}`);
|
|
350
368
|
}
|
|
351
369
|
|
|
@@ -593,8 +611,8 @@ function createGitHubRelease(version, title, changes) {
|
|
|
593
611
|
notes += '\n';
|
|
594
612
|
}
|
|
595
613
|
|
|
596
|
-
// Write notes to temp file
|
|
597
|
-
const tempFile = join(
|
|
614
|
+
// Write notes to a temp file in the project root (avoids os.tmpdir() for CWE-377)
|
|
615
|
+
const tempFile = join(root, `.release-notes-${randomBytes(16).toString('hex')}.tmp.md`);
|
|
598
616
|
writeFileSync(tempFile, notes, 'utf-8');
|
|
599
617
|
|
|
600
618
|
try {
|
|
@@ -656,8 +674,8 @@ function gitCommitTagPush(newVersion, title, changes, dryRun = false) {
|
|
|
656
674
|
return { success: false, stage: 'add', error: result.error };
|
|
657
675
|
}
|
|
658
676
|
|
|
659
|
-
// git commit using temp file for cross-platform compatibility
|
|
660
|
-
const tempFile = join(
|
|
677
|
+
// git commit using temp file for cross-platform compatibility (avoids os.tmpdir() for CWE-377)
|
|
678
|
+
const tempFile = join(root, `.commit-msg-${randomBytes(16).toString('hex')}.tmp`);
|
|
661
679
|
writeFileSync(tempFile, commitMessage, 'utf-8');
|
|
662
680
|
try {
|
|
663
681
|
result = execGitCommand(`git commit -F "${tempFile}"`, 'git commit');
|
|
@@ -769,8 +787,8 @@ function gitCommitTagNoPush(newVersion, title, changes) {
|
|
|
769
787
|
return { success: false, stage: 'add', error: result.error };
|
|
770
788
|
}
|
|
771
789
|
|
|
772
|
-
// git commit using temp file for cross-platform compatibility
|
|
773
|
-
const tempFile = join(
|
|
790
|
+
// git commit using temp file for cross-platform compatibility (avoids os.tmpdir() for CWE-377)
|
|
791
|
+
const tempFile = join(root, `.commit-msg-${randomBytes(16).toString('hex')}.tmp`);
|
|
774
792
|
writeFileSync(tempFile, commitMessage, 'utf-8');
|
|
775
793
|
try {
|
|
776
794
|
result = execGitCommand(`git commit -F "${tempFile}"`, 'git commit');
|
package/cli/scaffold.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Generate components, pages, stores, and other project files
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { mkdirSync, writeFileSync } from 'fs';
|
|
7
7
|
import { join, dirname, relative, basename } from 'path';
|
|
8
8
|
import { log } from './logger.js';
|
|
9
9
|
import { parseArgs } from './utils/file-utils.js';
|
|
@@ -957,13 +957,6 @@ export async function runScaffold(args) {
|
|
|
957
957
|
const fileName = toCase(name, type === 'component' || type === 'page' || type === 'layout' ? 'pascal' : 'camel');
|
|
958
958
|
const fullPath = join(process.cwd(), dir, fileName + scaffoldType.extension);
|
|
959
959
|
|
|
960
|
-
// Check if file already exists
|
|
961
|
-
if (existsSync(fullPath) && !options.force && !options.f) {
|
|
962
|
-
log.error(`File already exists: ${relative(process.cwd(), fullPath)}`);
|
|
963
|
-
log.info('Use --force to overwrite.');
|
|
964
|
-
process.exit(1);
|
|
965
|
-
}
|
|
966
|
-
|
|
967
960
|
// Generate content
|
|
968
961
|
const content = scaffoldType.template(name, {
|
|
969
962
|
withState: options.state !== false,
|
|
@@ -973,13 +966,21 @@ export async function runScaffold(args) {
|
|
|
973
966
|
|
|
974
967
|
// Create directory if needed
|
|
975
968
|
const outputDir = dirname(fullPath);
|
|
976
|
-
|
|
977
|
-
|
|
969
|
+
mkdirSync(outputDir, { recursive: true });
|
|
970
|
+
|
|
971
|
+
// Write file (atomic create unless --force)
|
|
972
|
+
const forceOverwrite = options.force || options.f;
|
|
973
|
+
try {
|
|
974
|
+
writeFileSync(fullPath, content, forceOverwrite ? undefined : { flag: 'wx' });
|
|
975
|
+
} catch (err) {
|
|
976
|
+
if (err.code === 'EEXIST') {
|
|
977
|
+
log.error(`File already exists: ${relative(process.cwd(), fullPath)}`);
|
|
978
|
+
log.info('Use --force to overwrite.');
|
|
979
|
+
process.exit(1);
|
|
980
|
+
}
|
|
981
|
+
throw err;
|
|
978
982
|
}
|
|
979
983
|
|
|
980
|
-
// Write file
|
|
981
|
-
writeFileSync(fullPath, content);
|
|
982
|
-
|
|
983
984
|
log.success(`Created ${type}: ${relative(process.cwd(), fullPath)}`);
|
|
984
985
|
|
|
985
986
|
// Show next steps
|
package/compiler/lexer.js
CHANGED
|
@@ -197,11 +197,18 @@ export class Token {
|
|
|
197
197
|
*/
|
|
198
198
|
export class Lexer {
|
|
199
199
|
constructor(source) {
|
|
200
|
+
if (source == null) {
|
|
201
|
+
throw new Error('Lexer: source must be a string, got ' + typeof source);
|
|
202
|
+
}
|
|
200
203
|
this.source = source;
|
|
201
204
|
this.pos = 0;
|
|
202
205
|
this.line = 1;
|
|
203
206
|
this.column = 1;
|
|
204
207
|
this.tokens = [];
|
|
208
|
+
// Block context tracking (avoids O(n) backward scans)
|
|
209
|
+
this._currentBlock = null; // 'view' | 'style' | 'state' | 'actions' | 'router' | 'store' | null
|
|
210
|
+
this._blockDepth = 0; // brace depth within current block
|
|
211
|
+
this._parenDepth = 0; // parentheses depth within current block (for expression context)
|
|
205
212
|
}
|
|
206
213
|
|
|
207
214
|
/**
|
|
@@ -264,6 +271,8 @@ export class Lexer {
|
|
|
264
271
|
}
|
|
265
272
|
} else if (char === '/' && this.peek() === '*') {
|
|
266
273
|
// Multi-line comment
|
|
274
|
+
const commentLine = this.line;
|
|
275
|
+
const commentCol = this.column;
|
|
267
276
|
this.advance(); // /
|
|
268
277
|
this.advance(); // *
|
|
269
278
|
while (!this.isEOF() && !(this.current() === '*' && this.peek() === '/')) {
|
|
@@ -272,6 +281,9 @@ export class Lexer {
|
|
|
272
281
|
if (!this.isEOF()) {
|
|
273
282
|
this.advance(); // *
|
|
274
283
|
this.advance(); // /
|
|
284
|
+
} else {
|
|
285
|
+
// Unclosed block comment — emit error token so downstream gets a clear diagnostic
|
|
286
|
+
this.tokens.push(new Token(TokenType.ERROR, 'Unterminated block comment', commentLine, commentCol));
|
|
275
287
|
}
|
|
276
288
|
} else {
|
|
277
289
|
break;
|
|
@@ -572,10 +584,19 @@ export class Lexer {
|
|
|
572
584
|
/**
|
|
573
585
|
* Tokenize the entire source
|
|
574
586
|
*/
|
|
587
|
+
/**
|
|
588
|
+
* Set of top-level block keywords that define context
|
|
589
|
+
*/
|
|
590
|
+
static BLOCK_KEYWORDS = new Set([
|
|
591
|
+
TokenType.VIEW, TokenType.STYLE, TokenType.STATE,
|
|
592
|
+
TokenType.ACTIONS, TokenType.ROUTER, TokenType.STORE
|
|
593
|
+
]);
|
|
594
|
+
|
|
575
595
|
tokenize() {
|
|
576
596
|
this.tokens = [];
|
|
577
|
-
|
|
578
|
-
|
|
597
|
+
this._currentBlock = null;
|
|
598
|
+
this._blockDepth = 0;
|
|
599
|
+
let _pendingBlock = null; // keyword waiting for its opening brace
|
|
579
600
|
|
|
580
601
|
while (!this.isEOF()) {
|
|
581
602
|
this.skipWhitespace();
|
|
@@ -645,20 +666,35 @@ export class Lexer {
|
|
|
645
666
|
switch (char) {
|
|
646
667
|
case '{':
|
|
647
668
|
this.advance();
|
|
648
|
-
|
|
669
|
+
if (_pendingBlock) {
|
|
670
|
+
this._currentBlock = _pendingBlock;
|
|
671
|
+
this._blockDepth = 1;
|
|
672
|
+
this._parenDepth = 0;
|
|
673
|
+
_pendingBlock = null;
|
|
674
|
+
} else if (this._currentBlock) {
|
|
675
|
+
this._blockDepth++;
|
|
676
|
+
}
|
|
649
677
|
this.tokens.push(new Token(TokenType.LBRACE, '{', startLine, startColumn));
|
|
650
678
|
continue;
|
|
651
679
|
case '}':
|
|
652
680
|
this.advance();
|
|
653
|
-
|
|
681
|
+
if (this._currentBlock) {
|
|
682
|
+
this._blockDepth--;
|
|
683
|
+
if (this._blockDepth === 0) {
|
|
684
|
+
this._currentBlock = null;
|
|
685
|
+
this._parenDepth = 0;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
654
688
|
this.tokens.push(new Token(TokenType.RBRACE, '}', startLine, startColumn));
|
|
655
689
|
continue;
|
|
656
690
|
case '(':
|
|
657
691
|
this.advance();
|
|
692
|
+
if (this._currentBlock) this._parenDepth++;
|
|
658
693
|
this.tokens.push(new Token(TokenType.LPAREN, '(', startLine, startColumn));
|
|
659
694
|
continue;
|
|
660
695
|
case ')':
|
|
661
696
|
this.advance();
|
|
697
|
+
if (this._currentBlock) this._parenDepth--;
|
|
662
698
|
this.tokens.push(new Token(TokenType.RPAREN, ')', startLine, startColumn));
|
|
663
699
|
continue;
|
|
664
700
|
case '[':
|
|
@@ -875,9 +911,14 @@ export class Lexer {
|
|
|
875
911
|
const forceIdent = inStyle && cssReservedWords.has(word);
|
|
876
912
|
|
|
877
913
|
if (shouldBeKeyword) {
|
|
878
|
-
this.
|
|
914
|
+
const token = this.readIdentifier(false);
|
|
915
|
+
this.tokens.push(token);
|
|
916
|
+
// Track top-level block keywords for context detection
|
|
917
|
+
if (Lexer.BLOCK_KEYWORDS.has(token.type) && !this._currentBlock) {
|
|
918
|
+
_pendingBlock = token.type.toLowerCase();
|
|
919
|
+
}
|
|
879
920
|
} else if (this.isViewContext() && this.couldBeSelector()) {
|
|
880
|
-
// Only treat as selector if
|
|
921
|
+
// Only treat as selector if in view context (checks paren depth)
|
|
881
922
|
this.tokens.push(this.readSelector());
|
|
882
923
|
} else {
|
|
883
924
|
this.tokens.push(this.readIdentifier(forceIdent));
|
|
@@ -896,23 +937,10 @@ export class Lexer {
|
|
|
896
937
|
|
|
897
938
|
/**
|
|
898
939
|
* Check if we're in a style context (inside style block)
|
|
940
|
+
* Uses tracked block context for O(1) lookup
|
|
899
941
|
*/
|
|
900
942
|
isStyleContext() {
|
|
901
|
-
|
|
902
|
-
for (let i = this.tokens.length - 1; i >= 0; i--) {
|
|
903
|
-
const token = this.tokens[i];
|
|
904
|
-
if (token.type === TokenType.STYLE) {
|
|
905
|
-
return true;
|
|
906
|
-
}
|
|
907
|
-
if (token.type === TokenType.STATE ||
|
|
908
|
-
token.type === TokenType.VIEW ||
|
|
909
|
-
token.type === TokenType.ACTIONS ||
|
|
910
|
-
token.type === TokenType.ROUTER ||
|
|
911
|
-
token.type === TokenType.STORE) {
|
|
912
|
-
return false;
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
return false;
|
|
943
|
+
return this._currentBlock === 'style';
|
|
916
944
|
}
|
|
917
945
|
|
|
918
946
|
/**
|
|
@@ -933,11 +961,13 @@ export class Lexer {
|
|
|
933
961
|
|
|
934
962
|
/**
|
|
935
963
|
* Check if we're in a view context where selectors are expected
|
|
964
|
+
* Uses tracked block context and paren depth for O(1) lookup
|
|
936
965
|
*/
|
|
937
966
|
isViewContext() {
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
967
|
+
if (this._currentBlock !== 'view') return false;
|
|
968
|
+
|
|
969
|
+
// Inside parentheses = expression context, not selector context
|
|
970
|
+
if (this._parenDepth > 0) return false;
|
|
941
971
|
|
|
942
972
|
// After @ token, next word is directive name (not selector)
|
|
943
973
|
const lastToken = this.tokens[this.tokens.length - 1];
|
|
@@ -945,36 +975,7 @@ export class Lexer {
|
|
|
945
975
|
return false;
|
|
946
976
|
}
|
|
947
977
|
|
|
948
|
-
|
|
949
|
-
const token = this.tokens[i];
|
|
950
|
-
|
|
951
|
-
// Track parentheses depth (backwards)
|
|
952
|
-
if (token.type === TokenType.RPAREN) {
|
|
953
|
-
parenDepth++;
|
|
954
|
-
} else if (token.type === TokenType.LPAREN) {
|
|
955
|
-
parenDepth--;
|
|
956
|
-
// If we go negative, we're inside parentheses (expression context)
|
|
957
|
-
if (parenDepth < 0) {
|
|
958
|
-
return false; // Inside expression, not selector context
|
|
959
|
-
}
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
if (token.type === TokenType.VIEW) {
|
|
963
|
-
inView = true;
|
|
964
|
-
break;
|
|
965
|
-
}
|
|
966
|
-
if (token.type === TokenType.STATE ||
|
|
967
|
-
token.type === TokenType.ACTIONS ||
|
|
968
|
-
token.type === TokenType.STYLE ||
|
|
969
|
-
token.type === TokenType.ROUTER ||
|
|
970
|
-
token.type === TokenType.STORE ||
|
|
971
|
-
token.type === TokenType.ROUTES ||
|
|
972
|
-
token.type === TokenType.GETTERS) {
|
|
973
|
-
return false;
|
|
974
|
-
}
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
return inView;
|
|
978
|
+
return true;
|
|
978
979
|
}
|
|
979
980
|
|
|
980
981
|
/**
|
package/compiler/parser/core.js
CHANGED
package/compiler/parser/state.js
CHANGED
|
@@ -34,15 +34,19 @@ Parser.prototype.parsePropsBlock = function() {
|
|
|
34
34
|
};
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
|
-
* Parse a
|
|
37
|
+
* Parse a property declaration (name: value) — shared by props and state blocks
|
|
38
38
|
*/
|
|
39
|
-
Parser.prototype.
|
|
39
|
+
Parser.prototype.parseProperty = function() {
|
|
40
40
|
const name = this.expect(TokenType.IDENT);
|
|
41
41
|
this.expect(TokenType.COLON);
|
|
42
42
|
const value = this.parseValue();
|
|
43
43
|
return new ASTNode(NodeType.Property, { name: name.value, value });
|
|
44
44
|
};
|
|
45
45
|
|
|
46
|
+
// Aliases for semantic clarity in callers
|
|
47
|
+
Parser.prototype.parsePropsProperty = Parser.prototype.parseProperty;
|
|
48
|
+
Parser.prototype.parseStateProperty = Parser.prototype.parseProperty;
|
|
49
|
+
|
|
46
50
|
// ============================================================
|
|
47
51
|
// State Block Parsing
|
|
48
52
|
// ============================================================
|
|
@@ -63,16 +67,6 @@ Parser.prototype.parseStateBlock = function() {
|
|
|
63
67
|
return new ASTNode(NodeType.StateBlock, { properties });
|
|
64
68
|
};
|
|
65
69
|
|
|
66
|
-
/**
|
|
67
|
-
* Parse a state property
|
|
68
|
-
*/
|
|
69
|
-
Parser.prototype.parseStateProperty = function() {
|
|
70
|
-
const name = this.expect(TokenType.IDENT);
|
|
71
|
-
this.expect(TokenType.COLON);
|
|
72
|
-
const value = this.parseValue();
|
|
73
|
-
return new ASTNode(NodeType.Property, { name: name.value, value });
|
|
74
|
-
};
|
|
75
|
-
|
|
76
70
|
// ============================================================
|
|
77
71
|
// Value Parsing Utilities
|
|
78
72
|
// ============================================================
|
package/compiler/parser/style.js
CHANGED
|
@@ -59,6 +59,8 @@ Parser.prototype.parseStyleBlock = function() {
|
|
|
59
59
|
// Parsing failed - likely preprocessor syntax (LESS/SASS/Stylus)
|
|
60
60
|
parseError = error;
|
|
61
61
|
rules = []; // Clear any partial parse
|
|
62
|
+
// Warn so users know structured CSS parsing fell back to raw mode
|
|
63
|
+
console.warn(`[pulse] CSS parse warning: falling back to raw mode — ${error.message}`);
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
// Restore position to after the closing }
|
|
@@ -195,7 +197,10 @@ Parser.prototype.parseStyleRule = function() {
|
|
|
195
197
|
};
|
|
196
198
|
|
|
197
199
|
/**
|
|
198
|
-
* Check if current position is a nested rule
|
|
200
|
+
* Check if current position is a nested rule.
|
|
201
|
+
* Scans forward for the first decisive token ({, :, or }) to determine
|
|
202
|
+
* if this is a nested rule (selector) or a property declaration.
|
|
203
|
+
* Bounded lookahead prevents O(n²) on deeply nested CSS.
|
|
199
204
|
*/
|
|
200
205
|
Parser.prototype.isNestedRule = function() {
|
|
201
206
|
const currentToken = this.peek(0);
|
|
@@ -207,35 +212,27 @@ Parser.prototype.isNestedRule = function() {
|
|
|
207
212
|
}
|
|
208
213
|
|
|
209
214
|
const startLine = currentToken.line;
|
|
210
|
-
|
|
215
|
+
// Bounded lookahead — selectors/property names are never longer than ~50 tokens
|
|
216
|
+
const maxLookahead = 50;
|
|
211
217
|
|
|
212
|
-
|
|
218
|
+
for (let i = 0; i < maxLookahead; i++) {
|
|
213
219
|
const token = this.peek(i);
|
|
220
|
+
if (!token || token.type === TokenType.EOF) return false;
|
|
214
221
|
|
|
215
|
-
// Found { before :
|
|
222
|
+
// Found { before : → nested rule
|
|
216
223
|
if (token.type === TokenType.LBRACE) return true;
|
|
217
224
|
|
|
218
|
-
// Found :
|
|
225
|
+
// Found : before { → property declaration
|
|
219
226
|
if (token.type === TokenType.COLON) return false;
|
|
220
227
|
|
|
221
|
-
// Found }
|
|
228
|
+
// Found } → end of current rule block
|
|
222
229
|
if (token.type === TokenType.RBRACE) return false;
|
|
223
230
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
while (this.peek(j) && this.peek(j).line === nextLine) {
|
|
228
|
-
const t = this.peek(j);
|
|
229
|
-
if (t.type === TokenType.LBRACE) return true;
|
|
230
|
-
if (t.type === TokenType.COLON) return false;
|
|
231
|
-
if (t.type === TokenType.RBRACE) return false;
|
|
232
|
-
j++;
|
|
233
|
-
}
|
|
234
|
-
return false;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
i++;
|
|
231
|
+
// If we've moved past the starting line without finding a decisive token,
|
|
232
|
+
// default to property (most common case in CSS)
|
|
233
|
+
if (i > 0 && token.line > startLine) return false;
|
|
238
234
|
}
|
|
235
|
+
|
|
239
236
|
return false;
|
|
240
237
|
};
|
|
241
238
|
|
package/compiler/parser/view.js
CHANGED
|
@@ -622,7 +622,5 @@ Parser.prototype.parseFocusTrapDirective = function() {
|
|
|
622
622
|
* Parse @srOnly directive - visually hidden but accessible text
|
|
623
623
|
*/
|
|
624
624
|
Parser.prototype.parseSrOnlyDirective = function() {
|
|
625
|
-
return new ASTNode(NodeType.
|
|
626
|
-
attrs: { srOnly: true }
|
|
627
|
-
});
|
|
625
|
+
return new ASTNode(NodeType.SrOnlyDirective, {});
|
|
628
626
|
};
|