ripple 0.2.188 → 0.2.190
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/package.json +8 -2
- package/src/compiler/identifier-utils.js +75 -0
- package/src/compiler/index.d.ts +11 -3
- package/src/compiler/index.js +2 -2
- package/src/compiler/phases/1-parse/index.js +121 -26
- package/src/compiler/phases/2-analyze/index.js +84 -8
- package/src/compiler/phases/2-analyze/prune.js +30 -12
- package/src/compiler/phases/2-analyze/validation.js +4 -0
- package/src/compiler/phases/3-transform/client/index.js +241 -82
- package/src/compiler/phases/3-transform/segments.js +129 -111
- package/src/compiler/phases/3-transform/server/index.js +93 -20
- package/src/compiler/types/import.d.ts +52 -0
- package/src/compiler/types/index.d.ts +45 -21
- package/src/compiler/types/parse.d.ts +10 -4
- package/src/compiler/utils.js +4 -4
- package/src/utils/builders.js +3 -2
- package/tests/client/basic/basic.attributes.test.ripple +33 -4
- package/tests/client/compiler/compiler.basic.test.ripple +46 -54
- package/tsconfig.json +28 -28
- package/types/index.d.ts +36 -42
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "Ripple is an elegant TypeScript UI framework",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Dominic Gannaway",
|
|
6
|
-
"version": "0.2.
|
|
6
|
+
"version": "0.2.190",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"module": "src/runtime/index-client.js",
|
|
9
9
|
"main": "src/runtime/index-client.js",
|
|
@@ -38,6 +38,12 @@
|
|
|
38
38
|
"require": "./src/compiler/index.js",
|
|
39
39
|
"default": "./src/compiler/index.js"
|
|
40
40
|
},
|
|
41
|
+
"./compiler/internal/import": {
|
|
42
|
+
"types": "./src/compiler/types/import.d.ts"
|
|
43
|
+
},
|
|
44
|
+
"./compiler/internal/identifier/utils": {
|
|
45
|
+
"default": "./src/compiler/identifier-utils.js"
|
|
46
|
+
},
|
|
41
47
|
"./validator": {
|
|
42
48
|
"types": "./types/index.d.ts",
|
|
43
49
|
"require": "./validator/index.js",
|
|
@@ -86,6 +92,6 @@
|
|
|
86
92
|
"vscode-languageserver-types": "^3.17.5"
|
|
87
93
|
},
|
|
88
94
|
"peerDependencies": {
|
|
89
|
-
"ripple": "0.2.
|
|
95
|
+
"ripple": "0.2.190"
|
|
90
96
|
}
|
|
91
97
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export const IDENTIFIER_OBFUSCATION_PREFIX = '_$_';
|
|
2
|
+
export const STYLE_IDENTIFIER = IDENTIFIER_OBFUSCATION_PREFIX + encode_utf16_char('#') + 'style';
|
|
3
|
+
export const SERVER_IDENTIFIER = IDENTIFIER_OBFUSCATION_PREFIX + encode_utf16_char('#') + 'server';
|
|
4
|
+
export const CSS_HASH_IDENTIFIER = IDENTIFIER_OBFUSCATION_PREFIX + 'hash';
|
|
5
|
+
|
|
6
|
+
const DECODE_UTF16_REGEX = /_u([0-9a-fA-F]{4})_/g;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {string} char
|
|
10
|
+
* @returns {string}
|
|
11
|
+
*/
|
|
12
|
+
function encode_utf16_char(char) {
|
|
13
|
+
return `_u${('0000' + char.charCodeAt(0).toString(16)).slice(-4)}_`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {string} encoded
|
|
18
|
+
* @returns {string}
|
|
19
|
+
*/
|
|
20
|
+
function decoded_utf16_string(encoded) {
|
|
21
|
+
return encoded.replace(DECODE_UTF16_REGEX, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {string} name
|
|
26
|
+
* @returns {string}
|
|
27
|
+
*/
|
|
28
|
+
export function obfuscate_identifier(name) {
|
|
29
|
+
let start = 0;
|
|
30
|
+
if (name[0] === name[0].toUpperCase()) {
|
|
31
|
+
start = 1;
|
|
32
|
+
}
|
|
33
|
+
const index = find_next_uppercase(name, start);
|
|
34
|
+
|
|
35
|
+
const first_part = name.slice(0, index);
|
|
36
|
+
const second_part = name.slice(index);
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
IDENTIFIER_OBFUSCATION_PREFIX +
|
|
40
|
+
(second_part ? second_part + '__' + first_part : first_part + '__')
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {string} name
|
|
46
|
+
* @returns {boolean}
|
|
47
|
+
*/
|
|
48
|
+
export function is_identifier_obfuscated(name) {
|
|
49
|
+
return name.startsWith(IDENTIFIER_OBFUSCATION_PREFIX);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @param {string} name
|
|
54
|
+
* @returns {string}
|
|
55
|
+
*/
|
|
56
|
+
export function deobfuscate_identifier(name) {
|
|
57
|
+
name = name.replaceAll(IDENTIFIER_OBFUSCATION_PREFIX, '');
|
|
58
|
+
const parts = name.split('__');
|
|
59
|
+
return decoded_utf16_string((parts[1] ? parts[1] : '') + parts[0]);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Finds the next uppercase character or returns name.length
|
|
64
|
+
* @param {string} name
|
|
65
|
+
* @param {number} start
|
|
66
|
+
* @returns {number}
|
|
67
|
+
*/
|
|
68
|
+
function find_next_uppercase(name, start) {
|
|
69
|
+
for (let i = start; i < name.length; i++) {
|
|
70
|
+
if (name[i] === name[i].toUpperCase()) {
|
|
71
|
+
return i;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return name.length;
|
|
75
|
+
}
|
package/src/compiler/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import type * as AST from 'estree';
|
|
2
2
|
import type {
|
|
3
3
|
CodeInformation as VolarCodeInformation,
|
|
4
4
|
Mapping as VolarMapping,
|
|
@@ -14,7 +14,7 @@ import type { SourceMapMappings } from '@jridgewell/sourcemap-codec';
|
|
|
14
14
|
*/
|
|
15
15
|
export interface CompileResult {
|
|
16
16
|
/** The transformed AST */
|
|
17
|
-
ast: Program;
|
|
17
|
+
ast: AST.Program;
|
|
18
18
|
/** The generated JavaScript code with source map */
|
|
19
19
|
js: {
|
|
20
20
|
code: string;
|
|
@@ -49,6 +49,14 @@ export interface PluginActionOverrides {
|
|
|
49
49
|
description?: string; // just for reference
|
|
50
50
|
// Generic location for embedded content (CSS, etc.)
|
|
51
51
|
location?: DefinitionLocation;
|
|
52
|
+
// Replace the type name in hover/definition with a different name
|
|
53
|
+
// And provide the path to import the type definitions from
|
|
54
|
+
// the `ripple` package directory, e.g. `types/index.d.ts`
|
|
55
|
+
// Currently only supported by the definition plugin
|
|
56
|
+
typeReplace?: {
|
|
57
|
+
name: string;
|
|
58
|
+
path: string;
|
|
59
|
+
};
|
|
52
60
|
}
|
|
53
61
|
| false;
|
|
54
62
|
}
|
|
@@ -94,7 +102,7 @@ export interface AnalyzeOptions extends ParseOptions, Pick<CompileOptions, 'mode
|
|
|
94
102
|
|
|
95
103
|
export interface VolarCompileOptions extends ParseOptions, SharedCompileOptions {}
|
|
96
104
|
|
|
97
|
-
export function parse(source: string, options?: ParseOptions): Program;
|
|
105
|
+
export function parse(source: string, options?: ParseOptions): AST.Program;
|
|
98
106
|
|
|
99
107
|
export function compile(source: string, filename: string, options?: CompileOptions): CompileResult;
|
|
100
108
|
|
package/src/compiler/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** @import
|
|
1
|
+
/** @import * as AST from 'estree' */
|
|
2
2
|
|
|
3
3
|
import { parse as parse_module } from './phases/1-parse/index.js';
|
|
4
4
|
import { analyze } from './phases/2-analyze/index.js';
|
|
@@ -9,7 +9,7 @@ import { convert_source_map_to_mappings } from './phases/3-transform/segments.js
|
|
|
9
9
|
/**
|
|
10
10
|
* Parse Ripple source code to ESTree AST
|
|
11
11
|
* @param {string} source
|
|
12
|
-
* @returns {Program}
|
|
12
|
+
* @returns {AST.Program}
|
|
13
13
|
*/
|
|
14
14
|
export function parse(source) {
|
|
15
15
|
return parse_module(source, undefined);
|
|
@@ -342,8 +342,12 @@ function RipplePlugin(config) {
|
|
|
342
342
|
if (this.input.slice(this.pos, this.pos + 4) === '#Map') {
|
|
343
343
|
const charAfter =
|
|
344
344
|
this.pos + 4 < this.input.length ? this.input.charCodeAt(this.pos + 4) : -1;
|
|
345
|
-
if (charAfter === 40) {
|
|
346
|
-
// ( character
|
|
345
|
+
if (charAfter === 40 || charAfter === 60) {
|
|
346
|
+
// ( or < character (for generics like #Map<string, number>)
|
|
347
|
+
this.pos += 4; // consume '#Map'
|
|
348
|
+
return this.finishToken(tt.name, '#Map');
|
|
349
|
+
} else if (this.#loose) {
|
|
350
|
+
// In loose mode, produce token even without parens (incomplete syntax)
|
|
347
351
|
this.pos += 4; // consume '#Map'
|
|
348
352
|
return this.finishToken(tt.name, '#Map');
|
|
349
353
|
}
|
|
@@ -351,8 +355,12 @@ function RipplePlugin(config) {
|
|
|
351
355
|
if (this.input.slice(this.pos, this.pos + 4) === '#Set') {
|
|
352
356
|
const charAfter =
|
|
353
357
|
this.pos + 4 < this.input.length ? this.input.charCodeAt(this.pos + 4) : -1;
|
|
354
|
-
if (charAfter === 40) {
|
|
355
|
-
// ( character
|
|
358
|
+
if (charAfter === 40 || charAfter === 60) {
|
|
359
|
+
// ( or < character (for generics like #Set<number>)
|
|
360
|
+
this.pos += 4; // consume '#Set'
|
|
361
|
+
return this.finishToken(tt.name, '#Set');
|
|
362
|
+
} else if (this.#loose) {
|
|
363
|
+
// In loose mode, produce token even without parens (incomplete syntax)
|
|
356
364
|
this.pos += 4; // consume '#Set'
|
|
357
365
|
return this.finishToken(tt.name, '#Set');
|
|
358
366
|
}
|
|
@@ -378,9 +386,29 @@ function RipplePlugin(config) {
|
|
|
378
386
|
}
|
|
379
387
|
}
|
|
380
388
|
|
|
389
|
+
if (this.input.slice(this.pos, this.pos + 6) === '#style') {
|
|
390
|
+
// Check that next char after 'style' is . (dot), [, whitespace, or EOF
|
|
391
|
+
const charAfter =
|
|
392
|
+
this.pos + 6 < this.input.length ? this.input.charCodeAt(this.pos + 6) : -1;
|
|
393
|
+
if (
|
|
394
|
+
charAfter === 46 || // . (dot)
|
|
395
|
+
charAfter === 91 || // [
|
|
396
|
+
charAfter === 32 || // space
|
|
397
|
+
charAfter === 9 || // tab
|
|
398
|
+
charAfter === 10 || // newline
|
|
399
|
+
charAfter === 13 || // carriage return
|
|
400
|
+
charAfter === -1 // EOF
|
|
401
|
+
) {
|
|
402
|
+
// { or . or whitespace or EOF
|
|
403
|
+
this.pos += 6; // consume '#style'
|
|
404
|
+
return this.finishToken(tt.name, '#style');
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
381
408
|
// Check if this is an invalid #Identifier pattern
|
|
382
|
-
// Valid patterns: #[, #{, #Map(, #Set(, #server
|
|
409
|
+
// Valid patterns: #[, #{, #Map(, #Map<, #Set(, #Set<, #server, #style
|
|
383
410
|
// If we see # followed by an uppercase letter that isn't Map or Set, it's an error
|
|
411
|
+
// In loose mode, allow incomplete identifiers like #M, #Ma, #S, #Se for autocomplete
|
|
384
412
|
if (nextChar >= 65 && nextChar <= 90) {
|
|
385
413
|
// A-Z
|
|
386
414
|
// Extract the identifier name
|
|
@@ -401,12 +429,34 @@ function RipplePlugin(config) {
|
|
|
401
429
|
}
|
|
402
430
|
const identName = this.input.slice(this.pos + 1, identEnd);
|
|
403
431
|
if (identName !== 'Map' && identName !== 'Set') {
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
);
|
|
432
|
+
// In loose mode, allow incomplete identifiers (prefixes of Map/Set)
|
|
433
|
+
// This supports autocomplete scenarios where user is still typing
|
|
434
|
+
const isIncompleteMap = 'Map'.startsWith(identName);
|
|
435
|
+
const isIncompleteSet = 'Set'.startsWith(identName);
|
|
436
|
+
|
|
437
|
+
if (!this.#loose || (!isIncompleteMap && !isIncompleteSet)) {
|
|
438
|
+
this.raise(
|
|
439
|
+
this.pos,
|
|
440
|
+
`Invalid tracked syntax '#${identName}'. Only #Map and #Set are currently supported using shorthand tracked syntax.`,
|
|
441
|
+
);
|
|
442
|
+
} else {
|
|
443
|
+
// In loose mode with valid prefix, consume the token and return it
|
|
444
|
+
// This allows the parser to handle incomplete syntax gracefully
|
|
445
|
+
this.pos = identEnd; // consume '#' + identifier
|
|
446
|
+
return this.finishToken(tt.name, '#' + identName);
|
|
447
|
+
}
|
|
408
448
|
}
|
|
409
449
|
}
|
|
450
|
+
|
|
451
|
+
// In loose mode, handle bare # or # followed by unrecognized characters
|
|
452
|
+
if (this.#loose) {
|
|
453
|
+
this.pos++; // consume '#'
|
|
454
|
+
return this.finishToken(tt.name, '#');
|
|
455
|
+
}
|
|
456
|
+
} else if (this.#loose) {
|
|
457
|
+
// In loose mode, handle bare # at EOF
|
|
458
|
+
this.pos++; // consume '#'
|
|
459
|
+
return this.finishToken(tt.name, '#');
|
|
410
460
|
}
|
|
411
461
|
}
|
|
412
462
|
if (code === 64) {
|
|
@@ -435,20 +485,43 @@ function RipplePlugin(config) {
|
|
|
435
485
|
// In JSX expressions, inside parentheses, assignments, etc.
|
|
436
486
|
// we want to treat @ as an identifier prefix rather than decorator
|
|
437
487
|
const currentType = this.type;
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
488
|
+
/**
|
|
489
|
+
* @param {Parse.TokenType} type
|
|
490
|
+
* @param {Parse.Parser} parser
|
|
491
|
+
* @param {Parse.TokTypes} tt
|
|
492
|
+
* @returns {boolean}
|
|
493
|
+
*/
|
|
494
|
+
function inExpression(type, parser, tt) {
|
|
495
|
+
return (
|
|
496
|
+
parser.exprAllowed ||
|
|
497
|
+
type === tt.braceL || // Inside { }
|
|
498
|
+
type === tt.parenL || // Inside ( )
|
|
499
|
+
type === tt.eq || // After =
|
|
500
|
+
type === tt.comma || // After ,
|
|
501
|
+
type === tt.colon || // After :
|
|
502
|
+
type === tt.question || // After ?
|
|
503
|
+
type === tt.logicalOR || // After ||
|
|
504
|
+
type === tt.logicalAND || // After &&
|
|
505
|
+
type === tt.dot || // After . (for member expressions like obj.@prop)
|
|
506
|
+
type === tt.questionDot // After ?. (for optional chaining like obj?.@prop)
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* @param {Parse.Parser} parser
|
|
512
|
+
* @param {Parse.TokTypes} tt
|
|
513
|
+
* @returns {boolean}
|
|
514
|
+
*/
|
|
515
|
+
function inAwait(parser, tt) {
|
|
516
|
+
return currentType === tt.name &&
|
|
517
|
+
parser.value === 'await' &&
|
|
518
|
+
parser.canAwait &&
|
|
519
|
+
parser.preToken
|
|
520
|
+
? inExpression(parser.preToken, parser, tt)
|
|
521
|
+
: false;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (inExpression(currentType, this, tt) || inAwait(this, tt)) {
|
|
452
525
|
return this.readAtIdentifier();
|
|
453
526
|
}
|
|
454
527
|
}
|
|
@@ -604,12 +677,32 @@ function RipplePlugin(config) {
|
|
|
604
677
|
return /** @type {AST.ServerIdentifier} */ (this.finishNode(node, 'ServerIdentifier'));
|
|
605
678
|
}
|
|
606
679
|
|
|
680
|
+
if (this.type === tt.name && this.value === '#style') {
|
|
681
|
+
const node = this.startNode();
|
|
682
|
+
this.next();
|
|
683
|
+
return /** @type {AST.StyleIdentifier} */ (this.finishNode(node, 'StyleIdentifier'));
|
|
684
|
+
}
|
|
685
|
+
|
|
607
686
|
// Check if this is #Map( or #Set(
|
|
608
687
|
if (this.type === tt.name && (this.value === '#Map' || this.value === '#Set')) {
|
|
609
688
|
const type = this.value === '#Map' ? 'TrackedMapExpression' : 'TrackedSetExpression';
|
|
610
689
|
return this.parseTrackedCollectionExpression(type);
|
|
611
690
|
}
|
|
612
691
|
|
|
692
|
+
// In loose mode, handle incomplete #Map/#Set prefixes (e.g., #M, #Ma, #S, #Se)
|
|
693
|
+
if (
|
|
694
|
+
this.#loose &&
|
|
695
|
+
this.type === tt.name &&
|
|
696
|
+
typeof this.value === 'string' &&
|
|
697
|
+
this.value.startsWith('#')
|
|
698
|
+
) {
|
|
699
|
+
// Return an Identifier node for incomplete tracked syntax
|
|
700
|
+
const node = /** @type {AST.Identifier} */ (this.startNode());
|
|
701
|
+
node.name = this.value;
|
|
702
|
+
this.next();
|
|
703
|
+
return this.finishNode(node, 'Identifier');
|
|
704
|
+
}
|
|
705
|
+
|
|
613
706
|
// Check if this is a tuple literal starting with #[
|
|
614
707
|
if (this.type === tt.bracketL && this.value === '#[') {
|
|
615
708
|
return this.parseTrackedArrayExpression();
|
|
@@ -676,7 +769,7 @@ function RipplePlugin(config) {
|
|
|
676
769
|
const node = /** @type {AST.ServerBlock} */ (this.startNode());
|
|
677
770
|
this.next();
|
|
678
771
|
|
|
679
|
-
const body = /** @type {AST.
|
|
772
|
+
const body = /** @type {AST.ServerBlockStatement} */ (this.startNode());
|
|
680
773
|
node.body = body;
|
|
681
774
|
body.body = [];
|
|
682
775
|
|
|
@@ -728,8 +821,10 @@ function RipplePlugin(config) {
|
|
|
728
821
|
);
|
|
729
822
|
}
|
|
730
823
|
|
|
731
|
-
|
|
732
|
-
|
|
824
|
+
// Don't consume parens or generics - they belong to NewExpression
|
|
825
|
+
// When used as "new #Map(...)" the next token is '('
|
|
826
|
+
// When used as "new #Map<K,V>(...)" the next token is '<' (relational)
|
|
827
|
+
if (this.type === tt.parenL || (this.type === tt.relational && this.value === '<')) {
|
|
733
828
|
node.arguments = [];
|
|
734
829
|
return this.finishNode(node, type);
|
|
735
830
|
}
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
AnalysisContext,
|
|
7
7
|
ScopeInterface,
|
|
8
8
|
Visitors,
|
|
9
|
+
TopScopedClasses,
|
|
10
|
+
StyleClasses,
|
|
9
11
|
} from '#compiler';
|
|
10
12
|
*/
|
|
11
13
|
/** @import * as AST from 'estree' */
|
|
@@ -200,6 +202,51 @@ const visitors = {
|
|
|
200
202
|
context.state.metadata.tracking = true;
|
|
201
203
|
}
|
|
202
204
|
|
|
205
|
+
// Track #style.className or #style['className'] references
|
|
206
|
+
if (node.object.type === 'StyleIdentifier') {
|
|
207
|
+
const component = is_inside_component(context, true);
|
|
208
|
+
|
|
209
|
+
if (!component) {
|
|
210
|
+
return error(
|
|
211
|
+
'`#style` can only be used within a component',
|
|
212
|
+
context.state.analysis.module.filename,
|
|
213
|
+
node,
|
|
214
|
+
);
|
|
215
|
+
} else {
|
|
216
|
+
component.metadata.styleIdentifierPresent = true;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** @type {string | null} */
|
|
220
|
+
let className = null;
|
|
221
|
+
|
|
222
|
+
if (!node.computed && node.property.type === 'Identifier') {
|
|
223
|
+
// #style.test
|
|
224
|
+
className = node.property.name;
|
|
225
|
+
} else if (
|
|
226
|
+
node.computed &&
|
|
227
|
+
node.property.type === 'Literal' &&
|
|
228
|
+
typeof node.property.value === 'string'
|
|
229
|
+
) {
|
|
230
|
+
// #style['test']
|
|
231
|
+
className = node.property.value;
|
|
232
|
+
} else {
|
|
233
|
+
// #style[expression] - dynamic, not allowed
|
|
234
|
+
error(
|
|
235
|
+
'`#style` property access must use a dot property or static string for css class name, not a dynamic expression',
|
|
236
|
+
context.state.analysis.module.filename,
|
|
237
|
+
node.property,
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (className !== null) {
|
|
242
|
+
context.state.metadata.styleClasses?.set(className, node.property);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return context.next();
|
|
246
|
+
} else if (node.object.type === 'ServerIdentifier') {
|
|
247
|
+
context.state.analysis.metadata.serverIdentifierPresent = true;
|
|
248
|
+
}
|
|
249
|
+
|
|
203
250
|
if (node.object.type === 'Identifier' && !node.object.tracked) {
|
|
204
251
|
const binding = context.state.scope.get(node.object.name);
|
|
205
252
|
|
|
@@ -372,7 +419,13 @@ const visitors = {
|
|
|
372
419
|
const elements = [];
|
|
373
420
|
|
|
374
421
|
// Track metadata for this component
|
|
375
|
-
const metadata = {
|
|
422
|
+
const metadata = {
|
|
423
|
+
await: false,
|
|
424
|
+
styleClasses: /** @type {StyleClasses} */ (new Map()),
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
/** @type {TopScopedClasses} */
|
|
428
|
+
const topScopedClasses = new Map();
|
|
376
429
|
|
|
377
430
|
context.next({
|
|
378
431
|
...context.state,
|
|
@@ -388,7 +441,27 @@ const visitors = {
|
|
|
388
441
|
analyze_css(css);
|
|
389
442
|
|
|
390
443
|
for (const node of elements) {
|
|
391
|
-
prune_css(css, node);
|
|
444
|
+
prune_css(css, node, metadata.styleClasses, topScopedClasses);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (topScopedClasses.size > 0) {
|
|
448
|
+
node.metadata.topScopedClasses = topScopedClasses;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (metadata.styleClasses.size > 0) {
|
|
452
|
+
node.metadata.styleClasses = metadata.styleClasses;
|
|
453
|
+
|
|
454
|
+
if (!context.state.loose) {
|
|
455
|
+
for (const [className, property] of metadata.styleClasses) {
|
|
456
|
+
if (!topScopedClasses?.has(className)) {
|
|
457
|
+
return error(
|
|
458
|
+
`CSS class ".${className}" does not exist in ${node.id?.name ? node.id.name : "this component's"} <style> block`,
|
|
459
|
+
context.state.analysis.module.filename,
|
|
460
|
+
property,
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
392
465
|
}
|
|
393
466
|
}
|
|
394
467
|
|
|
@@ -677,13 +750,13 @@ const visitors = {
|
|
|
677
750
|
|
|
678
751
|
// Store capitalized name for dynamic components/elements
|
|
679
752
|
if (node.id.tracked) {
|
|
680
|
-
const
|
|
681
|
-
const capitalized_name =
|
|
753
|
+
const source_name = node.id.name;
|
|
754
|
+
const capitalized_name = source_name.charAt(0).toUpperCase() + source_name.slice(1);
|
|
682
755
|
node.metadata.ts_name = capitalized_name;
|
|
683
|
-
node.metadata.
|
|
756
|
+
node.metadata.source_name = source_name;
|
|
684
757
|
|
|
685
758
|
// Mark the binding as a dynamic component so we can capitalize it everywhere
|
|
686
|
-
const binding = context.state.scope.get(
|
|
759
|
+
const binding = context.state.scope.get(source_name);
|
|
687
760
|
if (binding) {
|
|
688
761
|
if (!binding.metadata) {
|
|
689
762
|
binding.metadata = {};
|
|
@@ -889,13 +962,16 @@ export function analyze(ast, filename, options = {}) {
|
|
|
889
962
|
|
|
890
963
|
const { scope, scopes } = create_scopes(ast, scope_root, null);
|
|
891
964
|
|
|
892
|
-
const analysis = {
|
|
965
|
+
const analysis = /** @type {AnalysisResult} */ ({
|
|
893
966
|
module: { ast, scope, scopes, filename },
|
|
894
967
|
ast,
|
|
895
968
|
scope,
|
|
896
969
|
scopes,
|
|
897
970
|
component_metadata: [],
|
|
898
|
-
|
|
971
|
+
metadata: {
|
|
972
|
+
serverIdentifierPresent: false,
|
|
973
|
+
},
|
|
974
|
+
});
|
|
899
975
|
|
|
900
976
|
walk(
|
|
901
977
|
ast,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** @import * as AST from 'estree' */
|
|
2
|
-
/** @import { Visitors } from '#compiler' */
|
|
2
|
+
/** @import { Visitors, TopScopedClasses, StyleClasses } from '#compiler' */
|
|
3
3
|
/** @typedef {0 | 1} Direction */
|
|
4
4
|
|
|
5
5
|
import { walk } from 'zimmerframe';
|
|
@@ -15,6 +15,10 @@ const BACKWARD = 1;
|
|
|
15
15
|
// since the code is synchronous, this is safe
|
|
16
16
|
/** @type {string} */
|
|
17
17
|
let css_hash;
|
|
18
|
+
/** @type {StyleClasses} */
|
|
19
|
+
let style_identifier_classes;
|
|
20
|
+
/** @type {TopScopedClasses} */
|
|
21
|
+
let top_scoped_classes;
|
|
18
22
|
|
|
19
23
|
// CSS selector constants
|
|
20
24
|
/**
|
|
@@ -27,6 +31,15 @@ function create_descendant_combinator(start, end) {
|
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
/**
|
|
34
|
+
* @param {AST.CSS.RelativeSelector} relative_selector
|
|
35
|
+
* @param {AST.CSS.ClassSelector} selector
|
|
36
|
+
* @returns {boolean}
|
|
37
|
+
*/
|
|
38
|
+
function is_standalone_class_selector(relative_selector, selector) {
|
|
39
|
+
return relative_selector.selectors.length === 1 && relative_selector.selectors[0] === selector;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**`
|
|
30
43
|
* @param {number} start
|
|
31
44
|
* @param {number} end
|
|
32
45
|
* @returns {AST.CSS.RelativeSelector}
|
|
@@ -211,7 +224,6 @@ function apply_selector(relative_selectors, rule, element, direction) {
|
|
|
211
224
|
if (!element.metadata.css) {
|
|
212
225
|
element.metadata.css = {
|
|
213
226
|
scopedClasses: new Map(),
|
|
214
|
-
topScopedClasses: new Map(),
|
|
215
227
|
hash: css_hash,
|
|
216
228
|
};
|
|
217
229
|
}
|
|
@@ -225,14 +237,12 @@ function apply_selector(relative_selectors, rule, element, direction) {
|
|
|
225
237
|
});
|
|
226
238
|
}
|
|
227
239
|
|
|
228
|
-
// Also store in
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
if (isStandalone && !element.metadata.css.topScopedClasses.has(name)) {
|
|
235
|
-
element.metadata.css.topScopedClasses.set(name, {
|
|
240
|
+
// Also store in top_scoped_classes if standalone selector
|
|
241
|
+
if (
|
|
242
|
+
is_standalone_class_selector(relative_selector, selector) &&
|
|
243
|
+
!top_scoped_classes.has(name)
|
|
244
|
+
) {
|
|
245
|
+
top_scoped_classes.set(name, {
|
|
236
246
|
start: selector.start,
|
|
237
247
|
end: selector.end,
|
|
238
248
|
selector: selector,
|
|
@@ -939,7 +949,11 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
|
|
|
939
949
|
}
|
|
940
950
|
|
|
941
951
|
case 'ClassSelector': {
|
|
942
|
-
if (
|
|
952
|
+
if (
|
|
953
|
+
!attribute_matches(element, 'class', name, '~=', false) &&
|
|
954
|
+
(!style_identifier_classes.has(name) ||
|
|
955
|
+
!is_standalone_class_selector(relative_selector, selector))
|
|
956
|
+
) {
|
|
943
957
|
return false;
|
|
944
958
|
}
|
|
945
959
|
|
|
@@ -1047,10 +1061,14 @@ function rule_has_animation(rule) {
|
|
|
1047
1061
|
/**
|
|
1048
1062
|
* @param {AST.CSS.StyleSheet} css
|
|
1049
1063
|
* @param {AST.Element} element
|
|
1064
|
+
* @param {StyleClasses} styleClasses
|
|
1065
|
+
* @param {TopScopedClasses} topScopedClasses
|
|
1050
1066
|
* @return {void}
|
|
1051
1067
|
*/
|
|
1052
|
-
export function prune_css(css, element) {
|
|
1068
|
+
export function prune_css(css, element, styleClasses, topScopedClasses) {
|
|
1053
1069
|
css_hash = css.hash;
|
|
1070
|
+
style_identifier_classes = styleClasses;
|
|
1071
|
+
top_scoped_classes = topScopedClasses;
|
|
1054
1072
|
|
|
1055
1073
|
/** @type {Visitors<AST.CSS.Node, null>} */
|
|
1056
1074
|
const visitors = {
|
|
@@ -151,6 +151,10 @@ export function validate_nesting(element, context) {
|
|
|
151
151
|
context.state.analysis.module.filename,
|
|
152
152
|
element,
|
|
153
153
|
);
|
|
154
|
+
} else {
|
|
155
|
+
// if my parent has a set of invalid children
|
|
156
|
+
// and i'm not in it, then i'm valid
|
|
157
|
+
return;
|
|
154
158
|
}
|
|
155
159
|
}
|
|
156
160
|
}
|