ripple 0.2.189 → 0.2.191
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 +4 -4
- package/src/compiler/identifier-utils.js +75 -0
- package/src/compiler/phases/1-parse/index.js +64 -16
- package/src/compiler/phases/2-analyze/index.js +84 -8
- package/src/compiler/phases/2-analyze/prune.js +30 -12
- package/src/compiler/phases/3-transform/client/index.js +166 -33
- package/src/compiler/phases/3-transform/segments.js +51 -19
- package/src/compiler/phases/3-transform/server/index.js +93 -20
- package/src/compiler/types/index.d.ts +46 -23
- package/src/compiler/types/parse.d.ts +5 -0
- package/src/compiler/utils.js +4 -4
- package/src/utils/builders.js +3 -2
- package/tests/client/compiler/compiler.basic.test.ripple +11 -11
- package/types/index.d.ts +2 -2
- package/src/compiler/import-utils.js +0 -51
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.191",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"module": "src/runtime/index-client.js",
|
|
9
9
|
"main": "src/runtime/index-client.js",
|
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
"./compiler/internal/import": {
|
|
42
42
|
"types": "./src/compiler/types/import.d.ts"
|
|
43
43
|
},
|
|
44
|
-
"./compiler/internal/
|
|
45
|
-
"default": "./src/compiler/
|
|
44
|
+
"./compiler/internal/identifier/utils": {
|
|
45
|
+
"default": "./src/compiler/identifier-utils.js"
|
|
46
46
|
},
|
|
47
47
|
"./validator": {
|
|
48
48
|
"types": "./types/index.d.ts",
|
|
@@ -92,6 +92,6 @@
|
|
|
92
92
|
"vscode-languageserver-types": "^3.17.5"
|
|
93
93
|
},
|
|
94
94
|
"peerDependencies": {
|
|
95
|
-
"ripple": "0.2.
|
|
95
|
+
"ripple": "0.2.191"
|
|
96
96
|
}
|
|
97
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
|
+
}
|
|
@@ -386,8 +386,27 @@ function RipplePlugin(config) {
|
|
|
386
386
|
}
|
|
387
387
|
}
|
|
388
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
|
+
|
|
389
408
|
// Check if this is an invalid #Identifier pattern
|
|
390
|
-
// Valid patterns: #[, #{, #Map(, #Map<, #Set(, #Set<, #server
|
|
409
|
+
// Valid patterns: #[, #{, #Map(, #Map<, #Set(, #Set<, #server, #style
|
|
391
410
|
// If we see # followed by an uppercase letter that isn't Map or Set, it's an error
|
|
392
411
|
// In loose mode, allow incomplete identifiers like #M, #Ma, #S, #Se for autocomplete
|
|
393
412
|
if (nextChar >= 65 && nextChar <= 90) {
|
|
@@ -466,20 +485,43 @@ function RipplePlugin(config) {
|
|
|
466
485
|
// In JSX expressions, inside parentheses, assignments, etc.
|
|
467
486
|
// we want to treat @ as an identifier prefix rather than decorator
|
|
468
487
|
const currentType = this.type;
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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)) {
|
|
483
525
|
return this.readAtIdentifier();
|
|
484
526
|
}
|
|
485
527
|
}
|
|
@@ -635,6 +677,12 @@ function RipplePlugin(config) {
|
|
|
635
677
|
return /** @type {AST.ServerIdentifier} */ (this.finishNode(node, 'ServerIdentifier'));
|
|
636
678
|
}
|
|
637
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
|
+
|
|
638
686
|
// Check if this is #Map( or #Set(
|
|
639
687
|
if (this.type === tt.name && (this.value === '#Map' || this.value === '#Set')) {
|
|
640
688
|
const type = this.value === '#Map' ? 'TrackedMapExpression' : 'TrackedSetExpression';
|
|
@@ -721,7 +769,7 @@ function RipplePlugin(config) {
|
|
|
721
769
|
const node = /** @type {AST.ServerBlock} */ (this.startNode());
|
|
722
770
|
this.next();
|
|
723
771
|
|
|
724
|
-
const body = /** @type {AST.
|
|
772
|
+
const body = /** @type {AST.ServerBlockStatement} */ (this.startNode());
|
|
725
773
|
node.body = body;
|
|
726
774
|
body.body = [];
|
|
727
775
|
|
|
@@ -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 = {
|