@azerothjs/typescript-plugin 0.4.0-beta.3
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 +88 -0
- package/dist/index.js +1132 -0
- package/package.json +54 -0
package/README.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# @azerothjs/typescript-plugin
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
A TypeScript language-service plugin that teaches `tsserver` (the engine behind
|
|
6
|
+
VS Code's built-in TypeScript support, and any editor that uses it) to resolve
|
|
7
|
+
`.azeroth` imports from `.ts`/`.tsx` files with their REAL exported types -
|
|
8
|
+
default, named, and type exports. It is the AzerothJS counterpart to
|
|
9
|
+
`@vue/typescript-plugin`.
|
|
10
|
+
|
|
11
|
+
With the plugin installed a consuming app can delete its hand-written
|
|
12
|
+
`declare module '*.azeroth'` shims: a barrel like
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
export { default as Breadcrumb } from './breadcrumb.component.azeroth';
|
|
16
|
+
export type { BreadcrumbCrumb } from './breadcrumb.component.azeroth';
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
type-checks against the component's actual signature instead of `any`.
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
npm i -D @azerothjs/typescript-plugin
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Register it in the consuming project's `tsconfig.json`:
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"compilerOptions": {
|
|
32
|
+
"plugins": [{ "name": "@azerothjs/typescript-plugin" }]
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
In VS Code, also select **"Use Workspace Version"** of TypeScript (or rely on
|
|
38
|
+
the AzerothJS VS Code extension, which contributes the plugin automatically), so
|
|
39
|
+
the editor's TypeScript server loads the plugin.
|
|
40
|
+
|
|
41
|
+
## How it works
|
|
42
|
+
|
|
43
|
+
The plugin reuses the same virtual-code pipeline as the editor language server
|
|
44
|
+
(`@azerothjs/language-service`): a `.azeroth` file is compiled to a virtual
|
|
45
|
+
TypeScript module whose markup is rewritten to `h()` calls and whose surrounding
|
|
46
|
+
code - every `export` included - is preserved verbatim. The plugin decorates the
|
|
47
|
+
host so that:
|
|
48
|
+
|
|
49
|
+
- an `import './x.azeroth'` specifier resolves to a synthetic `x.azeroth.ts`;
|
|
50
|
+
- loading that synthetic file returns the compiled virtual module.
|
|
51
|
+
|
|
52
|
+
Because the virtual module carries the file's real exported declarations,
|
|
53
|
+
TypeScript infers real types across the `.ts` -> `.azeroth` boundary.
|
|
54
|
+
|
|
55
|
+
## Scope: editors, not `tsc`
|
|
56
|
+
|
|
57
|
+
TypeScript language-service plugins run **only inside `tsserver`** (editors), not
|
|
58
|
+
inside the command-line `tsc`. This is a TypeScript limitation, not a choice
|
|
59
|
+
here - it is also why `vue-tsc` exists. So:
|
|
60
|
+
|
|
61
|
+
- In the editor, this plugin gives real `.azeroth` types with no shim.
|
|
62
|
+
- For a command-line type-check gate over `.azeroth` files, use `azeroth-tsc`
|
|
63
|
+
(from `@azerothjs/language-server`).
|
|
64
|
+
|
|
65
|
+
A single combined `.ts` + `.azeroth` command-line checker (the `vue-tsc`
|
|
66
|
+
equivalent) is tracked as a follow-up.
|
|
67
|
+
|
|
68
|
+
## Building
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
npm run build -w @azerothjs/typescript-plugin
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`tsserver` loads plugins with `require()`, so the entry must be CommonJS; the
|
|
75
|
+
AzerothJS packages it reuses are ESM. `esbuild` bundles `src/index.ts` and those
|
|
76
|
+
ESM dependencies into a single self-contained `dist/index.js` (with `typescript`
|
|
77
|
+
left external - `tsserver` passes its own copy to the plugin factory).
|
|
78
|
+
|
|
79
|
+
## Testing
|
|
80
|
+
|
|
81
|
+
```sh
|
|
82
|
+
npx vitest run test/typescript-plugin
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The test drives the plugin's host decoration through a constructed
|
|
86
|
+
`ts.LanguageService` (what `tsserver` builds) over a fixture with no `.azeroth`
|
|
87
|
+
shim, asserting default/named/type imports resolve and that a genuine type error
|
|
88
|
+
still surfaces.
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,1132 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
10
|
+
for (let key of __getOwnPropNames(from))
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
12
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
13
|
+
}
|
|
14
|
+
return to;
|
|
15
|
+
};
|
|
16
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
17
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
18
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
19
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
20
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
21
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
22
|
+
mod
|
|
23
|
+
));
|
|
24
|
+
|
|
25
|
+
// src/decorate.ts
|
|
26
|
+
var import_node_path = __toESM(require("node:path"));
|
|
27
|
+
|
|
28
|
+
// ../language-service/src/ts-project.ts
|
|
29
|
+
var import_typescript = __toESM(require("typescript"), 1);
|
|
30
|
+
|
|
31
|
+
// ../compiler/src/scanner.ts
|
|
32
|
+
function isWhitespace(ch) {
|
|
33
|
+
return ch === " " || ch === " " || ch === "\n" || ch === "\r" || ch === "\f" || ch === "\v";
|
|
34
|
+
}
|
|
35
|
+
function isIdentStart(ch) {
|
|
36
|
+
return ch >= "a" && ch <= "z" || ch >= "A" && ch <= "Z" || ch === "_" || ch === "$";
|
|
37
|
+
}
|
|
38
|
+
function isIdentPart(ch) {
|
|
39
|
+
return isIdentStart(ch) || ch >= "0" && ch <= "9";
|
|
40
|
+
}
|
|
41
|
+
function skipLineComment(src, i) {
|
|
42
|
+
i += 2;
|
|
43
|
+
while (i < src.length && src[i] !== "\n") {
|
|
44
|
+
i++;
|
|
45
|
+
}
|
|
46
|
+
return i;
|
|
47
|
+
}
|
|
48
|
+
function skipBlockComment(src, i) {
|
|
49
|
+
i += 2;
|
|
50
|
+
while (i < src.length && !(src[i] === "*" && src[i + 1] === "/")) {
|
|
51
|
+
i++;
|
|
52
|
+
}
|
|
53
|
+
return Math.min(i + 2, src.length);
|
|
54
|
+
}
|
|
55
|
+
function skipString(src, i) {
|
|
56
|
+
const quote = src[i];
|
|
57
|
+
i++;
|
|
58
|
+
while (i < src.length) {
|
|
59
|
+
const ch = src[i];
|
|
60
|
+
if (ch === "\\") {
|
|
61
|
+
i += 2;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (ch === quote) {
|
|
65
|
+
return i + 1;
|
|
66
|
+
}
|
|
67
|
+
i++;
|
|
68
|
+
}
|
|
69
|
+
return i;
|
|
70
|
+
}
|
|
71
|
+
function skipTemplate(src, i) {
|
|
72
|
+
i++;
|
|
73
|
+
while (i < src.length) {
|
|
74
|
+
const ch = src[i];
|
|
75
|
+
if (ch === "\\") {
|
|
76
|
+
i += 2;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (ch === "`") {
|
|
80
|
+
return i + 1;
|
|
81
|
+
}
|
|
82
|
+
if (ch === "$" && src[i + 1] === "{") {
|
|
83
|
+
i = skipBalanced(src, i + 1);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
i++;
|
|
87
|
+
}
|
|
88
|
+
return i;
|
|
89
|
+
}
|
|
90
|
+
function skipRegex(src, i) {
|
|
91
|
+
i++;
|
|
92
|
+
let inClass = false;
|
|
93
|
+
while (i < src.length) {
|
|
94
|
+
const ch = src[i];
|
|
95
|
+
if (ch === "\\") {
|
|
96
|
+
i += 2;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (ch === "[") {
|
|
100
|
+
inClass = true;
|
|
101
|
+
} else if (ch === "]") {
|
|
102
|
+
inClass = false;
|
|
103
|
+
} else if (ch === "/" && !inClass) {
|
|
104
|
+
i++;
|
|
105
|
+
break;
|
|
106
|
+
} else if (ch === "\n") {
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
i++;
|
|
110
|
+
}
|
|
111
|
+
while (i < src.length && isIdentPart(src[i])) {
|
|
112
|
+
i++;
|
|
113
|
+
}
|
|
114
|
+
return i;
|
|
115
|
+
}
|
|
116
|
+
function skipBalanced(src, openIndex) {
|
|
117
|
+
const open = src[openIndex];
|
|
118
|
+
const close = open === "(" ? ")" : open === "[" ? "]" : "}";
|
|
119
|
+
let depth = 0;
|
|
120
|
+
let i = openIndex;
|
|
121
|
+
while (i < src.length) {
|
|
122
|
+
const ch = src[i];
|
|
123
|
+
if (ch === "/" && src[i + 1] === "/") {
|
|
124
|
+
i = skipLineComment(src, i);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (ch === "/" && src[i + 1] === "*") {
|
|
128
|
+
i = skipBlockComment(src, i);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (ch === '"' || ch === "'") {
|
|
132
|
+
i = skipString(src, i);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (ch === "`") {
|
|
136
|
+
i = skipTemplate(src, i);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (ch === open) {
|
|
140
|
+
depth++;
|
|
141
|
+
i++;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (ch === close) {
|
|
145
|
+
depth--;
|
|
146
|
+
i++;
|
|
147
|
+
if (depth === 0) {
|
|
148
|
+
return i;
|
|
149
|
+
}
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
i++;
|
|
153
|
+
}
|
|
154
|
+
return i;
|
|
155
|
+
}
|
|
156
|
+
var EXPR_KEYWORDS = /* @__PURE__ */ new Set([
|
|
157
|
+
"return",
|
|
158
|
+
"typeof",
|
|
159
|
+
"instanceof",
|
|
160
|
+
"in",
|
|
161
|
+
"of",
|
|
162
|
+
"do",
|
|
163
|
+
"else",
|
|
164
|
+
"yield",
|
|
165
|
+
"await",
|
|
166
|
+
"case",
|
|
167
|
+
"delete",
|
|
168
|
+
"void",
|
|
169
|
+
"new"
|
|
170
|
+
]);
|
|
171
|
+
var EXPR_CHARS = /* @__PURE__ */ new Set([
|
|
172
|
+
"",
|
|
173
|
+
"(",
|
|
174
|
+
"{",
|
|
175
|
+
"[",
|
|
176
|
+
",",
|
|
177
|
+
";",
|
|
178
|
+
":",
|
|
179
|
+
"?",
|
|
180
|
+
"=",
|
|
181
|
+
">",
|
|
182
|
+
"<",
|
|
183
|
+
"&",
|
|
184
|
+
"|",
|
|
185
|
+
"!",
|
|
186
|
+
"~",
|
|
187
|
+
"+",
|
|
188
|
+
"-",
|
|
189
|
+
"*",
|
|
190
|
+
"/",
|
|
191
|
+
"%",
|
|
192
|
+
"^",
|
|
193
|
+
"\n"
|
|
194
|
+
]);
|
|
195
|
+
function isExpressionPosition(prevChar, prevWord) {
|
|
196
|
+
if (prevWord !== "") {
|
|
197
|
+
return EXPR_KEYWORDS.has(prevWord);
|
|
198
|
+
}
|
|
199
|
+
return EXPR_CHARS.has(prevChar);
|
|
200
|
+
}
|
|
201
|
+
function scanTypeParams(src, openIndex) {
|
|
202
|
+
let depth = 0;
|
|
203
|
+
let i = openIndex;
|
|
204
|
+
while (i < src.length) {
|
|
205
|
+
const ch = src[i];
|
|
206
|
+
if (ch === "/" && src[i + 1] === "/") {
|
|
207
|
+
i = skipLineComment(src, i);
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (ch === "/" && src[i + 1] === "*") {
|
|
211
|
+
i = skipBlockComment(src, i);
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (ch === '"' || ch === "'") {
|
|
215
|
+
i = skipString(src, i);
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (ch === "`") {
|
|
219
|
+
i = skipTemplate(src, i);
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (ch === "(" || ch === "[" || ch === "{") {
|
|
223
|
+
i = skipBalanced(src, i);
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (ch === "=" && src[i + 1] === ">") {
|
|
227
|
+
i += 2;
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (ch === "<") {
|
|
231
|
+
depth++;
|
|
232
|
+
i++;
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
if (ch === ">") {
|
|
236
|
+
depth--;
|
|
237
|
+
i++;
|
|
238
|
+
if (depth === 0) {
|
|
239
|
+
return i;
|
|
240
|
+
}
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
i++;
|
|
244
|
+
}
|
|
245
|
+
return -1;
|
|
246
|
+
}
|
|
247
|
+
function tryGenericArrow(src, i) {
|
|
248
|
+
const afterAngles = scanTypeParams(src, i);
|
|
249
|
+
if (afterAngles === -1) {
|
|
250
|
+
return -1;
|
|
251
|
+
}
|
|
252
|
+
let k = afterAngles;
|
|
253
|
+
while (k < src.length && isWhitespace(src[k])) {
|
|
254
|
+
k++;
|
|
255
|
+
}
|
|
256
|
+
if (src[k] !== "(") {
|
|
257
|
+
return -1;
|
|
258
|
+
}
|
|
259
|
+
let m = skipBalanced(src, k);
|
|
260
|
+
while (m < src.length && isWhitespace(src[m])) {
|
|
261
|
+
m++;
|
|
262
|
+
}
|
|
263
|
+
if (src[m] === ":" || src[m] === "=" && src[m + 1] === ">") {
|
|
264
|
+
return afterAngles;
|
|
265
|
+
}
|
|
266
|
+
return -1;
|
|
267
|
+
}
|
|
268
|
+
function findMarkupStart(src, from) {
|
|
269
|
+
let i = from;
|
|
270
|
+
let prevChar = "";
|
|
271
|
+
let prevWord = "";
|
|
272
|
+
while (i < src.length) {
|
|
273
|
+
const ch = src[i];
|
|
274
|
+
if (ch === "/" && src[i + 1] === "/") {
|
|
275
|
+
i = skipLineComment(src, i);
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
if (ch === "/" && src[i + 1] === "*") {
|
|
279
|
+
i = skipBlockComment(src, i);
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
if (ch === '"' || ch === "'") {
|
|
283
|
+
i = skipString(src, i);
|
|
284
|
+
prevChar = '"';
|
|
285
|
+
prevWord = "";
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
if (ch === "`") {
|
|
289
|
+
i = skipTemplate(src, i);
|
|
290
|
+
prevChar = "`";
|
|
291
|
+
prevWord = "";
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if (ch === "/" && isExpressionPosition(prevChar, prevWord)) {
|
|
295
|
+
i = skipRegex(src, i);
|
|
296
|
+
prevChar = "/";
|
|
297
|
+
prevWord = "";
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
if (isWhitespace(ch)) {
|
|
301
|
+
if (ch === "\n") {
|
|
302
|
+
}
|
|
303
|
+
i++;
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
if (ch === "<") {
|
|
307
|
+
const next = src[i + 1];
|
|
308
|
+
if (isExpressionPosition(prevChar, prevWord) && (next === ">" || isIdentStart(next))) {
|
|
309
|
+
if (next !== ">") {
|
|
310
|
+
const past = tryGenericArrow(src, i);
|
|
311
|
+
if (past !== -1) {
|
|
312
|
+
prevChar = ">";
|
|
313
|
+
prevWord = "";
|
|
314
|
+
i = past;
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return i;
|
|
319
|
+
}
|
|
320
|
+
prevChar = "<";
|
|
321
|
+
prevWord = "";
|
|
322
|
+
i++;
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
if (isIdentStart(ch)) {
|
|
326
|
+
let j = i + 1;
|
|
327
|
+
while (j < src.length && isIdentPart(src[j])) {
|
|
328
|
+
j++;
|
|
329
|
+
}
|
|
330
|
+
prevWord = src.slice(i, j);
|
|
331
|
+
prevChar = src[j - 1];
|
|
332
|
+
i = j;
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
prevChar = ch;
|
|
336
|
+
prevWord = "";
|
|
337
|
+
i++;
|
|
338
|
+
}
|
|
339
|
+
return -1;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// ../compiler/src/parser.ts
|
|
343
|
+
var CompileError = class extends Error {
|
|
344
|
+
constructor(message, offset) {
|
|
345
|
+
super(message);
|
|
346
|
+
this.offset = offset;
|
|
347
|
+
this.name = "CompileError";
|
|
348
|
+
}
|
|
349
|
+
offset;
|
|
350
|
+
};
|
|
351
|
+
var MarkupParser = class _MarkupParser {
|
|
352
|
+
constructor(src, pos) {
|
|
353
|
+
this.src = src;
|
|
354
|
+
this.pos = pos;
|
|
355
|
+
}
|
|
356
|
+
src;
|
|
357
|
+
pos;
|
|
358
|
+
/** Entry point; `pos` must be at the opening `<`. */
|
|
359
|
+
parse() {
|
|
360
|
+
return this.parseElement();
|
|
361
|
+
}
|
|
362
|
+
peek(offset = 0) {
|
|
363
|
+
return this.src[this.pos + offset] ?? "";
|
|
364
|
+
}
|
|
365
|
+
skipWs() {
|
|
366
|
+
while (this.pos < this.src.length && isWhitespace(this.src[this.pos])) {
|
|
367
|
+
this.pos++;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
expect(ch) {
|
|
371
|
+
if (this.peek() !== ch) {
|
|
372
|
+
throw new CompileError(
|
|
373
|
+
`Expected '${ch}' but found '${this.peek() || "EOF"}'`,
|
|
374
|
+
this.pos
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
this.pos++;
|
|
378
|
+
}
|
|
379
|
+
/** `pos` at `<`. Parses an element or `<>...</>` fragment. */
|
|
380
|
+
parseElement() {
|
|
381
|
+
const start = this.pos;
|
|
382
|
+
this.expect("<");
|
|
383
|
+
if (this.peek() === ">") {
|
|
384
|
+
this.pos++;
|
|
385
|
+
const children2 = this.parseChildren();
|
|
386
|
+
this.expectClosingTag("");
|
|
387
|
+
return { kind: "fragment", children: children2, start, end: this.pos };
|
|
388
|
+
}
|
|
389
|
+
if (this.peek() !== "/" && !isIdentStart(this.peek())) {
|
|
390
|
+
throw new CompileError(
|
|
391
|
+
"Unexpected '<' in markup; write a literal '<' as {'<'} or <",
|
|
392
|
+
start
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
const tag = this.readTagName();
|
|
396
|
+
const attributes = this.parseAttributes();
|
|
397
|
+
this.skipWs();
|
|
398
|
+
if (this.peek() === "/") {
|
|
399
|
+
this.pos++;
|
|
400
|
+
this.expect(">");
|
|
401
|
+
return {
|
|
402
|
+
kind: "element",
|
|
403
|
+
tag,
|
|
404
|
+
isComponent: _MarkupParser.isComponentTag(tag),
|
|
405
|
+
attributes,
|
|
406
|
+
children: [],
|
|
407
|
+
start,
|
|
408
|
+
end: this.pos
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
this.expect(">");
|
|
412
|
+
const children = this.parseChildren();
|
|
413
|
+
this.expectClosingTag(tag);
|
|
414
|
+
return {
|
|
415
|
+
kind: "element",
|
|
416
|
+
tag,
|
|
417
|
+
isComponent: _MarkupParser.isComponentTag(tag),
|
|
418
|
+
attributes,
|
|
419
|
+
children,
|
|
420
|
+
start,
|
|
421
|
+
end: this.pos
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
static isComponentTag(tag) {
|
|
425
|
+
return /[A-Z]/.test(tag[0] ?? "") || tag.includes(".");
|
|
426
|
+
}
|
|
427
|
+
/** Reads a tag name: identifiers plus `.` (`Foo.Bar`) and `-` (custom elements). */
|
|
428
|
+
readTagName() {
|
|
429
|
+
const start = this.pos;
|
|
430
|
+
if (!isIdentStart(this.peek())) {
|
|
431
|
+
throw new CompileError("Expected a tag name", this.pos);
|
|
432
|
+
}
|
|
433
|
+
this.pos++;
|
|
434
|
+
while (this.pos < this.src.length) {
|
|
435
|
+
const ch = this.src[this.pos];
|
|
436
|
+
if (isIdentPart(ch) || ch === "." || ch === "-") {
|
|
437
|
+
this.pos++;
|
|
438
|
+
} else {
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return this.src.slice(start, this.pos);
|
|
443
|
+
}
|
|
444
|
+
parseAttributes() {
|
|
445
|
+
const attrs = [];
|
|
446
|
+
for (; ; ) {
|
|
447
|
+
this.skipWs();
|
|
448
|
+
const ch = this.peek();
|
|
449
|
+
if (ch === ">" || ch === "/" || ch === "") {
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
452
|
+
const attrStart = this.pos;
|
|
453
|
+
if (ch === "{") {
|
|
454
|
+
const end = skipBalanced(this.src, this.pos);
|
|
455
|
+
const inner = this.src.slice(this.pos + 1, end - 1).trim();
|
|
456
|
+
this.pos = end;
|
|
457
|
+
const code = inner.startsWith("...") ? inner.slice(3).trim() : inner;
|
|
458
|
+
attrs.push({
|
|
459
|
+
kind: "attribute",
|
|
460
|
+
name: null,
|
|
461
|
+
value: { kind: "expression", code },
|
|
462
|
+
spread: true,
|
|
463
|
+
start: attrStart,
|
|
464
|
+
end
|
|
465
|
+
});
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
const name = this.readAttributeName();
|
|
469
|
+
this.skipWs();
|
|
470
|
+
if (this.peek() !== "=") {
|
|
471
|
+
attrs.push({
|
|
472
|
+
kind: "attribute",
|
|
473
|
+
name,
|
|
474
|
+
value: { kind: "none" },
|
|
475
|
+
spread: false,
|
|
476
|
+
start: attrStart,
|
|
477
|
+
end: this.pos
|
|
478
|
+
});
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
this.pos++;
|
|
482
|
+
this.skipWs();
|
|
483
|
+
const valueChar = this.peek();
|
|
484
|
+
if (valueChar === "{") {
|
|
485
|
+
const end = skipBalanced(this.src, this.pos);
|
|
486
|
+
const code = this.src.slice(this.pos + 1, end - 1).trim();
|
|
487
|
+
this.pos = end;
|
|
488
|
+
attrs.push({
|
|
489
|
+
kind: "attribute",
|
|
490
|
+
name,
|
|
491
|
+
value: { kind: "expression", code },
|
|
492
|
+
spread: false,
|
|
493
|
+
start: attrStart,
|
|
494
|
+
end
|
|
495
|
+
});
|
|
496
|
+
} else if (valueChar === '"' || valueChar === "'") {
|
|
497
|
+
const end = skipString(this.src, this.pos);
|
|
498
|
+
const value = this.src.slice(this.pos + 1, end - 1);
|
|
499
|
+
this.pos = end;
|
|
500
|
+
attrs.push({
|
|
501
|
+
kind: "attribute",
|
|
502
|
+
name,
|
|
503
|
+
value: { kind: "static", value },
|
|
504
|
+
spread: false,
|
|
505
|
+
start: attrStart,
|
|
506
|
+
end
|
|
507
|
+
});
|
|
508
|
+
} else {
|
|
509
|
+
throw new CompileError(
|
|
510
|
+
`Expected a value for attribute '${name}'`,
|
|
511
|
+
this.pos
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return attrs;
|
|
516
|
+
}
|
|
517
|
+
/** Attribute names: identifiers plus `-` and `:` (`data-x`, `aria-label`). */
|
|
518
|
+
readAttributeName() {
|
|
519
|
+
const start = this.pos;
|
|
520
|
+
while (this.pos < this.src.length) {
|
|
521
|
+
const ch = this.src[this.pos];
|
|
522
|
+
if (isIdentPart(ch) || ch === "-" || ch === ":") {
|
|
523
|
+
this.pos++;
|
|
524
|
+
} else {
|
|
525
|
+
break;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
if (this.pos === start) {
|
|
529
|
+
throw new CompileError("Expected an attribute name", this.pos);
|
|
530
|
+
}
|
|
531
|
+
return this.src.slice(start, this.pos);
|
|
532
|
+
}
|
|
533
|
+
parseChildren() {
|
|
534
|
+
const children = [];
|
|
535
|
+
while (this.pos < this.src.length) {
|
|
536
|
+
if (this.peek() === "<" && this.peek(1) === "/") {
|
|
537
|
+
break;
|
|
538
|
+
}
|
|
539
|
+
if (this.peek() === "<") {
|
|
540
|
+
children.push(this.parseElement());
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
if (this.peek() === "{") {
|
|
544
|
+
const start = this.pos;
|
|
545
|
+
const end = skipBalanced(this.src, this.pos);
|
|
546
|
+
const code = this.src.slice(this.pos + 1, end - 1);
|
|
547
|
+
this.pos = end;
|
|
548
|
+
if (!_MarkupParser.isEmptyExpression(code)) {
|
|
549
|
+
children.push({ kind: "expression", code, start, end });
|
|
550
|
+
}
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
const text = this.readText();
|
|
554
|
+
if (text) {
|
|
555
|
+
children.push(text);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
return children;
|
|
559
|
+
}
|
|
560
|
+
/** Reads raw text up to the next `<` or `{`, normalised JSX-style. */
|
|
561
|
+
readText() {
|
|
562
|
+
const start = this.pos;
|
|
563
|
+
while (this.pos < this.src.length && this.peek() !== "<" && this.peek() !== "{") {
|
|
564
|
+
this.pos++;
|
|
565
|
+
}
|
|
566
|
+
const raw = this.src.slice(start, this.pos);
|
|
567
|
+
const lead = raw.length - raw.trimStart().length;
|
|
568
|
+
if (raw.slice(lead).startsWith("//")) {
|
|
569
|
+
throw new CompileError(
|
|
570
|
+
"Line comments (//) are not allowed in markup; use {/* ... */} instead",
|
|
571
|
+
start + lead
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
const value = _MarkupParser.normalizeText(raw);
|
|
575
|
+
if (value === "") {
|
|
576
|
+
return null;
|
|
577
|
+
}
|
|
578
|
+
return { kind: "text", value, start, end: this.pos };
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* JSX-style whitespace handling: collapse runs that are purely formatting
|
|
582
|
+
* (whitespace containing a newline) to a single space, and preserve
|
|
583
|
+
* meaningful same-line spacing like `Count: `.
|
|
584
|
+
*/
|
|
585
|
+
static normalizeText(raw) {
|
|
586
|
+
if (/^\s*$/.test(raw)) {
|
|
587
|
+
return "";
|
|
588
|
+
}
|
|
589
|
+
return raw.replace(/\s*\n\s*/g, " ");
|
|
590
|
+
}
|
|
591
|
+
/** True when a `{ ... }` hole has no actual expression (only comments/space). */
|
|
592
|
+
static isEmptyExpression(code) {
|
|
593
|
+
const stripped = code.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/[^\n]*/g, "").trim();
|
|
594
|
+
return stripped === "";
|
|
595
|
+
}
|
|
596
|
+
/** Consumes `</tag>` (or `</>` when `tag === ''`). */
|
|
597
|
+
expectClosingTag(tag) {
|
|
598
|
+
this.expect("<");
|
|
599
|
+
this.expect("/");
|
|
600
|
+
this.skipWs();
|
|
601
|
+
if (tag !== "") {
|
|
602
|
+
const closing = this.readTagName();
|
|
603
|
+
if (closing !== tag) {
|
|
604
|
+
throw new CompileError(
|
|
605
|
+
`Mismatched closing tag: expected </${tag}> but found </${closing}>`,
|
|
606
|
+
this.pos
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
this.skipWs();
|
|
611
|
+
this.expect(">");
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
function parseMarkup(src, start) {
|
|
615
|
+
const parser = new MarkupParser(src, start);
|
|
616
|
+
const node = parser.parse();
|
|
617
|
+
return { node, end: parser.pos };
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// ../compiler/src/codegen.ts
|
|
621
|
+
function walkComponentTags(node, visit) {
|
|
622
|
+
if (node.kind === "element" && node.isComponent) {
|
|
623
|
+
visit(node.tag);
|
|
624
|
+
}
|
|
625
|
+
for (const child of node.children) {
|
|
626
|
+
if (child.kind === "element" || child.kind === "fragment") {
|
|
627
|
+
walkComponentTags(child, visit);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// ../language-service/src/mapping.ts
|
|
633
|
+
var CodeMapping = class _CodeMapping {
|
|
634
|
+
/** Segments sorted by `sourceStart` (for original -> generated). */
|
|
635
|
+
bySource;
|
|
636
|
+
/** Segments sorted by `generatedStart` (for generated -> original). */
|
|
637
|
+
byGenerated;
|
|
638
|
+
constructor(segments) {
|
|
639
|
+
this.bySource = [...segments].sort((a, b) => a.sourceStart - b.sourceStart);
|
|
640
|
+
this.byGenerated = [...segments].sort((a, b) => a.generatedStart - b.generatedStart);
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Maps an offset in the original source to the virtual module, or `null`
|
|
644
|
+
* when it falls in non-mapped generated scaffolding. A position touching a
|
|
645
|
+
* segment's exclusive end still maps (so a caret right after an identifier
|
|
646
|
+
* resolves), preferring the segment that actually contains it.
|
|
647
|
+
*/
|
|
648
|
+
toGenerated(sourceOffset) {
|
|
649
|
+
const seg = _CodeMapping.find(this.bySource, sourceOffset, (segment) => segment.sourceStart, (segment) => segment.sourceEnd);
|
|
650
|
+
if (seg === null) {
|
|
651
|
+
return null;
|
|
652
|
+
}
|
|
653
|
+
return seg.generatedStart + (sourceOffset - seg.sourceStart);
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Maps an offset in the virtual module back to the original source, or
|
|
657
|
+
* `null` when it lands in generated scaffolding with no original origin.
|
|
658
|
+
*/
|
|
659
|
+
toOriginal(generatedOffset) {
|
|
660
|
+
const seg = _CodeMapping.find(this.byGenerated, generatedOffset, (segment) => segment.generatedStart, (segment) => segment.generatedEnd);
|
|
661
|
+
if (seg === null) {
|
|
662
|
+
return null;
|
|
663
|
+
}
|
|
664
|
+
return seg.sourceStart + (generatedOffset - seg.generatedStart);
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Maps an original `[start, end)` range to the virtual module. Returns
|
|
668
|
+
* `null` unless both ends land in the *same* segment, which guarantees the
|
|
669
|
+
* translated range is contiguous and meaningful (e.g. a rename edit).
|
|
670
|
+
*/
|
|
671
|
+
toGeneratedRange(sourceStart, sourceEnd) {
|
|
672
|
+
const seg = _CodeMapping.find(this.bySource, sourceStart, (segment) => segment.sourceStart, (segment) => segment.sourceEnd);
|
|
673
|
+
if (seg === null || sourceEnd > seg.sourceEnd) {
|
|
674
|
+
return null;
|
|
675
|
+
}
|
|
676
|
+
return {
|
|
677
|
+
start: seg.generatedStart + (sourceStart - seg.sourceStart),
|
|
678
|
+
end: seg.generatedStart + (sourceEnd - seg.sourceStart)
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* Maps a virtual `[start, end)` range back to the original source. Both
|
|
683
|
+
* ends must share a segment; otherwise the range straddles generated
|
|
684
|
+
* scaffolding and has no faithful original counterpart.
|
|
685
|
+
*/
|
|
686
|
+
toOriginalRange(generatedStart, generatedEnd) {
|
|
687
|
+
const seg = _CodeMapping.find(this.byGenerated, generatedStart, (segment) => segment.generatedStart, (segment) => segment.generatedEnd);
|
|
688
|
+
if (seg === null || generatedEnd > seg.generatedEnd) {
|
|
689
|
+
return null;
|
|
690
|
+
}
|
|
691
|
+
return {
|
|
692
|
+
start: seg.sourceStart + (generatedStart - seg.generatedStart),
|
|
693
|
+
end: seg.sourceStart + (generatedEnd - seg.generatedStart)
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Binary-searches `segments` (sorted by `start`) for the one whose
|
|
698
|
+
* `[start, end]` span contains `offset`, preferring a span that strictly
|
|
699
|
+
* contains it over one it only touches at the end.
|
|
700
|
+
*/
|
|
701
|
+
static find(segments, offset, start, end) {
|
|
702
|
+
let lo = 0;
|
|
703
|
+
let hi = segments.length - 1;
|
|
704
|
+
let touch = null;
|
|
705
|
+
while (lo <= hi) {
|
|
706
|
+
const mid = lo + hi >> 1;
|
|
707
|
+
const seg = segments[mid];
|
|
708
|
+
if (offset < start(seg)) {
|
|
709
|
+
hi = mid - 1;
|
|
710
|
+
} else if (offset > end(seg)) {
|
|
711
|
+
lo = mid + 1;
|
|
712
|
+
} else {
|
|
713
|
+
if (offset < end(seg)) {
|
|
714
|
+
return seg;
|
|
715
|
+
}
|
|
716
|
+
touch = seg;
|
|
717
|
+
hi = mid - 1;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
return touch;
|
|
721
|
+
}
|
|
722
|
+
};
|
|
723
|
+
|
|
724
|
+
// ../language-service/src/virtual-code.ts
|
|
725
|
+
var RUNTIME_MODULE = "@azerothjs/core";
|
|
726
|
+
var BUILTIN_COMPONENTS = [
|
|
727
|
+
"Show",
|
|
728
|
+
"For",
|
|
729
|
+
"Switch",
|
|
730
|
+
"Match",
|
|
731
|
+
"Portal",
|
|
732
|
+
"Dynamic",
|
|
733
|
+
"Suspense",
|
|
734
|
+
"ErrorBoundary",
|
|
735
|
+
"Transition",
|
|
736
|
+
"Outlet"
|
|
737
|
+
];
|
|
738
|
+
var BUILTIN_SET = new Set(BUILTIN_COMPONENTS);
|
|
739
|
+
var Builder = class {
|
|
740
|
+
constructor(src) {
|
|
741
|
+
this.src = src;
|
|
742
|
+
}
|
|
743
|
+
src;
|
|
744
|
+
out = "";
|
|
745
|
+
segments = [];
|
|
746
|
+
/** Appends generated scaffolding that has no original counterpart. */
|
|
747
|
+
emit(text) {
|
|
748
|
+
this.out += text;
|
|
749
|
+
}
|
|
750
|
+
/** Copies `[start, end)` from the source verbatim and records a mapping. */
|
|
751
|
+
copy(start, end, kind) {
|
|
752
|
+
if (end <= start) {
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
const generatedStart = this.out.length;
|
|
756
|
+
this.out += this.src.slice(start, end);
|
|
757
|
+
this.segments.push({
|
|
758
|
+
sourceStart: start,
|
|
759
|
+
sourceEnd: end,
|
|
760
|
+
generatedStart,
|
|
761
|
+
generatedEnd: this.out.length,
|
|
762
|
+
kind
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
};
|
|
766
|
+
function generateVirtualCode(source) {
|
|
767
|
+
const builder = new Builder(source);
|
|
768
|
+
const usedBuiltins = /* @__PURE__ */ new Set();
|
|
769
|
+
const collect = (node) => walkComponentTags(node, (tag) => {
|
|
770
|
+
if (BUILTIN_SET.has(tag)) {
|
|
771
|
+
usedBuiltins.add(tag);
|
|
772
|
+
}
|
|
773
|
+
});
|
|
774
|
+
const emitExpression = (start, end, kind) => {
|
|
775
|
+
let j = start;
|
|
776
|
+
for (; ; ) {
|
|
777
|
+
const m = findMarkupStart(source, j);
|
|
778
|
+
if (m === -1 || m >= end) {
|
|
779
|
+
builder.copy(j, end, kind);
|
|
780
|
+
break;
|
|
781
|
+
}
|
|
782
|
+
builder.copy(j, m, kind);
|
|
783
|
+
let parsed;
|
|
784
|
+
try {
|
|
785
|
+
parsed = parseMarkup(source, m);
|
|
786
|
+
} catch {
|
|
787
|
+
builder.copy(m, end, kind);
|
|
788
|
+
break;
|
|
789
|
+
}
|
|
790
|
+
collect(parsed.node);
|
|
791
|
+
emitNode(parsed.node);
|
|
792
|
+
j = parsed.end;
|
|
793
|
+
}
|
|
794
|
+
};
|
|
795
|
+
const emitDynamic = (span, isEventHandler, kind) => {
|
|
796
|
+
const code = source.slice(span.start, span.end).trim();
|
|
797
|
+
if (isEventHandler || isFunctionLiteral(code) || isBareReference(code) || isCollectionLiteral(code)) {
|
|
798
|
+
emitExpression(span.start, span.end, kind);
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
builder.emit("() => (");
|
|
802
|
+
emitExpression(span.start, span.end, kind);
|
|
803
|
+
builder.emit(")");
|
|
804
|
+
};
|
|
805
|
+
const emitAttribute = (attr, isHost) => {
|
|
806
|
+
if (attr.spread) {
|
|
807
|
+
builder.emit("...");
|
|
808
|
+
emitExpression(spreadSpan(source, attr).start, spreadSpan(source, attr).end, "attribute");
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
const name = attr.name;
|
|
812
|
+
builder.emit(`${objectKey(name)}: `);
|
|
813
|
+
if (attr.value.kind === "none") {
|
|
814
|
+
builder.emit("true");
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
if (attr.value.kind === "static") {
|
|
818
|
+
builder.emit(quoteString(attr.value.value));
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
const span = attrExprSpan(source, attr);
|
|
822
|
+
if (isHost && isEventName(name) && isFunctionLiteral(source.slice(span.start, span.end).trim())) {
|
|
823
|
+
builder.emit("(");
|
|
824
|
+
emitExpression(span.start, span.end, "attribute");
|
|
825
|
+
builder.emit(`) satisfies AzerothHandler<'${name}'>`);
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
emitDynamic(span, isEventName(name), "attribute");
|
|
829
|
+
};
|
|
830
|
+
const emitProps = (attrs, childrenEntry, isHost) => {
|
|
831
|
+
builder.emit("{ ");
|
|
832
|
+
attrs.forEach((attr, index) => {
|
|
833
|
+
if (index > 0) {
|
|
834
|
+
builder.emit(", ");
|
|
835
|
+
}
|
|
836
|
+
emitAttribute(attr, isHost);
|
|
837
|
+
});
|
|
838
|
+
if (childrenEntry) {
|
|
839
|
+
if (attrs.length > 0) {
|
|
840
|
+
builder.emit(", ");
|
|
841
|
+
}
|
|
842
|
+
childrenEntry();
|
|
843
|
+
}
|
|
844
|
+
builder.emit(" }");
|
|
845
|
+
};
|
|
846
|
+
const emitChild = (child) => {
|
|
847
|
+
if (child.kind === "text") {
|
|
848
|
+
builder.emit(quoteString(child.value));
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
if (child.kind === "expression") {
|
|
852
|
+
emitDynamic({ start: child.start + 1, end: child.end - 1 }, false, "expression");
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
emitNode(child);
|
|
856
|
+
};
|
|
857
|
+
const emitComponentChildren = (children) => {
|
|
858
|
+
if (children.length === 0) {
|
|
859
|
+
return null;
|
|
860
|
+
}
|
|
861
|
+
return () => {
|
|
862
|
+
builder.emit("children: ");
|
|
863
|
+
if (children.length === 1) {
|
|
864
|
+
const only = children[0];
|
|
865
|
+
if (only.kind === "expression") {
|
|
866
|
+
const span = { start: only.start + 1, end: only.end - 1 };
|
|
867
|
+
const code = source.slice(span.start, span.end).trim();
|
|
868
|
+
if (isFunctionLiteral(code)) {
|
|
869
|
+
emitExpression(span.start, span.end, "expression");
|
|
870
|
+
} else {
|
|
871
|
+
builder.emit("() => (");
|
|
872
|
+
emitExpression(span.start, span.end, "expression");
|
|
873
|
+
builder.emit(")");
|
|
874
|
+
}
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
builder.emit("() => ");
|
|
878
|
+
emitChild(only);
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
builder.emit("() => [");
|
|
882
|
+
children.forEach((child, index) => {
|
|
883
|
+
if (index > 0) {
|
|
884
|
+
builder.emit(", ");
|
|
885
|
+
}
|
|
886
|
+
emitChild(child);
|
|
887
|
+
});
|
|
888
|
+
builder.emit("]");
|
|
889
|
+
};
|
|
890
|
+
};
|
|
891
|
+
function emitNode(node) {
|
|
892
|
+
if (node.kind === "fragment") {
|
|
893
|
+
builder.emit("[");
|
|
894
|
+
node.children.forEach((child, index) => {
|
|
895
|
+
if (index > 0) {
|
|
896
|
+
builder.emit(", ");
|
|
897
|
+
}
|
|
898
|
+
emitChild(child);
|
|
899
|
+
});
|
|
900
|
+
builder.emit("]");
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
if (node.isComponent) {
|
|
904
|
+
const tagStart = node.start + 1;
|
|
905
|
+
builder.copy(tagStart, tagStart + node.tag.length, "tag");
|
|
906
|
+
builder.emit("(");
|
|
907
|
+
emitProps(node.attributes, emitComponentChildren(node.children), false);
|
|
908
|
+
builder.emit(")");
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
builder.emit(`h(${quoteString(node.tag)}, `);
|
|
912
|
+
emitProps(node.attributes, null, true);
|
|
913
|
+
for (const child of node.children) {
|
|
914
|
+
builder.emit(", ");
|
|
915
|
+
emitChild(child);
|
|
916
|
+
}
|
|
917
|
+
builder.emit(")");
|
|
918
|
+
}
|
|
919
|
+
let i = 0;
|
|
920
|
+
for (; ; ) {
|
|
921
|
+
const start = findMarkupStart(source, i);
|
|
922
|
+
if (start === -1) {
|
|
923
|
+
builder.copy(i, source.length, "script");
|
|
924
|
+
break;
|
|
925
|
+
}
|
|
926
|
+
builder.copy(i, start, "script");
|
|
927
|
+
let parsed;
|
|
928
|
+
try {
|
|
929
|
+
parsed = parseMarkup(source, start);
|
|
930
|
+
} catch {
|
|
931
|
+
builder.copy(start, source.length, "script");
|
|
932
|
+
break;
|
|
933
|
+
}
|
|
934
|
+
collect(parsed.node);
|
|
935
|
+
emitNode(parsed.node);
|
|
936
|
+
i = parsed.end;
|
|
937
|
+
}
|
|
938
|
+
return finalize(builder, source, usedBuiltins);
|
|
939
|
+
}
|
|
940
|
+
function finalize(builder, source, usedBuiltins) {
|
|
941
|
+
const hasMarkup = builder.segments.some((segment) => segment.kind !== "script") || builder.out !== source;
|
|
942
|
+
if (!hasMarkup) {
|
|
943
|
+
return { code: builder.out, mapping: new CodeMapping(builder.segments) };
|
|
944
|
+
}
|
|
945
|
+
const names = ["h", ...usedBuiltins].filter((name) => !alreadyImports(source, name));
|
|
946
|
+
const prefix = names.length > 0 ? `import { ${names.join(", ")} } from '${RUNTIME_MODULE}';
|
|
947
|
+
` : "";
|
|
948
|
+
const shift = prefix.length;
|
|
949
|
+
const segments = builder.segments.map((segment) => ({
|
|
950
|
+
...segment,
|
|
951
|
+
generatedStart: segment.generatedStart + shift,
|
|
952
|
+
generatedEnd: segment.generatedEnd + shift
|
|
953
|
+
}));
|
|
954
|
+
return { code: prefix + builder.out, mapping: new CodeMapping(segments) };
|
|
955
|
+
}
|
|
956
|
+
function alreadyImports(source, name) {
|
|
957
|
+
return new RegExp(`import\\s*\\{[^}]*\\b${name}\\b[^}]*\\}\\s*from`).test(source);
|
|
958
|
+
}
|
|
959
|
+
function attrExprSpan(source, attr) {
|
|
960
|
+
const open = source.indexOf("{", attr.start);
|
|
961
|
+
return trimSpan(source, open + 1, attr.end - 1);
|
|
962
|
+
}
|
|
963
|
+
function spreadSpan(source, attr) {
|
|
964
|
+
const open = source.indexOf("{", attr.start);
|
|
965
|
+
const close = skipBalanced(source, open);
|
|
966
|
+
const inner = trimSpan(source, open + 1, close - 1);
|
|
967
|
+
if (source.startsWith("...", inner.start)) {
|
|
968
|
+
return trimSpan(source, inner.start + 3, inner.end);
|
|
969
|
+
}
|
|
970
|
+
return inner;
|
|
971
|
+
}
|
|
972
|
+
function trimSpan(source, start, end) {
|
|
973
|
+
let s = start;
|
|
974
|
+
let e = end;
|
|
975
|
+
while (s < e && isWhitespace(source[s])) {
|
|
976
|
+
s++;
|
|
977
|
+
}
|
|
978
|
+
while (e > s && isWhitespace(source[e - 1])) {
|
|
979
|
+
e--;
|
|
980
|
+
}
|
|
981
|
+
return { start: s, end: e };
|
|
982
|
+
}
|
|
983
|
+
function isFunctionLiteral(code) {
|
|
984
|
+
const t = code.trim();
|
|
985
|
+
if (/^async\s+function\b/.test(t) || /^function\b/.test(t)) {
|
|
986
|
+
return true;
|
|
987
|
+
}
|
|
988
|
+
return /^(async\s+)?(\([^]*?\)|[A-Za-z_$][\w$]*)\s*=>/.test(t);
|
|
989
|
+
}
|
|
990
|
+
function isBareReference(code) {
|
|
991
|
+
return /^[A-Za-z_$][\w$]*(\s*\.\s*[A-Za-z_$][\w$]*)*$/.test(code.trim());
|
|
992
|
+
}
|
|
993
|
+
function isCollectionLiteral(code) {
|
|
994
|
+
const t = code.trim();
|
|
995
|
+
return t.startsWith("[") || t.startsWith("{");
|
|
996
|
+
}
|
|
997
|
+
function isEventName(name) {
|
|
998
|
+
return name.length > 2 && name.startsWith("on") && name[2] === name[2].toUpperCase();
|
|
999
|
+
}
|
|
1000
|
+
function objectKey(name) {
|
|
1001
|
+
return /^[A-Za-z_$][\w$]*$/.test(name) ? name : `'${name}'`;
|
|
1002
|
+
}
|
|
1003
|
+
function quoteString(value) {
|
|
1004
|
+
return `'${value.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n")}'`;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// ../language-service/src/ts-project.ts
|
|
1008
|
+
var VIRTUAL_SUFFIX = ".azeroth.ts";
|
|
1009
|
+
function isVirtualFile(fileName) {
|
|
1010
|
+
return fileName.endsWith(VIRTUAL_SUFFIX);
|
|
1011
|
+
}
|
|
1012
|
+
function toVirtualFile(azerothPath) {
|
|
1013
|
+
return `${azerothPath}.ts`;
|
|
1014
|
+
}
|
|
1015
|
+
function toAzerothPath(virtualFile) {
|
|
1016
|
+
return virtualFile.slice(0, -3);
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// src/decorate.ts
|
|
1020
|
+
var AZEROTH_EXT = ".azeroth";
|
|
1021
|
+
function isAzerothSpecifier(text) {
|
|
1022
|
+
return text.endsWith(AZEROTH_EXT) && (text.startsWith(".") || text.startsWith("/"));
|
|
1023
|
+
}
|
|
1024
|
+
function scriptKindFromName(ts2, fileName) {
|
|
1025
|
+
if (fileName.endsWith(".tsx")) {
|
|
1026
|
+
return ts2.ScriptKind.TSX;
|
|
1027
|
+
}
|
|
1028
|
+
if (fileName.endsWith(".jsx")) {
|
|
1029
|
+
return ts2.ScriptKind.JSX;
|
|
1030
|
+
}
|
|
1031
|
+
if (fileName.endsWith(".json")) {
|
|
1032
|
+
return ts2.ScriptKind.JSON;
|
|
1033
|
+
}
|
|
1034
|
+
if (/\.(?:m|c)?js$/.test(fileName)) {
|
|
1035
|
+
return ts2.ScriptKind.JS;
|
|
1036
|
+
}
|
|
1037
|
+
return ts2.ScriptKind.TS;
|
|
1038
|
+
}
|
|
1039
|
+
function resolveSibling(containingFile, specifier) {
|
|
1040
|
+
return import_node_path.default.resolve(import_node_path.default.dirname(containingFile), specifier).replace(/\\/g, "/");
|
|
1041
|
+
}
|
|
1042
|
+
function decorateLanguageServiceHost(ts2, host, options = {}) {
|
|
1043
|
+
const read = options.readAzeroth ?? ((azerothPath) => ts2.sys.readFile(azerothPath));
|
|
1044
|
+
const cache = /* @__PURE__ */ new Map();
|
|
1045
|
+
const virtualCodeFor = (azerothPath) => {
|
|
1046
|
+
const source = read(azerothPath);
|
|
1047
|
+
if (source === void 0) {
|
|
1048
|
+
return void 0;
|
|
1049
|
+
}
|
|
1050
|
+
const cached = cache.get(azerothPath);
|
|
1051
|
+
if (cached && cached.source === source) {
|
|
1052
|
+
return cached.code;
|
|
1053
|
+
}
|
|
1054
|
+
const code = generateVirtualCode(source).code;
|
|
1055
|
+
cache.set(azerothPath, { source, code });
|
|
1056
|
+
return code;
|
|
1057
|
+
};
|
|
1058
|
+
const origSnapshot = host.getScriptSnapshot?.bind(host);
|
|
1059
|
+
host.getScriptSnapshot = (fileName) => {
|
|
1060
|
+
if (isVirtualFile(fileName)) {
|
|
1061
|
+
const code = virtualCodeFor(toAzerothPath(fileName));
|
|
1062
|
+
return code === void 0 ? void 0 : ts2.ScriptSnapshot.fromString(code);
|
|
1063
|
+
}
|
|
1064
|
+
return origSnapshot ? origSnapshot(fileName) : void 0;
|
|
1065
|
+
};
|
|
1066
|
+
const origKind = host.getScriptKind?.bind(host);
|
|
1067
|
+
host.getScriptKind = (fileName) => {
|
|
1068
|
+
if (isVirtualFile(fileName)) {
|
|
1069
|
+
return ts2.ScriptKind.TS;
|
|
1070
|
+
}
|
|
1071
|
+
return origKind ? origKind(fileName) : scriptKindFromName(ts2, fileName);
|
|
1072
|
+
};
|
|
1073
|
+
const origVersion = host.getScriptVersion.bind(host);
|
|
1074
|
+
host.getScriptVersion = (fileName) => {
|
|
1075
|
+
if (isVirtualFile(fileName)) {
|
|
1076
|
+
const azerothPath = toAzerothPath(fileName);
|
|
1077
|
+
const mtime = ts2.sys.getModifiedTime?.(azerothPath)?.getTime() ?? 0;
|
|
1078
|
+
return `azeroth:${mtime}`;
|
|
1079
|
+
}
|
|
1080
|
+
return origVersion(fileName);
|
|
1081
|
+
};
|
|
1082
|
+
const origExists = host.fileExists?.bind(host);
|
|
1083
|
+
host.fileExists = (fileName) => {
|
|
1084
|
+
if (isVirtualFile(fileName)) {
|
|
1085
|
+
return read(toAzerothPath(fileName)) !== void 0;
|
|
1086
|
+
}
|
|
1087
|
+
return origExists ? origExists(fileName) : ts2.sys.fileExists(fileName);
|
|
1088
|
+
};
|
|
1089
|
+
const origReadFile = host.readFile?.bind(host);
|
|
1090
|
+
host.readFile = (fileName, encoding) => {
|
|
1091
|
+
if (isVirtualFile(fileName)) {
|
|
1092
|
+
return virtualCodeFor(toAzerothPath(fileName));
|
|
1093
|
+
}
|
|
1094
|
+
return origReadFile ? origReadFile(fileName, encoding) : ts2.sys.readFile(fileName, encoding);
|
|
1095
|
+
};
|
|
1096
|
+
const origResolve = host.resolveModuleNameLiterals?.bind(host);
|
|
1097
|
+
host.resolveModuleNameLiterals = (literals, containingFile, redirectedReference, compilerOptions, containingSourceFile, reusedNames) => {
|
|
1098
|
+
const base = origResolve ? origResolve(literals, containingFile, redirectedReference, compilerOptions, containingSourceFile, reusedNames) : literals.map((literal) => ts2.resolveModuleName(literal.text, containingFile, compilerOptions, host));
|
|
1099
|
+
return literals.map((literal, index) => {
|
|
1100
|
+
if (isAzerothSpecifier(literal.text)) {
|
|
1101
|
+
const azerothPath = resolveSibling(containingFile, literal.text);
|
|
1102
|
+
if (read(azerothPath) !== void 0) {
|
|
1103
|
+
return {
|
|
1104
|
+
resolvedModule: {
|
|
1105
|
+
resolvedFileName: toVirtualFile(azerothPath),
|
|
1106
|
+
extension: ts2.Extension.Ts,
|
|
1107
|
+
isExternalLibraryImport: false
|
|
1108
|
+
},
|
|
1109
|
+
failedLookupLocations: []
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
return base[index];
|
|
1114
|
+
});
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
// src/index.ts
|
|
1119
|
+
function init(modules) {
|
|
1120
|
+
const ts2 = modules.typescript;
|
|
1121
|
+
return {
|
|
1122
|
+
create(info) {
|
|
1123
|
+
decorateLanguageServiceHost(ts2, info.languageServiceHost);
|
|
1124
|
+
info.project.projectService.logger.info("[azerothjs] typescript-plugin: .azeroth module resolution enabled");
|
|
1125
|
+
return info.languageService;
|
|
1126
|
+
},
|
|
1127
|
+
getExternalFiles(project) {
|
|
1128
|
+
return project.getFileNames().filter((fileName) => fileName.endsWith(".azeroth"));
|
|
1129
|
+
}
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
module.exports = init;
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@azerothjs/typescript-plugin",
|
|
3
|
+
"version": "0.4.0-beta.3",
|
|
4
|
+
"description": "AzerothJS TypeScript language-service plugin — resolves .azeroth imports with real types in tsserver/editors",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"files":
|
|
7
|
+
[
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
10
|
+
"scripts":
|
|
11
|
+
{
|
|
12
|
+
"build": "node esbuild.mjs",
|
|
13
|
+
"prepublishOnly": "npm run build"
|
|
14
|
+
},
|
|
15
|
+
"keywords":
|
|
16
|
+
[
|
|
17
|
+
"azeroth",
|
|
18
|
+
"typescript",
|
|
19
|
+
"typescript-plugin",
|
|
20
|
+
"tsserver",
|
|
21
|
+
"language-service"
|
|
22
|
+
],
|
|
23
|
+
"devDependencies":
|
|
24
|
+
{
|
|
25
|
+
"@azerothjs/language-service": "0.4.0-beta.3",
|
|
26
|
+
"esbuild": "^0.28.0"
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies":
|
|
29
|
+
{
|
|
30
|
+
"typescript": ">=5"
|
|
31
|
+
},
|
|
32
|
+
"publishConfig":
|
|
33
|
+
{
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"author":
|
|
37
|
+
{
|
|
38
|
+
"name": "IntelligentQuantum",
|
|
39
|
+
"email": "IntelligentQuantum@Gmail.Com",
|
|
40
|
+
"url": "https://IntelligentQuantum.Dev/"
|
|
41
|
+
},
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"repository":
|
|
44
|
+
{
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": "git+https://github.com/IntelligentQuantum-Dev/AzerothJS.git",
|
|
47
|
+
"directory": "packages/typescript-plugin"
|
|
48
|
+
},
|
|
49
|
+
"homepage": "https://github.com/IntelligentQuantum-Dev/AzerothJS/tree/main/packages/typescript-plugin",
|
|
50
|
+
"bugs":
|
|
51
|
+
{
|
|
52
|
+
"url": "https://github.com/IntelligentQuantum-Dev/AzerothJS/issues"
|
|
53
|
+
}
|
|
54
|
+
}
|