@cplieger/deadset-ts 4.0.1 → 5.1.0
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/README.md +4 -4
- package/dist/bin/component-mapper.js +5 -0
- package/dist/bin/mapper-connection.js +101 -0
- package/dist/bin/node-host.js +24 -2
- package/dist/src/analysis.js +301 -41
- package/dist/src/component-files.js +612 -0
- package/dist/src/component-layer.js +132 -0
- package/dist/src/config.js +9 -2
- package/dist/src/configuration-files.js +290 -0
- package/dist/src/conformance-record.js +3 -9
- package/dist/src/contextual-uses.js +240 -0
- package/dist/src/convention-rows.js +389 -0
- package/dist/src/conventions.js +565 -0
- package/dist/src/dependencies.js +36 -3
- package/dist/src/destructuring.js +6 -6
- package/dist/src/discover.js +10 -3
- package/dist/src/emit-map.js +72 -0
- package/dist/src/entry-point-gaps.js +81 -35
- package/dist/src/entry-points.js +50 -32
- package/dist/src/exempt-classes.js +6 -1
- package/dist/src/exempt.js +32 -20
- package/dist/src/findings/completion.js +46 -32
- package/dist/src/findings/emitters.js +22 -1
- package/dist/src/findings/interfaces.js +6 -2
- package/dist/src/findings/reads-and-writes.js +53 -9
- package/dist/src/findings/unused-declarations.js +47 -3
- package/dist/src/generated-file.js +24 -0
- package/dist/src/graph.js +13 -5
- package/dist/src/interface-satisfaction.js +2 -68
- package/dist/src/inventory.js +8 -3
- package/dist/src/kinds.js +6 -5
- package/dist/src/manifest.js +37 -11
- package/dist/src/matrix.js +21 -13
- package/dist/src/partial.js +3 -1
- package/dist/src/pnpm-workspace.js +120 -0
- package/dist/src/position.js +17 -2
- package/dist/src/print.js +2 -0
- package/dist/src/query.js +15 -0
- package/dist/src/references.js +404 -36
- package/dist/src/report.js +26 -6
- package/dist/src/reporters/annotations.js +10 -3
- package/dist/src/reporters/template.js +3 -0
- package/dist/src/resolve.js +37 -11
- package/dist/src/roots.js +37 -8
- package/dist/src/run.js +13 -3
- package/dist/src/schema.js +2 -0
- package/dist/src/session.js +141 -34
- package/dist/src/setup-failure.js +23 -0
- package/dist/src/source-files.js +4 -61
- package/dist/src/stores.js +3 -0
- package/dist/src/sweep.js +4 -2
- package/dist/src/template-field.js +38 -6
- package/dist/src/test-support.js +59 -0
- package/dist/src/type-errors.js +252 -0
- package/dist/src/type-query-alias.js +92 -0
- package/dist/src/value-flow.js +74 -0
- package/dist/src/verbs/analyze.js +24 -4
- package/dist/src/verbs/explain.js +4 -1
- package/dist/src/verbs/print-projects.js +17 -56
- package/dist/src/verbs/print-retained.js +7 -2
- package/dist/src/verbs/print-roots.js +7 -2
- package/dist/src/version-range.js +255 -0
- package/dist/src/version.js +1 -1
- package/dist/src/workspace-manifests.js +68 -0
- package/dist/src/workspace-programs.js +342 -0
- package/dist/src/workspace-resolution.js +307 -0
- package/dist/src/workspace-run.js +75 -0
- package/dist/src/workspace.js +358 -0
- package/jsr.json +1 -1
- package/package.json +3 -2
- package/src/analysis.ts +475 -72
- package/src/component-files.ts +718 -0
- package/src/component-layer.ts +160 -0
- package/src/config.ts +11 -2
- package/src/configuration-files.ts +370 -0
- package/src/conformance-record.ts +3 -10
- package/src/contextual-uses.ts +354 -0
- package/src/convention-rows.ts +448 -0
- package/src/conventions.ts +788 -0
- package/src/dependencies.ts +49 -2
- package/src/destructuring.ts +16 -6
- package/src/discover.ts +10 -6
- package/src/emit-map.ts +93 -0
- package/src/entry-point-gaps.ts +92 -38
- package/src/entry-points.ts +79 -36
- package/src/exempt-classes.ts +6 -1
- package/src/exempt.ts +69 -20
- package/src/file-facts.ts +2 -0
- package/src/findings/completion.ts +55 -36
- package/src/findings/emitters.ts +32 -1
- package/src/findings/interfaces.ts +9 -2
- package/src/findings/reads-and-writes.ts +70 -9
- package/src/findings/unused-declarations.ts +60 -3
- package/src/generated-file.ts +27 -0
- package/src/graph.ts +15 -4
- package/src/host.ts +23 -0
- package/src/index.ts +1 -1
- package/src/interface-satisfaction.ts +1 -92
- package/src/inventory.ts +17 -3
- package/src/kinds.ts +7 -5
- package/src/manifest.ts +47 -14
- package/src/matrix.ts +43 -15
- package/src/partial.ts +6 -1
- package/src/pnpm-workspace.ts +135 -0
- package/src/position.ts +23 -2
- package/src/print.ts +2 -0
- package/src/query.ts +26 -0
- package/src/references.ts +472 -40
- package/src/report.ts +56 -6
- package/src/reporters/annotations.ts +12 -3
- package/src/reporters/template.ts +3 -0
- package/src/resolve.ts +65 -11
- package/src/roots.ts +70 -9
- package/src/run.ts +15 -5
- package/src/schema.ts +2 -0
- package/src/session.ts +218 -37
- package/src/setup-failure.ts +39 -0
- package/src/source-files.ts +4 -66
- package/src/stores.ts +9 -0
- package/src/sweep.ts +9 -2
- package/src/template-field.ts +42 -7
- package/src/test-support.ts +77 -0
- package/src/type-errors.ts +340 -0
- package/src/type-query-alias.ts +133 -0
- package/src/value-flow.ts +104 -0
- package/src/verbs/analyze.ts +38 -4
- package/src/verbs/explain.ts +4 -1
- package/src/verbs/print-projects.ts +17 -69
- package/src/verbs/print-retained.ts +7 -2
- package/src/verbs/print-roots.ts +7 -2
- package/src/verbs/verb.ts +6 -3
- package/src/version-range.ts +309 -0
- package/src/version.ts +1 -1
- package/src/workspace-manifests.ts +100 -0
- package/src/workspace-programs.ts +486 -0
- package/src/workspace-resolution.ts +393 -0
- package/src/workspace-run.ts +148 -0
- package/src/workspace.ts +463 -0
|
@@ -0,0 +1,718 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A component file read as markup holding script blocks, scanned only for what decides where
|
|
3
|
+
* a block starts and ends. Its blocks become one module that keeps every block at its own
|
|
4
|
+
* offsets and turns every other code unit but a line terminator into a space, so a position
|
|
5
|
+
* of the module is the same position of the file. A default export and the imports a `src`
|
|
6
|
+
* attribute names are appended past the file's end.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** The language one block is written in, as the module's virtual extension spells it. */
|
|
10
|
+
export type BlockLanguage = "ts" | "tsx" | "js" | "jsx";
|
|
11
|
+
|
|
12
|
+
/** One block the module holds, the frontmatter included. */
|
|
13
|
+
export interface Block {
|
|
14
|
+
/** The offset the block's markup starts at: the `<` of its start tag, or 0 for frontmatter. */
|
|
15
|
+
readonly tagStart: number;
|
|
16
|
+
readonly contentStart: number;
|
|
17
|
+
/** The offset one past the block's last code unit. */
|
|
18
|
+
readonly contentEnd: number;
|
|
19
|
+
readonly language: BlockLanguage;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** One file a `<script src>` element names, which the module imports whole. */
|
|
23
|
+
export interface SourceImport {
|
|
24
|
+
/** The specifier the module imports, relative to the component file. */
|
|
25
|
+
readonly specifier: string;
|
|
26
|
+
/** The offsets of the `src` attribute, name and value. */
|
|
27
|
+
readonly attributeStart: number;
|
|
28
|
+
readonly attributeEnd: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** A `<script` start tag beginning a line that the module holds no block for. */
|
|
32
|
+
export interface ComponentWarning {
|
|
33
|
+
/** The offset of the tag's `<`. */
|
|
34
|
+
readonly offset: number;
|
|
35
|
+
readonly reason: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** One run of markup an expression or an attribute value is written in. */
|
|
39
|
+
export interface MarkupAction {
|
|
40
|
+
/** The offset of the run's first code unit. */
|
|
41
|
+
readonly start: number;
|
|
42
|
+
readonly text: string;
|
|
43
|
+
/** The run as one line, the way an exemption record names its evidence. */
|
|
44
|
+
readonly action: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** What one component file's text is read as. */
|
|
48
|
+
export interface ComponentFile {
|
|
49
|
+
readonly blocks: readonly Block[];
|
|
50
|
+
readonly imports: readonly SourceImport[];
|
|
51
|
+
readonly warnings: readonly ComponentWarning[];
|
|
52
|
+
/** Every expression and attribute value of the markup, in file order. */
|
|
53
|
+
readonly markup: readonly MarkupAction[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The module the compiler reads in place of one component file. */
|
|
57
|
+
export interface ComponentModule {
|
|
58
|
+
/** The module's text: the file's length, then what the module appends. */
|
|
59
|
+
readonly text: string;
|
|
60
|
+
/** The virtual extension: the strongest language any block names, `.ts` for none. */
|
|
61
|
+
readonly extension: `.${BlockLanguage}`;
|
|
62
|
+
/**
|
|
63
|
+
* The span map in the compiler's tuple form: one verbatim segment per block and one
|
|
64
|
+
* atom segment from each appended import to the attribute that names it.
|
|
65
|
+
*/
|
|
66
|
+
readonly mappings: readonly (readonly [number, number, number, number, number])[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const LANGUAGES: ReadonlyMap<string, BlockLanguage> = new Map([
|
|
70
|
+
["ts", "ts"],
|
|
71
|
+
["tsx", "tsx"],
|
|
72
|
+
["js", "js"],
|
|
73
|
+
["jsx", "jsx"],
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
const STRENGTH: Readonly<Record<BlockLanguage, number>> = { js: 0, jsx: 1, ts: 2, tsx: 3 };
|
|
77
|
+
|
|
78
|
+
/** The `type` values a script element runs as a program under. */
|
|
79
|
+
const PROGRAM_TYPES: ReadonlySet<string> = new Set([
|
|
80
|
+
"",
|
|
81
|
+
"module",
|
|
82
|
+
"text/javascript",
|
|
83
|
+
"application/javascript",
|
|
84
|
+
"text/ecmascript",
|
|
85
|
+
"application/ecmascript",
|
|
86
|
+
"text/typescript",
|
|
87
|
+
"application/typescript",
|
|
88
|
+
]);
|
|
89
|
+
|
|
90
|
+
/** Elements with no content and no end tag. */
|
|
91
|
+
const VOID: ReadonlySet<string> = new Set([
|
|
92
|
+
"area",
|
|
93
|
+
"base",
|
|
94
|
+
"br",
|
|
95
|
+
"col",
|
|
96
|
+
"embed",
|
|
97
|
+
"hr",
|
|
98
|
+
"img",
|
|
99
|
+
"input",
|
|
100
|
+
"link",
|
|
101
|
+
"meta",
|
|
102
|
+
"source",
|
|
103
|
+
"track",
|
|
104
|
+
"wbr",
|
|
105
|
+
]);
|
|
106
|
+
|
|
107
|
+
/** Elements whose content is text up to their own end tag. */
|
|
108
|
+
const RAW_TEXT: ReadonlySet<string> = new Set(["script", "style", "textarea", "title"]);
|
|
109
|
+
|
|
110
|
+
const VERBATIM = 0;
|
|
111
|
+
const ATOM = 1;
|
|
112
|
+
|
|
113
|
+
function isLetter(code: number): boolean {
|
|
114
|
+
return (code >= 0x41 && code <= 0x5a) || (code >= 0x61 && code <= 0x7a);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function isSpace(code: number): boolean {
|
|
118
|
+
return code === 0x20 || code === 0x09 || code === 0x0a || code === 0x0c || code === 0x0d;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function isLineTerminator(code: number): boolean {
|
|
122
|
+
return code === 0x0a || code === 0x0d || code === 0x2028 || code === 0x2029;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function isNameCharacter(code: number): boolean {
|
|
126
|
+
return (
|
|
127
|
+
isLetter(code) ||
|
|
128
|
+
(code >= 0x30 && code <= 0x39) ||
|
|
129
|
+
code === 0x2d ||
|
|
130
|
+
code === 0x2e ||
|
|
131
|
+
code === 0x3a ||
|
|
132
|
+
code === 0x5f
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Whether the code unit at `offset` begins a line: the file's start, past a BOM, or after a line terminator. */
|
|
137
|
+
function beginsLine(text: string, offset: number): boolean {
|
|
138
|
+
if (offset === 0 || (offset === 1 && text.charCodeAt(0) === 0xfeff)) {
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
return isLineTerminator(text.charCodeAt(offset - 1));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** The line one offset is on, counted from one, every line terminator the compiler counts ending a line. */
|
|
145
|
+
export function lineOf(text: string, offset: number): number {
|
|
146
|
+
let line = 1;
|
|
147
|
+
for (let at = 0; at < offset && at < text.length; at += 1) {
|
|
148
|
+
const code = text.charCodeAt(at);
|
|
149
|
+
if (code === 0x0d && text.charCodeAt(at + 1) === 0x0a) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (isLineTerminator(code)) {
|
|
153
|
+
line += 1;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return line;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** The offset one past a tag name starting at `from`, or -1 where the `<` before it is text. */
|
|
160
|
+
function nameEnd(text: string, from: number): number {
|
|
161
|
+
let at = from;
|
|
162
|
+
while (at < text.length && isNameCharacter(text.charCodeAt(at))) {
|
|
163
|
+
at += 1;
|
|
164
|
+
}
|
|
165
|
+
if (at === text.length) {
|
|
166
|
+
return at;
|
|
167
|
+
}
|
|
168
|
+
const code = text.charCodeAt(at);
|
|
169
|
+
return isSpace(code) || code === 0x2f || code === 0x3e ? at : -1;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** The offset past the quoted string opening at `from`, or the text's length where it is not closed. */
|
|
173
|
+
function pastQuoted(text: string, from: number): number {
|
|
174
|
+
const quote = text.charCodeAt(from);
|
|
175
|
+
let at = from + 1;
|
|
176
|
+
while (at < text.length && text.charCodeAt(at) !== quote) {
|
|
177
|
+
at += text.charCodeAt(at) === 0x5c ? 2 : 1;
|
|
178
|
+
}
|
|
179
|
+
return Math.min(at + 1, text.length);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Whether a `<script` start tag begins a line at `offset`. */
|
|
183
|
+
function scriptTagAt(text: string, offset: number): boolean {
|
|
184
|
+
return (
|
|
185
|
+
text.charCodeAt(offset) === 0x3c &&
|
|
186
|
+
beginsLine(text, offset) &&
|
|
187
|
+
/^<script[\s/>]/iu.test(text.slice(offset, offset + 8))
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* The offset past the `}` matching the `{` at `from`, quoted strings skipped, or -1 where
|
|
193
|
+
* none matches before the end or before a `<script` start tag that begins a line, so a
|
|
194
|
+
* stray brace in markup never hides a block.
|
|
195
|
+
*/
|
|
196
|
+
function pastBraces(text: string, from: number): number {
|
|
197
|
+
let depth = 0;
|
|
198
|
+
let at = from;
|
|
199
|
+
while (at < text.length) {
|
|
200
|
+
const code = text.charCodeAt(at);
|
|
201
|
+
if (scriptTagAt(text, at)) {
|
|
202
|
+
return -1;
|
|
203
|
+
}
|
|
204
|
+
if (code === 0x22 || code === 0x27 || code === 0x60) {
|
|
205
|
+
at = pastQuoted(text, at);
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (code === 0x7b) {
|
|
209
|
+
depth += 1;
|
|
210
|
+
} else if (code === 0x7d) {
|
|
211
|
+
depth -= 1;
|
|
212
|
+
if (depth === 0) {
|
|
213
|
+
return at + 1;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
at += 1;
|
|
217
|
+
}
|
|
218
|
+
return -1;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
interface Attribute {
|
|
222
|
+
readonly name: string;
|
|
223
|
+
readonly value: string;
|
|
224
|
+
readonly start: number;
|
|
225
|
+
readonly end: number;
|
|
226
|
+
/** The offset of the value's first code unit, inside its quotes or braces. */
|
|
227
|
+
readonly valueStart: number;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
interface StartTag {
|
|
231
|
+
readonly name: string;
|
|
232
|
+
readonly end: number;
|
|
233
|
+
readonly selfClosing: boolean;
|
|
234
|
+
readonly attributes: readonly Attribute[];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** The start tag whose `<` is at `from`, to its `>`. A quoted or braced value may hold `>`. */
|
|
238
|
+
function readStartTag(text: string, from: number, nameStop: number): StartTag {
|
|
239
|
+
const name = text.slice(from + 1, nameStop).toLowerCase();
|
|
240
|
+
const attributes: Attribute[] = [];
|
|
241
|
+
let at = nameStop;
|
|
242
|
+
let selfClosing = false;
|
|
243
|
+
while (at < text.length) {
|
|
244
|
+
const code = text.charCodeAt(at);
|
|
245
|
+
if (code === 0x3e) {
|
|
246
|
+
return { name, end: at + 1, selfClosing, attributes };
|
|
247
|
+
}
|
|
248
|
+
if (isSpace(code)) {
|
|
249
|
+
at += 1;
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (code === 0x2f) {
|
|
253
|
+
selfClosing = text.charCodeAt(at + 1) === 0x3e;
|
|
254
|
+
at += 1;
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
selfClosing = false;
|
|
258
|
+
const start = at;
|
|
259
|
+
if (code === 0x7b) {
|
|
260
|
+
const past = pastBraces(text, at);
|
|
261
|
+
at = past < 0 ? text.length : past;
|
|
262
|
+
attributes.push({
|
|
263
|
+
name: "",
|
|
264
|
+
value: text.slice(start + 1, at - 1),
|
|
265
|
+
start,
|
|
266
|
+
end: at,
|
|
267
|
+
valueStart: start + 1,
|
|
268
|
+
});
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
while (at < text.length) {
|
|
272
|
+
const c = text.charCodeAt(at);
|
|
273
|
+
if (isSpace(c) || c === 0x2f || c === 0x3e || (c === 0x3d && at > start)) {
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
at += 1;
|
|
277
|
+
}
|
|
278
|
+
const attribute = text.slice(start, at);
|
|
279
|
+
while (at < text.length && isSpace(text.charCodeAt(at))) {
|
|
280
|
+
at += 1;
|
|
281
|
+
}
|
|
282
|
+
let value = "";
|
|
283
|
+
let valueStart = at;
|
|
284
|
+
if (text.charCodeAt(at) === 0x3d) {
|
|
285
|
+
at += 1;
|
|
286
|
+
while (at < text.length && isSpace(text.charCodeAt(at))) {
|
|
287
|
+
at += 1;
|
|
288
|
+
}
|
|
289
|
+
const opening = text.charCodeAt(at);
|
|
290
|
+
if (opening === 0x22 || opening === 0x27) {
|
|
291
|
+
const past = pastQuoted(text, at);
|
|
292
|
+
valueStart = at + 1;
|
|
293
|
+
value = text.slice(at + 1, Math.max(past - 1, at + 1));
|
|
294
|
+
at = past;
|
|
295
|
+
} else if (opening === 0x7b) {
|
|
296
|
+
const past = pastBraces(text, at);
|
|
297
|
+
const stop = past < 0 ? text.length : past;
|
|
298
|
+
valueStart = at + 1;
|
|
299
|
+
value = text.slice(at + 1, Math.max(stop - 1, at + 1));
|
|
300
|
+
at = stop;
|
|
301
|
+
} else {
|
|
302
|
+
valueStart = at;
|
|
303
|
+
while (at < text.length && !isSpace(text.charCodeAt(at)) && text.charCodeAt(at) !== 0x3e) {
|
|
304
|
+
at += 1;
|
|
305
|
+
}
|
|
306
|
+
value = text.slice(valueStart, at);
|
|
307
|
+
}
|
|
308
|
+
} else {
|
|
309
|
+
at = start + attribute.length;
|
|
310
|
+
}
|
|
311
|
+
attributes.push({ name: attribute.toLowerCase(), value, start, end: at, valueStart });
|
|
312
|
+
}
|
|
313
|
+
return { name, end: text.length, selfClosing, attributes };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* The offset of the `<` of the first end tag of `name` at or after `from`, case
|
|
318
|
+
* ignored and followed by a space, a solidus or `>`, or the text's length where none is.
|
|
319
|
+
*/
|
|
320
|
+
function rawTextEnd(text: string, name: string, from: number): number {
|
|
321
|
+
const lower = text.toLowerCase();
|
|
322
|
+
const needle = `</${name}`;
|
|
323
|
+
let at = lower.indexOf(needle, from);
|
|
324
|
+
while (at >= 0) {
|
|
325
|
+
const after = lower.charCodeAt(at + needle.length);
|
|
326
|
+
if (Number.isNaN(after) || isSpace(after) || after === 0x2f || after === 0x3e) {
|
|
327
|
+
return at;
|
|
328
|
+
}
|
|
329
|
+
at = lower.indexOf(needle, at + 1);
|
|
330
|
+
}
|
|
331
|
+
return text.length;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function pastClose(text: string, from: number): number {
|
|
335
|
+
const close = text.indexOf(">", from);
|
|
336
|
+
return close < 0 ? text.length : close + 1;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** The frontmatter block, where the file's first line is `---` and a later line closes it. */
|
|
340
|
+
function frontmatter(text: string): Block | undefined {
|
|
341
|
+
const start = text.charCodeAt(0) === 0xfeff ? 1 : 0;
|
|
342
|
+
const lines = /\r\n|[\n\r\u2028\u2029]/gu;
|
|
343
|
+
let lineStart = start;
|
|
344
|
+
let index = 0;
|
|
345
|
+
for (;;) {
|
|
346
|
+
lines.lastIndex = lineStart;
|
|
347
|
+
const match = lines.exec(text);
|
|
348
|
+
const lineEnd = match === null ? text.length : match.index;
|
|
349
|
+
const isFence = /^---[ \t]*$/u.test(text.slice(lineStart, lineEnd));
|
|
350
|
+
if (index === 0 && !isFence) {
|
|
351
|
+
return undefined;
|
|
352
|
+
}
|
|
353
|
+
if (index > 0 && isFence) {
|
|
354
|
+
return {
|
|
355
|
+
tagStart: 0,
|
|
356
|
+
contentStart: lineStartOf(text, start),
|
|
357
|
+
contentEnd: lineStart,
|
|
358
|
+
language: "ts",
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
if (match === null) {
|
|
362
|
+
return undefined;
|
|
363
|
+
}
|
|
364
|
+
lineStart = match.index + match[0].length;
|
|
365
|
+
index += 1;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** The offset past the first line that starts at `from`. */
|
|
370
|
+
function lineStartOf(text: string, from: number): number {
|
|
371
|
+
const lines = /\r\n|[\n\r\u2028\u2029]/gu;
|
|
372
|
+
lines.lastIndex = from;
|
|
373
|
+
const match = lines.exec(text);
|
|
374
|
+
return match === null ? text.length : match.index + match[0].length;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** The text of one action, collapsed to a line. */
|
|
378
|
+
function actionOf(text: string): string {
|
|
379
|
+
return text.trim().replace(/\s+/gu, " ");
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** Whether a `src` value names a file: no scheme, no root-relative path, not empty. */
|
|
383
|
+
function specifierOf(value: string): string | undefined {
|
|
384
|
+
const trimmed = value.trim();
|
|
385
|
+
if (trimmed === "" || trimmed.startsWith("/") || /^[A-Za-z][A-Za-z0-9+.-]*:/u.test(trimmed)) {
|
|
386
|
+
return undefined;
|
|
387
|
+
}
|
|
388
|
+
return trimmed.startsWith("./") || trimmed.startsWith("../") ? trimmed : `./${trimmed}`;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** The line-start `<script` tags one block's content holds. */
|
|
392
|
+
function scriptsInside(text: string, from: number, to: number): number[] {
|
|
393
|
+
const found: number[] = [];
|
|
394
|
+
const pattern = /<script(?=[\s/>])/giu;
|
|
395
|
+
pattern.lastIndex = from;
|
|
396
|
+
for (
|
|
397
|
+
let match = pattern.exec(text);
|
|
398
|
+
match !== null && match.index < to;
|
|
399
|
+
match = pattern.exec(text)
|
|
400
|
+
) {
|
|
401
|
+
if (beginsLine(text, match.index)) {
|
|
402
|
+
found.push(match.index);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return found;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* The blocks, the source imports, the warnings and the markup actions of one component file.
|
|
410
|
+
* The scan keeps a stack of open elements. A comment, a doctype or processing instruction and
|
|
411
|
+
* an expression in braces are text, and an end tag closes back to the nearest open element of
|
|
412
|
+
* its name. A raw-text element at the top level runs to its own end tag, and only a `script`
|
|
413
|
+
* element whose start tag begins a line at the top level is a block.
|
|
414
|
+
*/
|
|
415
|
+
export function readComponent(text: string): ComponentFile {
|
|
416
|
+
const blocks: Block[] = [];
|
|
417
|
+
const imports: SourceImport[] = [];
|
|
418
|
+
const warnings: ComponentWarning[] = [];
|
|
419
|
+
const markup: MarkupAction[] = [];
|
|
420
|
+
const front = frontmatter(text);
|
|
421
|
+
if (front !== undefined) {
|
|
422
|
+
blocks.push(front);
|
|
423
|
+
}
|
|
424
|
+
const open: string[] = [];
|
|
425
|
+
let at = front === undefined ? 0 : lineStartOf(text, front.contentEnd);
|
|
426
|
+
while (at < text.length) {
|
|
427
|
+
const code = text.charCodeAt(at);
|
|
428
|
+
if (code === 0x7b) {
|
|
429
|
+
const past = pastBraces(text, at);
|
|
430
|
+
if (past < 0) {
|
|
431
|
+
at += 1;
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
const inner = text.slice(at + 1, past - 1);
|
|
435
|
+
markup.push({ start: at + 1, text: inner, action: actionOf(text.slice(at, past)) });
|
|
436
|
+
at = past;
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
if (code !== 0x3c) {
|
|
440
|
+
at += 1;
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
const lt = at;
|
|
444
|
+
if (text.startsWith("<!--", lt)) {
|
|
445
|
+
const close = text.indexOf("-->", lt + 4);
|
|
446
|
+
at = close < 0 ? text.length : close + 3;
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
const next = text.charCodeAt(lt + 1);
|
|
450
|
+
if (next === 0x21 || next === 0x3f) {
|
|
451
|
+
at = pastClose(text, lt + 2);
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
if (next === 0x2f && isLetter(text.charCodeAt(lt + 2))) {
|
|
455
|
+
const stop = nameEnd(text, lt + 2);
|
|
456
|
+
if (stop >= 0) {
|
|
457
|
+
const depth = open.lastIndexOf(text.slice(lt + 2, stop).toLowerCase());
|
|
458
|
+
if (depth >= 0) {
|
|
459
|
+
open.length = depth;
|
|
460
|
+
}
|
|
461
|
+
at = pastClose(text, stop);
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
const nameStop = isLetter(next) ? nameEnd(text, lt + 1) : -1;
|
|
466
|
+
if (nameStop < 0) {
|
|
467
|
+
at = lt + 1;
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
const tag = readStartTag(text, lt, nameStop);
|
|
471
|
+
const rawText = open.length === 0 && RAW_TEXT.has(tag.name);
|
|
472
|
+
for (const attribute of rawText ? [] : tag.attributes) {
|
|
473
|
+
if (attribute.value !== "") {
|
|
474
|
+
markup.push({
|
|
475
|
+
start: attribute.valueStart,
|
|
476
|
+
text: attribute.value,
|
|
477
|
+
action: actionOf(text.slice(attribute.start, attribute.end)),
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
const line = beginsLine(text, lt);
|
|
482
|
+
if (tag.name === "script" && line && open.length > 0) {
|
|
483
|
+
warnings.push({ offset: lt, reason: "it is inside an element" });
|
|
484
|
+
}
|
|
485
|
+
if (rawText) {
|
|
486
|
+
const contentEnd = tag.selfClosing ? tag.end : rawTextEnd(text, tag.name, tag.end);
|
|
487
|
+
if (tag.name === "script" && line) {
|
|
488
|
+
script(tag, lt, contentEnd);
|
|
489
|
+
}
|
|
490
|
+
at =
|
|
491
|
+
tag.selfClosing || contentEnd >= text.length
|
|
492
|
+
? Math.max(tag.end, contentEnd)
|
|
493
|
+
: pastClose(text, contentEnd + 2);
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
if (!tag.selfClosing && !VOID.has(tag.name)) {
|
|
497
|
+
open.push(tag.name);
|
|
498
|
+
}
|
|
499
|
+
at = tag.end;
|
|
500
|
+
}
|
|
501
|
+
return { blocks, imports, warnings: warnings.sort((a, b) => a.offset - b.offset), markup };
|
|
502
|
+
|
|
503
|
+
/** Reads one top-level script element that begins a line. */
|
|
504
|
+
function script(tag: StartTag, lt: number, contentEnd: number): void {
|
|
505
|
+
const attribute = (name: string): Attribute | undefined =>
|
|
506
|
+
tag.attributes.find((one) => one.name === name);
|
|
507
|
+
const src = attribute("src");
|
|
508
|
+
if (src !== undefined) {
|
|
509
|
+
const specifier = specifierOf(src.value);
|
|
510
|
+
if (specifier === undefined) {
|
|
511
|
+
warnings.push({ offset: lt, reason: `its src ${JSON.stringify(src.value)} names no file` });
|
|
512
|
+
} else {
|
|
513
|
+
imports.push({ specifier, attributeStart: src.start, attributeEnd: src.end });
|
|
514
|
+
}
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
const type = (attribute("type")?.value ?? "").trim().toLowerCase();
|
|
518
|
+
if (!PROGRAM_TYPES.has(type)) {
|
|
519
|
+
warnings.push({
|
|
520
|
+
offset: lt,
|
|
521
|
+
reason: `its type ${JSON.stringify(type)} is not a script type`,
|
|
522
|
+
});
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
const lang = attribute("lang")?.value.trim().toLowerCase();
|
|
526
|
+
const language = lang === undefined ? "js" : LANGUAGES.get(lang);
|
|
527
|
+
if (language === undefined) {
|
|
528
|
+
warnings.push({
|
|
529
|
+
offset: lt,
|
|
530
|
+
reason: `its lang ${JSON.stringify(lang)} is not ts, tsx, js or jsx`,
|
|
531
|
+
});
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
if (tag.selfClosing) {
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
for (const inside of scriptsInside(text, tag.end, contentEnd)) {
|
|
538
|
+
warnings.push({ offset: inside, reason: "it begins a line inside a block" });
|
|
539
|
+
}
|
|
540
|
+
blocks.push({ tagStart: lt, contentStart: tag.end, contentEnd, language });
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Whether the module's text exports a default at its top level: an `export default`
|
|
546
|
+
* or an export clause naming `default`, outside every comment, string, template and
|
|
547
|
+
* regular expression, and outside every bracket.
|
|
548
|
+
*/
|
|
549
|
+
export function exportsDefault(text: string): boolean {
|
|
550
|
+
const words: string[] = [];
|
|
551
|
+
let depth = 0;
|
|
552
|
+
let clause = false;
|
|
553
|
+
let previous = "";
|
|
554
|
+
let at = 0;
|
|
555
|
+
const regexMayStart = (): boolean =>
|
|
556
|
+
previous === "" ||
|
|
557
|
+
/^[([{,;:?=!&|+\-*%<>~^]$/u.test(previous) ||
|
|
558
|
+
/^(return|typeof|instanceof|in|of|new|delete|void|throw|case|do|else|yield|await)$/u.test(
|
|
559
|
+
previous,
|
|
560
|
+
);
|
|
561
|
+
const skipTemplate = (from: number): number => {
|
|
562
|
+
let index = from + 1;
|
|
563
|
+
while (index < text.length) {
|
|
564
|
+
const c = text.charCodeAt(index);
|
|
565
|
+
if (c === 0x5c) {
|
|
566
|
+
index += 2;
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
if (c === 0x60) {
|
|
570
|
+
return index + 1;
|
|
571
|
+
}
|
|
572
|
+
if (c === 0x24 && text.charCodeAt(index + 1) === 0x7b) {
|
|
573
|
+
const past = pastBraces(text, index + 1);
|
|
574
|
+
index = past < 0 ? text.length : past;
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
index += 1;
|
|
578
|
+
}
|
|
579
|
+
return index;
|
|
580
|
+
};
|
|
581
|
+
while (at < text.length) {
|
|
582
|
+
const c = text.charCodeAt(at);
|
|
583
|
+
const pair = text.slice(at, at + 2);
|
|
584
|
+
if (pair === "//") {
|
|
585
|
+
while (at < text.length && !isLineTerminator(text.charCodeAt(at))) {
|
|
586
|
+
at += 1;
|
|
587
|
+
}
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
if (pair === "/*") {
|
|
591
|
+
const close = text.indexOf("*/", at + 2);
|
|
592
|
+
at = close < 0 ? text.length : close + 2;
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
if (c === 0x22 || c === 0x27) {
|
|
596
|
+
at = pastQuoted(text, at);
|
|
597
|
+
previous = "string";
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
if (c === 0x60) {
|
|
601
|
+
at = skipTemplate(at);
|
|
602
|
+
previous = "string";
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
if (c === 0x2f && regexMayStart()) {
|
|
606
|
+
let index = at + 1;
|
|
607
|
+
let inClass = false;
|
|
608
|
+
while (index < text.length && !isLineTerminator(text.charCodeAt(index))) {
|
|
609
|
+
const r = text.charCodeAt(index);
|
|
610
|
+
if (r === 0x5c) {
|
|
611
|
+
index += 2;
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
if (r === 0x5b) {
|
|
615
|
+
inClass = true;
|
|
616
|
+
} else if (r === 0x5d) {
|
|
617
|
+
inClass = false;
|
|
618
|
+
} else if (r === 0x2f && !inClass) {
|
|
619
|
+
break;
|
|
620
|
+
}
|
|
621
|
+
index += 1;
|
|
622
|
+
}
|
|
623
|
+
at = index + 1;
|
|
624
|
+
previous = "regex";
|
|
625
|
+
continue;
|
|
626
|
+
}
|
|
627
|
+
if (isSpace(c) || isLineTerminator(c)) {
|
|
628
|
+
at += 1;
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
631
|
+
const word = /[\p{ID_Start}$_][\p{ID_Continue}$\u200C\u200D]*/uy;
|
|
632
|
+
word.lastIndex = at;
|
|
633
|
+
const matched = word.exec(text);
|
|
634
|
+
if (matched !== null) {
|
|
635
|
+
const name = matched[0];
|
|
636
|
+
if (depth === 0 && words.at(-1) === "export" && name === "default") {
|
|
637
|
+
return true;
|
|
638
|
+
}
|
|
639
|
+
if (clause && name === "default") {
|
|
640
|
+
return true;
|
|
641
|
+
}
|
|
642
|
+
words.push(name);
|
|
643
|
+
previous = name;
|
|
644
|
+
at += name.length;
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
const char = text[at] ?? "";
|
|
648
|
+
if (char === "{" || char === "(" || char === "[") {
|
|
649
|
+
clause = depth === 0 && char === "{" && words.at(-1) === "export";
|
|
650
|
+
depth += 1;
|
|
651
|
+
} else if (char === "}" || char === ")" || char === "]") {
|
|
652
|
+
depth = Math.max(depth - 1, 0);
|
|
653
|
+
if (depth === 0) {
|
|
654
|
+
clause = false;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
words.push(char);
|
|
658
|
+
previous = char;
|
|
659
|
+
at += 1;
|
|
660
|
+
}
|
|
661
|
+
return false;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/** The module one component file's text is read as, and the span map back to the file. */
|
|
665
|
+
export function componentModule(text: string): ComponentModule {
|
|
666
|
+
const { blocks, imports } = readComponent(text);
|
|
667
|
+
const out: string[] = [];
|
|
668
|
+
let at = 0;
|
|
669
|
+
let language: BlockLanguage | undefined;
|
|
670
|
+
const mask = (from: number, to: number, statementBreak: boolean): void => {
|
|
671
|
+
for (let index = from; index < to; index += 1) {
|
|
672
|
+
const code = text.charCodeAt(index);
|
|
673
|
+
if (isLineTerminator(code) || (index === 0 && code === 0xfeff)) {
|
|
674
|
+
out.push(text[index] ?? "");
|
|
675
|
+
} else if (statementBreak && index === from) {
|
|
676
|
+
out.push(";");
|
|
677
|
+
} else {
|
|
678
|
+
out.push(" ");
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
};
|
|
682
|
+
const mappings: [number, number, number, number, number][] = [];
|
|
683
|
+
for (const block of blocks) {
|
|
684
|
+
if (language === undefined || STRENGTH[block.language] > STRENGTH[language]) {
|
|
685
|
+
language = block.language;
|
|
686
|
+
}
|
|
687
|
+
mask(at, block.tagStart, false);
|
|
688
|
+
mask(block.tagStart, block.contentStart, at > 0);
|
|
689
|
+
out.push(text.slice(block.contentStart, block.contentEnd));
|
|
690
|
+
const length = block.contentEnd - block.contentStart;
|
|
691
|
+
if (length > 0) {
|
|
692
|
+
mappings.push([block.contentStart, length, block.contentStart, length, VERBATIM]);
|
|
693
|
+
}
|
|
694
|
+
at = block.contentEnd;
|
|
695
|
+
}
|
|
696
|
+
mask(at, text.length, false);
|
|
697
|
+
const extension = `.${language ?? "ts"}` as const;
|
|
698
|
+
const typed = extension === ".ts" || extension === ".tsx";
|
|
699
|
+
let module = out.join("");
|
|
700
|
+
const tail: string[] = [];
|
|
701
|
+
if (!exportsDefault(module)) {
|
|
702
|
+
tail.push(typed ? "export default (0 as any);" : "export default {};");
|
|
703
|
+
}
|
|
704
|
+
module += `\n${tail.join("\n")}`;
|
|
705
|
+
imports.forEach((one, index) => {
|
|
706
|
+
const statement = `import * as __component_src_${String(index)} from ${JSON.stringify(one.specifier)};`;
|
|
707
|
+
module += "\n";
|
|
708
|
+
mappings.push([
|
|
709
|
+
module.length,
|
|
710
|
+
statement.length,
|
|
711
|
+
one.attributeStart,
|
|
712
|
+
one.attributeEnd - one.attributeStart,
|
|
713
|
+
ATOM,
|
|
714
|
+
]);
|
|
715
|
+
module += statement;
|
|
716
|
+
});
|
|
717
|
+
return { text: `${module}\n`, extension, mappings };
|
|
718
|
+
}
|