ripple 0.2.178 → 0.2.179
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 -3
- package/src/compiler/index.d.ts +17 -27
- package/src/compiler/index.js +12 -7
- package/src/compiler/phases/1-parse/index.js +4 -1
- package/src/compiler/phases/1-parse/style.js +78 -2
- package/src/compiler/phases/2-analyze/index.js +19 -9
- package/src/compiler/phases/3-transform/client/index.js +3 -2
- package/src/compiler/phases/3-transform/segments.js +8 -0
- package/src/compiler/phases/3-transform/server/index.js +4 -2
- package/src/compiler/phases/3-transform/stylesheet.js +62 -4
- package/src/compiler/types/index.d.ts +171 -3
- package/src/runtime/internal/client/composite.js +3 -3
- package/tests/client/composite/composite.render.test.ripple +21 -1
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.179",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"module": "src/runtime/index-client.js",
|
|
9
9
|
"main": "src/runtime/index-client.js",
|
|
@@ -80,9 +80,10 @@
|
|
|
80
80
|
"@types/estree-jsx": "^1.0.5",
|
|
81
81
|
"@types/node": "^24.3.0",
|
|
82
82
|
"typescript": "^5.9.2",
|
|
83
|
-
"@volar/language-core": "~2.4.23"
|
|
83
|
+
"@volar/language-core": "~2.4.23",
|
|
84
|
+
"vscode-languageserver-types": "^3.17.5"
|
|
84
85
|
},
|
|
85
86
|
"peerDependencies": {
|
|
86
|
-
"ripple": "0.2.
|
|
87
|
+
"ripple": "0.2.179"
|
|
87
88
|
}
|
|
88
89
|
}
|
package/src/compiler/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
CodeInformation as VolarCodeInformation,
|
|
4
4
|
Mapping as VolarMapping,
|
|
5
5
|
} from '@volar/language-core';
|
|
6
|
+
import type { DocumentHighlightKind } from 'vscode-languageserver-types';
|
|
6
7
|
|
|
7
8
|
// ============================================================================
|
|
8
9
|
// Compiler API Exports
|
|
@@ -23,6 +24,10 @@ export interface CompileResult {
|
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
export interface PluginActionOverrides {
|
|
27
|
+
/** Whether to enable word document highlighting for this mapping */
|
|
28
|
+
wordHighlight?: {
|
|
29
|
+
kind: DocumentHighlightKind;
|
|
30
|
+
};
|
|
26
31
|
/** TypeScript diagnostic codes to suppress for this mapping */
|
|
27
32
|
suppressedDiagnostics?: number[];
|
|
28
33
|
/** Custom hover documentation for this mapping, false to disable */
|
|
@@ -51,9 +56,6 @@ export interface CodeMapping extends VolarMapping<MappingData> {
|
|
|
51
56
|
data: MappingData;
|
|
52
57
|
}
|
|
53
58
|
|
|
54
|
-
/**
|
|
55
|
-
* Result of Volar mappings compilation
|
|
56
|
-
*/
|
|
57
59
|
export interface VolarMappingsResult {
|
|
58
60
|
code: string;
|
|
59
61
|
mappings: CodeMapping[];
|
|
@@ -64,42 +66,30 @@ export interface VolarMappingsResult {
|
|
|
64
66
|
/**
|
|
65
67
|
* Compilation options
|
|
66
68
|
*/
|
|
67
|
-
|
|
68
|
-
|
|
69
|
+
|
|
70
|
+
interface SharedCompileOptions {
|
|
71
|
+
minify_css?: boolean;
|
|
72
|
+
}
|
|
73
|
+
export interface CompileOptions extends SharedCompileOptions {
|
|
69
74
|
mode?: 'client' | 'server';
|
|
70
75
|
}
|
|
71
76
|
|
|
72
77
|
export interface ParseOptions {
|
|
73
|
-
/** Enable loose mode */
|
|
74
78
|
loose?: boolean;
|
|
75
79
|
}
|
|
76
80
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
export interface AnalyzeOptions extends ParseOptions, Pick<CompileOptions, 'mode'> {
|
|
82
|
+
to_ts?: boolean;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface VolarCompileOptions extends ParseOptions, SharedCompileOptions {}
|
|
86
|
+
|
|
83
87
|
export function parse(source: string, options?: ParseOptions): Program;
|
|
84
88
|
|
|
85
|
-
/**
|
|
86
|
-
* Compile Ripple source code to JS/CSS output
|
|
87
|
-
* @param source - The Ripple source code to compile
|
|
88
|
-
* @param filename - The filename for source map generation
|
|
89
|
-
* @param options - Compilation options (mode: 'client' or 'server')
|
|
90
|
-
* @returns The compilation result with AST, JS, and CSS
|
|
91
|
-
*/
|
|
92
89
|
export function compile(source: string, filename: string, options?: CompileOptions): CompileResult;
|
|
93
90
|
|
|
94
|
-
/**
|
|
95
|
-
* Compile Ripple source to Volar mappings for editor integration
|
|
96
|
-
* @param source - The Ripple source code
|
|
97
|
-
* @param filename - The filename for source map generation
|
|
98
|
-
* @param options - Parse options
|
|
99
|
-
* @returns Volar mappings object for editor integration
|
|
100
|
-
*/
|
|
101
91
|
export function compile_to_volar_mappings(
|
|
102
92
|
source: string,
|
|
103
93
|
filename: string,
|
|
104
|
-
options?:
|
|
94
|
+
options?: VolarCompileOptions,
|
|
105
95
|
): VolarMappingsResult;
|
package/src/compiler/index.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
/** @import { Program } from 'estree' */
|
|
2
|
-
/** @import { ParseOptions } from 'ripple/compiler' */
|
|
3
2
|
|
|
4
3
|
import { parse as parse_module } from './phases/1-parse/index.js';
|
|
5
4
|
import { analyze } from './phases/2-analyze/index.js';
|
|
@@ -20,7 +19,7 @@ export function parse(source) {
|
|
|
20
19
|
* Compile Ripple source code to JS/CSS output
|
|
21
20
|
* @param {string} source
|
|
22
21
|
* @param {string} filename
|
|
23
|
-
* @param {
|
|
22
|
+
* @param {CompileOptions} [options]
|
|
24
23
|
* @returns {object}
|
|
25
24
|
*/
|
|
26
25
|
export function compile(source, filename, options = {}) {
|
|
@@ -28,26 +27,32 @@ export function compile(source, filename, options = {}) {
|
|
|
28
27
|
const analysis = analyze(ast, filename, options);
|
|
29
28
|
const result =
|
|
30
29
|
options.mode === 'server'
|
|
31
|
-
? transform_server(filename, source, analysis)
|
|
32
|
-
: transform_client(filename, source, analysis, false);
|
|
30
|
+
? transform_server(filename, source, analysis, options?.minify_css ?? false)
|
|
31
|
+
: transform_client(filename, source, analysis, false, options?.minify_css ?? false);
|
|
33
32
|
|
|
34
33
|
return result;
|
|
35
34
|
}
|
|
36
35
|
|
|
37
36
|
/** @import { PostProcessingChanges, LineOffsets } from './phases/3-transform/client/index.js' */
|
|
38
|
-
/** @import { VolarMappingsResult } from 'ripple/compiler' */
|
|
37
|
+
/** @import { VolarMappingsResult, VolarCompileOptions, CompileOptions } from 'ripple/compiler' */
|
|
39
38
|
|
|
40
39
|
/**
|
|
41
40
|
* Compile Ripple component to Volar virtual code with TypeScript mappings
|
|
42
41
|
* @param {string} source
|
|
43
42
|
* @param {string} filename
|
|
44
|
-
* @param {
|
|
43
|
+
* @param {VolarCompileOptions} [options] - Compiler options
|
|
45
44
|
* @returns {VolarMappingsResult} Volar mappings object
|
|
46
45
|
*/
|
|
47
46
|
export function compile_to_volar_mappings(source, filename, options) {
|
|
48
47
|
const ast = parse_module(source, options);
|
|
49
48
|
const analysis = analyze(ast, filename, { to_ts: true, loose: !!options?.loose });
|
|
50
|
-
const transformed = transform_client(
|
|
49
|
+
const transformed = transform_client(
|
|
50
|
+
filename,
|
|
51
|
+
source,
|
|
52
|
+
analysis,
|
|
53
|
+
true,
|
|
54
|
+
options?.minify_css ?? false,
|
|
55
|
+
);
|
|
51
56
|
|
|
52
57
|
// Create volar mappings with esrap source map for accurate positioning
|
|
53
58
|
return convert_source_map_to_mappings(
|
|
@@ -2115,9 +2115,12 @@ function get_comment_handlers(source, comments, index = 0) {
|
|
|
2115
2115
|
}
|
|
2116
2116
|
// Handle empty Element nodes the same way as empty BlockStatements
|
|
2117
2117
|
if (node.type === 'Element' && (!node.children || node.children.length === 0)) {
|
|
2118
|
-
|
|
2118
|
+
// Collect all comments that fall within this empty element
|
|
2119
|
+
while (comments[0] && comments[0].start < node.end && comments[0].end < node.end) {
|
|
2119
2120
|
comment = /** @type {CommentWithLocation} */ (comments.shift());
|
|
2120
2121
|
(node.innerComments ||= []).push(comment);
|
|
2122
|
+
}
|
|
2123
|
+
if (node.innerComments && node.innerComments.length > 0) {
|
|
2121
2124
|
return;
|
|
2122
2125
|
}
|
|
2123
2126
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/** @import * as AST from 'estree' */
|
|
2
|
+
|
|
1
3
|
import { hash } from '../../utils.js';
|
|
2
4
|
|
|
3
5
|
const REGEX_MATCHER = /^[~^$*|]?=/;
|
|
@@ -17,6 +19,10 @@ const regex_whitespace = /\s/;
|
|
|
17
19
|
class Parser {
|
|
18
20
|
index = 0;
|
|
19
21
|
|
|
22
|
+
/**
|
|
23
|
+
* @param {string} template
|
|
24
|
+
* @param {boolean} loose
|
|
25
|
+
*/
|
|
20
26
|
constructor(template, loose) {
|
|
21
27
|
if (typeof template !== 'string') {
|
|
22
28
|
throw new TypeError('Template must be a string');
|
|
@@ -27,6 +33,7 @@ class Parser {
|
|
|
27
33
|
this.template = template.trimEnd();
|
|
28
34
|
}
|
|
29
35
|
|
|
36
|
+
/** @param {string} str */
|
|
30
37
|
match(str) {
|
|
31
38
|
const length = str.length;
|
|
32
39
|
if (length === 1) {
|
|
@@ -37,6 +44,11 @@ class Parser {
|
|
|
37
44
|
return this.template.slice(this.index, this.index + length) === str;
|
|
38
45
|
}
|
|
39
46
|
|
|
47
|
+
/**
|
|
48
|
+
* @param {string} str
|
|
49
|
+
* @param {boolean} required
|
|
50
|
+
* @param {boolean} required_in_loose
|
|
51
|
+
*/
|
|
40
52
|
eat(str, required = false, required_in_loose = true) {
|
|
41
53
|
if (this.match(str)) {
|
|
42
54
|
this.index += str.length;
|
|
@@ -50,6 +62,10 @@ class Parser {
|
|
|
50
62
|
return false;
|
|
51
63
|
}
|
|
52
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Match a regex at the current index
|
|
67
|
+
* @param {RegExp} pattern Should have a ^ anchor at the start so the regex doesn't search past the beginning, resulting in worse performance
|
|
68
|
+
*/
|
|
53
69
|
match_regex(pattern) {
|
|
54
70
|
const match = pattern.exec(this.template.slice(this.index));
|
|
55
71
|
if (!match || match.index !== 0) return null;
|
|
@@ -57,6 +73,10 @@ class Parser {
|
|
|
57
73
|
return match[0];
|
|
58
74
|
}
|
|
59
75
|
|
|
76
|
+
/**
|
|
77
|
+
* Search for a regex starting at the current index and return the result if it matches
|
|
78
|
+
* @param {RegExp} pattern Should have a ^ anchor at the start so the regex doesn't search past the beginning, resulting in worse performance
|
|
79
|
+
*/
|
|
60
80
|
read(pattern) {
|
|
61
81
|
const result = this.match_regex(pattern);
|
|
62
82
|
if (result) this.index += result.length;
|
|
@@ -69,6 +89,7 @@ class Parser {
|
|
|
69
89
|
}
|
|
70
90
|
}
|
|
71
91
|
|
|
92
|
+
/** @param {RegExp} pattern */
|
|
72
93
|
read_until(pattern) {
|
|
73
94
|
if (this.index >= this.template.length) {
|
|
74
95
|
if (this.loose) return '';
|
|
@@ -88,6 +109,11 @@ class Parser {
|
|
|
88
109
|
}
|
|
89
110
|
}
|
|
90
111
|
|
|
112
|
+
/**
|
|
113
|
+
* @param {string} content
|
|
114
|
+
* @param {{ loose?: boolean }} options
|
|
115
|
+
* @returns {Partial<AST.CSS.StyleSheet>}
|
|
116
|
+
*/
|
|
91
117
|
export function parse_style(content, options) {
|
|
92
118
|
const parser = new Parser(content, options.loose || false);
|
|
93
119
|
|
|
@@ -95,10 +121,11 @@ export function parse_style(content, options) {
|
|
|
95
121
|
source: content,
|
|
96
122
|
hash: `ripple-${hash(content)}`,
|
|
97
123
|
type: 'StyleSheet',
|
|
98
|
-
|
|
124
|
+
children: read_body(parser),
|
|
99
125
|
};
|
|
100
126
|
}
|
|
101
127
|
|
|
128
|
+
/** @param {Parser} parser */
|
|
102
129
|
function allow_comment_or_whitespace(parser) {
|
|
103
130
|
parser.allow_whitespace();
|
|
104
131
|
while (parser.match('/*') || parser.match('<!--')) {
|
|
@@ -116,7 +143,12 @@ function allow_comment_or_whitespace(parser) {
|
|
|
116
143
|
}
|
|
117
144
|
}
|
|
118
145
|
|
|
146
|
+
/**
|
|
147
|
+
* @param {Parser} parser
|
|
148
|
+
* @returns {Array<AST.CSS.Rule | AST.CSS.Atrule>}
|
|
149
|
+
*/
|
|
119
150
|
function read_body(parser) {
|
|
151
|
+
/** @type {Array<AST.CSS.Rule | AST.CSS.Atrule>} */
|
|
120
152
|
const children = [];
|
|
121
153
|
|
|
122
154
|
while (parser.index < parser.template.length) {
|
|
@@ -132,6 +164,10 @@ function read_body(parser) {
|
|
|
132
164
|
return children;
|
|
133
165
|
}
|
|
134
166
|
|
|
167
|
+
/**
|
|
168
|
+
* @param {Parser} parser
|
|
169
|
+
* @returns {AST.CSS.Atrule}
|
|
170
|
+
*/
|
|
135
171
|
function read_at_rule(parser) {
|
|
136
172
|
const start = parser.index;
|
|
137
173
|
parser.eat('@', true);
|
|
@@ -161,6 +197,10 @@ function read_at_rule(parser) {
|
|
|
161
197
|
};
|
|
162
198
|
}
|
|
163
199
|
|
|
200
|
+
/**
|
|
201
|
+
* @param {Parser} parser
|
|
202
|
+
* @returns {AST.CSS.Rule}
|
|
203
|
+
*/
|
|
164
204
|
function read_rule(parser) {
|
|
165
205
|
const start = parser.index;
|
|
166
206
|
|
|
@@ -178,6 +218,10 @@ function read_rule(parser) {
|
|
|
178
218
|
};
|
|
179
219
|
}
|
|
180
220
|
|
|
221
|
+
/**
|
|
222
|
+
* @param {Parser} parser
|
|
223
|
+
* @returns {AST.CSS.Block}
|
|
224
|
+
*/
|
|
181
225
|
function read_block(parser) {
|
|
182
226
|
const start = parser.index;
|
|
183
227
|
|
|
@@ -206,6 +250,12 @@ function read_block(parser) {
|
|
|
206
250
|
};
|
|
207
251
|
}
|
|
208
252
|
|
|
253
|
+
/**
|
|
254
|
+
* Reads a declaration, rule or at-rule
|
|
255
|
+
*
|
|
256
|
+
* @param {Parser} parser
|
|
257
|
+
* @returns {AST.CSS.Declaration | AST.CSS.Rule | AST.CSS.Atrule}
|
|
258
|
+
*/
|
|
209
259
|
function read_block_item(parser) {
|
|
210
260
|
if (parser.match('@')) {
|
|
211
261
|
return read_at_rule(parser);
|
|
@@ -221,6 +271,10 @@ function read_block_item(parser) {
|
|
|
221
271
|
return char === '{' ? read_rule(parser) : read_declaration(parser);
|
|
222
272
|
}
|
|
223
273
|
|
|
274
|
+
/**
|
|
275
|
+
* @param {Parser} parser
|
|
276
|
+
* @returns {AST.CSS.Declaration}
|
|
277
|
+
*/
|
|
224
278
|
function read_declaration(parser) {
|
|
225
279
|
const start = parser.index;
|
|
226
280
|
|
|
@@ -251,6 +305,10 @@ function read_declaration(parser) {
|
|
|
251
305
|
};
|
|
252
306
|
}
|
|
253
307
|
|
|
308
|
+
/**
|
|
309
|
+
* @param {Parser} parser
|
|
310
|
+
* @returns {string}
|
|
311
|
+
*/
|
|
254
312
|
function read_value(parser) {
|
|
255
313
|
let value = '';
|
|
256
314
|
let escaped = false;
|
|
@@ -287,6 +345,11 @@ function read_value(parser) {
|
|
|
287
345
|
throw new Error('Unexpected end of input');
|
|
288
346
|
}
|
|
289
347
|
|
|
348
|
+
/**
|
|
349
|
+
* @param {Parser} parser
|
|
350
|
+
* @param {boolean} [inside_pseudo_class]
|
|
351
|
+
* @returns {AST.CSS.SelectorList}
|
|
352
|
+
*/
|
|
290
353
|
function read_selector_list(parser, inside_pseudo_class = false) {
|
|
291
354
|
/** @type {AST.CSS.ComplexSelector[]} */
|
|
292
355
|
const children = [];
|
|
@@ -318,6 +381,10 @@ function read_selector_list(parser, inside_pseudo_class = false) {
|
|
|
318
381
|
throw new Error('Unexpected end of input');
|
|
319
382
|
}
|
|
320
383
|
|
|
384
|
+
/**
|
|
385
|
+
* @param {Parser} parser
|
|
386
|
+
* @returns {AST.CSS.Combinator | null}
|
|
387
|
+
*/
|
|
321
388
|
function read_combinator(parser) {
|
|
322
389
|
const start = parser.index;
|
|
323
390
|
parser.allow_whitespace();
|
|
@@ -349,6 +416,11 @@ function read_combinator(parser) {
|
|
|
349
416
|
return null;
|
|
350
417
|
}
|
|
351
418
|
|
|
419
|
+
/**
|
|
420
|
+
* @param {Parser} parser
|
|
421
|
+
* @param {boolean} [inside_pseudo_class]
|
|
422
|
+
* @returns {AST.CSS.ComplexSelector}
|
|
423
|
+
*/
|
|
352
424
|
function read_selector(parser, inside_pseudo_class = false) {
|
|
353
425
|
const list_start = parser.index;
|
|
354
426
|
|
|
@@ -547,7 +619,7 @@ function read_selector(parser, inside_pseudo_class = false) {
|
|
|
547
619
|
parser.allow_whitespace();
|
|
548
620
|
|
|
549
621
|
if (parser.match(',') || (inside_pseudo_class ? parser.match(')') : parser.match('{'))) {
|
|
550
|
-
|
|
622
|
+
throw new Error(`Invalid selector at parser.index: ${parser.index}`);
|
|
551
623
|
}
|
|
552
624
|
}
|
|
553
625
|
}
|
|
@@ -588,6 +660,10 @@ function read_attribute_value(parser) {
|
|
|
588
660
|
throw new Error('Unexpected end of input');
|
|
589
661
|
}
|
|
590
662
|
|
|
663
|
+
/**
|
|
664
|
+
* https://www.w3.org/TR/css-syntax-3/#ident-token-diagram
|
|
665
|
+
* @param {Parser} parser
|
|
666
|
+
*/
|
|
591
667
|
function read_identifier(parser) {
|
|
592
668
|
const start = parser.index;
|
|
593
669
|
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
/** @import {AnalyzeOptions} from 'ripple/compiler' */
|
|
2
|
+
/** @import * as AST from 'estree' */
|
|
3
|
+
|
|
1
4
|
import * as b from '../../../utils/builders.js';
|
|
2
5
|
import { walk } from 'zimmerframe';
|
|
3
6
|
import { create_scopes, ScopeRoot } from '../../scope.js';
|
|
@@ -564,14 +567,15 @@ const visitors = {
|
|
|
564
567
|
* @returns
|
|
565
568
|
*/
|
|
566
569
|
TryStatement(node, context) {
|
|
570
|
+
const { state } = context;
|
|
567
571
|
if (!is_inside_component(context)) {
|
|
568
572
|
return context.next();
|
|
569
573
|
}
|
|
570
574
|
|
|
571
575
|
if (node.pending) {
|
|
572
576
|
// Try/pending blocks indicate async operations
|
|
573
|
-
if (
|
|
574
|
-
|
|
577
|
+
if (state.metadata?.await === false) {
|
|
578
|
+
state.metadata.await = true;
|
|
575
579
|
}
|
|
576
580
|
|
|
577
581
|
node.metadata = {
|
|
@@ -579,12 +583,12 @@ const visitors = {
|
|
|
579
583
|
has_template: false,
|
|
580
584
|
};
|
|
581
585
|
|
|
582
|
-
context.visit(node.block,
|
|
586
|
+
context.visit(node.block, state);
|
|
583
587
|
|
|
584
|
-
if (!node.metadata.has_template) {
|
|
588
|
+
if (!node.metadata.has_template && !state.loose) {
|
|
585
589
|
error(
|
|
586
590
|
'Component try statements must contain a template in their main body. Move the try statement into an effect if it does not render anything.',
|
|
587
|
-
|
|
591
|
+
state.analysis.module.filename,
|
|
588
592
|
node,
|
|
589
593
|
);
|
|
590
594
|
}
|
|
@@ -594,19 +598,19 @@ const visitors = {
|
|
|
594
598
|
has_template: false,
|
|
595
599
|
};
|
|
596
600
|
|
|
597
|
-
context.visit(node.pending,
|
|
601
|
+
context.visit(node.pending, state);
|
|
598
602
|
|
|
599
|
-
if (!node.metadata.has_template) {
|
|
603
|
+
if (!node.metadata.has_template && !state.loose) {
|
|
600
604
|
error(
|
|
601
605
|
'Component try statements must contain a template in their "pending" body. Rendering a pending fallback is required to have a template.',
|
|
602
|
-
|
|
606
|
+
state.analysis.module.filename,
|
|
603
607
|
node,
|
|
604
608
|
);
|
|
605
609
|
}
|
|
606
610
|
}
|
|
607
611
|
|
|
608
612
|
if (node.finalizer) {
|
|
609
|
-
context.visit(node.finalizer,
|
|
613
|
+
context.visit(node.finalizer, state);
|
|
610
614
|
}
|
|
611
615
|
},
|
|
612
616
|
|
|
@@ -856,6 +860,12 @@ const visitors = {
|
|
|
856
860
|
},
|
|
857
861
|
};
|
|
858
862
|
|
|
863
|
+
/**
|
|
864
|
+
*
|
|
865
|
+
* @param {AST.Program} ast
|
|
866
|
+
* @param {string} filename
|
|
867
|
+
* @param {AnalyzeOptions} options
|
|
868
|
+
*/
|
|
859
869
|
export function analyze(ast, filename, options = {}) {
|
|
860
870
|
const scope_root = new ScopeRoot();
|
|
861
871
|
|
|
@@ -2862,9 +2862,10 @@ function create_tsx_with_typescript_support() {
|
|
|
2862
2862
|
* @param {string} source - Original source code
|
|
2863
2863
|
* @param {any} analysis - Analysis result
|
|
2864
2864
|
* @param {boolean} to_ts - Whether to generate TypeScript output
|
|
2865
|
+
* @param {boolean} minify_css - Whether to minify CSS output
|
|
2865
2866
|
* @returns {{ ast: any, js: { code: string, map: any, post_processing_changes?: PostProcessingChanges, line_offsets?: LineOffsets }, css: any }}
|
|
2866
2867
|
*/
|
|
2867
|
-
export function transform_client(filename, source, analysis, to_ts) {
|
|
2868
|
+
export function transform_client(filename, source, analysis, to_ts, minify_css) {
|
|
2868
2869
|
/**
|
|
2869
2870
|
* User's named imports from 'ripple' so we can reuse them in TS output
|
|
2870
2871
|
* when transforming shorthand syntax. E.g., if the user has already imported
|
|
@@ -3010,7 +3011,7 @@ export function transform_client(filename, source, analysis, to_ts) {
|
|
|
3010
3011
|
js.line_offsets = line_offsets;
|
|
3011
3012
|
}
|
|
3012
3013
|
|
|
3013
|
-
const css = render_stylesheets(state.stylesheets);
|
|
3014
|
+
const css = render_stylesheets(state.stylesheets, minify_css);
|
|
3014
3015
|
|
|
3015
3016
|
return {
|
|
3016
3017
|
ast: program,
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
import { walk } from 'zimmerframe';
|
|
18
18
|
import { build_source_to_generated_map, get_generated_position } from '../../source-map-utils.js';
|
|
19
|
+
import { DocumentHighlightKind } from 'vscode-languageserver-types';
|
|
19
20
|
|
|
20
21
|
/** @type {VolarCodeMapping['data']} */
|
|
21
22
|
export const mapping_data = {
|
|
@@ -523,6 +524,9 @@ export function convert_source_map_to_mappings(
|
|
|
523
524
|
generated: 'pending',
|
|
524
525
|
loc: pendingKeywordLoc,
|
|
525
526
|
metadata: {
|
|
527
|
+
wordHighlight: {
|
|
528
|
+
kind: DocumentHighlightKind.Text,
|
|
529
|
+
},
|
|
526
530
|
suppressedDiagnostics: [
|
|
527
531
|
1472, // 'catch' or 'finally' expected
|
|
528
532
|
2304, // Cannot find name 'pending'
|
|
@@ -1378,6 +1382,10 @@ export function convert_source_map_to_mappings(
|
|
|
1378
1382
|
|
|
1379
1383
|
// Add optional metadata from token if present
|
|
1380
1384
|
if (token.metadata) {
|
|
1385
|
+
if ('wordHighlight' in token.metadata) {
|
|
1386
|
+
customData.wordHighlight = token.metadata.wordHighlight;
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1381
1389
|
if ('suppressedDiagnostics' in token.metadata) {
|
|
1382
1390
|
customData.suppressedDiagnostics = token.metadata.suppressedDiagnostics;
|
|
1383
1391
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/** @import * as ESTree from 'estree' */
|
|
2
|
+
|
|
1
3
|
import * as b from '../../../../utils/builders.js';
|
|
2
4
|
import { walk } from 'zimmerframe';
|
|
3
5
|
import ts from 'esrap/languages/ts';
|
|
@@ -804,7 +806,7 @@ const visitors = {
|
|
|
804
806
|
},
|
|
805
807
|
};
|
|
806
808
|
|
|
807
|
-
export function transform_server(filename, source, analysis) {
|
|
809
|
+
export function transform_server(filename, source, analysis, minify_css) {
|
|
808
810
|
// Use component metadata collected during the analyze phase
|
|
809
811
|
const component_metadata = analysis.component_metadata || [];
|
|
810
812
|
|
|
@@ -823,7 +825,7 @@ export function transform_server(filename, source, analysis) {
|
|
|
823
825
|
walk(analysis.ast, { ...state, namespace: 'html' }, visitors)
|
|
824
826
|
);
|
|
825
827
|
|
|
826
|
-
const css = render_stylesheets(state.stylesheets);
|
|
828
|
+
const css = render_stylesheets(state.stylesheets, minify_css);
|
|
827
829
|
|
|
828
830
|
// Add CSS registration if there are stylesheets
|
|
829
831
|
if (state.stylesheets.length > 0 && css) {
|
|
@@ -1,25 +1,58 @@
|
|
|
1
|
+
/** @import * as AST from 'estree' */
|
|
2
|
+
/** @import { Visitors } from 'zimmerframe' */
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {{
|
|
6
|
+
* code: MagicString;
|
|
7
|
+
* hash: string;
|
|
8
|
+
* minify: boolean;
|
|
9
|
+
* selector: string;
|
|
10
|
+
* keyframes: Record<string, {
|
|
11
|
+
* indexes: number[];
|
|
12
|
+
* local: boolean | undefined;
|
|
13
|
+
* }>;
|
|
14
|
+
* specificity: {
|
|
15
|
+
* bumped: boolean
|
|
16
|
+
* }
|
|
17
|
+
* }} State
|
|
18
|
+
*/
|
|
19
|
+
|
|
1
20
|
import MagicString from 'magic-string';
|
|
2
21
|
import { walk } from 'zimmerframe';
|
|
3
22
|
|
|
4
23
|
const regex_css_browser_prefix = /^-((webkit)|(moz)|(o)|(ms))-/;
|
|
5
24
|
const regex_css_name_boundary = /^[\s,;}]$/;
|
|
6
25
|
|
|
26
|
+
/** @param {AST.CSS.Atrule} node */
|
|
7
27
|
const is_keyframes_node = (node) => remove_css_prefix(node.name) === 'keyframes';
|
|
8
28
|
|
|
29
|
+
/**
|
|
30
|
+
* @param {string} name
|
|
31
|
+
* @returns {string}
|
|
32
|
+
*/
|
|
9
33
|
function remove_css_prefix(name) {
|
|
10
34
|
return name.replace(regex_css_browser_prefix, '');
|
|
11
35
|
}
|
|
12
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Walk backwards until we find a non-whitespace character
|
|
39
|
+
* @param {number} end
|
|
40
|
+
* @param {State} state
|
|
41
|
+
*/
|
|
13
42
|
function remove_preceding_whitespace(end, state) {
|
|
14
43
|
let start = end;
|
|
15
44
|
while (/\s/.test(state.code.original[start - 1])) start--;
|
|
16
45
|
if (start < end) state.code.remove(start, end);
|
|
17
46
|
}
|
|
18
47
|
|
|
48
|
+
/** @param {AST.CSS.Rule} rule */
|
|
19
49
|
function is_used(rule) {
|
|
20
50
|
return rule.prelude.children.some((selector) => selector.metadata.used);
|
|
21
51
|
}
|
|
22
52
|
|
|
53
|
+
/**
|
|
54
|
+
* @param {Array<AST.CSS.Node>} path
|
|
55
|
+
*/
|
|
23
56
|
function is_in_global_block(path) {
|
|
24
57
|
return path.some((node) => node.type === 'Rule' && node.metadata.is_global_block);
|
|
25
58
|
}
|
|
@@ -27,7 +60,7 @@ function is_in_global_block(path) {
|
|
|
27
60
|
/**
|
|
28
61
|
* Check if we're inside a pseudo-class selector that's INSIDE a :global() wrapper
|
|
29
62
|
* or adjacent to a :global modifier
|
|
30
|
-
* @param {
|
|
63
|
+
* @param {AST.CSS.Node[]} path
|
|
31
64
|
*/
|
|
32
65
|
function is_in_global_pseudo(path) {
|
|
33
66
|
// Walk up the path to find if we're inside a :global() pseudo-class selector with args
|
|
@@ -83,6 +116,11 @@ function has_global_in_middle(rule) {
|
|
|
83
116
|
return false;
|
|
84
117
|
}
|
|
85
118
|
|
|
119
|
+
/**
|
|
120
|
+
* @param {AST.CSS.PseudoClassSelector} selector
|
|
121
|
+
* @param {AST.CSS.Combinator | null} combinator
|
|
122
|
+
* @param {State} state
|
|
123
|
+
*/
|
|
86
124
|
function remove_global_pseudo_class(selector, combinator, state) {
|
|
87
125
|
if (selector.args === null) {
|
|
88
126
|
let start = selector.start;
|
|
@@ -98,6 +136,10 @@ function remove_global_pseudo_class(selector, combinator, state) {
|
|
|
98
136
|
}
|
|
99
137
|
}
|
|
100
138
|
|
|
139
|
+
/**
|
|
140
|
+
* @param {AST.CSS.Rule} node
|
|
141
|
+
* @param {MagicString} code
|
|
142
|
+
*/
|
|
101
143
|
function escape_comment_close(node, code) {
|
|
102
144
|
let escaped = false;
|
|
103
145
|
let in_comment = false;
|
|
@@ -121,12 +163,16 @@ function escape_comment_close(node, code) {
|
|
|
121
163
|
}
|
|
122
164
|
}
|
|
123
165
|
|
|
166
|
+
/**
|
|
167
|
+
* @param {State} state
|
|
168
|
+
* @param {number} index
|
|
169
|
+
*/
|
|
124
170
|
function append_hash(state, index) {
|
|
125
171
|
state.code.prependRight(index, `${state.hash}-`);
|
|
126
172
|
}
|
|
127
173
|
|
|
128
174
|
/**
|
|
129
|
-
*
|
|
175
|
+
* @param {AST.CSS.Rule} rule
|
|
130
176
|
* @param {boolean} is_in_global_block
|
|
131
177
|
*/
|
|
132
178
|
function is_empty(rule, is_in_global_block) {
|
|
@@ -159,6 +205,7 @@ function is_empty(rule, is_in_global_block) {
|
|
|
159
205
|
return true;
|
|
160
206
|
}
|
|
161
207
|
|
|
208
|
+
/** @type {Visitors<AST.CSS.Node, State>} */
|
|
162
209
|
const visitors = {
|
|
163
210
|
_: (node, context) => {
|
|
164
211
|
context.state.code.addSourcemapLocation(node.start);
|
|
@@ -189,6 +236,7 @@ const visitors = {
|
|
|
189
236
|
const property = node.property && remove_css_prefix(node.property.toLowerCase());
|
|
190
237
|
if (property === 'animation' || property === 'animation-name') {
|
|
191
238
|
let index = node.start + node.property.length + 1;
|
|
239
|
+
/** @type {string} */
|
|
192
240
|
let name = '';
|
|
193
241
|
|
|
194
242
|
while (index < state.code.original.length) {
|
|
@@ -368,7 +416,10 @@ const visitors = {
|
|
|
368
416
|
) {
|
|
369
417
|
// Now check if this pseudo-class is part of a global RelativeSelector
|
|
370
418
|
for (let j = i - 2; j >= 0; j--) {
|
|
371
|
-
if (
|
|
419
|
+
if (
|
|
420
|
+
parentPath[j].type === 'RelativeSelector' &&
|
|
421
|
+
/** @type {AST.CSS.RelativeSelector} */ (parentPath[j]).metadata?.is_global
|
|
422
|
+
) {
|
|
372
423
|
insideScopingPseudo = true;
|
|
373
424
|
break;
|
|
374
425
|
}
|
|
@@ -462,7 +513,13 @@ const visitors = {
|
|
|
462
513
|
},
|
|
463
514
|
};
|
|
464
515
|
|
|
465
|
-
|
|
516
|
+
/**
|
|
517
|
+
* Render stylesheets to CSS string
|
|
518
|
+
* @param {AST.CSS.StyleSheet[]} stylesheets
|
|
519
|
+
* @param {boolean} [minify]
|
|
520
|
+
* @returns {string}
|
|
521
|
+
*/
|
|
522
|
+
export function render_stylesheets(stylesheets, minify = false) {
|
|
466
523
|
let css = '';
|
|
467
524
|
|
|
468
525
|
for (const stylesheet of stylesheets) {
|
|
@@ -470,6 +527,7 @@ export function render_stylesheets(stylesheets) {
|
|
|
470
527
|
const state = {
|
|
471
528
|
code,
|
|
472
529
|
hash: stylesheet.hash,
|
|
530
|
+
minify,
|
|
473
531
|
selector: `.${stylesheet.hash}`,
|
|
474
532
|
keyframes: {},
|
|
475
533
|
specificity: {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import type * as ESTree from 'estree';
|
|
2
2
|
|
|
3
3
|
// Ripple augmentation for ESTree function nodes
|
|
4
4
|
declare module 'estree' {
|
|
@@ -24,6 +24,174 @@ declare module 'estree' {
|
|
|
24
24
|
interface TryStatement {
|
|
25
25
|
pending?: BlockStatement | null;
|
|
26
26
|
}
|
|
27
|
+
|
|
28
|
+
export namespace CSS {
|
|
29
|
+
export interface BaseNode {
|
|
30
|
+
start: number;
|
|
31
|
+
end: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface StyleSheet extends BaseNode {
|
|
35
|
+
type: 'StyleSheet';
|
|
36
|
+
children: Array<Atrule | Rule>;
|
|
37
|
+
source: string;
|
|
38
|
+
hash: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface Atrule extends BaseNode {
|
|
42
|
+
type: 'Atrule';
|
|
43
|
+
name: string;
|
|
44
|
+
prelude: string;
|
|
45
|
+
block: Block | null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface Rule extends BaseNode {
|
|
49
|
+
type: 'Rule';
|
|
50
|
+
prelude: SelectorList;
|
|
51
|
+
block: Block;
|
|
52
|
+
metadata: {
|
|
53
|
+
parent_rule: Rule | null;
|
|
54
|
+
has_local_selectors: boolean;
|
|
55
|
+
is_global_block: boolean;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* A list of selectors, e.g. `a, b, c {}`
|
|
61
|
+
*/
|
|
62
|
+
export interface SelectorList extends BaseNode {
|
|
63
|
+
type: 'SelectorList';
|
|
64
|
+
/**
|
|
65
|
+
* The `a`, `b` and `c` in `a, b, c {}`
|
|
66
|
+
*/
|
|
67
|
+
children: ComplexSelector[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* A complex selector, e.g. `a b c {}`
|
|
72
|
+
*/
|
|
73
|
+
export interface ComplexSelector extends BaseNode {
|
|
74
|
+
type: 'ComplexSelector';
|
|
75
|
+
/**
|
|
76
|
+
* The `a`, `b` and `c` in `a b c {}`
|
|
77
|
+
*/
|
|
78
|
+
children: RelativeSelector[];
|
|
79
|
+
metadata: {
|
|
80
|
+
rule: Rule | null;
|
|
81
|
+
used: boolean;
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* A relative selector, e.g the `a` and `> b` in `a > b {}`
|
|
87
|
+
*/
|
|
88
|
+
export interface RelativeSelector extends BaseNode {
|
|
89
|
+
type: 'RelativeSelector';
|
|
90
|
+
/**
|
|
91
|
+
* In `a > b`, `> b` forms one relative selector, and `>` is the combinator. `null` for the first selector.
|
|
92
|
+
*/
|
|
93
|
+
combinator: null | Combinator;
|
|
94
|
+
/**
|
|
95
|
+
* The `b:is(...)` in `> b:is(...)`
|
|
96
|
+
*/
|
|
97
|
+
selectors: SimpleSelector[];
|
|
98
|
+
|
|
99
|
+
metadata: {
|
|
100
|
+
is_global: boolean;
|
|
101
|
+
is_global_like: boolean;
|
|
102
|
+
scoped: boolean;
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface TypeSelector extends BaseNode {
|
|
107
|
+
type: 'TypeSelector';
|
|
108
|
+
name: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface IdSelector extends BaseNode {
|
|
112
|
+
type: 'IdSelector';
|
|
113
|
+
name: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface ClassSelector extends BaseNode {
|
|
117
|
+
type: 'ClassSelector';
|
|
118
|
+
name: string;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface AttributeSelector extends BaseNode {
|
|
122
|
+
type: 'AttributeSelector';
|
|
123
|
+
name: string;
|
|
124
|
+
matcher: string | null;
|
|
125
|
+
value: string | null;
|
|
126
|
+
flags: string | null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface PseudoElementSelector extends BaseNode {
|
|
130
|
+
type: 'PseudoElementSelector';
|
|
131
|
+
name: string;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface PseudoClassSelector extends BaseNode {
|
|
135
|
+
type: 'PseudoClassSelector';
|
|
136
|
+
name: string;
|
|
137
|
+
args: SelectorList | null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export interface Percentage extends BaseNode {
|
|
141
|
+
type: 'Percentage';
|
|
142
|
+
value: string;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export interface NestingSelector extends BaseNode {
|
|
146
|
+
type: 'NestingSelector';
|
|
147
|
+
name: '&';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface Nth extends BaseNode {
|
|
151
|
+
type: 'Nth';
|
|
152
|
+
value: string;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export type SimpleSelector =
|
|
156
|
+
| TypeSelector
|
|
157
|
+
| IdSelector
|
|
158
|
+
| ClassSelector
|
|
159
|
+
| AttributeSelector
|
|
160
|
+
| PseudoElementSelector
|
|
161
|
+
| PseudoClassSelector
|
|
162
|
+
| Percentage
|
|
163
|
+
| Nth
|
|
164
|
+
| NestingSelector;
|
|
165
|
+
|
|
166
|
+
export interface Combinator extends BaseNode {
|
|
167
|
+
type: 'Combinator';
|
|
168
|
+
name: string;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export interface Block extends BaseNode {
|
|
172
|
+
type: 'Block';
|
|
173
|
+
children: Array<Declaration | Rule | Atrule>;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export interface Declaration extends BaseNode {
|
|
177
|
+
type: 'Declaration';
|
|
178
|
+
property: string;
|
|
179
|
+
value: string;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// for zimmerframe
|
|
183
|
+
export type Node =
|
|
184
|
+
| StyleSheet
|
|
185
|
+
| Rule
|
|
186
|
+
| Atrule
|
|
187
|
+
| SelectorList
|
|
188
|
+
| Block
|
|
189
|
+
| ComplexSelector
|
|
190
|
+
| RelativeSelector
|
|
191
|
+
| Combinator
|
|
192
|
+
| SimpleSelector
|
|
193
|
+
| Declaration;
|
|
194
|
+
}
|
|
27
195
|
}
|
|
28
196
|
|
|
29
197
|
declare module 'estree-jsx' {
|
|
@@ -32,11 +200,11 @@ declare module 'estree-jsx' {
|
|
|
32
200
|
}
|
|
33
201
|
|
|
34
202
|
interface JSXOpeningElement {
|
|
35
|
-
loc: SourceLocation;
|
|
203
|
+
loc: ESTree.SourceLocation;
|
|
36
204
|
}
|
|
37
205
|
|
|
38
206
|
interface JSXClosingElement {
|
|
39
|
-
loc: SourceLocation;
|
|
207
|
+
loc: ESTree.SourceLocation;
|
|
40
208
|
}
|
|
41
209
|
}
|
|
42
210
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** @import { Block } from '#client' */
|
|
2
2
|
|
|
3
3
|
import { branch, destroy_block, render, render_spread } from './blocks.js';
|
|
4
|
-
import { COMPOSITE_BLOCK,
|
|
4
|
+
import { COMPOSITE_BLOCK, DEFAULT_NAMESPACE, NAMESPACE_URI } from './constants.js';
|
|
5
5
|
import { active_block, active_namespace, with_ns } from './runtime.js';
|
|
6
6
|
import { top_element_to_ns } from './utils.js';
|
|
7
7
|
|
|
@@ -32,8 +32,8 @@ export function composite(get_component, node, props) {
|
|
|
32
32
|
var block = active_block;
|
|
33
33
|
/** @type {ComponentFunction} */ (component)(anchor, props, block);
|
|
34
34
|
});
|
|
35
|
-
} else {
|
|
36
|
-
// Custom element
|
|
35
|
+
} else if (component != null) {
|
|
36
|
+
// Custom element - only create if component is not null/undefined
|
|
37
37
|
var run = () => {
|
|
38
38
|
var block = /** @type {Block} */ (active_block);
|
|
39
39
|
|
|
@@ -59,7 +59,6 @@ describe('composite > render', () => {
|
|
|
59
59
|
let name = 'Click Me';
|
|
60
60
|
|
|
61
61
|
<Child class="my-button">{name}</Child>
|
|
62
|
-
|
|
63
62
|
}
|
|
64
63
|
|
|
65
64
|
component Child({ children, ...rest }: { children: string; class: string }) {
|
|
@@ -82,4 +81,25 @@ describe('composite > render', () => {
|
|
|
82
81
|
|
|
83
82
|
render(ArrayTest);
|
|
84
83
|
});
|
|
84
|
+
|
|
85
|
+
it('should not render <undefined> tag when a passed in component is undefined', () => {
|
|
86
|
+
component Child({ children, NonExistent, ...props }) {
|
|
87
|
+
<div {...props}>
|
|
88
|
+
<children />
|
|
89
|
+
<NonExistent />
|
|
90
|
+
</div>
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
component App() {
|
|
94
|
+
<Child />
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
render(App);
|
|
98
|
+
|
|
99
|
+
const div = container.querySelector('div');
|
|
100
|
+
const undefinedTag = container.querySelector('undefined');
|
|
101
|
+
|
|
102
|
+
expect(undefinedTag).toBeNull();
|
|
103
|
+
expect(div.innerHTML).not.toContain('<undefined');
|
|
104
|
+
});
|
|
85
105
|
});
|