-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathtsx.loader.mjs
More file actions
144 lines (114 loc) · 3.91 KB
/
tsx.loader.mjs
File metadata and controls
144 lines (114 loc) · 3.91 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
import path from 'node:path';
import { cwd } from 'node:process';
import { pathToFileURL } from 'node:url';
import { transform, transformSync } from 'esbuild';
import { getFilenameExt } from '@nodejs-loaders/parse-filename';
import { runForAsyncOrSync } from '@nodejs-loaders/chain-utils/run-normalised';
import { findEsbuildConfig } from './find-esbuild-config.mjs';
/** @typedef {import('../types.d.ts').FileURL} FileURL */
/**
* The load hook needs to know the parent URL to find the esbuild config.
* But load hooks don't have access to the parent URL.
* If you try to pass it as return value from the resolve hook, it will be overwritten by node.
*
* @type {Map<FileURL, FileURL>}
*/
export const parentURLs = new Map();
/**
* @type {import('node:module').ResolveHook}
*/
function resolveTSX(specifier, ctx, nextResolve) {
const nextResult = nextResolve(specifier);
return runForAsyncOrSync(nextResult, finaliseResolveTSX, ctx);
}
export { resolveTSX as resolve };
/**
*
* @param {import('node:module').ResolveFnOutput} resolvedResult Specifier has been fully resolved.
* @param {import('node:module').ResolveHookContext} ctx Context about the module.
* @returns
*/
function finaliseResolveTSX(resolvedResult, ctx) {
// Check against the fully resolved URL, not just the specifier, in case another loader has
// something to contribute to the resolution.
const ext = getFilenameExt(/** @type {FileURL} */ (resolvedResult.url));
parentURLs.set(
/** @type {FileURL} */ (resolvedResult.url),
/** @type {FileURL} */ (ctx.parentURL ?? pathToFileURL(path.join(cwd(), 'whatever.ext')).href),
);
if (ext === '.jsx') {
return {
...resolvedResult,
format: 'jsx',
};
}
if (ext === '.mts' || ext === '.ts' || ext === '.tsx') {
return {
...resolvedResult,
format: 'tsx',
};
}
return resolvedResult;
}
/**
* @type {import('node:module').LoadHook}
* @param {FileURL} url The fully resolved url.
*/
function loadTSX(url, ctx, nextLoad) {
if (ctx.format !== 'jsx' && ctx.format !== 'tsx') return nextLoad(url); // not (j|t)sx
const format = 'module';
const nextResult = nextLoad(url, { format });
return runForAsyncOrSync(nextResult, finaliseLoadTSX, url);
}
export { loadTSX as load };
/**
*
* @param {import('node:module').LoadFnOutput} loadResult Raw source has been retrieved.
* @param {FileURL} url The fully resolved module location.
* @param {boolean} wasPromise Whether the chain is sync or async.
*/
function finaliseLoadTSX({ format, source: rawSource }, url, wasPromise) {
if (!rawSource) return { format, source: undefined };
rawSource = `${rawSource}`; // byte array → string
const esbuildConfig = findEsbuildConfig(url, parentURLs.get(url));
if (esbuildConfig.jsx === 'transform') rawSource = `import * as React from 'react';\n${rawSource}`;
if (wasPromise) {
return transform(rawSource, { sourcefile: url, ...esbuildConfig })
.catch((f) => handleTrasformErrors(f, url))
.then(({ code: source, warnings }) => {
// oxlint-disable-next-line no-console
if (warnings.length) console.warn(...warnings);
return {
format,
source,
};
});
}
try {
const { code: source, warnings } = transformSync(rawSource, { sourcefile: url, ...esbuildConfig });
// oxlint-disable-next-line no-console
if (warnings.length) console.warn(...warnings);
return {
format,
source,
};
}
catch (f) { handleTrasformErrors(f, url) }
}
function handleTrasformErrors(f, url) {
if (!('errors' in f) || !('warnings' in f)) throw f;
const failure = /** @type {import('esbuild').TransformFailure} */ (f);
for (const {
location: { column, line, lineText },
text,
} of failure.errors) {
// oxlint-disable-next-line no-console
console.error(`TranspileError: ${text}\n at ${url}:${line}:${column}\n at: ${lineText}\n`);
}
// oxlint-disable-next-line no-console
if (failure.warnings.length) console.warn(...failure.warnings);
return {
code: null,
warnings: [],
};
}