@tsrx/core 0.0.1
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 +52 -0
- package/src/analyze/css-analyze.js +160 -0
- package/src/analyze/validation.js +167 -0
- package/src/comment-utils.js +91 -0
- package/src/constants.js +21 -0
- package/src/errors.js +82 -0
- package/src/helpers.d.ts +11 -0
- package/src/identifier-utils.js +80 -0
- package/src/index.js +141 -0
- package/src/parse/index.js +772 -0
- package/src/parse/style.js +704 -0
- package/src/scope.js +476 -0
- package/src/source-map-utils.js +358 -0
- package/src/transform/segments.js +2140 -0
- package/src/transform/stylesheet.js +545 -0
- package/src/utils/ast.js +273 -0
- package/src/utils/builders.js +1287 -0
- package/src/utils/escaping.js +26 -0
- package/src/utils/events.js +154 -0
- package/src/utils/hashing.js +15 -0
- package/src/utils/normalize_css_property_name.js +23 -0
- package/src/utils/patterns.js +24 -0
- package/src/utils/sanitize_template_string.js +7 -0
- package/src/utils.js +165 -0
- package/types/acorn.d.ts +3 -0
- package/types/estree-jsx.d.ts +3 -0
- package/types/estree.d.ts +3 -0
- package/types/index.d.ts +1550 -0
- package/types/parse.d.ts +1722 -0
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tsrx/core",
|
|
3
|
+
"description": "Core compiler infrastructure for tsrx-based frameworks",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"author": "Dominic Gannaway",
|
|
6
|
+
"version": "0.0.1",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/Ripple-TS/ripple.git",
|
|
11
|
+
"directory": "packages/tsrx"
|
|
12
|
+
},
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"default": "./src/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./types": {
|
|
18
|
+
"types": "./types/index.d.ts"
|
|
19
|
+
},
|
|
20
|
+
"./types/estree": {
|
|
21
|
+
"types": "./types/estree.d.ts"
|
|
22
|
+
},
|
|
23
|
+
"./types/estree-jsx": {
|
|
24
|
+
"types": "./types/estree-jsx.d.ts"
|
|
25
|
+
},
|
|
26
|
+
"./types/acorn": {
|
|
27
|
+
"types": "./types/acorn.d.ts"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@jridgewell/sourcemap-codec": "catalog:default",
|
|
32
|
+
"@sveltejs/acorn-typescript": "catalog:default",
|
|
33
|
+
"acorn": "catalog:default",
|
|
34
|
+
"esrap": "catalog:default",
|
|
35
|
+
"is-reference": "catalog:default",
|
|
36
|
+
"magic-string": "catalog:default",
|
|
37
|
+
"zimmerframe": "catalog:default",
|
|
38
|
+
"@types/estree": "catalog:default",
|
|
39
|
+
"@types/estree-jsx": "catalog:default"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/node": "catalog:default",
|
|
43
|
+
"@typescript-eslint/types": "catalog:default",
|
|
44
|
+
"typescript": "catalog:default",
|
|
45
|
+
"@volar/language-core": "catalog:default",
|
|
46
|
+
"vscode-languageserver-types": "catalog:default"
|
|
47
|
+
},
|
|
48
|
+
"files": [
|
|
49
|
+
"src",
|
|
50
|
+
"types"
|
|
51
|
+
]
|
|
52
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/** @import * as AST from 'estree' */
|
|
2
|
+
|
|
3
|
+
import { walk } from 'zimmerframe';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* True if is `:global` without arguments
|
|
7
|
+
* @param {AST.CSS.SimpleSelector} simple_selector
|
|
8
|
+
*/
|
|
9
|
+
function is_global_block_selector(simple_selector) {
|
|
10
|
+
return (
|
|
11
|
+
simple_selector.type === 'PseudoClassSelector' &&
|
|
12
|
+
simple_selector.name === 'global' &&
|
|
13
|
+
simple_selector.args === null
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* True if is `:global(...)` or `:global` and no pseudo class that is scoped.
|
|
19
|
+
* @param {AST.CSS.RelativeSelector} relative_selector
|
|
20
|
+
*/
|
|
21
|
+
function is_global(relative_selector) {
|
|
22
|
+
const first = relative_selector.selectors[0];
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
first?.type === 'PseudoClassSelector' &&
|
|
26
|
+
first.name === 'global' &&
|
|
27
|
+
(first.args === null ||
|
|
28
|
+
// Only these two selector types keep the whole selector global, because e.g.
|
|
29
|
+
// :global(button).x means that the selector is still scoped because of the .x
|
|
30
|
+
relative_selector.selectors.every(
|
|
31
|
+
(selector) =>
|
|
32
|
+
selector.type === 'PseudoClassSelector' || selector.type === 'PseudoElementSelector',
|
|
33
|
+
))
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Analyze CSS and set metadata for global selectors
|
|
39
|
+
* @param {AST.CSS.Node} css - The CSS AST
|
|
40
|
+
*/
|
|
41
|
+
export function analyze_css(css) {
|
|
42
|
+
walk(css, /** @type {{ rule: AST.CSS.Rule | null }} */ ({ rule: null }), {
|
|
43
|
+
Rule(node, context) {
|
|
44
|
+
node.metadata.parent_rule = context.state.rule;
|
|
45
|
+
|
|
46
|
+
// Check for :global blocks
|
|
47
|
+
// A global block is when the selector starts with :global and has no local selectors before it
|
|
48
|
+
for (const complex_selector of node.prelude.children) {
|
|
49
|
+
let is_global_block = false;
|
|
50
|
+
|
|
51
|
+
for (
|
|
52
|
+
let selector_idx = 0;
|
|
53
|
+
selector_idx < complex_selector.children.length;
|
|
54
|
+
selector_idx++
|
|
55
|
+
) {
|
|
56
|
+
const child = complex_selector.children[selector_idx];
|
|
57
|
+
const idx = child.selectors.findIndex(is_global_block_selector);
|
|
58
|
+
|
|
59
|
+
if (is_global_block) {
|
|
60
|
+
// All selectors after :global are unscoped
|
|
61
|
+
child.metadata.is_global_like = true;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Only set is_global_block if this is the FIRST RelativeSelector and it starts with :global
|
|
65
|
+
if (selector_idx === 0 && idx === 0) {
|
|
66
|
+
// `child` starts with `:global` and is the first selector in the chain
|
|
67
|
+
is_global_block = true;
|
|
68
|
+
node.metadata.is_global_block = is_global_block;
|
|
69
|
+
} else if (idx === 0) {
|
|
70
|
+
// :global appears later in the selector chain (e.g., `div :global p`)
|
|
71
|
+
// Set is_global_block for marking subsequent selectors as global-like
|
|
72
|
+
is_global_block = true;
|
|
73
|
+
} else if (idx !== -1) {
|
|
74
|
+
// `:global` is not at the start - this is invalid but we'll let it through for now
|
|
75
|
+
// The transform phase will handle removal
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Pass the current rule as state to nested nodes
|
|
81
|
+
const state = { rule: node };
|
|
82
|
+
context.visit(node.prelude, state);
|
|
83
|
+
context.visit(node.block, state);
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
ComplexSelector(node, context) {
|
|
87
|
+
// Set the rule metadata before analyzing children
|
|
88
|
+
node.metadata.rule = context.state.rule;
|
|
89
|
+
|
|
90
|
+
context.next(); // analyze relevant selectors first
|
|
91
|
+
|
|
92
|
+
{
|
|
93
|
+
const global = node.children.find(is_global);
|
|
94
|
+
|
|
95
|
+
if (global) {
|
|
96
|
+
const is_nested = context.path.at(-2)?.type === 'PseudoClassSelector';
|
|
97
|
+
if (
|
|
98
|
+
is_nested &&
|
|
99
|
+
!(/** @type {AST.CSS.PseudoClassSelector} */ (global.selectors[0]).args)
|
|
100
|
+
) {
|
|
101
|
+
throw new Error(`A :global selector cannot be inside a pseudoclass.`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const idx = node.children.indexOf(global);
|
|
105
|
+
const first = /** @type {AST.CSS.PseudoClassSelector} */ (global.selectors[0]);
|
|
106
|
+
if (first.args !== null && idx !== 0 && idx !== node.children.length - 1) {
|
|
107
|
+
// ensure `:global(...)` is not used in the middle of a selector (but multiple `global(...)` in sequence are ok)
|
|
108
|
+
for (let i = idx + 1; i < node.children.length; i++) {
|
|
109
|
+
if (!is_global(node.children[i])) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
`:global(...) can be at the start or end of a selector sequence, but not in the middle.`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Set is_global metadata
|
|
120
|
+
node.metadata.is_global = node.children.every(
|
|
121
|
+
({ metadata }) => metadata.is_global || metadata.is_global_like,
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
node.metadata.used ||= node.metadata.is_global;
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
PseudoClassSelector(node, context) {
|
|
128
|
+
// Walk into :is(), :where(), :has(), and :not() to initialize metadata for nested selectors
|
|
129
|
+
if (
|
|
130
|
+
(node.name === 'is' ||
|
|
131
|
+
node.name === 'where' ||
|
|
132
|
+
node.name === 'has' ||
|
|
133
|
+
node.name === 'not') &&
|
|
134
|
+
node.args
|
|
135
|
+
) {
|
|
136
|
+
context.next();
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
RelativeSelector(node, context) {
|
|
140
|
+
// Check if this selector is a :global selector
|
|
141
|
+
node.metadata.is_global = node.selectors.length >= 1 && is_global(node);
|
|
142
|
+
|
|
143
|
+
// Check for :root and other global-like selectors
|
|
144
|
+
if (
|
|
145
|
+
node.selectors.length >= 1 &&
|
|
146
|
+
node.selectors.every(
|
|
147
|
+
(selector) =>
|
|
148
|
+
selector.type === 'PseudoClassSelector' || selector.type === 'PseudoElementSelector',
|
|
149
|
+
)
|
|
150
|
+
) {
|
|
151
|
+
const first = node.selectors[0];
|
|
152
|
+
node.metadata.is_global_like ||=
|
|
153
|
+
(first.type === 'PseudoClassSelector' && first.name === 'host') ||
|
|
154
|
+
(first.type === 'PseudoClassSelector' && first.name === 'root');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
context.next();
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
@import * as AST from 'estree';
|
|
3
|
+
@import { AnalysisContext, CompileError } from '../../types/index';
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { error } from '../errors.js';
|
|
7
|
+
|
|
8
|
+
const invalid_nestings = {
|
|
9
|
+
// <p> cannot contain block-level elements
|
|
10
|
+
p: new Set([
|
|
11
|
+
'address',
|
|
12
|
+
'article',
|
|
13
|
+
'aside',
|
|
14
|
+
'blockquote',
|
|
15
|
+
'details',
|
|
16
|
+
'div',
|
|
17
|
+
'dl',
|
|
18
|
+
'fieldset',
|
|
19
|
+
'figcaption',
|
|
20
|
+
'figure',
|
|
21
|
+
'footer',
|
|
22
|
+
'form',
|
|
23
|
+
'h1',
|
|
24
|
+
'h2',
|
|
25
|
+
'h3',
|
|
26
|
+
'h4',
|
|
27
|
+
'h5',
|
|
28
|
+
'h6',
|
|
29
|
+
'header',
|
|
30
|
+
'hgroup',
|
|
31
|
+
'hr',
|
|
32
|
+
'main',
|
|
33
|
+
'menu',
|
|
34
|
+
'nav',
|
|
35
|
+
'ol',
|
|
36
|
+
'p',
|
|
37
|
+
'pre',
|
|
38
|
+
'section',
|
|
39
|
+
'table',
|
|
40
|
+
'ul',
|
|
41
|
+
]),
|
|
42
|
+
// <span> cannot contain block-level elements
|
|
43
|
+
span: new Set([
|
|
44
|
+
'address',
|
|
45
|
+
'article',
|
|
46
|
+
'aside',
|
|
47
|
+
'blockquote',
|
|
48
|
+
'details',
|
|
49
|
+
'div',
|
|
50
|
+
'dl',
|
|
51
|
+
'fieldset',
|
|
52
|
+
'figcaption',
|
|
53
|
+
'figure',
|
|
54
|
+
'footer',
|
|
55
|
+
'form',
|
|
56
|
+
'h1',
|
|
57
|
+
'h2',
|
|
58
|
+
'h3',
|
|
59
|
+
'h4',
|
|
60
|
+
'h5',
|
|
61
|
+
'h6',
|
|
62
|
+
'header',
|
|
63
|
+
'hgroup',
|
|
64
|
+
'hr',
|
|
65
|
+
'main',
|
|
66
|
+
'menu',
|
|
67
|
+
'nav',
|
|
68
|
+
'ol',
|
|
69
|
+
'p',
|
|
70
|
+
'pre',
|
|
71
|
+
'section',
|
|
72
|
+
'table',
|
|
73
|
+
'ul',
|
|
74
|
+
]),
|
|
75
|
+
// Interactive elements cannot be nested
|
|
76
|
+
a: new Set(['a', 'button']),
|
|
77
|
+
button: new Set(['a', 'button']),
|
|
78
|
+
// Form elements
|
|
79
|
+
label: new Set(['label']),
|
|
80
|
+
form: new Set(['form']),
|
|
81
|
+
// Headings cannot be nested within each other
|
|
82
|
+
h1: new Set(['h1', 'h2', 'h3', 'h4', 'h5', 'h6']),
|
|
83
|
+
h2: new Set(['h1', 'h2', 'h3', 'h4', 'h5', 'h6']),
|
|
84
|
+
h3: new Set(['h1', 'h2', 'h3', 'h4', 'h5', 'h6']),
|
|
85
|
+
h4: new Set(['h1', 'h2', 'h3', 'h4', 'h5', 'h6']),
|
|
86
|
+
h5: new Set(['h1', 'h2', 'h3', 'h4', 'h5', 'h6']),
|
|
87
|
+
h6: new Set(['h1', 'h2', 'h3', 'h4', 'h5', 'h6']),
|
|
88
|
+
// Table structure
|
|
89
|
+
table: new Set(['table', 'tr', 'td', 'th']), // Can only contain caption, colgroup, thead, tbody, tfoot
|
|
90
|
+
thead: new Set(['caption', 'colgroup', 'thead', 'tbody', 'tfoot', 'td', 'th']), // Can only contain tr
|
|
91
|
+
tbody: new Set(['caption', 'colgroup', 'thead', 'tbody', 'tfoot', 'td', 'th']), // Can only contain tr
|
|
92
|
+
tfoot: new Set(['caption', 'colgroup', 'thead', 'tbody', 'tfoot', 'td', 'th']), // Can only contain tr
|
|
93
|
+
tr: new Set(['caption', 'colgroup', 'thead', 'tbody', 'tfoot', 'tr']), // Can only contain td and th
|
|
94
|
+
td: new Set(['td', 'th']), // Cannot nest td/th elements
|
|
95
|
+
th: new Set(['td', 'th']), // Cannot nest td/th elements
|
|
96
|
+
// Media elements
|
|
97
|
+
picture: new Set(['picture']),
|
|
98
|
+
// Main landmark - only one per document, cannot be nested
|
|
99
|
+
main: new Set(['main']),
|
|
100
|
+
// Other semantic restrictions
|
|
101
|
+
figcaption: new Set(['figcaption']),
|
|
102
|
+
dt: new Set([
|
|
103
|
+
'header',
|
|
104
|
+
'footer',
|
|
105
|
+
'article',
|
|
106
|
+
'aside',
|
|
107
|
+
'nav',
|
|
108
|
+
'section',
|
|
109
|
+
'h1',
|
|
110
|
+
'h2',
|
|
111
|
+
'h3',
|
|
112
|
+
'h4',
|
|
113
|
+
'h5',
|
|
114
|
+
'h6',
|
|
115
|
+
]),
|
|
116
|
+
// No interactive content inside summary
|
|
117
|
+
summary: new Set(['summary']),
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* @param {AST.Element} element
|
|
122
|
+
* @returns {string | null}
|
|
123
|
+
*/
|
|
124
|
+
function get_element_tag(element) {
|
|
125
|
+
return element.id.type === 'Identifier' ? element.id.name : null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* @param {AST.Element} element
|
|
130
|
+
* @param {AnalysisContext} context
|
|
131
|
+
* @param {CompileError[]} [errors]
|
|
132
|
+
*/
|
|
133
|
+
export function validate_nesting(element, context, errors) {
|
|
134
|
+
const tag = get_element_tag(element);
|
|
135
|
+
|
|
136
|
+
if (tag === null) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
for (let i = context.path.length - 1; i >= 0; i--) {
|
|
141
|
+
const parent = context.path[i];
|
|
142
|
+
if (parent.type === 'Element') {
|
|
143
|
+
const parent_tag = get_element_tag(parent);
|
|
144
|
+
if (parent_tag === null) {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (parent_tag in invalid_nestings) {
|
|
149
|
+
const validation_set =
|
|
150
|
+
invalid_nestings[/** @type {keyof typeof invalid_nestings} */ (parent_tag)];
|
|
151
|
+
if (validation_set.has(tag)) {
|
|
152
|
+
error(
|
|
153
|
+
`Invalid HTML nesting: <${tag}> cannot be a descendant of <${parent_tag}>.`,
|
|
154
|
+
context.state.analysis.module.filename,
|
|
155
|
+
element,
|
|
156
|
+
errors,
|
|
157
|
+
context.state.analysis.comments,
|
|
158
|
+
);
|
|
159
|
+
} else {
|
|
160
|
+
// if my parent has a set of invalid children
|
|
161
|
+
// and i'm not in it, then i'm valid
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import * as AST from 'estree'
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Check if a comment is a TypeScript pragma (line comment)
|
|
7
|
+
* @param {AST.CommentWithLocation} comment
|
|
8
|
+
* @returns {boolean}
|
|
9
|
+
*/
|
|
10
|
+
export function is_ts_pragma(comment) {
|
|
11
|
+
if (comment.type !== 'Line') return false;
|
|
12
|
+
|
|
13
|
+
const pragmas = ['@ts-ignore', '@ts-expect-error', '@ts-nocheck', '@ts-check'];
|
|
14
|
+
return pragmas.some((pragma) => comment.value.trimStart().startsWith(pragma));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Check if a comment is a triple-slash directive
|
|
19
|
+
* /// <reference path="..." />
|
|
20
|
+
* @param {AST.CommentWithLocation} comment
|
|
21
|
+
* @returns {boolean}
|
|
22
|
+
*/
|
|
23
|
+
export function is_triple_slash_directive(comment) {
|
|
24
|
+
if (comment.type !== 'Line') return false;
|
|
25
|
+
|
|
26
|
+
// Triple slash directives start with / after the // is stripped
|
|
27
|
+
// So the value should start with / followed by <reference, <amd-module, or <amd-dependency
|
|
28
|
+
const value = comment.value.trim();
|
|
29
|
+
return /^\/\s*<(reference|amd-module|amd-dependency)/.test(value);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Check if a comment is a JSDoc comment with TypeScript annotations
|
|
34
|
+
* Examples: block comments containing `@type`, `@typedef`, `@param`, `@returns`, etc.
|
|
35
|
+
* @param {AST.CommentWithLocation} comment
|
|
36
|
+
* @returns {boolean}
|
|
37
|
+
*/
|
|
38
|
+
export function is_jsdoc_ts_annotation(comment) {
|
|
39
|
+
if (comment.type !== 'Block') return false;
|
|
40
|
+
|
|
41
|
+
// JSDoc comments start with /** which means the value starts with * after /* is stripped
|
|
42
|
+
if (!comment.value.startsWith('*')) return false;
|
|
43
|
+
|
|
44
|
+
// Check if it contains TypeScript-relevant tags
|
|
45
|
+
const tsAnnotations = [
|
|
46
|
+
'@type',
|
|
47
|
+
'@typedef',
|
|
48
|
+
'@param',
|
|
49
|
+
'@returns',
|
|
50
|
+
'@template',
|
|
51
|
+
'@extends',
|
|
52
|
+
'@implements',
|
|
53
|
+
'@satisfies',
|
|
54
|
+
'@overload',
|
|
55
|
+
'@import',
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
return tsAnnotations.some((annotation) => comment.value.includes(annotation));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Check if a comment should be preserved in to_ts mode
|
|
63
|
+
* @param {AST.CommentWithLocation} comment
|
|
64
|
+
* @returns {boolean}
|
|
65
|
+
*/
|
|
66
|
+
export function should_preserve_comment(comment) {
|
|
67
|
+
return (
|
|
68
|
+
is_ts_pragma(comment) || is_triple_slash_directive(comment) || is_jsdoc_ts_annotation(comment)
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Format a comment for output
|
|
74
|
+
* @param {AST.CommentWithLocation} comment
|
|
75
|
+
* @returns {string}
|
|
76
|
+
*/
|
|
77
|
+
export function format_comment(comment) {
|
|
78
|
+
if (comment.type === 'Line') {
|
|
79
|
+
// Check if it's a triple-slash directive (value starts with /)
|
|
80
|
+
if (comment.value.trimStart().startsWith('/')) {
|
|
81
|
+
return `/// ${comment.value.trimStart().slice(1)}`;
|
|
82
|
+
}
|
|
83
|
+
return `// ${comment.value.trim()}`;
|
|
84
|
+
} else {
|
|
85
|
+
// Block comment - check if it's a JSDoc (value starts with *)
|
|
86
|
+
if (comment.value.startsWith('*')) {
|
|
87
|
+
return `/** ${comment.value.trim().slice(1)} */`;
|
|
88
|
+
}
|
|
89
|
+
return `/* ${comment.value.trim()} */`;
|
|
90
|
+
}
|
|
91
|
+
}
|
package/src/constants.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export const TEMPLATE_FRAGMENT = 1;
|
|
2
|
+
export const TEMPLATE_USE_IMPORT_NODE = 1 << 1;
|
|
3
|
+
export const IS_CONTROLLED = 1 << 2;
|
|
4
|
+
export const IS_INDEXED = 1 << 3;
|
|
5
|
+
export const TEMPLATE_SVG_NAMESPACE = 1 << 5;
|
|
6
|
+
export const TEMPLATE_MATHML_NAMESPACE = 1 << 6;
|
|
7
|
+
|
|
8
|
+
export const HYDRATION_START = '[';
|
|
9
|
+
export const HYDRATION_END = ']';
|
|
10
|
+
export const HYDRATION_ERROR = {};
|
|
11
|
+
|
|
12
|
+
export const BLOCK_OPEN = `<!--${HYDRATION_START}-->`;
|
|
13
|
+
export const BLOCK_CLOSE = `<!--${HYDRATION_END}-->`;
|
|
14
|
+
export const EMPTY_COMMENT = `<!---->`;
|
|
15
|
+
|
|
16
|
+
export const ELEMENT_NODE = 1;
|
|
17
|
+
export const TEXT_NODE = 3;
|
|
18
|
+
export const COMMENT_NODE = 8;
|
|
19
|
+
export const DOCUMENT_FRAGMENT_NODE = 11;
|
|
20
|
+
|
|
21
|
+
export const DEFAULT_NAMESPACE = 'html';
|
package/src/errors.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
@import * as AST from 'estree';
|
|
3
|
+
@import { CompileError } from '../types/index';
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
*
|
|
8
|
+
* @param {string} message
|
|
9
|
+
* @param {string | null} filename
|
|
10
|
+
* @param {AST.Node | AST.NodeWithLocation} node
|
|
11
|
+
* @param {CompileError[]} [errors]
|
|
12
|
+
* @param {AST.CommentWithLocation[]} [comments]
|
|
13
|
+
* @returns {void}
|
|
14
|
+
*/
|
|
15
|
+
export function error(message, filename, node, errors, comments) {
|
|
16
|
+
if (errors && comments && is_error_suppressed(node, comments)) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const error = /** @type {CompileError} */ (new Error(message));
|
|
21
|
+
|
|
22
|
+
// same as the acorn compiler error
|
|
23
|
+
error.pos = node.start ?? undefined;
|
|
24
|
+
error.raisedAt = node.end ?? undefined;
|
|
25
|
+
|
|
26
|
+
// custom properties
|
|
27
|
+
error.fileName = filename;
|
|
28
|
+
error.end = node.end ?? undefined;
|
|
29
|
+
error.loc = !node.loc
|
|
30
|
+
? undefined
|
|
31
|
+
: {
|
|
32
|
+
start: {
|
|
33
|
+
line: node.loc.start.line,
|
|
34
|
+
column: node.loc.start.column,
|
|
35
|
+
},
|
|
36
|
+
end: {
|
|
37
|
+
line: node.loc.end.line,
|
|
38
|
+
column: node.loc.end.column,
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
if (errors) {
|
|
43
|
+
error.type = 'usage';
|
|
44
|
+
errors.push(error);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
error.type = 'fatal';
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @param {AST.CommentWithLocation} comment
|
|
54
|
+
* @return {boolean}
|
|
55
|
+
*/
|
|
56
|
+
function is_error_suppress_comment(comment) {
|
|
57
|
+
const text = comment.value.trim();
|
|
58
|
+
return (
|
|
59
|
+
text.startsWith('@tsrx-ignore') ||
|
|
60
|
+
text.startsWith('@tsrx-expect-error') ||
|
|
61
|
+
text.startsWith('@ripple-ignore') ||
|
|
62
|
+
text.startsWith('@ripple-expect-error')
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @param {AST.Node | AST.NodeWithLocation} node
|
|
68
|
+
* @param {AST.CommentWithLocation[]} comments
|
|
69
|
+
*/
|
|
70
|
+
function is_error_suppressed(node, comments) {
|
|
71
|
+
if (node.loc) {
|
|
72
|
+
const node_start_line = node.loc.start.line;
|
|
73
|
+
for (const comment of comments) {
|
|
74
|
+
if (comment.type === 'Line' && comment.loc.start.line === node_start_line - 1) {
|
|
75
|
+
if (is_error_suppress_comment(comment)) {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
}
|
package/src/helpers.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type RequireAllOrNone<T, K extends keyof T> =
|
|
2
|
+
| (T & Required<Pick<T, K>>)
|
|
3
|
+
| (T & { [P in K]?: never });
|
|
4
|
+
|
|
5
|
+
export type RequiredPresent<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;
|
|
6
|
+
|
|
7
|
+
export type Nullable<T> = T | null;
|
|
8
|
+
|
|
9
|
+
export type Nullish<T> = T | null | undefined;
|
|
10
|
+
|
|
11
|
+
export type NestedArray<T> = (T | NestedArray<T>)[];
|
|
@@ -0,0 +1,80 @@
|
|
|
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
|
+
* Finds the next uppercase character or returns name.length
|
|
18
|
+
* @param {string} name
|
|
19
|
+
* @param {number} start
|
|
20
|
+
* @returns {number}
|
|
21
|
+
*/
|
|
22
|
+
function find_next_uppercase(name, start) {
|
|
23
|
+
for (let i = start; i < name.length; i++) {
|
|
24
|
+
if (name[i] === name[i].toUpperCase()) {
|
|
25
|
+
return i;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return name.length;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {string} encoded
|
|
33
|
+
* @returns {string}
|
|
34
|
+
*/
|
|
35
|
+
function decode_utf16_string(encoded) {
|
|
36
|
+
return encoded.replace(DECODE_UTF16_REGEX, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {string} name
|
|
41
|
+
* @returns {string}
|
|
42
|
+
*/
|
|
43
|
+
export function obfuscate_identifier(name) {
|
|
44
|
+
const first_char = name[0];
|
|
45
|
+
let start = 0;
|
|
46
|
+
if (first_char === '@' || first_char === '#') {
|
|
47
|
+
const encoded = encode_utf16_char(first_char);
|
|
48
|
+
name = encoded + name.slice(1);
|
|
49
|
+
start = encoded.length;
|
|
50
|
+
} else if (first_char === first_char.toUpperCase()) {
|
|
51
|
+
start = 1;
|
|
52
|
+
}
|
|
53
|
+
const index = find_next_uppercase(name, start);
|
|
54
|
+
|
|
55
|
+
const first_part = name.slice(0, index);
|
|
56
|
+
const second_part = name.slice(index);
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
IDENTIFIER_OBFUSCATION_PREFIX +
|
|
60
|
+
(second_part ? second_part + '__' + first_part : first_part + '__')
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @param {string} name
|
|
66
|
+
* @returns {boolean}
|
|
67
|
+
*/
|
|
68
|
+
export function is_identifier_obfuscated(name) {
|
|
69
|
+
return name.startsWith(IDENTIFIER_OBFUSCATION_PREFIX);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @param {string} name
|
|
74
|
+
* @returns {string}
|
|
75
|
+
*/
|
|
76
|
+
export function deobfuscate_identifier(name) {
|
|
77
|
+
name = name.replaceAll(IDENTIFIER_OBFUSCATION_PREFIX, '');
|
|
78
|
+
const parts = name.split('__');
|
|
79
|
+
return decode_utf16_string((parts[1] ? parts[1] : '') + parts[0]);
|
|
80
|
+
}
|