hadars 0.1.33 → 0.1.34
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/dist/cli.js +4 -2
- package/dist/loader.cjs +95 -3
- package/dist/ssr-watch.js +4 -2
- package/package.json +1 -1
- package/src/utils/loader.ts +137 -17
- package/src/utils/rspack.ts +2 -0
package/dist/cli.js
CHANGED
|
@@ -1181,7 +1181,8 @@ var getConfigBase = (mode, isServerBuild = false) => {
|
|
|
1181
1181
|
// Transforms loadModule('./path') based on build target.
|
|
1182
1182
|
// Runs before swc-loader (loaders execute right-to-left).
|
|
1183
1183
|
{
|
|
1184
|
-
loader: loaderPath
|
|
1184
|
+
loader: loaderPath,
|
|
1185
|
+
options: { server: isServerBuild }
|
|
1185
1186
|
},
|
|
1186
1187
|
{
|
|
1187
1188
|
loader: "builtin:swc-loader",
|
|
@@ -1212,7 +1213,8 @@ var getConfigBase = (mode, isServerBuild = false) => {
|
|
|
1212
1213
|
exclude: [loaderPath],
|
|
1213
1214
|
use: [
|
|
1214
1215
|
{
|
|
1215
|
-
loader: loaderPath
|
|
1216
|
+
loader: loaderPath,
|
|
1217
|
+
options: { server: isServerBuild }
|
|
1216
1218
|
},
|
|
1217
1219
|
{
|
|
1218
1220
|
loader: "builtin:swc-loader",
|
package/dist/loader.cjs
CHANGED
|
@@ -22,7 +22,8 @@ __export(loader_exports, {
|
|
|
22
22
|
});
|
|
23
23
|
module.exports = __toCommonJS(loader_exports);
|
|
24
24
|
function loader(source) {
|
|
25
|
-
const
|
|
25
|
+
const opts = this.getOptions?.() ?? {};
|
|
26
|
+
const isServer = typeof opts.server === "boolean" ? opts.server : this.target === "node" || this.target === "async-node";
|
|
26
27
|
const resourcePath = this.resourcePath ?? this.resource ?? "(unknown)";
|
|
27
28
|
let swc;
|
|
28
29
|
try {
|
|
@@ -51,7 +52,22 @@ function swcTransform(swc, source, isServer, resourcePath) {
|
|
|
51
52
|
if (node.type !== "CallExpression")
|
|
52
53
|
return;
|
|
53
54
|
const callee = node.callee;
|
|
54
|
-
if (!callee || callee.type !== "Identifier"
|
|
55
|
+
if (!callee || callee.type !== "Identifier")
|
|
56
|
+
return;
|
|
57
|
+
const name = callee.value;
|
|
58
|
+
if (!isServer && name === "useServerData") {
|
|
59
|
+
const args2 = node.arguments;
|
|
60
|
+
if (!args2 || args2.length < 2)
|
|
61
|
+
return;
|
|
62
|
+
const fnArg = args2[1].expression ?? args2[1];
|
|
63
|
+
replacements.push({
|
|
64
|
+
start: fnArg.span.start - fileOffset,
|
|
65
|
+
end: fnArg.span.end - fileOffset,
|
|
66
|
+
replacement: "()=>undefined"
|
|
67
|
+
});
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (name !== "loadModule")
|
|
55
71
|
return;
|
|
56
72
|
const args = node.arguments;
|
|
57
73
|
if (!args || args.length === 0)
|
|
@@ -136,11 +152,87 @@ function countLeadingNonCodeBytes(source) {
|
|
|
136
152
|
}
|
|
137
153
|
const LOAD_MODULE_RE = /\bloadModule\s*(?:<(?:[^<>]|<[^<>]*>)*>\s*)?\(\s*(['"`])((?:\\.|(?!\1)[^\\])*)\1\s*\)/gs;
|
|
138
154
|
const DYNAMIC_LOAD_MODULE_RE = /\bloadModule\s*(?:<(?:[^<>]|<[^<>]*>)*>\s*)?\(/g;
|
|
155
|
+
function scanExpressionEnd(source, pos) {
|
|
156
|
+
let depth = 0;
|
|
157
|
+
let i = pos;
|
|
158
|
+
while (i < source.length) {
|
|
159
|
+
const ch = source[i];
|
|
160
|
+
if (ch === "(" || ch === "[" || ch === "{") {
|
|
161
|
+
depth++;
|
|
162
|
+
i++;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (ch === ")" || ch === "]" || ch === "}") {
|
|
166
|
+
if (depth === 0)
|
|
167
|
+
break;
|
|
168
|
+
depth--;
|
|
169
|
+
i++;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (ch === "," && depth === 0)
|
|
173
|
+
break;
|
|
174
|
+
if (ch === '"' || ch === "'" || ch === "`") {
|
|
175
|
+
const q = ch;
|
|
176
|
+
i++;
|
|
177
|
+
while (i < source.length && source[i] !== q) {
|
|
178
|
+
if (source[i] === "\\")
|
|
179
|
+
i++;
|
|
180
|
+
i++;
|
|
181
|
+
}
|
|
182
|
+
i++;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (ch === "/" && source[i + 1] === "/") {
|
|
186
|
+
while (i < source.length && source[i] !== "\n")
|
|
187
|
+
i++;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (ch === "/" && source[i + 1] === "*") {
|
|
191
|
+
i += 2;
|
|
192
|
+
while (i + 1 < source.length && !(source[i] === "*" && source[i + 1] === "/"))
|
|
193
|
+
i++;
|
|
194
|
+
i += 2;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
i++;
|
|
198
|
+
}
|
|
199
|
+
return i;
|
|
200
|
+
}
|
|
201
|
+
function stripUseServerDataFns(source) {
|
|
202
|
+
const CALL_RE = /\buseServerData\s*(?:<(?:[^<>]|<[^<>]*>)*>\s*)?\(/g;
|
|
203
|
+
let result = "";
|
|
204
|
+
let lastIndex = 0;
|
|
205
|
+
let match;
|
|
206
|
+
CALL_RE.lastIndex = 0;
|
|
207
|
+
while ((match = CALL_RE.exec(source)) !== null) {
|
|
208
|
+
const callStart = match.index;
|
|
209
|
+
let i = match.index + match[0].length;
|
|
210
|
+
while (i < source.length && /\s/.test(source[i]))
|
|
211
|
+
i++;
|
|
212
|
+
i = scanExpressionEnd(source, i);
|
|
213
|
+
if (i >= source.length || source[i] !== ",")
|
|
214
|
+
continue;
|
|
215
|
+
i++;
|
|
216
|
+
while (i < source.length && /\s/.test(source[i]))
|
|
217
|
+
i++;
|
|
218
|
+
const fnStart = i;
|
|
219
|
+
const fnEnd = scanExpressionEnd(source, i);
|
|
220
|
+
if (fnEnd <= fnStart)
|
|
221
|
+
continue;
|
|
222
|
+
result += source.slice(lastIndex, fnStart) + "()=>undefined";
|
|
223
|
+
lastIndex = fnEnd;
|
|
224
|
+
CALL_RE.lastIndex = fnEnd;
|
|
225
|
+
}
|
|
226
|
+
return lastIndex === 0 ? source : result + source.slice(lastIndex);
|
|
227
|
+
}
|
|
139
228
|
function regexTransform(source, isServer, resourcePath) {
|
|
140
|
-
|
|
229
|
+
let transformed = source.replace(
|
|
141
230
|
LOAD_MODULE_RE,
|
|
142
231
|
(_match, quote, modulePath) => isServer ? `Promise.resolve(require(${quote}${modulePath}${quote}))` : `import(${quote}${modulePath}${quote})`
|
|
143
232
|
);
|
|
233
|
+
if (!isServer) {
|
|
234
|
+
transformed = stripUseServerDataFns(transformed);
|
|
235
|
+
}
|
|
144
236
|
let match;
|
|
145
237
|
DYNAMIC_LOAD_MODULE_RE.lastIndex = 0;
|
|
146
238
|
while ((match = DYNAMIC_LOAD_MODULE_RE.exec(transformed)) !== null) {
|
package/dist/ssr-watch.js
CHANGED
|
@@ -52,7 +52,8 @@ var getConfigBase = (mode, isServerBuild = false) => {
|
|
|
52
52
|
// Transforms loadModule('./path') based on build target.
|
|
53
53
|
// Runs before swc-loader (loaders execute right-to-left).
|
|
54
54
|
{
|
|
55
|
-
loader: loaderPath
|
|
55
|
+
loader: loaderPath,
|
|
56
|
+
options: { server: isServerBuild }
|
|
56
57
|
},
|
|
57
58
|
{
|
|
58
59
|
loader: "builtin:swc-loader",
|
|
@@ -83,7 +84,8 @@ var getConfigBase = (mode, isServerBuild = false) => {
|
|
|
83
84
|
exclude: [loaderPath],
|
|
84
85
|
use: [
|
|
85
86
|
{
|
|
86
|
-
loader: loaderPath
|
|
87
|
+
loader: loaderPath,
|
|
88
|
+
options: { server: isServerBuild }
|
|
87
89
|
},
|
|
88
90
|
{
|
|
89
91
|
loader: "builtin:swc-loader",
|
package/package.json
CHANGED
package/src/utils/loader.ts
CHANGED
|
@@ -1,34 +1,49 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Rspack/webpack loader that
|
|
3
|
-
*
|
|
2
|
+
* Rspack/webpack loader that applies two source-level transforms based on the
|
|
3
|
+
* compilation target (web vs node):
|
|
4
4
|
*
|
|
5
|
+
* ── loadModule('path') ────────────────────────────────────────────────────────
|
|
5
6
|
* - web (browser): replaced with `import('./path')` — rspack treats this as
|
|
6
7
|
* a true dynamic import and splits the module into a separate chunk.
|
|
7
|
-
*
|
|
8
8
|
* - node (SSR): replaced with `Promise.resolve(require('./path'))` —
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* bundled statically, wrapped in Promise.resolve to keep the API shape.
|
|
10
|
+
*
|
|
11
|
+
* ── useServerData(key, fn) ───────────────────────────────────────────────────
|
|
12
|
+
* - web (browser): the second argument `fn` is replaced with `()=>undefined`.
|
|
13
|
+
* `fn` is a server-only callback that may reference internal endpoints,
|
|
14
|
+
* credentials, or other sensitive information. It is never called in the
|
|
15
|
+
* browser (the hook returns the SSR-cached value immediately), but without
|
|
16
|
+
* this transform it would still be compiled into the client bundle — exposing
|
|
17
|
+
* those details to anyone who inspects the JS. Stripping it at bundle time
|
|
18
|
+
* prevents the leak entirely.
|
|
19
|
+
* - node (SSR): kept as-is — the real fn is needed to fetch data.
|
|
12
20
|
*
|
|
13
21
|
* Transformation strategy:
|
|
14
22
|
* Primary — SWC AST parsing via @swc/core. Handles any valid TS/JS syntax
|
|
15
23
|
* including arbitrarily-nested generics, comments, and string
|
|
16
|
-
* literals that contain the
|
|
17
|
-
* Fallback —
|
|
24
|
+
* literals that contain the function names.
|
|
25
|
+
* Fallback — Scanner-based transform used when @swc/core is unavailable.
|
|
18
26
|
*
|
|
19
|
-
* Example
|
|
27
|
+
* Example:
|
|
20
28
|
*
|
|
21
|
-
*
|
|
29
|
+
* // Source (shared component):
|
|
30
|
+
* const user = useServerData('user', () => db.getUser(req.userId));
|
|
22
31
|
*
|
|
23
|
-
* //
|
|
24
|
-
* const
|
|
32
|
+
* // Client bundle after transform:
|
|
33
|
+
* const user = useServerData('user', ()=>undefined);
|
|
25
34
|
*
|
|
26
|
-
* //
|
|
27
|
-
* const
|
|
35
|
+
* // Server bundle (unchanged):
|
|
36
|
+
* const user = useServerData('user', () => db.getUser(req.userId));
|
|
28
37
|
*/
|
|
29
38
|
|
|
30
39
|
export default function loader(this: any, source: string): string {
|
|
31
|
-
|
|
40
|
+
// Prefer the explicit `server` option injected by rspack.ts over the legacy
|
|
41
|
+
// `this.target` heuristic (which is unreliable when `target` is not set in
|
|
42
|
+
// the rspack config — rspack then reports 'web' for every build).
|
|
43
|
+
const opts = this.getOptions?.() ?? {};
|
|
44
|
+
const isServer: boolean = (typeof opts.server === 'boolean')
|
|
45
|
+
? opts.server
|
|
46
|
+
: (this.target === 'node' || this.target === 'async-node');
|
|
32
47
|
const resourcePath: string = this.resourcePath ?? this.resource ?? '(unknown)';
|
|
33
48
|
|
|
34
49
|
let swc: any;
|
|
@@ -80,7 +95,26 @@ function swcTransform(this: any, swc: any, source: string, isServer: boolean, re
|
|
|
80
95
|
if (node.type !== 'CallExpression') return;
|
|
81
96
|
|
|
82
97
|
const callee = node.callee;
|
|
83
|
-
if (!callee || callee.type !== 'Identifier'
|
|
98
|
+
if (!callee || callee.type !== 'Identifier') return;
|
|
99
|
+
|
|
100
|
+
const name: string = callee.value;
|
|
101
|
+
|
|
102
|
+
// ── useServerData(key, fn) — strip fn on client builds ────────────────
|
|
103
|
+
if (!isServer && name === 'useServerData') {
|
|
104
|
+
const args: any[] = node.arguments;
|
|
105
|
+
if (!args || args.length < 2) return;
|
|
106
|
+
const fnArg = args[1].expression ?? args[1];
|
|
107
|
+
// Normalise to 0-based local byte offsets and replace with stub.
|
|
108
|
+
replacements.push({
|
|
109
|
+
start: fnArg.span.start - fileOffset,
|
|
110
|
+
end: fnArg.span.end - fileOffset,
|
|
111
|
+
replacement: '()=>undefined',
|
|
112
|
+
});
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ── loadModule(path) ─────────────────────────────────────────────────
|
|
117
|
+
if (name !== 'loadModule') return;
|
|
84
118
|
|
|
85
119
|
const args: any[] = node.arguments;
|
|
86
120
|
if (!args || args.length === 0) return;
|
|
@@ -199,13 +233,99 @@ const LOAD_MODULE_RE =
|
|
|
199
233
|
// (i.e. a dynamic / non-literal path argument).
|
|
200
234
|
const DYNAMIC_LOAD_MODULE_RE = /\bloadModule\s*(?:<(?:[^<>]|<[^<>]*>)*>\s*)?\(/g;
|
|
201
235
|
|
|
236
|
+
/**
|
|
237
|
+
* Scan forward from `pos` in `source`, skipping over a balanced JS expression
|
|
238
|
+
* (handles nested parens/brackets/braces and string literals).
|
|
239
|
+
* Returns the index of the first character AFTER the expression
|
|
240
|
+
* (i.e. the position of the trailing `,` or `)` at depth 0).
|
|
241
|
+
*/
|
|
242
|
+
function scanExpressionEnd(source: string, pos: number): number {
|
|
243
|
+
let depth = 0;
|
|
244
|
+
let i = pos;
|
|
245
|
+
while (i < source.length) {
|
|
246
|
+
const ch = source[i]!;
|
|
247
|
+
if (ch === '(' || ch === '[' || ch === '{') { depth++; i++; continue; }
|
|
248
|
+
if (ch === ')' || ch === ']' || ch === '}') {
|
|
249
|
+
if (depth === 0) break; // end of expression — closing delimiter of outer call
|
|
250
|
+
depth--; i++; continue;
|
|
251
|
+
}
|
|
252
|
+
if (ch === ',' && depth === 0) break; // end of expression — next argument
|
|
253
|
+
// String / template literals
|
|
254
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
255
|
+
const q = ch; i++;
|
|
256
|
+
while (i < source.length && source[i] !== q) {
|
|
257
|
+
if (source[i] === '\\') i++; // escape sequence
|
|
258
|
+
i++;
|
|
259
|
+
}
|
|
260
|
+
i++; // closing quote
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
// Line comment
|
|
264
|
+
if (ch === '/' && source[i + 1] === '/') {
|
|
265
|
+
while (i < source.length && source[i] !== '\n') i++;
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
// Block comment
|
|
269
|
+
if (ch === '/' && source[i + 1] === '*') {
|
|
270
|
+
i += 2;
|
|
271
|
+
while (i + 1 < source.length && !(source[i] === '*' && source[i + 1] === '/')) i++;
|
|
272
|
+
i += 2;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
i++;
|
|
276
|
+
}
|
|
277
|
+
return i;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Strip the `fn` argument from `useServerData(key, fn)` calls in client builds.
|
|
282
|
+
* Uses a character-level scanner to handle arbitrary fn expressions (arrow
|
|
283
|
+
* functions with nested calls, async functions, object literals, etc.).
|
|
284
|
+
*/
|
|
285
|
+
function stripUseServerDataFns(source: string): string {
|
|
286
|
+
// Match `useServerData` + optional generic + opening paren
|
|
287
|
+
const CALL_RE = /\buseServerData\s*(?:<(?:[^<>]|<[^<>]*>)*>\s*)?\(/g;
|
|
288
|
+
let result = '';
|
|
289
|
+
let lastIndex = 0;
|
|
290
|
+
let match: RegExpExecArray | null;
|
|
291
|
+
CALL_RE.lastIndex = 0;
|
|
292
|
+
while ((match = CALL_RE.exec(source)) !== null) {
|
|
293
|
+
const callStart = match.index;
|
|
294
|
+
let i = match.index + match[0].length;
|
|
295
|
+
// Skip whitespace before first arg
|
|
296
|
+
while (i < source.length && /\s/.test(source[i]!)) i++;
|
|
297
|
+
// Skip first argument (key: string or array)
|
|
298
|
+
i = scanExpressionEnd(source, i);
|
|
299
|
+
// Expect comma separator
|
|
300
|
+
if (i >= source.length || source[i] !== ',') continue;
|
|
301
|
+
i++; // skip comma
|
|
302
|
+
// Skip whitespace before fn
|
|
303
|
+
while (i < source.length && /\s/.test(source[i]!)) i++;
|
|
304
|
+
const fnStart = i;
|
|
305
|
+
// Scan to end of fn argument
|
|
306
|
+
const fnEnd = scanExpressionEnd(source, i);
|
|
307
|
+
if (fnEnd <= fnStart) continue;
|
|
308
|
+
// Emit everything up to fn, then the stub, skip the original fn
|
|
309
|
+
result += source.slice(lastIndex, fnStart) + '()=>undefined';
|
|
310
|
+
lastIndex = fnEnd;
|
|
311
|
+
// Advance regex past this call to avoid re-matching
|
|
312
|
+
CALL_RE.lastIndex = fnEnd;
|
|
313
|
+
}
|
|
314
|
+
return lastIndex === 0 ? source : result + source.slice(lastIndex);
|
|
315
|
+
}
|
|
316
|
+
|
|
202
317
|
function regexTransform(this: any, source: string, isServer: boolean, resourcePath: string): string {
|
|
203
|
-
|
|
318
|
+
let transformed = source.replace(LOAD_MODULE_RE, (_match, quote, modulePath) =>
|
|
204
319
|
isServer
|
|
205
320
|
? `Promise.resolve(require(${quote}${modulePath}${quote}))`
|
|
206
321
|
: `import(${quote}${modulePath}${quote})`
|
|
207
322
|
);
|
|
208
323
|
|
|
324
|
+
// Strip server-only fn arguments from useServerData on client builds.
|
|
325
|
+
if (!isServer) {
|
|
326
|
+
transformed = stripUseServerDataFns(transformed);
|
|
327
|
+
}
|
|
328
|
+
|
|
209
329
|
// Warn for any remaining dynamic calls
|
|
210
330
|
let match: RegExpExecArray | null;
|
|
211
331
|
DYNAMIC_LOAD_MODULE_RE.lastIndex = 0;
|
package/src/utils/rspack.ts
CHANGED
|
@@ -59,6 +59,7 @@ const getConfigBase = (mode: "development" | "production", isServerBuild = false
|
|
|
59
59
|
// Runs before swc-loader (loaders execute right-to-left).
|
|
60
60
|
{
|
|
61
61
|
loader: loaderPath,
|
|
62
|
+
options: { server: isServerBuild },
|
|
62
63
|
},
|
|
63
64
|
{
|
|
64
65
|
loader: 'builtin:swc-loader',
|
|
@@ -90,6 +91,7 @@ const getConfigBase = (mode: "development" | "production", isServerBuild = false
|
|
|
90
91
|
use: [
|
|
91
92
|
{
|
|
92
93
|
loader: loaderPath,
|
|
94
|
+
options: { server: isServerBuild },
|
|
93
95
|
},
|
|
94
96
|
{
|
|
95
97
|
loader: 'builtin:swc-loader',
|