@zipadee/javascript 0.0.18 → 0.0.20
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/lib/serve.js +165 -202
- package/package.json +3 -3
package/lib/serve.js
CHANGED
|
@@ -1,18 +1,13 @@
|
|
|
1
|
-
import {HttpError} from '@zipadee/core';
|
|
2
|
-
import {send} from '@zipadee/static';
|
|
3
|
-
import {
|
|
4
|
-
decodePath,
|
|
5
|
-
pathIsHidden,
|
|
6
|
-
resolvePath,
|
|
7
|
-
stat,
|
|
8
|
-
} from '@zipadee/static/lib/utils.js';
|
|
1
|
+
import { HttpError } from '@zipadee/core';
|
|
2
|
+
import { send } from '@zipadee/static';
|
|
3
|
+
import { decodePath, pathIsHidden, resolvePath, stat, } from '@zipadee/static/lib/utils.js';
|
|
9
4
|
import resolve from 'enhanced-resolve';
|
|
10
|
-
import {init, parse} from 'es-module-lexer';
|
|
5
|
+
import { init, parse } from 'es-module-lexer';
|
|
11
6
|
import baseFS from 'fs';
|
|
12
7
|
import fs from 'node:fs/promises';
|
|
13
8
|
import path from 'node:path';
|
|
14
|
-
import {parseImportAttributes} from './attributes.js';
|
|
15
|
-
const {CachedInputFileSystem, ResolverFactory} = resolve;
|
|
9
|
+
import { parseImportAttributes } from './attributes.js';
|
|
10
|
+
const { CachedInputFileSystem, ResolverFactory } = resolve;
|
|
16
11
|
await init;
|
|
17
12
|
// TODO:
|
|
18
13
|
// - Add caching for both specifier resolution and files
|
|
@@ -27,202 +22,170 @@ await init;
|
|
|
27
22
|
* Serve static JavaScript files from a `root` directory.
|
|
28
23
|
*/
|
|
29
24
|
export const serve = (opts) => {
|
|
30
|
-
|
|
31
|
-
opts.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const isCssModule = req.url.searchParams.get('type') === 'css-module';
|
|
54
|
-
const transform = isCssModule || extensions.includes(parsedPath.ext);
|
|
55
|
-
if (filePath.startsWith(rootPathPrefix)) {
|
|
56
|
-
filePath = filePath.substring(rootPathPrefix.length);
|
|
57
|
-
filePath = filePath.slice(parsedPath.root.length);
|
|
58
|
-
filePath = resolvePath(root, filePath);
|
|
59
|
-
} else {
|
|
60
|
-
filePath = filePath.slice(parsedPath.root.length);
|
|
61
|
-
filePath = resolvePath(base, filePath);
|
|
62
|
-
}
|
|
63
|
-
if (pathIsHidden(root, filePath)) {
|
|
64
|
-
return await next();
|
|
65
|
-
}
|
|
66
|
-
const stats = await stat(filePath);
|
|
67
|
-
if (stats.isDirectory()) {
|
|
68
|
-
return await next();
|
|
69
|
-
}
|
|
70
|
-
if (!transform) {
|
|
71
|
-
const relativePath = path.relative(root, filePath);
|
|
72
|
-
await send(req, res, relativePath, {root});
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
if (isCssModule) {
|
|
76
|
-
const source = await fs.readFile(filePath, 'utf8');
|
|
77
|
-
res.type = 'text/javascript';
|
|
78
|
-
res.body = `const styleSheet = new CSSStyleSheet();
|
|
79
|
-
styleSheet.replaceSync(\`${source.replace(/`/g, '\\`')}\`);
|
|
80
|
-
export default styleSheet;
|
|
81
|
-
`;
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
// Derive the mount prefix by removing the request path which mount() sets
|
|
85
|
-
// from the URL path.
|
|
86
|
-
const mountPrefix = req.url.pathname.substring(
|
|
87
|
-
0,
|
|
88
|
-
req.url.pathname.length - req.path.length,
|
|
89
|
-
);
|
|
90
|
-
const source = await fs.readFile(filePath, 'utf8');
|
|
91
|
-
const [imports, _exports, _facade, _hasModuleSyntax] = parse(
|
|
92
|
-
source,
|
|
93
|
-
filePath,
|
|
94
|
-
);
|
|
95
|
-
let output = '';
|
|
96
|
-
let lastIndex = 0;
|
|
97
|
-
for (const impt of imports) {
|
|
98
|
-
const {
|
|
99
|
-
t: type,
|
|
100
|
-
s: start,
|
|
101
|
-
e: specifierEnd,
|
|
102
|
-
n: unescaped,
|
|
103
|
-
a: assert,
|
|
104
|
-
se: importEnd,
|
|
105
|
-
} = impt;
|
|
106
|
-
if (type === 1) {
|
|
107
|
-
// Static import
|
|
108
|
-
let importSpecifier =
|
|
109
|
-
unescaped ?? source.substring(start, specifierEnd);
|
|
110
|
-
let resolve = false;
|
|
111
|
-
let cssImportTransform = false;
|
|
112
|
-
let relativeImport = false;
|
|
113
|
-
let absoluteImport = false;
|
|
114
|
-
// If the specifier is relative or absolute, and has an extension,
|
|
115
|
-
// we don't need to resolve it
|
|
116
|
-
if (
|
|
117
|
-
importSpecifier.startsWith('.') ||
|
|
118
|
-
importSpecifier.startsWith('/')
|
|
119
|
-
) {
|
|
120
|
-
if (path.extname(importSpecifier) === '') {
|
|
121
|
-
// If we don't have an extension, we need to resolve it, but fix the
|
|
122
|
-
// resolved path to be relative to the current file
|
|
123
|
-
relativeImport = importSpecifier.startsWith('.');
|
|
124
|
-
absoluteImport = importSpecifier.startsWith('/');
|
|
125
|
-
resolve = true;
|
|
126
|
-
}
|
|
127
|
-
} else {
|
|
128
|
-
// If the import is not relative or absolute, we need to resolve it
|
|
129
|
-
// using the resolver
|
|
130
|
-
resolve = true;
|
|
25
|
+
const root = opts.root === undefined ? process.cwd() : path.resolve(opts.root);
|
|
26
|
+
const base = opts.base === undefined ? root : resolvePath(root, opts.base);
|
|
27
|
+
const { rootPathPrefix = '/__root__', extensions = ['.js', '.mjs'], conditions = ['browser', 'import'], cssModules = false, } = opts;
|
|
28
|
+
const resolver = ResolverFactory.createResolver({
|
|
29
|
+
fileSystem: new CachedInputFileSystem(baseFS, 4000),
|
|
30
|
+
roots: [root, base],
|
|
31
|
+
extensions: ['.js', '.json'],
|
|
32
|
+
conditionNames: conditions,
|
|
33
|
+
mainFields: ['module', 'browser', 'main'],
|
|
34
|
+
});
|
|
35
|
+
return async (req, res, next) => {
|
|
36
|
+
if (!(req.method === 'HEAD' || req.method === 'GET')) {
|
|
37
|
+
// TODO: implement HEAD?
|
|
38
|
+
return await next();
|
|
39
|
+
}
|
|
40
|
+
let filePath = decodePath(req.path);
|
|
41
|
+
const parsedPath = path.parse(filePath);
|
|
42
|
+
const isCssModule = req.url.searchParams.get('type') === 'css-module';
|
|
43
|
+
const transform = isCssModule || extensions.includes(parsedPath.ext);
|
|
44
|
+
if (filePath.startsWith(rootPathPrefix)) {
|
|
45
|
+
filePath = filePath.substring(rootPathPrefix.length);
|
|
46
|
+
filePath = filePath.slice(parsedPath.root.length);
|
|
47
|
+
filePath = resolvePath(root, filePath);
|
|
131
48
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const attributesSource = source.slice(assert, importEnd);
|
|
136
|
-
attributes = parseImportAttributes(attributesSource);
|
|
137
|
-
if (attributes.get('type') === 'css') {
|
|
138
|
-
cssImportTransform = true;
|
|
139
|
-
}
|
|
49
|
+
else {
|
|
50
|
+
filePath = filePath.slice(parsedPath.root.length);
|
|
51
|
+
filePath = resolvePath(base, filePath);
|
|
140
52
|
}
|
|
141
|
-
if (
|
|
142
|
-
|
|
143
|
-
lastIndex = specifierEnd;
|
|
144
|
-
continue;
|
|
53
|
+
if (pathIsHidden(root, filePath)) {
|
|
54
|
+
return await next();
|
|
145
55
|
}
|
|
146
|
-
const
|
|
147
|
-
if (
|
|
148
|
-
|
|
149
|
-
// base path
|
|
150
|
-
importSpecifier = path.join(base, importSpecifier);
|
|
56
|
+
const stats = await stat(filePath);
|
|
57
|
+
if (stats.isDirectory()) {
|
|
58
|
+
return await next();
|
|
151
59
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
resolveFromPath,
|
|
157
|
-
importSpecifier,
|
|
158
|
-
{},
|
|
159
|
-
(err, result) => {
|
|
160
|
-
if (err) {
|
|
161
|
-
rej(err);
|
|
162
|
-
} else if (result === undefined || result === false) {
|
|
163
|
-
rej(new Error('Could not resolve import'));
|
|
164
|
-
} else {
|
|
165
|
-
res(new URL(result, fileURL));
|
|
166
|
-
}
|
|
167
|
-
},
|
|
168
|
-
);
|
|
169
|
-
});
|
|
170
|
-
const resolvedImportPath = resolvedImportURL.pathname;
|
|
171
|
-
if (!resolvedImportPath.startsWith(root)) {
|
|
172
|
-
throw new HttpError(
|
|
173
|
-
500,
|
|
174
|
-
undefined,
|
|
175
|
-
`Attempted to resolve import outside of root:\n root: ${root}\n resolved: ${resolvedImportPath}`,
|
|
176
|
-
);
|
|
60
|
+
if (!transform) {
|
|
61
|
+
const relativePath = path.relative(root, filePath);
|
|
62
|
+
await send(req, res, relativePath, { root });
|
|
63
|
+
return;
|
|
177
64
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
resolvedImportPath,
|
|
187
|
-
);
|
|
188
|
-
if (!resolvedImport.startsWith('.')) {
|
|
189
|
-
resolvedImport = './' + resolvedImport;
|
|
190
|
-
}
|
|
191
|
-
} else if (resolvedImportPath.startsWith(base)) {
|
|
192
|
-
// Imports within the base path are rewritten to be relative to the
|
|
193
|
-
// mounted path.
|
|
194
|
-
resolvedImport = resolvedImportPath.substring(base.length);
|
|
195
|
-
} else {
|
|
196
|
-
resolvedImport = path.join(
|
|
197
|
-
mountPrefix,
|
|
198
|
-
rootPathPrefix,
|
|
199
|
-
resolvedImportPath.substring(root.length),
|
|
200
|
-
);
|
|
65
|
+
if (isCssModule) {
|
|
66
|
+
const source = await fs.readFile(filePath, 'utf8');
|
|
67
|
+
res.type = 'text/javascript';
|
|
68
|
+
res.body = `const styleSheet = new CSSStyleSheet();
|
|
69
|
+
styleSheet.replaceSync(\`${source.replace(/`/g, '\\`')}\`);
|
|
70
|
+
export default styleSheet;
|
|
71
|
+
`;
|
|
72
|
+
return;
|
|
201
73
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
74
|
+
// Derive the mount prefix by removing the request path which mount() sets
|
|
75
|
+
// from the URL path.
|
|
76
|
+
const mountPrefix = req.url.pathname.substring(0, req.url.pathname.length - req.path.length);
|
|
77
|
+
const source = await fs.readFile(filePath, 'utf8');
|
|
78
|
+
const [imports, _exports, _facade, _hasModuleSyntax] = parse(source, filePath);
|
|
79
|
+
let output = '';
|
|
80
|
+
let lastIndex = 0;
|
|
81
|
+
for (const impt of imports) {
|
|
82
|
+
const { t: type, s: start, e: specifierEnd, n: unescaped, a: assert, se: importEnd, } = impt;
|
|
83
|
+
if (type === 1) {
|
|
84
|
+
// Static import
|
|
85
|
+
let importSpecifier = unescaped ?? source.substring(start, specifierEnd);
|
|
86
|
+
let resolve = false;
|
|
87
|
+
let cssImportTransform = false;
|
|
88
|
+
let relativeImport = false;
|
|
89
|
+
let absoluteImport = false;
|
|
90
|
+
// If the specifier is relative or absolute, and has an extension,
|
|
91
|
+
// we don't need to resolve it
|
|
92
|
+
if (importSpecifier.startsWith('.') ||
|
|
93
|
+
importSpecifier.startsWith('/')) {
|
|
94
|
+
if (path.extname(importSpecifier) === '') {
|
|
95
|
+
// If we don't have an extension, we need to resolve it, but fix the
|
|
96
|
+
// resolved path to be relative to the current file
|
|
97
|
+
relativeImport = importSpecifier.startsWith('.');
|
|
98
|
+
absoluteImport = importSpecifier.startsWith('/');
|
|
99
|
+
resolve = true;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
// If the import is not relative or absolute, we need to resolve it
|
|
104
|
+
// using the resolver
|
|
105
|
+
resolve = true;
|
|
106
|
+
}
|
|
107
|
+
const hasAttributes = assert !== -1;
|
|
108
|
+
let attributes;
|
|
109
|
+
if (cssModules && hasAttributes) {
|
|
110
|
+
const attributesSource = source.slice(assert, importEnd);
|
|
111
|
+
attributes = parseImportAttributes(attributesSource);
|
|
112
|
+
if (attributes.get('type') === 'css') {
|
|
113
|
+
cssImportTransform = true;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (!resolve && !cssImportTransform) {
|
|
117
|
+
output += `${source.substring(lastIndex, start)}${importSpecifier}`;
|
|
118
|
+
lastIndex = specifierEnd;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const fileURL = new URL(filePath, 'file://');
|
|
122
|
+
if (absoluteImport) {
|
|
123
|
+
// If the import is absolute, we need to resolve it relative to the
|
|
124
|
+
// base path
|
|
125
|
+
importSpecifier = path.join(base, importSpecifier);
|
|
126
|
+
}
|
|
127
|
+
const resolveFromPath = path.dirname(filePath);
|
|
128
|
+
const resolvedImportURL = await new Promise((res, rej) => {
|
|
129
|
+
resolver.resolve({}, resolveFromPath, importSpecifier, {}, (err, result) => {
|
|
130
|
+
if (err) {
|
|
131
|
+
rej(err);
|
|
132
|
+
}
|
|
133
|
+
else if (result === undefined || result === false) {
|
|
134
|
+
rej(new Error('Could not resolve import'));
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
res(new URL(result, fileURL));
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
const resolvedImportPath = resolvedImportURL.pathname;
|
|
142
|
+
if (!resolvedImportPath.startsWith(root)) {
|
|
143
|
+
throw new HttpError(500, undefined, `Attempted to resolve import outside of root:\n root: ${root}\n resolved: ${resolvedImportPath}`);
|
|
144
|
+
}
|
|
145
|
+
let resolvedImport;
|
|
146
|
+
if (relativeImport) {
|
|
147
|
+
// For relative imports, we resolve the path relative to the current
|
|
148
|
+
// file. This keeps the rewritten import as similar as possible to the
|
|
149
|
+
// original import. Most likely in this case we're just adding a file
|
|
150
|
+
// extension.
|
|
151
|
+
resolvedImport = path.relative(path.dirname(filePath), resolvedImportPath);
|
|
152
|
+
if (!resolvedImport.startsWith('.')) {
|
|
153
|
+
resolvedImport = './' + resolvedImport;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
else if (resolvedImportPath.startsWith(base)) {
|
|
157
|
+
// Imports within the base path are rewritten to be relative to the
|
|
158
|
+
// mounted path.
|
|
159
|
+
resolvedImport = resolvedImportPath.substring(base.length);
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
resolvedImport = path.join(mountPrefix, rootPathPrefix, resolvedImportPath.substring(root.length));
|
|
163
|
+
}
|
|
164
|
+
if (cssImportTransform) {
|
|
165
|
+
attributes?.delete('type');
|
|
166
|
+
let attributesSource = '';
|
|
167
|
+
if (attributes?.size) {
|
|
168
|
+
const attrs = Array.from(attributes.entries())
|
|
169
|
+
.map(([key, value]) => `${key}: '${value}'`)
|
|
170
|
+
.join(', ');
|
|
171
|
+
attributesSource = ` with {${attrs}}`;
|
|
172
|
+
}
|
|
173
|
+
output +=
|
|
174
|
+
source.substring(lastIndex, start) +
|
|
175
|
+
resolvedImport +
|
|
176
|
+
'?type=css-module' +
|
|
177
|
+
source.substring(specifierEnd, specifierEnd + 1) +
|
|
178
|
+
attributesSource;
|
|
179
|
+
lastIndex = importEnd;
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
output += source.substring(lastIndex, start) + resolvedImport;
|
|
183
|
+
lastIndex = specifierEnd;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
221
186
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
res.body = output + source.substring(lastIndex);
|
|
226
|
-
};
|
|
187
|
+
res.type = 'text/javascript';
|
|
188
|
+
res.body = output + source.substring(lastIndex);
|
|
189
|
+
};
|
|
227
190
|
};
|
|
228
|
-
//# sourceMappingURL=serve.js.map
|
|
191
|
+
//# sourceMappingURL=serve.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zipadee/javascript",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.20",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -57,8 +57,8 @@
|
|
|
57
57
|
}
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
|
-
"@zipadee/core": "^0.0.
|
|
61
|
-
"@zipadee/static": "^0.0.
|
|
60
|
+
"@zipadee/core": "^0.0.20",
|
|
61
|
+
"@zipadee/static": "^0.0.20",
|
|
62
62
|
"dedent": "^1.6.0",
|
|
63
63
|
"enhanced-resolve": "^5.17.1",
|
|
64
64
|
"es-module-lexer": "^1.5.4"
|