/
js.ts
123 lines (105 loc) · 4.35 KB
/
js.ts
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
import { dirname, isAbsolute, posix, relative, resolve, sep } from 'path';
import type polka from 'polka';
import type { Plugin } from 'rollup';
import { createPluginContainer } from '../rollup-plugin-container';
import { promises as fs } from 'fs';
import { transformImports } from '../transform-imports';
interface JSMiddlewareOpts {
root: string;
plugins: Plugin[];
}
// TODO: make this configurable
const jsExts = /\.(?:[jt]sx?|[cm]js)$/;
// Minimal version of https://github.com/preactjs/wmr/blob/main/packages/wmr/src/wmr-middleware.js
export const jsMiddleware = ({
root,
plugins,
}: JSMiddlewareOpts): polka.Middleware => {
const rollupPlugins = createPluginContainer(plugins);
rollupPlugins.buildStart();
return async (req, res, next) => {
try {
// Normalized path starting with slash
const path = posix.normalize(req.path);
let id: string;
let file: string;
if (path.startsWith('/@npm/')) {
id = path.slice(1);
file = ''; // This should never be read
} else {
// Remove leading slash, and convert slashes to os-specific slashes
const osPath = path.slice(1).split(posix.sep).join(sep);
// Absolute file path
file = resolve(root, osPath);
// Rollup-style CWD-relative Unix-normalized path "id":
id = `./${relative(root, file)
.replace(/^\.\//, '')
.replace(/^\0/, '')
.split(sep)
.join(posix.sep)}`;
}
res.setHeader('Content-Type', 'application/javascript;charset=utf-8');
const resolved = await rollupPlugins.resolveId(id);
const resolvedId = (
typeof resolved === 'object' ? resolved?.id : resolved
) as string;
let code: string | undefined;
if (typeof req.query['inline-code'] === 'string') {
code = req.query['inline-code'];
} else {
const result = resolvedId && (await rollupPlugins.load(resolvedId));
code = typeof result === 'object' ? result?.code : result;
}
if (!code && code !== '') {
// If it doesn't have a js-like extension,
// and none of the rollup plugins provided a load hook for it
// and it doesn't have the ?import param (added for non-JS assets that can be imported into JS, like css)
// Then treat it as a static asset
if (!jsExts.test(resolvedId) && req.query.import === undefined)
return next();
// Always use the resolved id as the basis for our file
let file = resolvedId;
file = file.split(posix.sep).join(sep);
if (!isAbsolute(file)) file = resolve(root, file);
code = await fs.readFile(file, 'utf-8');
}
code = await rollupPlugins.transform(code, id);
// Normalize import paths
// Resolve all the imports and replace them, and inline the resulting resolved paths
// This makes different ways of importing the same path (e.g. extensionless imports, etc.)
// all dedupe to the same module so it is only executed once
code = await transformImports(code, id, {
async resolveId(spec) {
if (/^(data:|https?:|\/\/)/.test(spec)) return spec;
const resolved = await rollupPlugins.resolveId(spec, file);
if (resolved) {
spec = typeof resolved === 'object' ? resolved.id : resolved;
if (spec.startsWith('@npm/')) return `/${spec}`;
if (/^(\/|\\|[a-z]:\\)/i.test(spec)) {
spec = relative(dirname(file), spec).split(sep).join(posix.sep);
if (!/^\.?\.?\//.test(spec)) spec = `./${spec}`;
}
if (typeof resolved === 'object' && resolved.external) {
if (/^(data|https?):/.test(spec)) return spec;
spec = relative(root, spec).split(sep).join(posix.sep);
if (!/^(\/|[\w-]+:)/.test(spec)) spec = `/${spec}`;
return spec;
}
}
// If it wasn't resovled, and doesn't have a js-like extension
// add the ?import query param so it is clear
// that the request needs to end up as JS that can be imported
if (!jsExts.test(spec)) return `${spec}?import`;
return spec;
},
});
if (!code) return next();
res.writeHead(200, {
'Content-Length': Buffer.byteLength(code, 'utf-8'),
});
res.end(code);
} catch (error) {
next(error);
}
};
};