module-tsx 0.0.3 → 0.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 +3 -0
- package/dist/index.cdn.mjs +688 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.dev.mjs +1411 -0
- package/dist/index.js +27 -8
- package/dist/index.mjs +20 -20
- package/dist/index.umd.js +20 -20
- package/package.json +26 -5
package/README.md
CHANGED
|
@@ -96,6 +96,9 @@ npx skills add yieldray/module-tsx
|
|
|
96
96
|
<!-- ESM -->
|
|
97
97
|
<script type="module" src="https://esm.sh/module-tsx"></script>
|
|
98
98
|
|
|
99
|
+
<!-- ESM (DEV mode) -->
|
|
100
|
+
<script type="module" src="https://esm.sh/module-tsx/dev"></script>
|
|
101
|
+
|
|
99
102
|
<!-- ESM (self-contained, no external dependencies) -->
|
|
100
103
|
<script
|
|
101
104
|
type="module"
|
|
@@ -0,0 +1,688 @@
|
|
|
1
|
+
import ts from "https://esm.sh/typescript@5.9.3";
|
|
2
|
+
//#region src/error.ts
|
|
3
|
+
/** Custom error class for module-tsx */
|
|
4
|
+
var ModuleTSXError = class ModuleTSXError extends Error {
|
|
5
|
+
constructor(message, options) {
|
|
6
|
+
super(message, options);
|
|
7
|
+
this.name = "ModuleTSXError";
|
|
8
|
+
if ("captureStackTrace" in Error) Error.captureStackTrace(this, ModuleTSXError);
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
/** Log warnings with consistent formatting */
|
|
12
|
+
function warn(message, ...args) {
|
|
13
|
+
console.warn(`[module-tsx] ${message}`, ...args);
|
|
14
|
+
}
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region src/import-map.ts
|
|
17
|
+
var ImportMap = class {
|
|
18
|
+
imports;
|
|
19
|
+
scopes;
|
|
20
|
+
integrity;
|
|
21
|
+
constructor(imports = /* @__PURE__ */ new Map(), scopes = /* @__PURE__ */ new Map(), integrity = /* @__PURE__ */ new Map()) {
|
|
22
|
+
this.imports = imports;
|
|
23
|
+
this.scopes = scopes;
|
|
24
|
+
this.integrity = integrity;
|
|
25
|
+
}
|
|
26
|
+
/** Parse a JSON import map string against a base URL. */
|
|
27
|
+
static parse(input, baseURL) {
|
|
28
|
+
return parseImportMapString(input, typeof baseURL === "string" ? new URL(baseURL) : baseURL);
|
|
29
|
+
}
|
|
30
|
+
/** Build an ImportMap from a plain object (same shape as the JSON format). */
|
|
31
|
+
static of(json, baseURL) {
|
|
32
|
+
return parseImportMapString(JSON.stringify(json), typeof baseURL === "string" ? new URL(baseURL) : baseURL);
|
|
33
|
+
}
|
|
34
|
+
/** Merge newImportMap into oldImportMap in place (spec § "merge existing and new import maps"). */
|
|
35
|
+
static merge(oldImportMap, newImportMap, resolvedModuleSet = []) {
|
|
36
|
+
mergeExistingAndNewImportMaps(oldImportMap, newImportMap, resolvedModuleSet);
|
|
37
|
+
}
|
|
38
|
+
/** Resolve a specifier against an import map (spec § "resolve a module specifier"). */
|
|
39
|
+
static resolve(specifier, importMap, baseURL) {
|
|
40
|
+
return resolveFromImportMap(specifier, importMap, baseURL);
|
|
41
|
+
}
|
|
42
|
+
/** Parse all <script type="importmap"> elements from the DOM. */
|
|
43
|
+
static fromDOM() {
|
|
44
|
+
return parseImportMaps();
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
function resolveURLLikeModuleSpecifier(specifier, baseURL) {
|
|
48
|
+
if (specifier.startsWith("/") || specifier.startsWith("./") || specifier.startsWith("../")) try {
|
|
49
|
+
return new URL(specifier, baseURL);
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
return new URL(specifier);
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function normalizeSpecifierKey(specifierKey, baseURL) {
|
|
60
|
+
if (specifierKey === "") {
|
|
61
|
+
warn("Specifier keys may not be the empty string.");
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
const url = resolveURLLikeModuleSpecifier(specifierKey, baseURL);
|
|
65
|
+
if (url !== null) return url.href;
|
|
66
|
+
return specifierKey;
|
|
67
|
+
}
|
|
68
|
+
function sortAndNormalizeSpecifierMap(originalMap, baseURL) {
|
|
69
|
+
const normalized = /* @__PURE__ */ new Map();
|
|
70
|
+
for (const [specifierKey, value] of Object.entries(originalMap)) {
|
|
71
|
+
const normalizedKey = normalizeSpecifierKey(specifierKey, baseURL);
|
|
72
|
+
if (normalizedKey === null) continue;
|
|
73
|
+
if (typeof value !== "string") {
|
|
74
|
+
warn(`Import map addresses must be strings; ignoring key "${specifierKey}".`);
|
|
75
|
+
normalized.set(normalizedKey, null);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const addressURL = resolveURLLikeModuleSpecifier(value, baseURL);
|
|
79
|
+
if (addressURL === null) {
|
|
80
|
+
warn(`Invalid address "${value}" for key "${specifierKey}".`);
|
|
81
|
+
normalized.set(normalizedKey, null);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (specifierKey.endsWith("/") && !addressURL.href.endsWith("/")) {
|
|
85
|
+
warn(`Invalid address "${value}" for specifier key "${specifierKey}": since the specifier key ends with "/", the address must as well.`);
|
|
86
|
+
normalized.set(normalizedKey, null);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
normalized.set(normalizedKey, addressURL);
|
|
90
|
+
}
|
|
91
|
+
return sortedDescending(normalized);
|
|
92
|
+
}
|
|
93
|
+
function sortAndNormalizeScopes(originalMap, baseURL) {
|
|
94
|
+
const normalized = /* @__PURE__ */ new Map();
|
|
95
|
+
for (const [scopePrefix, potentialSpecifierMap] of Object.entries(originalMap)) {
|
|
96
|
+
if (typeof potentialSpecifierMap !== "object" || potentialSpecifierMap === null || Array.isArray(potentialSpecifierMap)) throw new TypeError(`The value of the scope with prefix "${scopePrefix}" must be a JSON object.`);
|
|
97
|
+
let scopePrefixURL;
|
|
98
|
+
try {
|
|
99
|
+
scopePrefixURL = new URL(scopePrefix, baseURL);
|
|
100
|
+
} catch {
|
|
101
|
+
warn(`Scope prefix URL "${scopePrefix}" is not parseable.`);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
normalized.set(scopePrefixURL.href, sortAndNormalizeSpecifierMap(potentialSpecifierMap, baseURL));
|
|
105
|
+
}
|
|
106
|
+
return sortedDescending(normalized);
|
|
107
|
+
}
|
|
108
|
+
function normalizeModuleIntegrityMap(originalMap, baseURL) {
|
|
109
|
+
const normalized = /* @__PURE__ */ new Map();
|
|
110
|
+
for (const [key, value] of Object.entries(originalMap)) {
|
|
111
|
+
const url = resolveURLLikeModuleSpecifier(key, baseURL);
|
|
112
|
+
if (url === null) {
|
|
113
|
+
warn(`Invalid URL key "${key}" in integrity map.`);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (typeof value !== "string") {
|
|
117
|
+
warn(`Integrity values must be strings; ignoring key "${key}".`);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
normalized.set(url.href, value);
|
|
121
|
+
}
|
|
122
|
+
return normalized;
|
|
123
|
+
}
|
|
124
|
+
function sortedDescending(map) {
|
|
125
|
+
return new Map([...map.entries()].sort((a, b) => a[0] < b[0] ? 1 : a[0] > b[0] ? -1 : 0));
|
|
126
|
+
}
|
|
127
|
+
function parseImportMapString(input, baseURL) {
|
|
128
|
+
let parsed;
|
|
129
|
+
try {
|
|
130
|
+
parsed = JSON.parse(input);
|
|
131
|
+
} catch {
|
|
132
|
+
throw new TypeError("Failed to parse import map: not valid JSON.");
|
|
133
|
+
}
|
|
134
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new TypeError("Import map: top-level value must be a JSON object.");
|
|
135
|
+
const obj = parsed;
|
|
136
|
+
let imports = /* @__PURE__ */ new Map();
|
|
137
|
+
if ("imports" in obj) {
|
|
138
|
+
if (typeof obj.imports !== "object" || obj.imports === null || Array.isArray(obj.imports)) throw new TypeError("Import map: \"imports\" must be a JSON object.");
|
|
139
|
+
imports = sortAndNormalizeSpecifierMap(obj.imports, baseURL);
|
|
140
|
+
}
|
|
141
|
+
let scopes = /* @__PURE__ */ new Map();
|
|
142
|
+
if ("scopes" in obj) {
|
|
143
|
+
if (typeof obj.scopes !== "object" || obj.scopes === null || Array.isArray(obj.scopes)) throw new TypeError("Import map: \"scopes\" must be a JSON object.");
|
|
144
|
+
scopes = sortAndNormalizeScopes(obj.scopes, baseURL);
|
|
145
|
+
}
|
|
146
|
+
let integrity = /* @__PURE__ */ new Map();
|
|
147
|
+
if ("integrity" in obj) {
|
|
148
|
+
if (typeof obj.integrity !== "object" || obj.integrity === null || Array.isArray(obj.integrity)) throw new TypeError("Import map: \"integrity\" must be a JSON object.");
|
|
149
|
+
integrity = normalizeModuleIntegrityMap(obj.integrity, baseURL);
|
|
150
|
+
}
|
|
151
|
+
for (const key of Object.keys(obj)) if (key !== "imports" && key !== "scopes" && key !== "integrity") warn(`Invalid top-level key "${key}" in import map.`);
|
|
152
|
+
return new ImportMap(imports, scopes, integrity);
|
|
153
|
+
}
|
|
154
|
+
function mergeSpecifierMaps(newMap, oldMap) {
|
|
155
|
+
const merged = new Map(oldMap);
|
|
156
|
+
for (const [specifier, url] of newMap) {
|
|
157
|
+
if (merged.has(specifier)) {
|
|
158
|
+
warn(`Import map merge: ignoring duplicate specifier key "${specifier}".`);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
merged.set(specifier, url);
|
|
162
|
+
}
|
|
163
|
+
return sortedDescending(merged);
|
|
164
|
+
}
|
|
165
|
+
function mergeExistingAndNewImportMaps(oldImportMap, newImportMap, resolvedModuleSet) {
|
|
166
|
+
const newImportMapScopes = new Map([...newImportMap.scopes.entries()].map(([k, v]) => [k, new Map(v)]));
|
|
167
|
+
const newImportMapImports = new Map(newImportMap.imports);
|
|
168
|
+
for (const [scopePrefix, scopeImports] of newImportMapScopes) {
|
|
169
|
+
for (const record of resolvedModuleSet) {
|
|
170
|
+
if (!(scopePrefix === record.serializedBaseURL || scopePrefix.endsWith("/") && record.serializedBaseURL.startsWith(scopePrefix))) continue;
|
|
171
|
+
for (const specifierKey of [...scopeImports.keys()]) if (specifierKey === record.specifier || specifierKey.endsWith("/") && record.specifier.startsWith(specifierKey) && (record.specifierAsURL === null || isSpecialURL(record.specifierAsURL))) {
|
|
172
|
+
warn(`Import map merge: ignoring scope rule "${specifierKey}" under "${scopePrefix}" — already resolved.`);
|
|
173
|
+
scopeImports.delete(specifierKey);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (oldImportMap.scopes.has(scopePrefix)) oldImportMap.scopes.set(scopePrefix, mergeSpecifierMaps(scopeImports, oldImportMap.scopes.get(scopePrefix)));
|
|
177
|
+
else oldImportMap.scopes.set(scopePrefix, scopeImports);
|
|
178
|
+
}
|
|
179
|
+
const resortedScopes = sortedDescending(oldImportMap.scopes);
|
|
180
|
+
oldImportMap.scopes.clear();
|
|
181
|
+
for (const [k, v] of resortedScopes) oldImportMap.scopes.set(k, v);
|
|
182
|
+
for (const [url, integrity] of newImportMap.integrity) {
|
|
183
|
+
if (oldImportMap.integrity.has(url)) {
|
|
184
|
+
warn(`Import map merge: ignoring duplicate integrity entry for "${url}".`);
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
oldImportMap.integrity.set(url, integrity);
|
|
188
|
+
}
|
|
189
|
+
for (const record of resolvedModuleSet) for (const specifierKey of [...newImportMapImports.keys()]) if (specifierKey.startsWith(record.specifier)) {
|
|
190
|
+
warn(`Import map merge: ignoring global rule "${specifierKey}" — already resolved.`);
|
|
191
|
+
newImportMapImports.delete(specifierKey);
|
|
192
|
+
}
|
|
193
|
+
const merged = mergeSpecifierMaps(newImportMapImports, oldImportMap.imports);
|
|
194
|
+
oldImportMap.imports.clear();
|
|
195
|
+
for (const [k, v] of merged) oldImportMap.imports.set(k, v);
|
|
196
|
+
}
|
|
197
|
+
function parseImportMaps() {
|
|
198
|
+
const base = new URL(document.baseURI || location.href);
|
|
199
|
+
const result = new ImportMap();
|
|
200
|
+
for (const script of document.querySelectorAll("script[type=\"importmap\"]")) try {
|
|
201
|
+
mergeExistingAndNewImportMaps(result, parseImportMapString(script.textContent || "{}", base), []);
|
|
202
|
+
} catch (error) {
|
|
203
|
+
warn("Failed to parse importmap script:", error);
|
|
204
|
+
}
|
|
205
|
+
return result;
|
|
206
|
+
}
|
|
207
|
+
function resolveImportsMatch(normalizedSpecifier, asURL, specifierMap) {
|
|
208
|
+
for (const [specifierKey, resolutionResult] of specifierMap) {
|
|
209
|
+
if (specifierKey === normalizedSpecifier) {
|
|
210
|
+
if (resolutionResult === null) throw new TypeError(`Resolution of "${specifierKey}" was blocked by a null entry in the import map.`);
|
|
211
|
+
return resolutionResult;
|
|
212
|
+
}
|
|
213
|
+
if (specifierKey.endsWith("/") && normalizedSpecifier.startsWith(specifierKey) && (asURL === null || isSpecialURL(asURL))) {
|
|
214
|
+
if (resolutionResult === null) throw new TypeError(`Resolution of "${specifierKey}" was blocked by a null entry in the import map.`);
|
|
215
|
+
const afterPrefix = normalizedSpecifier.slice(specifierKey.length);
|
|
216
|
+
let url;
|
|
217
|
+
try {
|
|
218
|
+
url = new URL(afterPrefix, resolutionResult);
|
|
219
|
+
} catch {
|
|
220
|
+
throw new TypeError(`Resolution of "${normalizedSpecifier}" was blocked: "${afterPrefix}" could not be URL-parsed relative to "${resolutionResult.href}".`);
|
|
221
|
+
}
|
|
222
|
+
if (!url.href.startsWith(resolutionResult.href)) throw new TypeError(`Resolution of "${normalizedSpecifier}" was blocked due to backtracking above its prefix "${specifierKey}".`);
|
|
223
|
+
return url;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
function isSpecialURL(url) {
|
|
229
|
+
return [
|
|
230
|
+
"ftp:",
|
|
231
|
+
"file:",
|
|
232
|
+
"http:",
|
|
233
|
+
"https:",
|
|
234
|
+
"ws:",
|
|
235
|
+
"wss:"
|
|
236
|
+
].includes(url.protocol);
|
|
237
|
+
}
|
|
238
|
+
function resolveFromImportMap(specifier, importMap, baseURL) {
|
|
239
|
+
const base = typeof baseURL === "string" ? new URL(baseURL) : baseURL;
|
|
240
|
+
const asURL = resolveURLLikeModuleSpecifier(specifier, base);
|
|
241
|
+
const normalizedSpecifier = asURL !== null ? asURL.href : specifier;
|
|
242
|
+
const serializedBaseURL = base.href;
|
|
243
|
+
for (const [scopePrefix, scopeImports] of importMap.scopes) if (scopePrefix === serializedBaseURL || scopePrefix.endsWith("/") && serializedBaseURL.startsWith(scopePrefix)) {
|
|
244
|
+
const match = resolveImportsMatch(normalizedSpecifier, asURL, scopeImports);
|
|
245
|
+
if (match !== null) return {
|
|
246
|
+
url: match.href,
|
|
247
|
+
record: {
|
|
248
|
+
serializedBaseURL,
|
|
249
|
+
specifier: normalizedSpecifier,
|
|
250
|
+
specifierAsURL: asURL
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
const match = resolveImportsMatch(normalizedSpecifier, asURL, importMap.imports);
|
|
255
|
+
if (match !== null) return {
|
|
256
|
+
url: match.href,
|
|
257
|
+
record: {
|
|
258
|
+
serializedBaseURL,
|
|
259
|
+
specifier: normalizedSpecifier,
|
|
260
|
+
specifierAsURL: asURL
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
//#endregion
|
|
265
|
+
//#region src/loader.ts
|
|
266
|
+
const cssLoader = (sourceUrl, sourceCode) => {
|
|
267
|
+
return `\
|
|
268
|
+
const style = document.createElement("style");
|
|
269
|
+
style.dataset.href = ${JSON.stringify(sourceUrl)}; // for debugging purposes
|
|
270
|
+
style.textContent = ${JSON.stringify(sourceCode)};
|
|
271
|
+
document.head.appendChild(style);
|
|
272
|
+
`;
|
|
273
|
+
};
|
|
274
|
+
const cssModuleLoader = (sourceUrl, sourceCode) => {
|
|
275
|
+
const pathname = new URL(sourceUrl).pathname;
|
|
276
|
+
const filename = pathname.substring(pathname.lastIndexOf("/") + 1) || "index.css";
|
|
277
|
+
const { map, css } = cssToModule(sourceCode, filename.slice(0, filename.indexOf(".")).replace(/[^a-zA-Z0-9]/g, "_"));
|
|
278
|
+
return `\
|
|
279
|
+
const style = document.createElement("style");
|
|
280
|
+
style.dataset.href = ${JSON.stringify(sourceUrl)}; // for debugging purposes
|
|
281
|
+
style.textContent = ${JSON.stringify(css)};
|
|
282
|
+
document.head.appendChild(style);
|
|
283
|
+
export default ${JSON.stringify(map)};
|
|
284
|
+
`;
|
|
285
|
+
};
|
|
286
|
+
function cssToModule(cssString, prefix) {
|
|
287
|
+
const sheet = new CSSStyleSheet();
|
|
288
|
+
sheet.replaceSync(cssString);
|
|
289
|
+
const jsonMap = {};
|
|
290
|
+
const getHash = (name) => {
|
|
291
|
+
const p = prefix ? `${prefix}_` : "";
|
|
292
|
+
if (!jsonMap[name]) jsonMap[name] = `${p}${name}_${Math.random().toString(36).slice(2, 7)}`;
|
|
293
|
+
return jsonMap[name];
|
|
294
|
+
};
|
|
295
|
+
const processRules = (ruleList) => {
|
|
296
|
+
for (const rule of ruleList) if (rule instanceof CSSStyleRule) rule.selectorText = rule.selectorText.replace(/\.([a-zA-Z_][\w-]*)/g, (_match, className) => {
|
|
297
|
+
return `.${getHash(className)}`;
|
|
298
|
+
});
|
|
299
|
+
else if (rule instanceof CSSGroupingRule) processRules(rule.cssRules);
|
|
300
|
+
};
|
|
301
|
+
processRules(sheet.cssRules);
|
|
302
|
+
return {
|
|
303
|
+
css: Array.from(sheet.cssRules).map((rule) => rule.cssText).join("\n"),
|
|
304
|
+
map: jsonMap
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
//#endregion
|
|
308
|
+
//#region src/network.ts
|
|
309
|
+
async function fetchResponse(input, init) {
|
|
310
|
+
const url = input instanceof Request ? input.url : String(input);
|
|
311
|
+
let res;
|
|
312
|
+
try {
|
|
313
|
+
res = await fetch(input, init);
|
|
314
|
+
} catch (cause) {
|
|
315
|
+
throw new ModuleTSXError(`Failed to fetch module ${url}: Network error`, { cause });
|
|
316
|
+
}
|
|
317
|
+
if (!res.ok) throw new ModuleTSXError(`Failed to fetch module ${url}: ${res.status} ${res.statusText}`);
|
|
318
|
+
return res;
|
|
319
|
+
}
|
|
320
|
+
//#endregion
|
|
321
|
+
//#region src/specifier.ts
|
|
322
|
+
function isBareSpecifier(specifier) {
|
|
323
|
+
if (isRelativeSpecifier(specifier)) return false;
|
|
324
|
+
try {
|
|
325
|
+
new URL(specifier);
|
|
326
|
+
return false;
|
|
327
|
+
} catch {
|
|
328
|
+
return true;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
function isRelativeSpecifier(specifier) {
|
|
332
|
+
return specifier.startsWith("./") || specifier.startsWith("../") || specifier.startsWith("/");
|
|
333
|
+
}
|
|
334
|
+
function collectSpecifiers(sourceFile) {
|
|
335
|
+
const set = /* @__PURE__ */ new Set();
|
|
336
|
+
const visit = (node) => {
|
|
337
|
+
const addSpecifier = (literal) => {
|
|
338
|
+
if (literal) set.add(literal.text);
|
|
339
|
+
};
|
|
340
|
+
if (ts.isImportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) addSpecifier(node.moduleSpecifier);
|
|
341
|
+
if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) {
|
|
342
|
+
const arg = node.arguments[0];
|
|
343
|
+
if (arg && ts.isStringLiteral(arg)) addSpecifier(arg);
|
|
344
|
+
}
|
|
345
|
+
if (ts.isExportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) addSpecifier(node.moduleSpecifier);
|
|
346
|
+
ts.forEachChild(node, visit);
|
|
347
|
+
};
|
|
348
|
+
visit(sourceFile);
|
|
349
|
+
return set;
|
|
350
|
+
}
|
|
351
|
+
function createRewriteImportTransformer(specifierMap) {
|
|
352
|
+
const rewriteSpecifier = (specifier) => {
|
|
353
|
+
return specifierMap.get(specifier) ?? specifier;
|
|
354
|
+
};
|
|
355
|
+
const transformer = (context) => {
|
|
356
|
+
const visitNode = (node) => {
|
|
357
|
+
if (ts.isImportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
|
|
358
|
+
const next = rewriteSpecifier(node.moduleSpecifier.text);
|
|
359
|
+
if (next !== node.moduleSpecifier.text) return ts.factory.updateImportDeclaration(node, node.modifiers, node.importClause, ts.factory.createStringLiteral(next), node.assertClause);
|
|
360
|
+
}
|
|
361
|
+
if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) {
|
|
362
|
+
const arg = node.arguments[0];
|
|
363
|
+
if (arg && ts.isStringLiteral(arg)) {
|
|
364
|
+
const next = rewriteSpecifier(arg.text);
|
|
365
|
+
if (next !== arg.text) return ts.factory.updateCallExpression(node, node.expression, node.typeArguments, [ts.factory.createStringLiteral(next), ...node.arguments.slice(1)]);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
if (ts.isExportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
|
|
369
|
+
const next = rewriteSpecifier(node.moduleSpecifier.text);
|
|
370
|
+
if (next !== node.moduleSpecifier.text) return ts.factory.updateExportDeclaration(node, node.modifiers, node.isTypeOnly, node.exportClause, ts.factory.createStringLiteral(next), node.assertClause);
|
|
371
|
+
}
|
|
372
|
+
return ts.visitEachChild(node, visitNode, context);
|
|
373
|
+
};
|
|
374
|
+
return (sf) => ts.visitNode(sf, visitNode);
|
|
375
|
+
};
|
|
376
|
+
return transformer;
|
|
377
|
+
}
|
|
378
|
+
//#endregion
|
|
379
|
+
//#region src/react.ts
|
|
380
|
+
/** Check if code uses JSX without React import */
|
|
381
|
+
function needsReactImport(sourceFile) {
|
|
382
|
+
let hasJSX = false;
|
|
383
|
+
let hasReactVariable = false;
|
|
384
|
+
function visitNode(node) {
|
|
385
|
+
if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node) || ts.isJsxFragment(node)) hasJSX = true;
|
|
386
|
+
if (ts.isImportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
|
|
387
|
+
if (node.importClause) {
|
|
388
|
+
if (node.importClause.name?.text === "React") hasReactVariable = true;
|
|
389
|
+
if (node.importClause.namedBindings && ts.isNamespaceImport(node.importClause.namedBindings)) {
|
|
390
|
+
if (node.importClause.namedBindings.name.text === "React") hasReactVariable = true;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
if (ts.isVariableDeclaration(node) && node.name && ts.isIdentifier(node.name)) {
|
|
395
|
+
if (node.name.text === "React") hasReactVariable = true;
|
|
396
|
+
}
|
|
397
|
+
ts.forEachChild(node, visitNode);
|
|
398
|
+
}
|
|
399
|
+
visitNode(sourceFile);
|
|
400
|
+
return hasJSX && !hasReactVariable;
|
|
401
|
+
}
|
|
402
|
+
/** Add React import statement to the top */
|
|
403
|
+
function addReactImport(sourceFile) {
|
|
404
|
+
let reactSpecifier = "react";
|
|
405
|
+
function findReactSpecifier(node) {
|
|
406
|
+
if (ts.isImportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
|
|
407
|
+
const specifier = node.moduleSpecifier.text;
|
|
408
|
+
if (specifier === "react" || /^react@/.test(specifier) || specifier.endsWith("/react") || /\/react@/.test(specifier)) {
|
|
409
|
+
reactSpecifier = specifier;
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
ts.forEachChild(node, findReactSpecifier);
|
|
414
|
+
}
|
|
415
|
+
findReactSpecifier(sourceFile);
|
|
416
|
+
const statements = [ts.factory.createImportDeclaration(void 0, ts.factory.createImportClause(false, ts.factory.createIdentifier("React"), void 0), ts.factory.createStringLiteral(reactSpecifier), void 0), ...sourceFile.statements];
|
|
417
|
+
return ts.factory.updateSourceFile(sourceFile, statements, sourceFile.isDeclarationFile, sourceFile.referencedFiles, sourceFile.typeReferenceDirectives, sourceFile.hasNoDefaultLib, sourceFile.libReferenceDirectives);
|
|
418
|
+
}
|
|
419
|
+
//#endregion
|
|
420
|
+
//#region src/source-tracker.ts
|
|
421
|
+
var SourceTransformTracker = class {
|
|
422
|
+
sourceMap = /* @__PURE__ */ new Map();
|
|
423
|
+
inFlightSourceMap = /* @__PURE__ */ new Map();
|
|
424
|
+
blobMap = /* @__PURE__ */ new Map();
|
|
425
|
+
originalSourceMap = /* @__PURE__ */ new Map();
|
|
426
|
+
get(sourceType, sourceUrl) {
|
|
427
|
+
return this.sourceMap.get(this.getSourceKey(sourceType, sourceUrl));
|
|
428
|
+
}
|
|
429
|
+
set(sourceType, sourceUrl, blobUrl, originalSource) {
|
|
430
|
+
this.sourceMap.set(this.getSourceKey(sourceType, sourceUrl), blobUrl);
|
|
431
|
+
this.blobMap.set(blobUrl, sourceUrl);
|
|
432
|
+
if (originalSource != null) this.originalSourceMap.set(blobUrl, originalSource);
|
|
433
|
+
}
|
|
434
|
+
getOriginalSource(blobUrl) {
|
|
435
|
+
return this.originalSourceMap.get(blobUrl);
|
|
436
|
+
}
|
|
437
|
+
isInFlight(sourceType, sourceUrl) {
|
|
438
|
+
return this.inFlightSourceMap.has(this.getSourceKey(sourceType, sourceUrl));
|
|
439
|
+
}
|
|
440
|
+
getSourceUrlByBlob(blobUrl) {
|
|
441
|
+
return this.blobMap.get(blobUrl);
|
|
442
|
+
}
|
|
443
|
+
/** Deduplicates concurrent transforms for the same URL. */
|
|
444
|
+
runWithDedup(sourceType, sourceUrl, run) {
|
|
445
|
+
const sourceKey = this.getSourceKey(sourceType, sourceUrl);
|
|
446
|
+
const inFlight = this.inFlightSourceMap.get(sourceKey);
|
|
447
|
+
if (inFlight) return inFlight;
|
|
448
|
+
const task = run();
|
|
449
|
+
this.inFlightSourceMap.set(sourceKey, task);
|
|
450
|
+
task.finally(() => {
|
|
451
|
+
this.inFlightSourceMap.delete(sourceKey);
|
|
452
|
+
});
|
|
453
|
+
return task;
|
|
454
|
+
}
|
|
455
|
+
getSourceKey(sourceType, sourceUrl) {
|
|
456
|
+
return `${sourceType}:${sourceUrl}`;
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
//#endregion
|
|
460
|
+
//#region src/ts.ts
|
|
461
|
+
function createSourceFile(code, fileName) {
|
|
462
|
+
try {
|
|
463
|
+
return ts.createSourceFile(fileName, code, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
|
464
|
+
} catch (cause) {
|
|
465
|
+
throw new ModuleTSXError(`Failed to create typescript source file ${fileName}`, { cause });
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
function printSourceFile(sourceFile) {
|
|
469
|
+
try {
|
|
470
|
+
const code = ts.createPrinter({
|
|
471
|
+
newLine: ts.NewLineKind.LineFeed,
|
|
472
|
+
removeComments: false
|
|
473
|
+
}).printFile(sourceFile);
|
|
474
|
+
return ts.transpile(code, {
|
|
475
|
+
target: ts.ScriptTarget.Latest,
|
|
476
|
+
module: ts.ModuleKind.ESNext,
|
|
477
|
+
noCheck: true,
|
|
478
|
+
declaration: false,
|
|
479
|
+
jsx: ts.JsxEmit.React
|
|
480
|
+
});
|
|
481
|
+
} catch (cause) {
|
|
482
|
+
throw new ModuleTSXError(`Failed to print typescript source file ${sourceFile.fileName}`, { cause });
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
function transform(sourceFile, transformers) {
|
|
486
|
+
try {
|
|
487
|
+
const result = ts.transform(sourceFile, transformers);
|
|
488
|
+
const transformedFile = result.transformed[0];
|
|
489
|
+
result.dispose();
|
|
490
|
+
return transformedFile;
|
|
491
|
+
} catch (cause) {
|
|
492
|
+
throw new ModuleTSXError(`Failed to transform typescript source file ${sourceFile.fileName}`, { cause });
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
//#endregion
|
|
496
|
+
//#region src/module-tsx.ts
|
|
497
|
+
var ModuleTSX = class extends EventTarget {
|
|
498
|
+
baseUrl;
|
|
499
|
+
importMap;
|
|
500
|
+
fetch;
|
|
501
|
+
resolveBareSpecifier;
|
|
502
|
+
resolvedModuleSet = [];
|
|
503
|
+
sourceTracker = new SourceTransformTracker();
|
|
504
|
+
fetchText = async (url) => {
|
|
505
|
+
return this.fetch(url).then((res) => res.text());
|
|
506
|
+
};
|
|
507
|
+
constructor(config) {
|
|
508
|
+
super();
|
|
509
|
+
this.baseUrl = config?.baseUrl ?? location.href;
|
|
510
|
+
this.importMap = config?.importMap ?? new ImportMap();
|
|
511
|
+
this.fetch = config?.fetch ?? fetchResponse;
|
|
512
|
+
this.resolveBareSpecifier = typeof config?.resolveBareSpecifier === "function" ? config?.resolveBareSpecifier : (specifier) => (config?.resolveBareSpecifier ?? "https://esm.sh/") + specifier;
|
|
513
|
+
}
|
|
514
|
+
/** Add a new import map, merging it into the existing one per the spec.
|
|
515
|
+
* Rules that conflict with already-resolved modules are silently dropped. */
|
|
516
|
+
addImportMap(newImportMap) {
|
|
517
|
+
ImportMap.merge(this.importMap, newImportMap, this.resolvedModuleSet);
|
|
518
|
+
}
|
|
519
|
+
/** Resolve a blob URL back to its original source URL. Returns undefined if not found. */
|
|
520
|
+
getSourceUrlByBlob(blobUrl) {
|
|
521
|
+
return this.sourceTracker.getSourceUrlByBlob(blobUrl);
|
|
522
|
+
}
|
|
523
|
+
/** Get the original source code that was compiled into a given blob URL. */
|
|
524
|
+
getOriginalSource(blobUrl) {
|
|
525
|
+
return this.sourceTracker.getOriginalSource(blobUrl);
|
|
526
|
+
}
|
|
527
|
+
emit(type, detail) {
|
|
528
|
+
this.dispatchEvent(new CustomEvent(type, { detail }));
|
|
529
|
+
this.dispatchEvent(new CustomEvent("*", { detail: {
|
|
530
|
+
type,
|
|
531
|
+
payload: detail
|
|
532
|
+
} }));
|
|
533
|
+
}
|
|
534
|
+
async import(id, options) {
|
|
535
|
+
this.emit("import", { id });
|
|
536
|
+
try {
|
|
537
|
+
if (isBareSpecifier(id)) {
|
|
538
|
+
const resolved = ImportMap.resolve(id, this.importMap, this.baseUrl);
|
|
539
|
+
if (resolved) {
|
|
540
|
+
this.resolvedModuleSet.push(resolved.record);
|
|
541
|
+
id = resolved.url;
|
|
542
|
+
} else id = this.resolveBareSpecifier(id);
|
|
543
|
+
}
|
|
544
|
+
const url = isRelativeSpecifier(id) ? new URL(id, this.baseUrl).href : id;
|
|
545
|
+
const code = await this.fetchText(url);
|
|
546
|
+
return this.importCode(url, code, options);
|
|
547
|
+
} catch (error) {
|
|
548
|
+
this.emit("import:error", {
|
|
549
|
+
id,
|
|
550
|
+
error
|
|
551
|
+
});
|
|
552
|
+
throw error;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
async importCode(sourceUrl, code, options) {
|
|
556
|
+
try {
|
|
557
|
+
return await import(await this.transformSourceModule("esm", sourceUrl, code), options);
|
|
558
|
+
} catch (error) {
|
|
559
|
+
this.emit("import:error", {
|
|
560
|
+
id: sourceUrl,
|
|
561
|
+
error
|
|
562
|
+
});
|
|
563
|
+
throw error;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
/** Transform module source code and return a blob URL with the transformed content */
|
|
567
|
+
async transformSourceModule(sourceType, sourceUrl, sourceCode) {
|
|
568
|
+
const cachedBlobUrl = this.sourceTracker.get(sourceType, sourceUrl);
|
|
569
|
+
if (cachedBlobUrl) return cachedBlobUrl;
|
|
570
|
+
return this.sourceTracker.runWithDedup(sourceType, sourceUrl, async () => {
|
|
571
|
+
const loader = this.getLoaderByResourceType(sourceType);
|
|
572
|
+
const code = `import.meta.url=${JSON.stringify(sourceUrl)};\n` + await loader(sourceUrl, sourceCode);
|
|
573
|
+
const blob = new Blob([code], { type: "text/javascript" });
|
|
574
|
+
const blobUrl = URL.createObjectURL(blob);
|
|
575
|
+
this.sourceTracker.set(sourceType, sourceUrl, blobUrl, sourceCode);
|
|
576
|
+
return blobUrl;
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
getLoaderByResourceType(type) {
|
|
580
|
+
switch (type) {
|
|
581
|
+
case "css": return cssLoader;
|
|
582
|
+
case "css-module": return cssModuleLoader;
|
|
583
|
+
case "esm": return this.tsxLoader.bind(this);
|
|
584
|
+
default: throw new ModuleTSXError(`Unsupported resource type: ${type}`);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
async tsxLoader(sourceUrl, sourceCode) {
|
|
588
|
+
this.emit("transform", { sourceUrl });
|
|
589
|
+
try {
|
|
590
|
+
const sourceFile = createSourceFile(sourceCode, getFileName(sourceUrl));
|
|
591
|
+
const specifiers = collectSpecifiers(sourceFile);
|
|
592
|
+
const rewrittenSpecifiers = await this.resolveSpecifiers(specifiers, sourceUrl);
|
|
593
|
+
let workingSourceFile = sourceFile;
|
|
594
|
+
if (needsReactImport(workingSourceFile)) {
|
|
595
|
+
workingSourceFile = addReactImport(workingSourceFile);
|
|
596
|
+
if (!rewrittenSpecifiers.has("react")) {
|
|
597
|
+
const reactUrl = await this.resolveSpecifier("react", sourceUrl);
|
|
598
|
+
if (reactUrl !== "react") rewrittenSpecifiers.set("react", reactUrl);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
const transformers = [createRewriteImportTransformer(rewrittenSpecifiers)];
|
|
602
|
+
return printSourceFile(transform(workingSourceFile, transformers));
|
|
603
|
+
} catch (error) {
|
|
604
|
+
this.emit("transform:error", {
|
|
605
|
+
sourceUrl,
|
|
606
|
+
error
|
|
607
|
+
});
|
|
608
|
+
throw error;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
async resolveLocalUrl(fullUrl) {
|
|
612
|
+
const { pathname } = new URL(fullUrl);
|
|
613
|
+
if (pathname.endsWith(".module.css")) return this.transformSourceModule("css-module", fullUrl, await this.fetchText(fullUrl));
|
|
614
|
+
if (pathname.endsWith(".css")) return this.transformSourceModule("css", fullUrl, await this.fetchText(fullUrl));
|
|
615
|
+
if (pathname.endsWith(".wasm")) return fullUrl;
|
|
616
|
+
if (this.sourceTracker.isInFlight("esm", fullUrl)) return fullUrl;
|
|
617
|
+
//! ^ transformSourceModule is recursive ^
|
|
618
|
+
return this.transformSourceModule("esm", fullUrl, await this.fetchText(fullUrl));
|
|
619
|
+
}
|
|
620
|
+
async resolveSpecifier(specifier, sourceUrl) {
|
|
621
|
+
const resolved = ImportMap.resolve(specifier, this.importMap, sourceUrl);
|
|
622
|
+
if (resolved) {
|
|
623
|
+
this.resolvedModuleSet.push(resolved.record);
|
|
624
|
+
const { pathname } = new URL(resolved.url);
|
|
625
|
+
if (pathname.endsWith(".module.css")) return this.transformSourceModule("css-module", resolved.url, await this.fetchText(resolved.url));
|
|
626
|
+
if (pathname.endsWith(".css")) return this.transformSourceModule("css", resolved.url, await this.fetchText(resolved.url));
|
|
627
|
+
return resolved.url;
|
|
628
|
+
}
|
|
629
|
+
if (isRelativeSpecifier(specifier)) return this.resolveLocalUrl(new URL(specifier, sourceUrl).href);
|
|
630
|
+
if (specifier.startsWith("node:")) return `https://raw.esm.sh/@jspm/core/nodelibs/browser/${specifier.slice(5)}.js`;
|
|
631
|
+
const bareSpecifier = specifier.startsWith("npm:") ? specifier.slice(4) : specifier;
|
|
632
|
+
if (specifier.startsWith("npm:") || isBareSpecifier(specifier)) {
|
|
633
|
+
const subpath = bareSpecifier.startsWith("@") ? bareSpecifier.split("/").slice(2).join("/") : bareSpecifier.split("/").slice(1).join("/");
|
|
634
|
+
const url = this.resolveBareSpecifier(bareSpecifier);
|
|
635
|
+
if (subpath.endsWith(".css")) return this.transformSourceModule("css", url, await this.fetchText(url));
|
|
636
|
+
return url;
|
|
637
|
+
}
|
|
638
|
+
return specifier;
|
|
639
|
+
}
|
|
640
|
+
async resolveSpecifiers(specifiers, sourceUrl) {
|
|
641
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
642
|
+
const tasks = Array.from(specifiers).map(async (specifier) => {
|
|
643
|
+
const specifier2 = await this.resolveSpecifier(specifier, sourceUrl);
|
|
644
|
+
if (specifier !== specifier2) resolved.set(specifier, specifier2);
|
|
645
|
+
});
|
|
646
|
+
await Promise.all(tasks);
|
|
647
|
+
return resolved;
|
|
648
|
+
}
|
|
649
|
+
};
|
|
650
|
+
function getFileName(sourceUrl) {
|
|
651
|
+
try {
|
|
652
|
+
return new URL(sourceUrl).pathname;
|
|
653
|
+
} catch {
|
|
654
|
+
return "temp.tsx";
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
//#endregion
|
|
658
|
+
//#region src/index.ts
|
|
659
|
+
/**
|
|
660
|
+
* The singleton global instance of ModuleTSX.
|
|
661
|
+
*/
|
|
662
|
+
const instance = new ModuleTSX({ importMap: ImportMap.fromDOM() });
|
|
663
|
+
const TYPE_ATTRIBUTE_VALUE = "module-tsx";
|
|
664
|
+
async function sideEffect() {
|
|
665
|
+
const importScript = async (script) => {
|
|
666
|
+
const src = script.src;
|
|
667
|
+
if (src) return instance.import(src);
|
|
668
|
+
else {
|
|
669
|
+
const code = script.innerHTML || "";
|
|
670
|
+
return instance.importCode(document.location.href, code);
|
|
671
|
+
}
|
|
672
|
+
};
|
|
673
|
+
for (const s of Array.from(document.querySelectorAll(`script[type="${TYPE_ATTRIBUTE_VALUE}"]`))) {
|
|
674
|
+
const script = s;
|
|
675
|
+
if (!script.async && script.defer) warn(`script with type="${TYPE_ATTRIBUTE_VALUE}" does not support defer attribute. Use async or no attribute instead.`);
|
|
676
|
+
for (const key of ["integrity", "crossorigin"]) if (script[key]) warn(`script with type="${TYPE_ATTRIBUTE_VALUE}" does not support ${key} attribute.`);
|
|
677
|
+
if (script.async) importScript(script);
|
|
678
|
+
else await importScript(script);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* Since this module can be loaded as both ESM and UMD, we listen for DOMContentLoaded to ensure all type="module-tsx" tags are present.
|
|
683
|
+
* ESM scripts are always deferred, so the document is already fully parsed when this module executes and the listener fires immediately.
|
|
684
|
+
* Classic scripts run inline as the parser encounters them, so they must wait for DOMContentLoaded.
|
|
685
|
+
*/
|
|
686
|
+
document.addEventListener("DOMContentLoaded", sideEffect);
|
|
687
|
+
//#endregion
|
|
688
|
+
export { ImportMap, ModuleTSX, ModuleTSXError, instance };
|