module-tsx 0.0.0 → 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -0
- package/dist/index.d.ts +74 -0
- package/dist/index.js +447 -0
- package/dist/index.mjs +369 -204
- package/dist/index.umd.js +118 -104
- package/package.json +19 -5
- package/dist/index.d.mts +0 -8
package/README.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# module-tsx
|
|
2
|
+
|
|
3
|
+
Run TypeScript (and React) module directly in the browser.
|
|
4
|
+
|
|
5
|
+
# Usage
|
|
6
|
+
|
|
7
|
+
```html
|
|
8
|
+
<!-- Load This Library -->
|
|
9
|
+
<script type="module" src="https://esm.sh/module-tsx"></script>
|
|
10
|
+
|
|
11
|
+
<div id="root"></div>
|
|
12
|
+
|
|
13
|
+
<!-- Write Your TypeScript Module -->
|
|
14
|
+
<script type="module-tsx">
|
|
15
|
+
import React from "react"; // <- This will be converted to https://esm.sh/react
|
|
16
|
+
import { createRoot } from "react-dom/client"; // <- This will also be converted automatically
|
|
17
|
+
import App from "./src/App.tsx"; // <- Your TypeScript module will also be compiled on the fly
|
|
18
|
+
import lib from "https://my.cdn.domain"; // <- Direct http import will NOT be compiled
|
|
19
|
+
|
|
20
|
+
const root = document.getElementById("root") as HTMLDivElement; // <- You can write TypeScript directly
|
|
21
|
+
createRoot(root).render(
|
|
22
|
+
<React.StrictMode>
|
|
23
|
+
<App />
|
|
24
|
+
</React.StrictMode>
|
|
25
|
+
// ^ You can also write JSX/TSX directly
|
|
26
|
+
);
|
|
27
|
+
</script>
|
|
28
|
+
<!-- OR -->
|
|
29
|
+
<script type="module-tsx" src="./main.tsx"></script>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
You publish the **source code**, and users can run it directly in the browser **without any build step**.
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
// src/App.tsx
|
|
36
|
+
export default function App() {
|
|
37
|
+
return <div>Hello, module-tsx!</div>;
|
|
38
|
+
}
|
|
39
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
//#region src/importmap.d.ts
|
|
2
|
+
interface ImportMapData {
|
|
3
|
+
imports?: Record<string, string>;
|
|
4
|
+
scopes?: Record<string, Record<string, string>>;
|
|
5
|
+
}
|
|
6
|
+
//#endregion
|
|
7
|
+
//#region src/module-tsx.d.ts
|
|
8
|
+
interface ModuleTSXConfig {
|
|
9
|
+
/**
|
|
10
|
+
* The base URL to resolve relative module specifiers.
|
|
11
|
+
* This is typically the URL of the main script or the HTML page.
|
|
12
|
+
*/
|
|
13
|
+
baseUrl?: string;
|
|
14
|
+
fetch?: (fullURL: string) => Promise<Response>;
|
|
15
|
+
importMap?: ImportMapData;
|
|
16
|
+
/**
|
|
17
|
+
* Given a bare specifier, return a full URL to load the module.
|
|
18
|
+
* This can be used to convert for example import "react" to import "https://esm.sh/react".
|
|
19
|
+
* If not provided, it will default to using "https://esm.sh/" as the base URL for bare specifiers.
|
|
20
|
+
* @default "https://esm.sh/"
|
|
21
|
+
*/
|
|
22
|
+
resolveBareSpecifier?: string | ((specifier: string) => string);
|
|
23
|
+
}
|
|
24
|
+
interface ModuleTSXEventMap {
|
|
25
|
+
import: CustomEvent<{
|
|
26
|
+
id: string;
|
|
27
|
+
}>;
|
|
28
|
+
"import:error": CustomEvent<{
|
|
29
|
+
id: string;
|
|
30
|
+
error: any;
|
|
31
|
+
}>;
|
|
32
|
+
transform: CustomEvent<{
|
|
33
|
+
sourceUrl: string;
|
|
34
|
+
}>;
|
|
35
|
+
"transform:error": CustomEvent<{
|
|
36
|
+
sourceUrl: string;
|
|
37
|
+
error: any;
|
|
38
|
+
}>;
|
|
39
|
+
}
|
|
40
|
+
interface IModuleTSX extends EventTarget {
|
|
41
|
+
addEventListener<T extends keyof ModuleTSXEventMap>(type: T, listener: (this: ModuleTSX, ev: ModuleTSXEventMap[T]) => any, options?: boolean | AddEventListenerOptions): void;
|
|
42
|
+
}
|
|
43
|
+
declare class ModuleTSX extends EventTarget implements IModuleTSX {
|
|
44
|
+
readonly baseUrl: string;
|
|
45
|
+
readonly importMap: ImportMapData;
|
|
46
|
+
readonly fetch: (url: string) => Promise<Response>;
|
|
47
|
+
readonly resolveBareSpecifier: (specifier: string) => string;
|
|
48
|
+
private readonly sourceTracker;
|
|
49
|
+
private readonly fetchText;
|
|
50
|
+
constructor(config?: ModuleTSXConfig);
|
|
51
|
+
private emit;
|
|
52
|
+
import(id: string, options?: any): Promise<any>;
|
|
53
|
+
importCode(sourceUrl: string, code: string, options?: any): Promise<any>;
|
|
54
|
+
/** Transform module source code and return a blob URL with the transformed content */
|
|
55
|
+
private transformSourceModule;
|
|
56
|
+
private getLoaderByResourceType;
|
|
57
|
+
private tsxLoader;
|
|
58
|
+
private resolveSpecifier;
|
|
59
|
+
private resolveSpecifiers;
|
|
60
|
+
}
|
|
61
|
+
//#endregion
|
|
62
|
+
//#region src/error.d.ts
|
|
63
|
+
/** Custom error class for module-tsx */
|
|
64
|
+
declare class ModuleTSXError extends Error {
|
|
65
|
+
constructor(message?: string, options?: ErrorOptions);
|
|
66
|
+
}
|
|
67
|
+
//#endregion
|
|
68
|
+
//#region src/index.d.ts
|
|
69
|
+
/**
|
|
70
|
+
* The singleton global instance of ModuleTSX.
|
|
71
|
+
*/
|
|
72
|
+
declare const instance: ModuleTSX;
|
|
73
|
+
//#endregion
|
|
74
|
+
export { ModuleTSX, ModuleTSXError, instance };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
|
|
3
|
+
//#region src/error.ts
|
|
4
|
+
/** Custom error class for module-tsx */
|
|
5
|
+
var ModuleTSXError = class ModuleTSXError extends Error {
|
|
6
|
+
constructor(message, options) {
|
|
7
|
+
super(message, options);
|
|
8
|
+
this.name = "ModuleTSXError";
|
|
9
|
+
if ("captureStackTrace" in Error) Error.captureStackTrace(this, ModuleTSXError);
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
/** Log warnings with consistent formatting */
|
|
13
|
+
function warn(message, ...args) {
|
|
14
|
+
console.warn(`[module-tsx] ${message}`, ...args);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region src/importmap.ts
|
|
19
|
+
function parseImportMaps() {
|
|
20
|
+
const result = {
|
|
21
|
+
imports: {},
|
|
22
|
+
scopes: {}
|
|
23
|
+
};
|
|
24
|
+
const mappedSpecifiers = /* @__PURE__ */ new Set();
|
|
25
|
+
const scripts = document.querySelectorAll("script[type=\"importmap\"]");
|
|
26
|
+
for (const script of scripts) try {
|
|
27
|
+
const data = JSON.parse(script.textContent || "{}");
|
|
28
|
+
if (data.imports) {
|
|
29
|
+
for (const [specifier, url] of Object.entries(data.imports)) if (!mappedSpecifiers.has(specifier)) {
|
|
30
|
+
result.imports[specifier] = url;
|
|
31
|
+
mappedSpecifiers.add(specifier);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (data.scopes) for (const [scope, imports] of Object.entries(data.scopes)) {
|
|
35
|
+
if (!result.scopes[scope]) result.scopes[scope] = {};
|
|
36
|
+
for (const [specifier, url] of Object.entries(imports)) {
|
|
37
|
+
const scopedKey = `${scope}::${specifier}`;
|
|
38
|
+
if (!mappedSpecifiers.has(scopedKey)) {
|
|
39
|
+
result.scopes[scope][specifier] = url;
|
|
40
|
+
mappedSpecifiers.add(scopedKey);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
} catch (error) {
|
|
45
|
+
warn(`Failed to parse importmap script:`, error);
|
|
46
|
+
}
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Resolve specifier from import maps, checking scopes by specificity then global imports
|
|
51
|
+
*/
|
|
52
|
+
function resolveFromImportMap(specifier, importMaps, sourceUrl) {
|
|
53
|
+
if (sourceUrl && importMaps.scopes) {
|
|
54
|
+
const matchingScopes = Object.keys(importMaps.scopes).filter((scope) => sourceUrl.startsWith(scope)).sort((a, b) => b.length - a.length);
|
|
55
|
+
for (const scope of matchingScopes) {
|
|
56
|
+
const scopedImports = importMaps.scopes[scope];
|
|
57
|
+
if (scopedImports && scopedImports[specifier]) return scopedImports[specifier];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (importMaps.imports && importMaps.imports[specifier]) return importMaps.imports[specifier];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
//#endregion
|
|
64
|
+
//#region src/loader.ts
|
|
65
|
+
const cssLoader = (sourceUrl, sourceCode) => {
|
|
66
|
+
return `\
|
|
67
|
+
const style = document.createElement("style");
|
|
68
|
+
style.dataset.href = ${JSON.stringify(sourceUrl)}; // for debugging purposes
|
|
69
|
+
style.textContent = ${JSON.stringify(sourceCode)};
|
|
70
|
+
document.head.appendChild(style);
|
|
71
|
+
`;
|
|
72
|
+
};
|
|
73
|
+
const cssModuleLoader = (sourceUrl, sourceCode) => {
|
|
74
|
+
const pathname = new URL(sourceUrl).pathname;
|
|
75
|
+
const filename = pathname.substring(pathname.lastIndexOf("/") + 1) || "index.css";
|
|
76
|
+
const { map, css } = cssToModule(sourceCode, filename.slice(0, filename.indexOf(".")).replace(/[^a-zA-Z0-9]/g, "_"));
|
|
77
|
+
return `\
|
|
78
|
+
const style = document.createElement("style");
|
|
79
|
+
style.dataset.href = ${JSON.stringify(sourceUrl)}; // for debugging purposes
|
|
80
|
+
style.textContent = ${JSON.stringify(css)};
|
|
81
|
+
document.head.appendChild(style);
|
|
82
|
+
export default ${JSON.stringify(map)};
|
|
83
|
+
`;
|
|
84
|
+
};
|
|
85
|
+
function cssToModule(cssString, prefix) {
|
|
86
|
+
const sheet = new CSSStyleSheet();
|
|
87
|
+
sheet.replaceSync(cssString);
|
|
88
|
+
const jsonMap = {};
|
|
89
|
+
const getHash = (name) => {
|
|
90
|
+
const p = prefix ? `${prefix}_` : "";
|
|
91
|
+
if (!jsonMap[name]) jsonMap[name] = `${p}${name}_${Math.random().toString(36).slice(2, 7)}`;
|
|
92
|
+
return jsonMap[name];
|
|
93
|
+
};
|
|
94
|
+
const processRules = (ruleList) => {
|
|
95
|
+
for (const rule of ruleList) if (rule instanceof CSSStyleRule) rule.selectorText = rule.selectorText.replace(/\.([a-zA-Z_][\w-]*)/g, (_match, className) => {
|
|
96
|
+
return `.${getHash(className)}`;
|
|
97
|
+
});
|
|
98
|
+
else if (rule instanceof CSSGroupingRule) processRules(rule.cssRules);
|
|
99
|
+
};
|
|
100
|
+
processRules(sheet.cssRules);
|
|
101
|
+
return {
|
|
102
|
+
css: Array.from(sheet.cssRules).map((rule) => rule.cssText).join("\n"),
|
|
103
|
+
map: jsonMap
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
//#endregion
|
|
108
|
+
//#region src/network.ts
|
|
109
|
+
async function fetchResponse(input, init) {
|
|
110
|
+
const res = await fetch(input, init);
|
|
111
|
+
if (!res.ok) throw new ModuleTSXError(`Failed to fetch resource ${res.url}: ${res.status}`);
|
|
112
|
+
return res;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
//#endregion
|
|
116
|
+
//#region src/specifier.ts
|
|
117
|
+
function isBareSpecifier(specifier) {
|
|
118
|
+
if (specifier.match(/^\.*\//)) return false;
|
|
119
|
+
try {
|
|
120
|
+
new URL(specifier);
|
|
121
|
+
return false;
|
|
122
|
+
} catch {
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function isRelativeSpecifier(specifier) {
|
|
127
|
+
return specifier.startsWith(".") || specifier.startsWith("/");
|
|
128
|
+
}
|
|
129
|
+
function collectSpecifiers(sourceFile) {
|
|
130
|
+
const set = /* @__PURE__ */ new Set();
|
|
131
|
+
const visit = (node) => {
|
|
132
|
+
const addSpecifier = (literal) => {
|
|
133
|
+
if (literal) set.add(literal.text);
|
|
134
|
+
};
|
|
135
|
+
if (ts.isImportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) addSpecifier(node.moduleSpecifier);
|
|
136
|
+
if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) {
|
|
137
|
+
const arg = node.arguments[0];
|
|
138
|
+
if (arg && ts.isStringLiteral(arg)) addSpecifier(arg);
|
|
139
|
+
}
|
|
140
|
+
if (ts.isExportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) addSpecifier(node.moduleSpecifier);
|
|
141
|
+
ts.forEachChild(node, visit);
|
|
142
|
+
};
|
|
143
|
+
visit(sourceFile);
|
|
144
|
+
return set;
|
|
145
|
+
}
|
|
146
|
+
function createRewriteImportTransformer(specifierMap) {
|
|
147
|
+
const rewriteSpecifier = (specifier) => {
|
|
148
|
+
return specifierMap.get(specifier) ?? specifier;
|
|
149
|
+
};
|
|
150
|
+
const transformer = (context) => {
|
|
151
|
+
const visitNode = (node) => {
|
|
152
|
+
if (ts.isImportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
|
|
153
|
+
const next = rewriteSpecifier(node.moduleSpecifier.text);
|
|
154
|
+
if (next !== node.moduleSpecifier.text) return ts.factory.updateImportDeclaration(node, node.modifiers, node.importClause, ts.factory.createStringLiteral(next), node.assertClause);
|
|
155
|
+
}
|
|
156
|
+
if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) {
|
|
157
|
+
const arg = node.arguments[0];
|
|
158
|
+
if (arg && ts.isStringLiteral(arg)) {
|
|
159
|
+
const next = rewriteSpecifier(arg.text);
|
|
160
|
+
if (next !== arg.text) return ts.factory.updateCallExpression(node, node.expression, node.typeArguments, [ts.factory.createStringLiteral(next), ...node.arguments.slice(1)]);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (ts.isExportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
|
|
164
|
+
const next = rewriteSpecifier(node.moduleSpecifier.text);
|
|
165
|
+
if (next !== node.moduleSpecifier.text) return ts.factory.updateExportDeclaration(node, node.modifiers, node.isTypeOnly, node.exportClause, ts.factory.createStringLiteral(next), node.assertClause);
|
|
166
|
+
}
|
|
167
|
+
return ts.visitEachChild(node, visitNode, context);
|
|
168
|
+
};
|
|
169
|
+
return (sf) => ts.visitNode(sf, visitNode);
|
|
170
|
+
};
|
|
171
|
+
return transformer;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
//#endregion
|
|
175
|
+
//#region src/react.ts
|
|
176
|
+
/** Check if code uses JSX without React import */
|
|
177
|
+
function needsReactImport(sourceFile) {
|
|
178
|
+
let hasJSX = false;
|
|
179
|
+
let hasReactVariable = false;
|
|
180
|
+
function visitNode(node) {
|
|
181
|
+
if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node) || ts.isJsxFragment(node)) hasJSX = true;
|
|
182
|
+
if (ts.isImportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
|
|
183
|
+
if (node.importClause) {
|
|
184
|
+
if (node.importClause.name?.text === "React") hasReactVariable = true;
|
|
185
|
+
if (node.importClause.namedBindings && ts.isNamespaceImport(node.importClause.namedBindings)) {
|
|
186
|
+
if (node.importClause.namedBindings.name.text === "React") hasReactVariable = true;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (ts.isVariableDeclaration(node) && node.name && ts.isIdentifier(node.name)) {
|
|
191
|
+
if (node.name.text === "React") hasReactVariable = true;
|
|
192
|
+
}
|
|
193
|
+
ts.forEachChild(node, visitNode);
|
|
194
|
+
}
|
|
195
|
+
visitNode(sourceFile);
|
|
196
|
+
return hasJSX && !hasReactVariable;
|
|
197
|
+
}
|
|
198
|
+
/** Add React import statement to the top */
|
|
199
|
+
function addReactImport(sourceFile) {
|
|
200
|
+
let reactSpecifier = "react";
|
|
201
|
+
function findReactSpecifier(node) {
|
|
202
|
+
if (ts.isImportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
|
|
203
|
+
const specifier = node.moduleSpecifier.text;
|
|
204
|
+
if (specifier === "react" || /^react@/.test(specifier) || specifier.endsWith("/react") || /\/react@/.test(specifier)) {
|
|
205
|
+
reactSpecifier = specifier;
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
ts.forEachChild(node, findReactSpecifier);
|
|
210
|
+
}
|
|
211
|
+
findReactSpecifier(sourceFile);
|
|
212
|
+
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];
|
|
213
|
+
return ts.factory.updateSourceFile(sourceFile, statements, sourceFile.isDeclarationFile, sourceFile.referencedFiles, sourceFile.typeReferenceDirectives, sourceFile.hasNoDefaultLib, sourceFile.libReferenceDirectives);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
//#endregion
|
|
217
|
+
//#region src/source-tracker.ts
|
|
218
|
+
var SourceTransformTracker = class {
|
|
219
|
+
sourceMap = /* @__PURE__ */ new Map();
|
|
220
|
+
inFlightSourceMap = /* @__PURE__ */ new Map();
|
|
221
|
+
blobMap = /* @__PURE__ */ new Map();
|
|
222
|
+
get(sourceType, sourceUrl) {
|
|
223
|
+
return this.sourceMap.get(this.getSourceKey(sourceType, sourceUrl));
|
|
224
|
+
}
|
|
225
|
+
set(sourceType, sourceUrl, blobUrl) {
|
|
226
|
+
this.sourceMap.set(this.getSourceKey(sourceType, sourceUrl), blobUrl);
|
|
227
|
+
this.blobMap.set(blobUrl, sourceUrl);
|
|
228
|
+
}
|
|
229
|
+
isInFlight(sourceType, sourceUrl) {
|
|
230
|
+
return this.inFlightSourceMap.has(this.getSourceKey(sourceType, sourceUrl));
|
|
231
|
+
}
|
|
232
|
+
getSourceUrlByBlob(blobUrl) {
|
|
233
|
+
return this.blobMap.get(blobUrl);
|
|
234
|
+
}
|
|
235
|
+
/** Deduplicates concurrent transforms for the same URL. */
|
|
236
|
+
runWithDedup(sourceType, sourceUrl, run) {
|
|
237
|
+
const sourceKey = this.getSourceKey(sourceType, sourceUrl);
|
|
238
|
+
const inFlight = this.inFlightSourceMap.get(sourceKey);
|
|
239
|
+
if (inFlight) return inFlight;
|
|
240
|
+
const task = run();
|
|
241
|
+
this.inFlightSourceMap.set(sourceKey, task);
|
|
242
|
+
task.finally(() => {
|
|
243
|
+
this.inFlightSourceMap.delete(sourceKey);
|
|
244
|
+
});
|
|
245
|
+
return task;
|
|
246
|
+
}
|
|
247
|
+
getSourceKey(sourceType, sourceUrl) {
|
|
248
|
+
return `${sourceType}:${sourceUrl}`;
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
//#endregion
|
|
253
|
+
//#region src/ts.ts
|
|
254
|
+
function createSourceFile(code, fileName) {
|
|
255
|
+
try {
|
|
256
|
+
return ts.createSourceFile(fileName, code, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
|
257
|
+
} catch (cause) {
|
|
258
|
+
throw new ModuleTSXError(`Failed to create typescript source file ${fileName}`, { cause });
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
function printSourceFile(sourceFile) {
|
|
262
|
+
try {
|
|
263
|
+
const code = ts.createPrinter({
|
|
264
|
+
newLine: ts.NewLineKind.LineFeed,
|
|
265
|
+
removeComments: false
|
|
266
|
+
}).printFile(sourceFile);
|
|
267
|
+
return ts.transpile(code, {
|
|
268
|
+
target: ts.ScriptTarget.Latest,
|
|
269
|
+
module: ts.ModuleKind.ESNext,
|
|
270
|
+
noCheck: true,
|
|
271
|
+
declaration: false,
|
|
272
|
+
jsx: ts.JsxEmit.React
|
|
273
|
+
});
|
|
274
|
+
} catch (cause) {
|
|
275
|
+
throw new ModuleTSXError(`Failed to print typescript source file ${sourceFile.fileName}`, { cause });
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
function transform(sourceFile, transformers) {
|
|
279
|
+
try {
|
|
280
|
+
const result = ts.transform(sourceFile, transformers);
|
|
281
|
+
const transformedFile = result.transformed[0];
|
|
282
|
+
result.dispose();
|
|
283
|
+
return transformedFile;
|
|
284
|
+
} catch (cause) {
|
|
285
|
+
throw new ModuleTSXError(`Failed to transform typescript source file ${sourceFile.fileName}`, { cause });
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
//#endregion
|
|
290
|
+
//#region src/module-tsx.ts
|
|
291
|
+
var ModuleTSX = class extends EventTarget {
|
|
292
|
+
baseUrl;
|
|
293
|
+
importMap;
|
|
294
|
+
fetch;
|
|
295
|
+
resolveBareSpecifier;
|
|
296
|
+
sourceTracker = new SourceTransformTracker();
|
|
297
|
+
fetchText = async (url) => {
|
|
298
|
+
return this.fetch(url).then((res) => res.text());
|
|
299
|
+
};
|
|
300
|
+
constructor(config) {
|
|
301
|
+
super();
|
|
302
|
+
this.baseUrl = config?.baseUrl ?? location.href;
|
|
303
|
+
this.importMap = config?.importMap ?? parseImportMaps();
|
|
304
|
+
this.fetch = config?.fetch ?? fetchResponse;
|
|
305
|
+
this.resolveBareSpecifier = typeof config?.resolveBareSpecifier === "function" ? config?.resolveBareSpecifier : (specifier) => (config?.resolveBareSpecifier ?? "https://esm.sh/") + specifier;
|
|
306
|
+
}
|
|
307
|
+
emit(type, detail) {
|
|
308
|
+
this.dispatchEvent(new CustomEvent(type, { detail }));
|
|
309
|
+
this.dispatchEvent(new CustomEvent("*", { detail: {
|
|
310
|
+
type,
|
|
311
|
+
payload: detail
|
|
312
|
+
} }));
|
|
313
|
+
}
|
|
314
|
+
async import(id, options) {
|
|
315
|
+
this.emit("import", { id });
|
|
316
|
+
try {
|
|
317
|
+
if (isBareSpecifier(id)) {
|
|
318
|
+
const mappedSpecifier = resolveFromImportMap(id, this.importMap, this.baseUrl);
|
|
319
|
+
if (mappedSpecifier) id = mappedSpecifier;
|
|
320
|
+
else id = this.resolveBareSpecifier(id);
|
|
321
|
+
}
|
|
322
|
+
const url = isRelativeSpecifier(id) ? new URL(id, this.baseUrl).href : id;
|
|
323
|
+
const code = await this.fetchText(url);
|
|
324
|
+
return this.importCode(url, code, options);
|
|
325
|
+
} catch (error) {
|
|
326
|
+
this.emit("import:error", {
|
|
327
|
+
id,
|
|
328
|
+
error
|
|
329
|
+
});
|
|
330
|
+
throw error;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
async importCode(sourceUrl, code, options) {
|
|
334
|
+
return await import(await this.transformSourceModule("esm", sourceUrl, code), options);
|
|
335
|
+
}
|
|
336
|
+
/** Transform module source code and return a blob URL with the transformed content */
|
|
337
|
+
async transformSourceModule(sourceType, sourceUrl, sourceCode) {
|
|
338
|
+
const cachedBlobUrl = this.sourceTracker.get(sourceType, sourceUrl);
|
|
339
|
+
if (cachedBlobUrl) return cachedBlobUrl;
|
|
340
|
+
return this.sourceTracker.runWithDedup(sourceType, sourceUrl, async () => {
|
|
341
|
+
const loader = this.getLoaderByResourceType(sourceType);
|
|
342
|
+
const code = `import.meta.url=${JSON.stringify(sourceUrl)};\n` + await loader(sourceUrl, sourceCode);
|
|
343
|
+
const blob = new Blob([code], { type: "text/javascript" });
|
|
344
|
+
const blobUrl = URL.createObjectURL(blob);
|
|
345
|
+
this.sourceTracker.set(sourceType, sourceUrl, blobUrl);
|
|
346
|
+
return blobUrl;
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
getLoaderByResourceType(type) {
|
|
350
|
+
switch (type) {
|
|
351
|
+
case "css": return cssLoader;
|
|
352
|
+
case "css-module": return cssModuleLoader;
|
|
353
|
+
case "esm": return this.tsxLoader.bind(this);
|
|
354
|
+
default: throw new ModuleTSXError(`Unsupported resource type: ${type}`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
async tsxLoader(sourceUrl, sourceCode) {
|
|
358
|
+
this.emit("transform", { sourceUrl });
|
|
359
|
+
try {
|
|
360
|
+
const sourceFile = createSourceFile(sourceCode, getFileName(sourceUrl));
|
|
361
|
+
const specifiers = collectSpecifiers(sourceFile);
|
|
362
|
+
const rewrittenSpecifiers = await this.resolveSpecifiers(specifiers, sourceUrl);
|
|
363
|
+
let workingSourceFile = sourceFile;
|
|
364
|
+
if (needsReactImport(workingSourceFile)) workingSourceFile = addReactImport(workingSourceFile);
|
|
365
|
+
const transformers = [createRewriteImportTransformer(rewrittenSpecifiers)];
|
|
366
|
+
return printSourceFile(transform(workingSourceFile, transformers));
|
|
367
|
+
} catch (error) {
|
|
368
|
+
this.emit("transform:error", {
|
|
369
|
+
sourceUrl,
|
|
370
|
+
error
|
|
371
|
+
});
|
|
372
|
+
throw error;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
async resolveSpecifier(specifier, sourceUrl) {
|
|
376
|
+
const mappedSpecifier = resolveFromImportMap(specifier, this.importMap, sourceUrl);
|
|
377
|
+
if (mappedSpecifier) return mappedSpecifier;
|
|
378
|
+
const getCssUrl = async (fullURL) => {
|
|
379
|
+
return await this.transformSourceModule("css", fullURL, await this.fetchText(fullURL));
|
|
380
|
+
};
|
|
381
|
+
const toCDNUrl = (specifier) => {
|
|
382
|
+
const subpath = specifier.startsWith("@") ? specifier.split("/").slice(2).join("/") : specifier.split("/").slice(1).join("/");
|
|
383
|
+
const url = this.resolveBareSpecifier(specifier);
|
|
384
|
+
if (subpath.endsWith(".css")) return getCssUrl(url);
|
|
385
|
+
return url;
|
|
386
|
+
};
|
|
387
|
+
if (isRelativeSpecifier(specifier)) {
|
|
388
|
+
const targetUrl = new URL(specifier, sourceUrl);
|
|
389
|
+
if (targetUrl.pathname.endsWith(".module.css")) return await this.transformSourceModule("css-module", targetUrl.href, await this.fetchText(targetUrl.href));
|
|
390
|
+
else if (targetUrl.pathname.endsWith(".css")) return getCssUrl(targetUrl.href);
|
|
391
|
+
else if (targetUrl.pathname.endsWith(".wasm")) return targetUrl.href;
|
|
392
|
+
else {
|
|
393
|
+
if (this.sourceTracker.isInFlight("esm", targetUrl.href)) return targetUrl.href;
|
|
394
|
+
//! ^ transformSourceModule is recursive ^
|
|
395
|
+
return await this.transformSourceModule("esm", targetUrl.href, await this.fetchText(targetUrl.href));
|
|
396
|
+
}
|
|
397
|
+
} else if (specifier.startsWith("node:")) return `https://raw.esm.sh/@jspm/core/nodelibs/browser/${specifier.slice(5)}.js`;
|
|
398
|
+
else if (specifier.startsWith("npm:")) return toCDNUrl(specifier.slice(4));
|
|
399
|
+
else if (isBareSpecifier(specifier)) return toCDNUrl(specifier);
|
|
400
|
+
else return specifier;
|
|
401
|
+
}
|
|
402
|
+
async resolveSpecifiers(specifiers, sourceUrl) {
|
|
403
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
404
|
+
const tasks = Array.from(specifiers).map(async (specifier) => {
|
|
405
|
+
const specifier2 = await this.resolveSpecifier(specifier, sourceUrl);
|
|
406
|
+
if (specifier !== specifier2) resolved.set(specifier, specifier2);
|
|
407
|
+
});
|
|
408
|
+
await Promise.all(tasks);
|
|
409
|
+
return resolved;
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
function getFileName(sourceUrl) {
|
|
413
|
+
try {
|
|
414
|
+
return new URL(sourceUrl).pathname;
|
|
415
|
+
} catch {
|
|
416
|
+
return "temp.tsx";
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
//#endregion
|
|
421
|
+
//#region src/index.ts
|
|
422
|
+
/**
|
|
423
|
+
* The singleton global instance of ModuleTSX.
|
|
424
|
+
*/
|
|
425
|
+
const instance = new ModuleTSX();
|
|
426
|
+
const TYPE_ATTRIBUTE_VALUE = "module-tsx";
|
|
427
|
+
async function sideEffect() {
|
|
428
|
+
const importScript = async (script) => {
|
|
429
|
+
const src = script.src;
|
|
430
|
+
if (src) return instance.import(src);
|
|
431
|
+
else {
|
|
432
|
+
const code = script.innerHTML || "";
|
|
433
|
+
return instance.importCode(document.location.href, code);
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
for (const s of Array.from(document.querySelectorAll(`script[type="${TYPE_ATTRIBUTE_VALUE}"]`))) {
|
|
437
|
+
const script = s;
|
|
438
|
+
if (!script.async && script.defer) warn(`script with type="${TYPE_ATTRIBUTE_VALUE}" does not support defer attribute. Use async or no attribute instead.`);
|
|
439
|
+
for (const key in ["integrity", "crossorigin"]) if (script[key]) warn(`script with type="${TYPE_ATTRIBUTE_VALUE}" does not support ${key} attribute.`);
|
|
440
|
+
if (script.async) importScript(script);
|
|
441
|
+
else await importScript(script);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
document.addEventListener("DOMContentLoaded", sideEffect);
|
|
445
|
+
|
|
446
|
+
//#endregion
|
|
447
|
+
export { ModuleTSX, ModuleTSXError, instance };
|