ripple 0.2.188 → 0.2.189
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/import-utils.js +51 -0
- package/src/compiler/index.d.ts +11 -3
- package/src/compiler/index.js +2 -2
- package/src/compiler/phases/1-parse/index.js +58 -11
- package/src/compiler/phases/2-analyze/validation.js +4 -0
- package/src/compiler/phases/3-transform/client/index.js +77 -51
- package/src/compiler/phases/3-transform/segments.js +80 -94
- package/src/compiler/types/import.d.ts +52 -0
- package/src/compiler/types/index.d.ts +3 -2
- package/src/compiler/types/parse.d.ts +5 -4
- 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 +34 -40
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.189",
|
|
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/import/utils": {
|
|
45
|
+
"default": "./src/compiler/import-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.189"
|
|
90
96
|
}
|
|
91
97
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export const IMPORT_OBFUSCATION_PREFIX = '_$_';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param {string} name
|
|
5
|
+
* @returns {string}
|
|
6
|
+
*/
|
|
7
|
+
export function obfuscate_imported(name) {
|
|
8
|
+
let start = 0;
|
|
9
|
+
if (name[0] === name[0].toUpperCase()) {
|
|
10
|
+
start = 1;
|
|
11
|
+
}
|
|
12
|
+
const index = find_next_uppercase(name, start);
|
|
13
|
+
|
|
14
|
+
const first_part = name.slice(0, index);
|
|
15
|
+
const second_part = name.slice(index);
|
|
16
|
+
|
|
17
|
+
return IMPORT_OBFUSCATION_PREFIX + (second_part ? second_part + '__' + first_part : first_part);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {string} name
|
|
22
|
+
* @returns {boolean}
|
|
23
|
+
*/
|
|
24
|
+
export function is_imported_obfuscated(name) {
|
|
25
|
+
return name.startsWith(IMPORT_OBFUSCATION_PREFIX);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {string} name
|
|
30
|
+
* @returns {string}
|
|
31
|
+
*/
|
|
32
|
+
export function deobfuscate_imported(name) {
|
|
33
|
+
name = name.replace(IMPORT_OBFUSCATION_PREFIX, '');
|
|
34
|
+
const parts = name.split('__');
|
|
35
|
+
return (parts[1] ? parts[1] : '') + parts[0];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Finds the next uppercase character or returns name.length
|
|
40
|
+
* @param {string} name
|
|
41
|
+
* @param {number} start
|
|
42
|
+
* @returns {number}
|
|
43
|
+
*/
|
|
44
|
+
function find_next_uppercase(name, start) {
|
|
45
|
+
for (let i = start; i < name.length; i++) {
|
|
46
|
+
if (name[i] === name[i].toUpperCase()) {
|
|
47
|
+
return i;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return name.length;
|
|
51
|
+
}
|
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
|
}
|
|
@@ -379,8 +387,9 @@ function RipplePlugin(config) {
|
|
|
379
387
|
}
|
|
380
388
|
|
|
381
389
|
// Check if this is an invalid #Identifier pattern
|
|
382
|
-
// Valid patterns: #[, #{, #Map(, #Set(, #server
|
|
390
|
+
// Valid patterns: #[, #{, #Map(, #Map<, #Set(, #Set<, #server
|
|
383
391
|
// If we see # followed by an uppercase letter that isn't Map or Set, it's an error
|
|
392
|
+
// In loose mode, allow incomplete identifiers like #M, #Ma, #S, #Se for autocomplete
|
|
384
393
|
if (nextChar >= 65 && nextChar <= 90) {
|
|
385
394
|
// A-Z
|
|
386
395
|
// Extract the identifier name
|
|
@@ -401,12 +410,34 @@ function RipplePlugin(config) {
|
|
|
401
410
|
}
|
|
402
411
|
const identName = this.input.slice(this.pos + 1, identEnd);
|
|
403
412
|
if (identName !== 'Map' && identName !== 'Set') {
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
);
|
|
413
|
+
// In loose mode, allow incomplete identifiers (prefixes of Map/Set)
|
|
414
|
+
// This supports autocomplete scenarios where user is still typing
|
|
415
|
+
const isIncompleteMap = 'Map'.startsWith(identName);
|
|
416
|
+
const isIncompleteSet = 'Set'.startsWith(identName);
|
|
417
|
+
|
|
418
|
+
if (!this.#loose || (!isIncompleteMap && !isIncompleteSet)) {
|
|
419
|
+
this.raise(
|
|
420
|
+
this.pos,
|
|
421
|
+
`Invalid tracked syntax '#${identName}'. Only #Map and #Set are currently supported using shorthand tracked syntax.`,
|
|
422
|
+
);
|
|
423
|
+
} else {
|
|
424
|
+
// In loose mode with valid prefix, consume the token and return it
|
|
425
|
+
// This allows the parser to handle incomplete syntax gracefully
|
|
426
|
+
this.pos = identEnd; // consume '#' + identifier
|
|
427
|
+
return this.finishToken(tt.name, '#' + identName);
|
|
428
|
+
}
|
|
408
429
|
}
|
|
409
430
|
}
|
|
431
|
+
|
|
432
|
+
// In loose mode, handle bare # or # followed by unrecognized characters
|
|
433
|
+
if (this.#loose) {
|
|
434
|
+
this.pos++; // consume '#'
|
|
435
|
+
return this.finishToken(tt.name, '#');
|
|
436
|
+
}
|
|
437
|
+
} else if (this.#loose) {
|
|
438
|
+
// In loose mode, handle bare # at EOF
|
|
439
|
+
this.pos++; // consume '#'
|
|
440
|
+
return this.finishToken(tt.name, '#');
|
|
410
441
|
}
|
|
411
442
|
}
|
|
412
443
|
if (code === 64) {
|
|
@@ -610,6 +641,20 @@ function RipplePlugin(config) {
|
|
|
610
641
|
return this.parseTrackedCollectionExpression(type);
|
|
611
642
|
}
|
|
612
643
|
|
|
644
|
+
// In loose mode, handle incomplete #Map/#Set prefixes (e.g., #M, #Ma, #S, #Se)
|
|
645
|
+
if (
|
|
646
|
+
this.#loose &&
|
|
647
|
+
this.type === tt.name &&
|
|
648
|
+
typeof this.value === 'string' &&
|
|
649
|
+
this.value.startsWith('#')
|
|
650
|
+
) {
|
|
651
|
+
// Return an Identifier node for incomplete tracked syntax
|
|
652
|
+
const node = /** @type {AST.Identifier} */ (this.startNode());
|
|
653
|
+
node.name = this.value;
|
|
654
|
+
this.next();
|
|
655
|
+
return this.finishNode(node, 'Identifier');
|
|
656
|
+
}
|
|
657
|
+
|
|
613
658
|
// Check if this is a tuple literal starting with #[
|
|
614
659
|
if (this.type === tt.bracketL && this.value === '#[') {
|
|
615
660
|
return this.parseTrackedArrayExpression();
|
|
@@ -728,8 +773,10 @@ function RipplePlugin(config) {
|
|
|
728
773
|
);
|
|
729
774
|
}
|
|
730
775
|
|
|
731
|
-
|
|
732
|
-
|
|
776
|
+
// Don't consume parens or generics - they belong to NewExpression
|
|
777
|
+
// When used as "new #Map(...)" the next token is '('
|
|
778
|
+
// When used as "new #Map<K,V>(...)" the next token is '<' (relational)
|
|
779
|
+
if (this.type === tt.parenL || (this.type === tt.relational && this.value === '<')) {
|
|
733
780
|
node.arguments = [];
|
|
734
781
|
return this.finishNode(node, type);
|
|
735
782
|
}
|
|
@@ -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
|
}
|
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
determine_namespace_for_children,
|
|
53
53
|
index_to_key,
|
|
54
54
|
} from '../../../utils.js';
|
|
55
|
+
import { obfuscate_imported } from '../../../import-utils.js';
|
|
55
56
|
import is_reference from 'is-reference';
|
|
56
57
|
import { object } from '../../../../utils/ast.js';
|
|
57
58
|
import { render_stylesheets } from '../stylesheet.js';
|
|
@@ -314,14 +315,13 @@ function visit_title_element(node, context) {
|
|
|
314
315
|
* @param {TransformClientContext} context
|
|
315
316
|
* @returns {string}
|
|
316
317
|
*/
|
|
317
|
-
function
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
context.state.imports.add(`import { ${name} } from 'ripple'`);
|
|
318
|
+
function set_hidden_import_from_ripple(name, context) {
|
|
319
|
+
name = obfuscate_imported(name);
|
|
320
|
+
if (!context.state.imports.has(`import { ${name} } from 'ripple/compiler/internal/import'`)) {
|
|
321
|
+
context.state.imports.add(`import { ${name} } from 'ripple/compiler/internal/import'`);
|
|
322
322
|
}
|
|
323
323
|
|
|
324
|
-
return
|
|
324
|
+
return name;
|
|
325
325
|
}
|
|
326
326
|
|
|
327
327
|
/** @type {Visitors<AST.Node, TransformClientState>} */
|
|
@@ -531,25 +531,31 @@ const visitors = {
|
|
|
531
531
|
}
|
|
532
532
|
|
|
533
533
|
// Special handling for TrackedMapExpression and TrackedSetExpression
|
|
534
|
-
// When source is "new #Map(...)", the callee is TrackedMapExpression
|
|
535
|
-
// and the actual arguments are in NewExpression.arguments
|
|
534
|
+
// When source is "new #Map(...)" or "new #Map<K,V>(...)", the callee is TrackedMapExpression
|
|
535
|
+
// with empty arguments and the actual arguments are in NewExpression.arguments
|
|
536
536
|
if (callee.type === 'TrackedMapExpression' || callee.type === 'TrackedSetExpression') {
|
|
537
537
|
// Use NewExpression's arguments (the callee has empty arguments from parser)
|
|
538
538
|
const argsToUse = node.arguments.length > 0 ? node.arguments : callee.arguments;
|
|
539
539
|
|
|
540
540
|
if (context.state.to_ts) {
|
|
541
541
|
const className = callee.type === 'TrackedMapExpression' ? 'TrackedMap' : 'TrackedSet';
|
|
542
|
-
const alias =
|
|
542
|
+
const alias = set_hidden_import_from_ripple(className, context);
|
|
543
543
|
const calleeId = b.id(alias);
|
|
544
544
|
calleeId.loc = callee.loc;
|
|
545
545
|
calleeId.metadata = {
|
|
546
546
|
tracked_shorthand: callee.type === 'TrackedMapExpression' ? '#Map' : '#Set',
|
|
547
547
|
path: [...context.path],
|
|
548
548
|
};
|
|
549
|
-
|
|
549
|
+
/** @type {AST.NewExpression} */
|
|
550
|
+
const newExpr = b.new(
|
|
550
551
|
calleeId,
|
|
551
552
|
.../** @type {AST.Expression[]} */ (argsToUse.map((arg) => context.visit(arg))),
|
|
552
553
|
);
|
|
554
|
+
// Preserve typeArguments for generics syntax like new #Map<string, number>()
|
|
555
|
+
if (node.typeArguments) {
|
|
556
|
+
newExpr.typeArguments = node.typeArguments;
|
|
557
|
+
}
|
|
558
|
+
return newExpr;
|
|
553
559
|
}
|
|
554
560
|
|
|
555
561
|
const helperName = callee.type === 'TrackedMapExpression' ? 'tracked_map' : 'tracked_set';
|
|
@@ -590,7 +596,7 @@ const visitors = {
|
|
|
590
596
|
|
|
591
597
|
TrackedArrayExpression(node, context) {
|
|
592
598
|
if (context.state.to_ts) {
|
|
593
|
-
const arrayAlias =
|
|
599
|
+
const arrayAlias = set_hidden_import_from_ripple('TrackedArray', context);
|
|
594
600
|
|
|
595
601
|
return b.call(
|
|
596
602
|
b.member(b.id(arrayAlias), b.id('from')),
|
|
@@ -615,7 +621,7 @@ const visitors = {
|
|
|
615
621
|
|
|
616
622
|
TrackedObjectExpression(node, context) {
|
|
617
623
|
if (context.state.to_ts) {
|
|
618
|
-
const objectAlias =
|
|
624
|
+
const objectAlias = set_hidden_import_from_ripple('TrackedObject', context);
|
|
619
625
|
|
|
620
626
|
return b.new(
|
|
621
627
|
b.id(objectAlias),
|
|
@@ -1282,7 +1288,7 @@ const visitors = {
|
|
|
1282
1288
|
b.stmt(b.call('_$_.set_class', id, key, hash_arg, b.literal(is_html))),
|
|
1283
1289
|
expression,
|
|
1284
1290
|
identity: attr_value,
|
|
1285
|
-
initial: b.
|
|
1291
|
+
initial: b.call(b.id('Symbol')),
|
|
1286
1292
|
});
|
|
1287
1293
|
} else {
|
|
1288
1294
|
state.init?.push(
|
|
@@ -2254,7 +2260,7 @@ function transform_ts_child(node, context) {
|
|
|
2254
2260
|
const argument = visit(attr.argument, { ...state, metadata });
|
|
2255
2261
|
return b.jsx_spread_attribute(/** @type {AST.Expression} */ (argument));
|
|
2256
2262
|
} else if (attr.type === 'RefAttribute') {
|
|
2257
|
-
const createRefKeyAlias =
|
|
2263
|
+
const createRefKeyAlias = set_hidden_import_from_ripple('createRefKey', context);
|
|
2258
2264
|
const metadata = { await: false };
|
|
2259
2265
|
const argument = visit(attr.argument, { ...state, metadata });
|
|
2260
2266
|
const wrapper = b.object([
|
|
@@ -3148,8 +3154,18 @@ function create_tsx_with_typescript_support() {
|
|
|
3148
3154
|
handle_function(node, context);
|
|
3149
3155
|
},
|
|
3150
3156
|
ImportDeclaration(node, context) {
|
|
3151
|
-
|
|
3152
|
-
|
|
3157
|
+
const loc = /** @type {AST.SourceLocation} */ (node.loc);
|
|
3158
|
+
// Write 'import' keyword with source location
|
|
3159
|
+
// to mark the beginning of the import statement for a full import mapping
|
|
3160
|
+
// The semicolon at the end with location will mark the end of the import statement
|
|
3161
|
+
context.location(loc.start.line, loc.start.column);
|
|
3162
|
+
context.write('import');
|
|
3163
|
+
|
|
3164
|
+
// Handle 'import type' syntax (importKind on the declaration itself)
|
|
3165
|
+
if (node.importKind === 'type') {
|
|
3166
|
+
context.write(' type');
|
|
3167
|
+
}
|
|
3168
|
+
|
|
3153
3169
|
context.write(' ');
|
|
3154
3170
|
|
|
3155
3171
|
// Write specifiers - handle default, namespace, and named imports
|
|
@@ -3199,6 +3215,29 @@ function create_tsx_with_typescript_support() {
|
|
|
3199
3215
|
|
|
3200
3216
|
// Write source
|
|
3201
3217
|
context.visit(node.source);
|
|
3218
|
+
// Write semicolon at the end
|
|
3219
|
+
// and record its position to mark the end of the import statement
|
|
3220
|
+
// This should work regardless of whether the source has a semi or not
|
|
3221
|
+
context.location(loc.end.line, loc.end.column - 1);
|
|
3222
|
+
context.write(';');
|
|
3223
|
+
},
|
|
3224
|
+
ImportDefaultSpecifier(node, context) {
|
|
3225
|
+
context.visit(node.local);
|
|
3226
|
+
},
|
|
3227
|
+
ImportNamespaceSpecifier(node, context) {
|
|
3228
|
+
context.write('* as ');
|
|
3229
|
+
context.visit(node.local);
|
|
3230
|
+
},
|
|
3231
|
+
ImportSpecifier(node, context) {
|
|
3232
|
+
if (node.importKind === 'type') {
|
|
3233
|
+
context.write('type ');
|
|
3234
|
+
}
|
|
3235
|
+
context.visit(node.imported);
|
|
3236
|
+
// Only write 'as local' if imported !== local
|
|
3237
|
+
if (/** @type {AST.Identifier} */ (node.imported).name !== node.local.name) {
|
|
3238
|
+
context.write(' as ');
|
|
3239
|
+
context.visit(node.local);
|
|
3240
|
+
}
|
|
3202
3241
|
},
|
|
3203
3242
|
JSXOpeningElement(node, context) {
|
|
3204
3243
|
// Set location for '<'
|
|
@@ -3242,9 +3281,28 @@ function create_tsx_with_typescript_support() {
|
|
|
3242
3281
|
}
|
|
3243
3282
|
}
|
|
3244
3283
|
context.write('[');
|
|
3245
|
-
//
|
|
3284
|
+
// Handle TSTypeParameter inline - mapped types use 'in' not 'extends'
|
|
3246
3285
|
if (node.typeParameter) {
|
|
3247
|
-
|
|
3286
|
+
const tp = node.typeParameter;
|
|
3287
|
+
if (tp.loc) {
|
|
3288
|
+
context.location(tp.loc.start.line, tp.loc.start.column);
|
|
3289
|
+
}
|
|
3290
|
+
// Write the parameter name
|
|
3291
|
+
if (typeof tp.name === 'string') {
|
|
3292
|
+
context.write(tp.name);
|
|
3293
|
+
} else if (tp.name && tp.name.name) {
|
|
3294
|
+
context.write(tp.name.name);
|
|
3295
|
+
}
|
|
3296
|
+
// In mapped types, constraint uses 'in' instead of 'extends'
|
|
3297
|
+
if (tp.constraint) {
|
|
3298
|
+
context.write(' in ');
|
|
3299
|
+
context.visit(tp.constraint);
|
|
3300
|
+
}
|
|
3301
|
+
// Handle 'as' clause for key remapping (e.g., { [K in Keys as NewKey]: V })
|
|
3302
|
+
if (node.nameType) {
|
|
3303
|
+
context.write(' as ');
|
|
3304
|
+
context.visit(node.nameType);
|
|
3305
|
+
}
|
|
3248
3306
|
}
|
|
3249
3307
|
context.write(']');
|
|
3250
3308
|
if (node.optional) {
|
|
@@ -3255,11 +3313,9 @@ function create_tsx_with_typescript_support() {
|
|
|
3255
3313
|
}
|
|
3256
3314
|
}
|
|
3257
3315
|
context.write(': ');
|
|
3258
|
-
// Visit the value type
|
|
3316
|
+
// Visit the value type
|
|
3259
3317
|
if (node.typeAnnotation) {
|
|
3260
3318
|
context.visit(node.typeAnnotation);
|
|
3261
|
-
} else if (node.nameType) {
|
|
3262
|
-
context.visit(node.nameType);
|
|
3263
3319
|
}
|
|
3264
3320
|
context.write(' }');
|
|
3265
3321
|
},
|
|
@@ -3391,39 +3447,9 @@ function create_tsx_with_typescript_support() {
|
|
|
3391
3447
|
* @returns {{ ast: AST.Program, js: { code: string, map: SourceMapMappings, post_processing_changes?: PostProcessingChanges, line_offsets?: LineOffsets }, css: string }}
|
|
3392
3448
|
*/
|
|
3393
3449
|
export function transform_client(filename, source, analysis, to_ts, minify_css) {
|
|
3394
|
-
/**
|
|
3395
|
-
* User's named imports from 'ripple' so we can reuse them in TS output
|
|
3396
|
-
* when transforming shorthand syntax. E.g., if the user has already imported
|
|
3397
|
-
* TrackedArray, we want to reuse that import instead of importing it again
|
|
3398
|
-
* if we encounter `#[]`. It's a Map of export name to local name in case the
|
|
3399
|
-
* user renamed something when importing.
|
|
3400
|
-
* @type {Map<string, string>}
|
|
3401
|
-
*/
|
|
3402
|
-
const ripple_user_imports = new Map(); // exported -> local
|
|
3403
|
-
if (analysis && analysis.ast && Array.isArray(analysis.ast.body)) {
|
|
3404
|
-
for (const stmt of analysis.ast.body) {
|
|
3405
|
-
if (
|
|
3406
|
-
stmt &&
|
|
3407
|
-
stmt.type === 'ImportDeclaration' &&
|
|
3408
|
-
stmt.source &&
|
|
3409
|
-
stmt.source.value === 'ripple'
|
|
3410
|
-
) {
|
|
3411
|
-
for (const spec of stmt.specifiers || []) {
|
|
3412
|
-
if (spec.type === 'ImportSpecifier' && spec.imported && spec.local) {
|
|
3413
|
-
ripple_user_imports.set(
|
|
3414
|
-
/** @type {AST.Identifier} */ (spec.imported).name,
|
|
3415
|
-
spec.local.name,
|
|
3416
|
-
);
|
|
3417
|
-
}
|
|
3418
|
-
}
|
|
3419
|
-
}
|
|
3420
|
-
}
|
|
3421
|
-
}
|
|
3422
|
-
|
|
3423
3450
|
/** @type {TransformClientState} */
|
|
3424
3451
|
const state = {
|
|
3425
3452
|
imports: new Set(),
|
|
3426
|
-
ripple_user_imports,
|
|
3427
3453
|
events: new Set(),
|
|
3428
3454
|
template: null,
|
|
3429
3455
|
hoisted: [],
|
|
@@ -24,7 +24,6 @@
|
|
|
24
24
|
@typedef {{
|
|
25
25
|
source: string | null | undefined;
|
|
26
26
|
generated: string;
|
|
27
|
-
is_full_import_statement?: boolean;
|
|
28
27
|
loc: AST.SourceLocation;
|
|
29
28
|
end_loc?: AST.SourceLocation;
|
|
30
29
|
metadata?: PluginActionOverrides;
|
|
@@ -327,17 +326,6 @@ export function convert_source_map_to_mappings(
|
|
|
327
326
|
const src_line_offsets = build_line_offsets(source);
|
|
328
327
|
const gen_line_offsets = build_line_offsets(generated_code);
|
|
329
328
|
|
|
330
|
-
/**
|
|
331
|
-
* Convert generated line/column to byte offset using pre-computed line_offsets
|
|
332
|
-
* @param {number} line
|
|
333
|
-
* @param {number} column
|
|
334
|
-
* @returns {number}
|
|
335
|
-
*/
|
|
336
|
-
const gen_loc_to_offset = (line, column) => {
|
|
337
|
-
if (line === 1) return column;
|
|
338
|
-
return line_offsets[line - 1] + column;
|
|
339
|
-
};
|
|
340
|
-
|
|
341
329
|
const [src_to_gen_map] = build_src_to_gen_map(
|
|
342
330
|
source_map,
|
|
343
331
|
post_processing_changes,
|
|
@@ -382,8 +370,19 @@ export function convert_source_map_to_mappings(
|
|
|
382
370
|
loc: node.loc,
|
|
383
371
|
});
|
|
384
372
|
} else {
|
|
373
|
+
const token = /** @type {Token} */ ({
|
|
374
|
+
source: node.name,
|
|
375
|
+
generated: node.name,
|
|
376
|
+
loc: node.loc,
|
|
377
|
+
});
|
|
378
|
+
if (node.name === '#') {
|
|
379
|
+
// Suppress 'Invalid character' to allow typing out the shorthands
|
|
380
|
+
token.metadata = {
|
|
381
|
+
suppressedDiagnostics: [1127],
|
|
382
|
+
};
|
|
383
|
+
}
|
|
385
384
|
// No transformation - source and generated names are the same
|
|
386
|
-
tokens.push(
|
|
385
|
+
tokens.push(token);
|
|
387
386
|
}
|
|
388
387
|
}
|
|
389
388
|
return; // Leaf node, don't traverse further
|
|
@@ -409,15 +408,37 @@ export function convert_source_map_to_mappings(
|
|
|
409
408
|
} else if (node.type === 'ImportDeclaration') {
|
|
410
409
|
isImportDeclarationPresent = true;
|
|
411
410
|
|
|
412
|
-
// Add import
|
|
413
|
-
//
|
|
414
|
-
|
|
411
|
+
// Add 'import' keyword token to anchor statement-level diagnostics
|
|
412
|
+
// And the last character of the statement (semicolon or closing brace)
|
|
413
|
+
// (e.g., when ALL imports are unused, TS reports on the whole statement)
|
|
414
|
+
// We only map the 'import' and the last character
|
|
415
|
+
// to avoid overlapping with individual specifier mappings
|
|
416
|
+
// which would interfere when only SOME imports are unused.
|
|
417
|
+
if (node.loc) {
|
|
415
418
|
tokens.push({
|
|
416
|
-
source: '',
|
|
417
|
-
generated: '',
|
|
418
|
-
loc:
|
|
419
|
-
|
|
420
|
-
|
|
419
|
+
source: 'import',
|
|
420
|
+
generated: 'import',
|
|
421
|
+
loc: {
|
|
422
|
+
start: node.loc.start,
|
|
423
|
+
end: {
|
|
424
|
+
line: node.loc.start.line,
|
|
425
|
+
column: node.loc.start.column + 'import'.length,
|
|
426
|
+
},
|
|
427
|
+
},
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
tokens.push({
|
|
431
|
+
source:
|
|
432
|
+
source[loc_to_offset(node.loc.end.line, node.loc.end.column - 1, src_line_offsets)],
|
|
433
|
+
// we always add `;' in the generated import
|
|
434
|
+
generated: ';',
|
|
435
|
+
loc: {
|
|
436
|
+
start: {
|
|
437
|
+
line: node.loc.end.line,
|
|
438
|
+
column: node.loc.end.column - 1,
|
|
439
|
+
},
|
|
440
|
+
end: node.loc.end,
|
|
441
|
+
},
|
|
421
442
|
});
|
|
422
443
|
}
|
|
423
444
|
|
|
@@ -536,6 +557,7 @@ export function convert_source_map_to_mappings(
|
|
|
536
557
|
sourceOffsets: [sourceOffset],
|
|
537
558
|
generatedOffsets: [offset],
|
|
538
559
|
lengths: [length],
|
|
560
|
+
generatedLengths: [length],
|
|
539
561
|
data: {
|
|
540
562
|
...mapping_data,
|
|
541
563
|
customData: {
|
|
@@ -813,10 +835,12 @@ export function convert_source_map_to_mappings(
|
|
|
813
835
|
// '```ripple\npending\n```\n\nRipple-specific keyword for try/pending blocks.\n\nThe `pending` block executes while async operations inside the `try` block are awaiting. This provides a built-in loading state for async components.',
|
|
814
836
|
// },
|
|
815
837
|
|
|
816
|
-
//
|
|
838
|
+
// Example of a custom definition and its type definition file
|
|
817
839
|
// definition: {
|
|
818
|
-
//
|
|
819
|
-
//
|
|
840
|
+
// typeReplace: {
|
|
841
|
+
// name: 'SomeType',
|
|
842
|
+
// path: 'types/index.d.ts',
|
|
843
|
+
// },
|
|
820
844
|
// },
|
|
821
845
|
},
|
|
822
846
|
});
|
|
@@ -1623,90 +1647,50 @@ export function convert_source_map_to_mappings(
|
|
|
1623
1647
|
for (const token of tokens) {
|
|
1624
1648
|
const source_text = token.source ?? '';
|
|
1625
1649
|
const gen_text = token.generated;
|
|
1626
|
-
|
|
1627
1650
|
const source_start = loc_to_offset(
|
|
1628
1651
|
token.loc.start.line,
|
|
1629
1652
|
token.loc.start.column,
|
|
1630
1653
|
src_line_offsets,
|
|
1631
1654
|
);
|
|
1655
|
+
const source_length = source_text.length;
|
|
1656
|
+
const gen_length = gen_text.length;
|
|
1657
|
+
const gen_line_col = get_generated_position(
|
|
1658
|
+
token.loc.start.line,
|
|
1659
|
+
token.loc.start.column,
|
|
1660
|
+
src_to_gen_map,
|
|
1661
|
+
);
|
|
1662
|
+
const gen_start = loc_to_offset(gen_line_col.line, gen_line_col.column, gen_line_offsets);
|
|
1632
1663
|
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
/** @type {number} */
|
|
1638
|
-
let gen_start;
|
|
1639
|
-
|
|
1640
|
-
if (token.is_full_import_statement) {
|
|
1641
|
-
const end_loc = /** @type {AST.SourceLocation} */ (token.end_loc).end;
|
|
1642
|
-
const source_end = loc_to_offset(end_loc.line, end_loc.column, src_line_offsets);
|
|
1643
|
-
|
|
1644
|
-
// Look up where import keyword and source literal map to in generated code
|
|
1645
|
-
const gen_start_pos = get_generated_position(
|
|
1646
|
-
token.loc.start.line,
|
|
1647
|
-
token.loc.start.column,
|
|
1648
|
-
src_to_gen_map,
|
|
1649
|
-
);
|
|
1650
|
-
const gen_end_pos = get_generated_position(end_loc.line, end_loc.column, src_to_gen_map);
|
|
1651
|
-
|
|
1652
|
-
gen_start = gen_loc_to_offset(gen_start_pos.line, gen_start_pos.column);
|
|
1653
|
-
const gen_end = gen_loc_to_offset(gen_end_pos.line, gen_end_pos.column);
|
|
1654
|
-
|
|
1655
|
-
source_length = source_end - source_start;
|
|
1656
|
-
gen_length = gen_end - gen_start;
|
|
1657
|
-
|
|
1658
|
-
data = {
|
|
1659
|
-
// we only want verification here, like unused imports
|
|
1660
|
-
// since this is synthetic and otherwise we'll get duplicated actions like intellisense
|
|
1661
|
-
// each imported specifier has its own mapping
|
|
1662
|
-
verification: true,
|
|
1663
|
-
customData: {
|
|
1664
|
-
generatedLengths: [gen_length],
|
|
1665
|
-
},
|
|
1666
|
-
};
|
|
1667
|
-
} else {
|
|
1668
|
-
const gen_line_col = get_generated_position(
|
|
1669
|
-
token.loc.start.line,
|
|
1670
|
-
token.loc.start.column,
|
|
1671
|
-
src_to_gen_map,
|
|
1672
|
-
);
|
|
1673
|
-
gen_start = gen_loc_to_offset(gen_line_col.line, gen_line_col.column);
|
|
1674
|
-
|
|
1675
|
-
/** @type {CustomMappingData} */
|
|
1676
|
-
const customData = {
|
|
1677
|
-
generatedLengths: [gen_length],
|
|
1678
|
-
};
|
|
1679
|
-
|
|
1680
|
-
// Add optional metadata from token if present
|
|
1681
|
-
if (token.metadata) {
|
|
1682
|
-
if ('wordHighlight' in token.metadata) {
|
|
1683
|
-
customData.wordHighlight = token.metadata.wordHighlight;
|
|
1684
|
-
}
|
|
1664
|
+
/** @type {CustomMappingData} */
|
|
1665
|
+
const customData = {
|
|
1666
|
+
generatedLengths: [gen_length],
|
|
1667
|
+
};
|
|
1685
1668
|
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1669
|
+
// Add optional metadata from token if present
|
|
1670
|
+
if (token.metadata) {
|
|
1671
|
+
if ('wordHighlight' in token.metadata) {
|
|
1672
|
+
customData.wordHighlight = token.metadata.wordHighlight;
|
|
1673
|
+
}
|
|
1674
|
+
if ('suppressedDiagnostics' in token.metadata) {
|
|
1675
|
+
customData.suppressedDiagnostics = token.metadata.suppressedDiagnostics;
|
|
1676
|
+
}
|
|
1677
|
+
if ('hover' in token.metadata) {
|
|
1678
|
+
customData.hover = token.metadata.hover;
|
|
1679
|
+
}
|
|
1680
|
+
if ('definition' in token.metadata) {
|
|
1681
|
+
customData.definition = token.metadata.definition;
|
|
1695
1682
|
}
|
|
1696
|
-
|
|
1697
|
-
data = {
|
|
1698
|
-
...mapping_data,
|
|
1699
|
-
customData,
|
|
1700
|
-
};
|
|
1701
1683
|
}
|
|
1702
1684
|
|
|
1703
|
-
// !IMPORTANT: don't set generatedLengths, otherwise Volar will use that vs our source
|
|
1704
|
-
// We're adding it to our custom metadata instead as we need it for patching positions
|
|
1705
1685
|
mappings.push({
|
|
1706
1686
|
sourceOffsets: [source_start],
|
|
1707
1687
|
generatedOffsets: [gen_start],
|
|
1708
1688
|
lengths: [source_length],
|
|
1709
|
-
|
|
1689
|
+
generatedLengths: [gen_length],
|
|
1690
|
+
data: {
|
|
1691
|
+
...mapping_data,
|
|
1692
|
+
customData,
|
|
1693
|
+
},
|
|
1710
1694
|
});
|
|
1711
1695
|
}
|
|
1712
1696
|
|
|
@@ -1720,6 +1704,7 @@ export function convert_source_map_to_mappings(
|
|
|
1720
1704
|
sourceOffsets: [0],
|
|
1721
1705
|
generatedOffsets: [0],
|
|
1722
1706
|
lengths: [1],
|
|
1707
|
+
generatedLengths: [1],
|
|
1723
1708
|
data: {
|
|
1724
1709
|
...mapping_data,
|
|
1725
1710
|
customData: {
|
|
@@ -1737,6 +1722,7 @@ export function convert_source_map_to_mappings(
|
|
|
1737
1722
|
sourceOffsets: [region.start],
|
|
1738
1723
|
generatedOffsets: [0],
|
|
1739
1724
|
lengths: [region.content.length],
|
|
1725
|
+
generatedLengths: [region.content.length],
|
|
1740
1726
|
data: {
|
|
1741
1727
|
...mapping_data,
|
|
1742
1728
|
customData: {
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for Hidden Imports in TSX Generated Code
|
|
3
|
+
*
|
|
4
|
+
* Adds uniquely named exports to account for hidden imports
|
|
5
|
+
* in the tsx generated code for language server / IDE support.
|
|
6
|
+
* This is necessary because we need to keep hidden imports named differently
|
|
7
|
+
* for full TS support including adding missing imports in source and
|
|
8
|
+
* property reporting missing imports.
|
|
9
|
+
*
|
|
10
|
+
* The types are obfuscated to avoid name collisions and provide
|
|
11
|
+
* sufficiently different names so that TS cannot attempt to infer that
|
|
12
|
+
* the user made a mistake when the user is missing an import.
|
|
13
|
+
*
|
|
14
|
+
* e.g.
|
|
15
|
+
* // import { TrackedMap } from 'ripple'; -- assume TrackedMap import is missing
|
|
16
|
+
* const map = new TrackedMap();
|
|
17
|
+
*
|
|
18
|
+
* If a type in the hidden import contains 'TrackedMap', e.g. '__TrackedMap',
|
|
19
|
+
* TS would suggest to the user that they meant to use '__TrackedMap' instead of 'TrackedMap'.
|
|
20
|
+
*
|
|
21
|
+
* Add additional types as needed if they are used in hidden imports.
|
|
22
|
+
*
|
|
23
|
+
* This file is used by the package.json in exports
|
|
24
|
+
* The exports path is used by the TS compiler to resolve types.
|
|
25
|
+
*
|
|
26
|
+
* The intellisense is intercepted by hover language plugin
|
|
27
|
+
* to replace the obfuscated names with the actual types.
|
|
28
|
+
*
|
|
29
|
+
* Do not rename or move without updating those paths.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import {
|
|
33
|
+
TrackedMap as _$_Map__Tracked,
|
|
34
|
+
TrackedSet as _$_Set__Tracked,
|
|
35
|
+
TrackedArray as _$_Array__Tracked,
|
|
36
|
+
TrackedObject as _$_Object__Tracked,
|
|
37
|
+
TrackedURL as _$_URL__Tracked,
|
|
38
|
+
TrackedURLSearchParams as _$_URLSearchParams__Tracked,
|
|
39
|
+
TrackedDate as _$_Date__Tracked,
|
|
40
|
+
createRefKey as _$_RefKey__create,
|
|
41
|
+
} from 'ripple';
|
|
42
|
+
|
|
43
|
+
export {
|
|
44
|
+
_$_Map__Tracked,
|
|
45
|
+
_$_Set__Tracked,
|
|
46
|
+
_$_Array__Tracked,
|
|
47
|
+
_$_Object__Tracked,
|
|
48
|
+
_$_URL__Tracked,
|
|
49
|
+
_$_URLSearchParams__Tracked,
|
|
50
|
+
_$_Date__Tracked,
|
|
51
|
+
_$_RefKey__create,
|
|
52
|
+
};
|
|
@@ -149,7 +149,9 @@ declare module 'estree' {
|
|
|
149
149
|
interface ImportSpecifier {
|
|
150
150
|
importKind: TSESTree.ImportSpecifier['importKind'];
|
|
151
151
|
}
|
|
152
|
-
interface ExportNamedDeclaration
|
|
152
|
+
interface ExportNamedDeclaration
|
|
153
|
+
// doesn't seem we're using parent and assertions, removing to avoid builders errors
|
|
154
|
+
extends Omit<TSESTree.ExportNamedDeclaration, 'exportKind' | 'parent' | 'assertions'> {
|
|
153
155
|
exportKind: TSESTree.ExportNamedDeclaration['exportKind'];
|
|
154
156
|
}
|
|
155
157
|
|
|
@@ -1161,7 +1163,6 @@ export interface TransformClientState extends BaseState {
|
|
|
1161
1163
|
init: Array<AST.Statement> | null;
|
|
1162
1164
|
metadata: BaseStateMetaData;
|
|
1163
1165
|
namespace: NameSpace;
|
|
1164
|
-
ripple_user_imports: Map<string, string>;
|
|
1165
1166
|
setup: Array<AST.Statement> | null;
|
|
1166
1167
|
stylesheets: Array<AST.CSS.StyleSheet>;
|
|
1167
1168
|
template: Array<string | AST.Expression> | null;
|
|
@@ -60,7 +60,7 @@ declare module 'zimmerframe' {
|
|
|
60
60
|
export function walk(
|
|
61
61
|
node: AST.CSS.Node,
|
|
62
62
|
state: any,
|
|
63
|
-
visitors: RippleCompiler.Visitors<
|
|
63
|
+
visitors: RippleCompiler.Visitors<AST.CSS.Node, any>,
|
|
64
64
|
): AST.CSS.Node;
|
|
65
65
|
}
|
|
66
66
|
|
|
@@ -611,12 +611,12 @@ export namespace Parse {
|
|
|
611
611
|
* @param type The node type string (e.g., "Identifier", "BinaryExpression")
|
|
612
612
|
* @returns The finished node
|
|
613
613
|
*/
|
|
614
|
-
finishNode<T extends AST.Node>(node: T, type:
|
|
614
|
+
finishNode<T extends AST.Node>(node: T, type: T['type']): T;
|
|
615
615
|
|
|
616
616
|
/**
|
|
617
617
|
* Finish a node at a specific position
|
|
618
618
|
*/
|
|
619
|
-
finishNodeAt<T extends AST.Node>(node: T, type:
|
|
619
|
+
finishNodeAt<T extends AST.Node>(node: T, type: T['type'], pos: number, loc: AST.Position): T;
|
|
620
620
|
|
|
621
621
|
/**
|
|
622
622
|
* Start a new node at current position
|
|
@@ -925,7 +925,8 @@ export namespace Parse {
|
|
|
925
925
|
| AST.TrackedSetExpression
|
|
926
926
|
| AST.TrackedArrayExpression
|
|
927
927
|
| AST.TrackedObjectExpression
|
|
928
|
-
| AST.Component
|
|
928
|
+
| AST.Component
|
|
929
|
+
| AST.Identifier;
|
|
929
930
|
|
|
930
931
|
/** Default handler for parseExprAtom when no other case matches */
|
|
931
932
|
parseExprAtomDefault(): AST.Expression;
|
|
@@ -64,7 +64,7 @@ describe('basic client > attribute rendering', () => {
|
|
|
64
64
|
[
|
|
65
65
|
'ccc',
|
|
66
66
|
'ddd',
|
|
67
|
-
{eee: true, fff: false},
|
|
67
|
+
{ eee: true, fff: false },
|
|
68
68
|
],
|
|
69
69
|
]}
|
|
70
70
|
>
|
|
@@ -107,7 +107,7 @@ describe('basic client > attribute rendering', () => {
|
|
|
107
107
|
>
|
|
108
108
|
{'Toggle'}
|
|
109
109
|
</button>
|
|
110
|
-
<div class={{active: @active, inactive: !@active}}>{'Dynamic Class'}</div>
|
|
110
|
+
<div class={{ active: @active, inactive: !@active }}>{'Dynamic Class'}</div>
|
|
111
111
|
|
|
112
112
|
<style>
|
|
113
113
|
.active {
|
|
@@ -139,6 +139,35 @@ describe('basic client > attribute rendering', () => {
|
|
|
139
139
|
expect(div.classList.contains('active')).toBe(false);
|
|
140
140
|
});
|
|
141
141
|
|
|
142
|
+
it('applies scoped ripple classs to multiple elements with dynamic class expressions', () => {
|
|
143
|
+
component Basic() {
|
|
144
|
+
let selected = track(1);
|
|
145
|
+
|
|
146
|
+
<div class={@selected === 0 ? 'selected' : ''}>{`div 1`}</div>
|
|
147
|
+
<div class={@selected === 0 ? 'selected' : ''}>{`div 2`}</div>
|
|
148
|
+
|
|
149
|
+
<style>
|
|
150
|
+
div {
|
|
151
|
+
background: green;
|
|
152
|
+
color: white;
|
|
153
|
+
}
|
|
154
|
+
div.selected {
|
|
155
|
+
background: indigo;
|
|
156
|
+
}
|
|
157
|
+
</style>
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
render(Basic);
|
|
161
|
+
|
|
162
|
+
const divs = container.querySelectorAll('div');
|
|
163
|
+
|
|
164
|
+
divs.forEach((div) => {
|
|
165
|
+
expect(Array.from(div.classList).some((className) => className.startsWith('ripple-'))).toBe(
|
|
166
|
+
true,
|
|
167
|
+
);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
142
171
|
it('render dynamic id attribute', () => {
|
|
143
172
|
component Basic() {
|
|
144
173
|
let count = track(0);
|
|
@@ -211,7 +240,7 @@ describe('basic client > attribute rendering', () => {
|
|
|
211
240
|
>
|
|
212
241
|
{'Change Color'}
|
|
213
242
|
</button>
|
|
214
|
-
<div style={{color: @color, fontWeight: 'bold'}}>{'Dynamic Style'}</div>
|
|
243
|
+
<div style={{ color: @color, fontWeight: 'bold' }}>{'Dynamic Style'}</div>
|
|
215
244
|
}
|
|
216
245
|
|
|
217
246
|
render(Basic);
|
|
@@ -265,7 +294,7 @@ describe('basic client > attribute rendering', () => {
|
|
|
265
294
|
}
|
|
266
295
|
|
|
267
296
|
<button onClick={toggleColor}>{'Change Color'}</button>
|
|
268
|
-
<div style={{...@style}}>{'Dynamic Style'}</div>
|
|
297
|
+
<div style={{ ...@style }}>{'Dynamic Style'}</div>
|
|
269
298
|
}
|
|
270
299
|
|
|
271
300
|
render(Basic);
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { parse, compile, compile_to_volar_mappings } from 'ripple/compiler';
|
|
2
|
+
import { obfuscate_imported } from 'ripple/compiler/internal/import/utils';
|
|
3
|
+
import type * as AST from 'estree';
|
|
2
4
|
|
|
3
|
-
function count_occurrences(string, subString) {
|
|
5
|
+
function count_occurrences(string: string, subString: string): number {
|
|
4
6
|
let count = 0;
|
|
5
7
|
let pos = string.indexOf(subString);
|
|
6
8
|
|
|
@@ -25,15 +27,21 @@ describe('compiler > basics', () => {
|
|
|
25
27
|
|
|
26
28
|
let input = source.replace('#style', style1);
|
|
27
29
|
let ast = parse(input);
|
|
28
|
-
expect(
|
|
30
|
+
expect(
|
|
31
|
+
((ast.body[0] as AST.ExportNamedDeclaration).declaration as AST.Component).css.source,
|
|
32
|
+
).toEqual(style1);
|
|
29
33
|
|
|
30
34
|
input = source.replace('#style', style2);
|
|
31
35
|
ast = parse(input);
|
|
32
|
-
expect(
|
|
36
|
+
expect(
|
|
37
|
+
((ast.body[0] as AST.ExportNamedDeclaration).declaration as AST.Component).css.source,
|
|
38
|
+
).toEqual(style2);
|
|
33
39
|
|
|
34
40
|
input = source.replace('#style', style3);
|
|
35
41
|
ast = parse(input);
|
|
36
|
-
expect(
|
|
42
|
+
expect(
|
|
43
|
+
((ast.body[0] as AST.ExportNamedDeclaration).declaration as AST.Component).css.source,
|
|
44
|
+
).toEqual(style3);
|
|
37
45
|
});
|
|
38
46
|
|
|
39
47
|
it('renders without crashing', () => {
|
|
@@ -113,7 +121,7 @@ describe('compiler > basics', () => {
|
|
|
113
121
|
},
|
|
114
122
|
};
|
|
115
123
|
|
|
116
|
-
const config3: { [key:
|
|
124
|
+
const config3: { [key: string]: RecordValue } = {
|
|
117
125
|
test: {
|
|
118
126
|
a: 'test3',
|
|
119
127
|
b: 3,
|
|
@@ -192,6 +200,10 @@ describe('compiler > basics', () => {
|
|
|
192
200
|
method<T>(): T {
|
|
193
201
|
return this.value;
|
|
194
202
|
}
|
|
203
|
+
|
|
204
|
+
constructor(value: T) {
|
|
205
|
+
this.value = value;
|
|
206
|
+
}
|
|
195
207
|
}
|
|
196
208
|
|
|
197
209
|
let flag = true;
|
|
@@ -219,8 +231,10 @@ describe('compiler > basics', () => {
|
|
|
219
231
|
const result = compile(source, 'test.ripple', { mode: 'client' });
|
|
220
232
|
});
|
|
221
233
|
|
|
222
|
-
it(
|
|
223
|
-
|
|
234
|
+
it(
|
|
235
|
+
'imports and uses only obfuscated Tracked imports when encountering only shorthand syntax',
|
|
236
|
+
() => {
|
|
237
|
+
const source = `
|
|
224
238
|
import {
|
|
225
239
|
TrackedArray,
|
|
226
240
|
TrackedObject,
|
|
@@ -238,17 +252,18 @@ component App() {
|
|
|
238
252
|
<div {ref () => {}} />
|
|
239
253
|
}
|
|
240
254
|
`;
|
|
241
|
-
|
|
255
|
+
const result = compile_to_volar_mappings(source, 'test.ripple').code;
|
|
242
256
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
257
|
+
expect(count_occurrences(result, 'TrackedArray')).toBe(1);
|
|
258
|
+
expect(count_occurrences(result, 'TrackedObject')).toBe(1);
|
|
259
|
+
expect(count_occurrences(result, 'TrackedSet')).toBe(1);
|
|
260
|
+
expect(count_occurrences(result, 'TrackedMap')).toBe(1);
|
|
261
|
+
expect(count_occurrences(result, 'createRefKey')).toBe(1);
|
|
262
|
+
},
|
|
263
|
+
);
|
|
249
264
|
|
|
250
265
|
it(
|
|
251
|
-
'
|
|
266
|
+
'adds obfuscated imports and keeps renamed Tracked imports intact when encountering shorthand syntax',
|
|
252
267
|
() => {
|
|
253
268
|
const source = `
|
|
254
269
|
import {
|
|
@@ -270,43 +285,21 @@ component App() {
|
|
|
270
285
|
`;
|
|
271
286
|
const result = compile_to_volar_mappings(source, 'test.ripple').code;
|
|
272
287
|
|
|
273
|
-
expect(count_occurrences(result, 'TrackedArray')).toBe(
|
|
274
|
-
expect(count_occurrences(result, 'TA')).toBe(
|
|
275
|
-
expect(count_occurrences(result, 'TrackedObject')).toBe(
|
|
276
|
-
expect(count_occurrences(result, 'TO')).toBe(
|
|
277
|
-
expect(count_occurrences(result, 'TrackedSet')).toBe(
|
|
278
|
-
expect(count_occurrences(result, 'TS')).toBe(
|
|
279
|
-
expect(count_occurrences(result, 'TrackedMap')).toBe(
|
|
280
|
-
expect(count_occurrences(result, 'TM')).toBe(
|
|
281
|
-
expect(count_occurrences(result, 'createRefKey')).toBe(
|
|
282
|
-
expect(count_occurrences(result, 'crk')).toBe(
|
|
288
|
+
expect(count_occurrences(result, obfuscate_imported('TrackedArray'))).toBe(2);
|
|
289
|
+
expect(count_occurrences(result, 'TA')).toBe(1);
|
|
290
|
+
expect(count_occurrences(result, obfuscate_imported('TrackedObject'))).toBe(2);
|
|
291
|
+
expect(count_occurrences(result, 'TO')).toBe(1);
|
|
292
|
+
expect(count_occurrences(result, obfuscate_imported('TrackedSet'))).toBe(2);
|
|
293
|
+
expect(count_occurrences(result, 'TS')).toBe(1);
|
|
294
|
+
expect(count_occurrences(result, obfuscate_imported('TrackedMap'))).toBe(2);
|
|
295
|
+
expect(count_occurrences(result, 'TM')).toBe(1);
|
|
296
|
+
expect(count_occurrences(result, obfuscate_imported('createRefKey'))).toBe(2);
|
|
297
|
+
expect(count_occurrences(result, 'crk')).toBe(1);
|
|
283
298
|
},
|
|
284
299
|
);
|
|
285
300
|
|
|
286
|
-
it('adds
|
|
287
|
-
const source = `
|
|
288
|
-
component App() {
|
|
289
|
-
const items = #[1, 2, 3];
|
|
290
|
-
const obj = #{ a: 1, b: 2, c: 3 };
|
|
291
|
-
const set = new #Set([1, 2, 3]);
|
|
292
|
-
const map = new #Map([['a', 1], ['b', 2], ['c', 3]]);
|
|
293
|
-
|
|
294
|
-
<div {ref () => {}} />
|
|
295
|
-
}
|
|
296
|
-
`;
|
|
297
|
-
const result = compile_to_volar_mappings(source, 'test.ripple').code;
|
|
298
|
-
|
|
299
|
-
expect(count_occurrences(result, 'TrackedArray')).toBe(2);
|
|
300
|
-
expect(count_occurrences(result, 'TrackedObject')).toBe(2);
|
|
301
|
-
expect(count_occurrences(result, 'TrackedSet')).toBe(2);
|
|
302
|
-
expect(count_occurrences(result, 'TrackedMap')).toBe(2);
|
|
303
|
-
expect(count_occurrences(result, 'createRefKey')).toBe(2);
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
it('only adds missing imports for shorthand syntax, reusing existing ones', () => {
|
|
301
|
+
it('adds hidden obfuscated imports for shorthand syntax', () => {
|
|
307
302
|
const source = `
|
|
308
|
-
import { TrackedArray, TrackedMap, createRefKey as crk } from 'ripple';
|
|
309
|
-
|
|
310
303
|
component App() {
|
|
311
304
|
const items = #[1, 2, 3];
|
|
312
305
|
const obj = #{ a: 1, b: 2, c: 3 };
|
|
@@ -318,12 +311,11 @@ component App() {
|
|
|
318
311
|
`;
|
|
319
312
|
const result = compile_to_volar_mappings(source, 'test.ripple').code;
|
|
320
313
|
|
|
321
|
-
expect(count_occurrences(result, 'TrackedArray')).toBe(2);
|
|
322
|
-
expect(count_occurrences(result, 'TrackedObject')).toBe(2);
|
|
323
|
-
expect(count_occurrences(result, 'TrackedSet')).toBe(2);
|
|
324
|
-
expect(count_occurrences(result, 'TrackedMap')).toBe(2);
|
|
325
|
-
expect(count_occurrences(result, 'createRefKey')).toBe(
|
|
326
|
-
expect(count_occurrences(result, 'crk')).toBe(2);
|
|
314
|
+
expect(count_occurrences(result, obfuscate_imported('TrackedArray'))).toBe(2);
|
|
315
|
+
expect(count_occurrences(result, obfuscate_imported('TrackedObject'))).toBe(2);
|
|
316
|
+
expect(count_occurrences(result, obfuscate_imported('TrackedSet'))).toBe(2);
|
|
317
|
+
expect(count_occurrences(result, obfuscate_imported('TrackedMap'))).toBe(2);
|
|
318
|
+
expect(count_occurrences(result, obfuscate_imported('createRefKey'))).toBe(2);
|
|
327
319
|
});
|
|
328
320
|
|
|
329
321
|
it('should not error on having js below markup in the same scope', () => {
|
package/tsconfig.json
CHANGED
|
@@ -1,32 +1,32 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"module": "esnext",
|
|
4
|
+
"lib": ["esnext", "dom", "dom.iterable"],
|
|
5
|
+
"target": "esnext",
|
|
6
|
+
"noEmit": true,
|
|
7
|
+
"moduleResolution": "bundler",
|
|
8
|
+
"resolveJsonModule": true,
|
|
9
|
+
"noEmitOnError": true,
|
|
10
|
+
"noErrorTruncation": true,
|
|
11
|
+
"allowSyntheticDefaultImports": true,
|
|
12
|
+
"verbatimModuleSyntax": true,
|
|
13
|
+
"types": ["node", "vitest/globals"],
|
|
14
|
+
"jsx": "preserve",
|
|
15
|
+
"jsxImportSource": "ripple",
|
|
16
|
+
"strict": true,
|
|
17
|
+
"allowJs": true,
|
|
18
|
+
"checkJs": true,
|
|
19
|
+
"paths": {
|
|
20
|
+
"ripple": ["./types/index.d.ts"],
|
|
21
21
|
"rollup": ["./shims/rollup-estree-types.d.ts"],
|
|
22
22
|
"rollup/*": ["./shims/rollup-estree-types.d.ts"]
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"include": [
|
|
26
|
+
"./*.js",
|
|
27
|
+
"./src/",
|
|
28
|
+
"./tests/**/*.test.ripple",
|
|
29
|
+
"./tests/**/*.d.ts",
|
|
30
|
+
"./tests/**/*.js"
|
|
31
|
+
]
|
|
32
32
|
}
|
package/types/index.d.ts
CHANGED
|
@@ -10,18 +10,18 @@ export type CompatOptions = {
|
|
|
10
10
|
[key: string]: CompatApi;
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
-
export
|
|
13
|
+
export function mount(
|
|
14
14
|
component: () => void,
|
|
15
15
|
options: { target: HTMLElement; props?: Record<string, any>; compat?: CompatOptions },
|
|
16
16
|
): () => void;
|
|
17
17
|
|
|
18
|
-
export
|
|
18
|
+
export function tick(): Promise<void>;
|
|
19
19
|
|
|
20
|
-
export
|
|
20
|
+
export function untrack<T>(fn: () => T): T;
|
|
21
21
|
|
|
22
|
-
export
|
|
22
|
+
export function flushSync<T>(fn?: () => T): T;
|
|
23
23
|
|
|
24
|
-
export
|
|
24
|
+
export function effect(fn: (() => void) | (() => () => void)): void;
|
|
25
25
|
|
|
26
26
|
export interface TrackedArrayConstructor {
|
|
27
27
|
new <T>(...elements: T[]): TrackedArray<T>; // must be used with `new`
|
|
@@ -32,16 +32,16 @@ export interface TrackedArrayConstructor {
|
|
|
32
32
|
|
|
33
33
|
export interface TrackedArray<T> extends Array<T> {}
|
|
34
34
|
|
|
35
|
-
export
|
|
35
|
+
export const TrackedArray: TrackedArrayConstructor;
|
|
36
36
|
|
|
37
|
-
export
|
|
37
|
+
export class Context<T> {
|
|
38
38
|
constructor(initial_value: T);
|
|
39
39
|
get(): T;
|
|
40
40
|
set(value: T): void;
|
|
41
41
|
#private;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
export
|
|
44
|
+
export class TrackedSet<T> extends Set<T> {
|
|
45
45
|
isDisjointFrom<U>(other: ReadonlySetLike<U> | TrackedSet<U>): boolean;
|
|
46
46
|
isSubsetOf<U>(other: ReadonlySetLike<U> | TrackedSet<U>): boolean;
|
|
47
47
|
isSupersetOf<U>(other: ReadonlySetLike<U> | TrackedSet<U>): boolean;
|
|
@@ -53,7 +53,7 @@ export declare class TrackedSet<T> extends Set<T> {
|
|
|
53
53
|
#private;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
export
|
|
56
|
+
export class TrackedMap<K, V> extends Map<K, V> {
|
|
57
57
|
toJSON(): [K, V][];
|
|
58
58
|
#private;
|
|
59
59
|
}
|
|
@@ -81,7 +81,7 @@ declare global {
|
|
|
81
81
|
};
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
export
|
|
84
|
+
export function createRefKey(): symbol;
|
|
85
85
|
|
|
86
86
|
// Base Tracked interface - all tracked values have a '#v' property containing the actual value
|
|
87
87
|
export interface Tracked<V> {
|
|
@@ -115,24 +115,20 @@ type SplitResult<T extends Props, K extends readonly (keyof T)[]> = [
|
|
|
115
115
|
Tracked<RestKeys<T, K>>,
|
|
116
116
|
];
|
|
117
117
|
|
|
118
|
-
export
|
|
118
|
+
export function get<V>(tracked: Tracked<V>): V;
|
|
119
119
|
|
|
120
|
-
export
|
|
120
|
+
export function set<V>(tracked: Tracked<V>, value: V): void;
|
|
121
121
|
|
|
122
122
|
// Overload for function values - infers the return type of the function
|
|
123
|
-
export
|
|
123
|
+
export function track<V>(
|
|
124
124
|
value: () => V,
|
|
125
125
|
get?: (v: InferComponent<V>) => InferComponent<V>,
|
|
126
126
|
set?: (next: InferComponent<V>, prev: InferComponent<V>) => InferComponent<V>,
|
|
127
127
|
): Tracked<InferComponent<V>>;
|
|
128
128
|
// Overload for non-function values
|
|
129
|
-
export
|
|
130
|
-
value?: V,
|
|
131
|
-
get?: (v: V) => V,
|
|
132
|
-
set?: (next: V, prev: V) => V,
|
|
133
|
-
): Tracked<V>;
|
|
129
|
+
export function track<V>(value?: V, get?: (v: V) => V, set?: (next: V, prev: V) => V): Tracked<V>;
|
|
134
130
|
|
|
135
|
-
export
|
|
131
|
+
export function trackSplit<V extends Props, const K extends readonly (keyof V)[]>(
|
|
136
132
|
value: V,
|
|
137
133
|
splitKeys: K,
|
|
138
134
|
): SplitResult<V, K>;
|
|
@@ -219,7 +215,7 @@ export interface TrackedObjectConstructor {
|
|
|
219
215
|
new <T extends object>(obj: T): TrackedObject<T>;
|
|
220
216
|
}
|
|
221
217
|
|
|
222
|
-
export
|
|
218
|
+
export const TrackedObject: TrackedObjectConstructor;
|
|
223
219
|
|
|
224
220
|
export class TrackedDate extends Date {
|
|
225
221
|
constructor(...params: any[]);
|
|
@@ -252,7 +248,7 @@ export interface MediaQuery extends Tracked<boolean> {
|
|
|
252
248
|
_brand: void;
|
|
253
249
|
}
|
|
254
250
|
|
|
255
|
-
export
|
|
251
|
+
export const MediaQuery: {
|
|
256
252
|
new (query: string, fallback?: boolean | undefined): Tracked<boolean>;
|
|
257
253
|
};
|
|
258
254
|
|
|
@@ -268,7 +264,7 @@ export function Portal<V = HTMLElement>({
|
|
|
268
264
|
* @param {Tracked<V>} tracked
|
|
269
265
|
* @returns {(node: HTMLInputElement | HTMLSelectElement) => void}
|
|
270
266
|
*/
|
|
271
|
-
export
|
|
267
|
+
export function bindValue<V>(
|
|
272
268
|
tracked: Tracked<V>,
|
|
273
269
|
): (node: HTMLInputElement | HTMLSelectElement) => void;
|
|
274
270
|
|
|
@@ -276,36 +272,34 @@ export declare function bindValue<V>(
|
|
|
276
272
|
* @param {Tracked<V>} tracked
|
|
277
273
|
* @returns {(node: HTMLInputElement) => void}
|
|
278
274
|
*/
|
|
279
|
-
export
|
|
275
|
+
export function bindChecked<V>(tracked: Tracked<V>): (node: HTMLInputElement) => void;
|
|
280
276
|
|
|
281
|
-
export
|
|
277
|
+
export function bindClientWidth<V>(tracked: Tracked<V>): (node: HTMLElement) => void;
|
|
282
278
|
|
|
283
|
-
export
|
|
279
|
+
export function bindClientHeight<V>(tracked: Tracked<V>): (node: HTMLElement) => void;
|
|
284
280
|
|
|
285
|
-
export
|
|
281
|
+
export function bindContentRect<V>(tracked: Tracked<V>): (node: HTMLElement) => void;
|
|
286
282
|
|
|
287
|
-
export
|
|
283
|
+
export function bindContentBoxSize<V>(tracked: Tracked<V>): (node: HTMLElement) => void;
|
|
288
284
|
|
|
289
|
-
export
|
|
285
|
+
export function bindBorderBoxSize<V>(tracked: Tracked<V>): (node: HTMLElement) => void;
|
|
290
286
|
|
|
291
|
-
export
|
|
292
|
-
tracked: Tracked<V>,
|
|
293
|
-
): (node: HTMLElement) => void;
|
|
287
|
+
export function bindDevicePixelContentBoxSize<V>(tracked: Tracked<V>): (node: HTMLElement) => void;
|
|
294
288
|
|
|
295
|
-
export
|
|
289
|
+
export function bindInnerHTML<V>(tracked: Tracked<V>): (node: HTMLElement) => void;
|
|
296
290
|
|
|
297
|
-
export
|
|
291
|
+
export function bindInnerText<V>(tracked: Tracked<V>): (node: HTMLElement) => void;
|
|
298
292
|
|
|
299
|
-
export
|
|
293
|
+
export function bindTextContent<V>(tracked: Tracked<V>): (node: HTMLElement) => void;
|
|
300
294
|
|
|
301
|
-
export
|
|
295
|
+
export function bindNode<V>(tracked: Tracked<V>): (node: HTMLElement) => void;
|
|
302
296
|
|
|
303
|
-
export
|
|
297
|
+
export function bindGroup<V>(tracked: Tracked<V>): (node: HTMLInputElement) => void;
|
|
304
298
|
|
|
305
|
-
export
|
|
299
|
+
export function bindOffsetHeight<V>(tracked: Tracked<V>): (node: HTMLElement) => void;
|
|
306
300
|
|
|
307
|
-
export
|
|
301
|
+
export function bindOffsetWidth<V>(tracked: Tracked<V>): (node: HTMLElement) => void;
|
|
308
302
|
|
|
309
|
-
export
|
|
303
|
+
export function bindIndeterminate<V>(tracked: Tracked<V>): (node: HTMLInputElement) => void;
|
|
310
304
|
|
|
311
|
-
export
|
|
305
|
+
export function bindFiles<V>(tracked: Tracked<V>): (node: HTMLInputElement) => void;
|