Skip to content

Commit 86ecaf9

Browse files
committed
fix: add custom serializer to provide complex data type for esm loader
1 parent 00e5176 commit 86ecaf9

20 files changed

Lines changed: 653 additions & 106 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "html-bundler-webpack-plugin",
3-
"version": "4.20.0-beta.3",
3+
"version": "4.20.0-beta.4",
44
"description": "Generates complete single-page or multi-page website from source assets. Build-in support for Markdown, Eta, EJS, Handlebars, Nunjucks, Pug. Alternative to html-webpack-plugin.",
55
"keywords": [
66
"html",

src/Common/FileSystem/ModuleLoader/child.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// This file is used in the ./loader.js
22

3-
import { serialize } from 'node:v8';
3+
import { serialize } from './serializer.js';
44

55
const moduleUrl = process.env.MODULE_URL;
66

src/Common/FileSystem/ModuleLoader/loader.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
const path = require('path');
22
const fs = require('fs');
33
const { pathToFileURL } = require('url');
4-
const { deserialize } = require('node:v8');
4+
const { deserialize } = require('./serializer');
55
const { fork } = require('node:child_process');
66

77
const childScript = path.resolve(__dirname, './child.mjs');
@@ -41,7 +41,7 @@ function loader(filePath) {
4141
reject(Object.assign(new Error(msg.error.message), msg.error));
4242
} else if (msg.buffer) {
4343
try {
44-
const result = deserialize(Buffer.from(msg.buffer));
44+
const result = deserialize(msg.buffer);
4545
resolve(result);
4646
} catch (error) {
4747
reject(error);
Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
const vm = require('vm');
2+
3+
function serialize(obj) {
4+
const seen = new WeakMap();
5+
const refs = [];
6+
let id = 0;
7+
8+
function encode(value) {
9+
if (value instanceof Error) {
10+
return {
11+
__type: 'Error',
12+
name: value.name,
13+
message: value.message,
14+
stack: value.stack,
15+
};
16+
}
17+
18+
if (typeof value === 'function') {
19+
return {
20+
__type: 'Function',
21+
source: value.toString(),
22+
};
23+
}
24+
25+
if (typeof value === 'bigint') {
26+
return {
27+
__type: 'BigInt',
28+
value: value.toString(),
29+
};
30+
}
31+
32+
if (typeof value === 'symbol') {
33+
return {
34+
__type: 'Symbol',
35+
value: value.toString(),
36+
};
37+
}
38+
39+
if (Buffer.isBuffer(value)) {
40+
return {
41+
__type: 'Buffer',
42+
data: value.toString('base64'),
43+
};
44+
}
45+
46+
if (value instanceof ArrayBuffer) {
47+
return {
48+
__type: 'ArrayBuffer',
49+
data: Array.from(new Uint8Array(value)),
50+
};
51+
}
52+
53+
// Handle TypedArrays (e.g., Uint8Array, Int32Array, etc.)
54+
if (ArrayBuffer.isView(value)) {
55+
return {
56+
__type: 'TypedArray',
57+
type: value.constructor.name,
58+
data: Array.from(value),
59+
};
60+
}
61+
62+
if (value instanceof Date) {
63+
return {
64+
__type: 'Date',
65+
value: value.toISOString(),
66+
};
67+
}
68+
69+
if (value instanceof RegExp) {
70+
return {
71+
__type: 'RegExp',
72+
source: value.source,
73+
flags: value.flags,
74+
};
75+
}
76+
77+
if (value instanceof URL) {
78+
return {
79+
__type: 'URL',
80+
href: value.href,
81+
};
82+
}
83+
84+
if (typeof value === 'object' && value !== null) {
85+
if (seen.has(value)) {
86+
return { __ref: seen.get(value) };
87+
}
88+
89+
const currentId = id++;
90+
seen.set(value, currentId);
91+
refs[currentId] = null;
92+
93+
if (Array.isArray(value)) {
94+
refs[currentId] = value.map(encode);
95+
return { __ref: currentId };
96+
}
97+
98+
if (value instanceof Map) {
99+
refs[currentId] = {
100+
__type: 'Map',
101+
entries: Array.from(value.entries()).map(([k, v]) => [encode(k), encode(v)]),
102+
};
103+
return { __ref: currentId };
104+
}
105+
106+
if (value instanceof Set) {
107+
refs[currentId] = {
108+
__type: 'Set',
109+
values: Array.from(value.values()).map(encode),
110+
};
111+
return { __ref: currentId };
112+
}
113+
114+
const constructorName = value.constructor?.name;
115+
const objData = {};
116+
for (const [key, val] of Object.entries(value)) {
117+
objData[key] = encode(val);
118+
}
119+
120+
refs[currentId] =
121+
constructorName === 'Object'
122+
? objData
123+
: {
124+
__type: 'Instance',
125+
constructor: constructorName,
126+
data: objData,
127+
};
128+
129+
return { __ref: currentId };
130+
}
131+
132+
return value;
133+
}
134+
135+
const root = encode(obj);
136+
return JSON.stringify({ root, refs });
137+
}
138+
139+
function deserialize(serialized, options = {}) {
140+
const { root, refs } = JSON.parse(serialized);
141+
const constructed = new Array(refs.length);
142+
143+
function revive(value) {
144+
if (value && typeof value === 'object') {
145+
if ('__ref' in value) {
146+
return constructed[value.__ref];
147+
}
148+
149+
//console.log('*** revive: ', { value });
150+
151+
switch (value.__type) {
152+
case 'Error': {
153+
const error = new Error(value.message);
154+
error.name = value.name;
155+
error.stack = value.stack;
156+
return error;
157+
}
158+
159+
case 'Function': {
160+
try {
161+
const script = new vm.Script(`(${value.source})`);
162+
return script.runInNewContext();
163+
} catch {
164+
return () => {
165+
throw new Error('Failed to deserialize function');
166+
};
167+
}
168+
}
169+
170+
case 'BigInt':
171+
return BigInt(value.value);
172+
173+
case 'Symbol':
174+
return Symbol(value.value.slice(7, -1));
175+
176+
case 'Buffer':
177+
return Buffer.from(value.data, 'base64');
178+
179+
case 'ArrayBuffer':
180+
const buffer = new ArrayBuffer(value.data.length);
181+
const uint8View = new Uint8Array(buffer);
182+
uint8View.set(value.data);
183+
return buffer;
184+
185+
case 'TypedArray':
186+
const TypedArrayConstructor = globalThis[value.type];
187+
return new TypedArrayConstructor(value.data);
188+
189+
case 'Date':
190+
return new Date(value.value);
191+
192+
case 'RegExp':
193+
return new RegExp(value.source, value.flags);
194+
195+
case 'URL':
196+
return new URL(value.href);
197+
198+
case 'Map':
199+
return new Map();
200+
201+
case 'Set':
202+
return new Set();
203+
204+
case 'Instance': {
205+
const obj = {};
206+
// Handle nested objects first
207+
for (const [key, val] of Object.entries(value.data)) {
208+
obj[key] = revive(val);
209+
}
210+
211+
// Try to recreate the instance from the constructor
212+
const Cls = globalThis[value.constructor];
213+
if (Cls && typeof Cls === 'function') {
214+
// Recreate instance by calling the constructor with the correct data
215+
const instance = new Cls(...Object.values(obj));
216+
Object.setPrototypeOf(instance, Cls.prototype); // Ensure the prototype chain is correct
217+
218+
console.log('*** Instance: ', { serialized, obj });
219+
220+
return instance;
221+
}
222+
223+
// If no constructor was found, return plain object
224+
Object.setPrototypeOf(obj, Object.prototype);
225+
return obj;
226+
}
227+
}
228+
}
229+
230+
return value;
231+
}
232+
233+
// Restoring references after the instances and objects have been constructed
234+
for (let i = 0; i < refs.length; i++) {
235+
const ref = refs[i];
236+
if (ref && typeof ref === 'object') {
237+
if (ref.__type === 'Map') {
238+
constructed[i] = new Map();
239+
} else if (ref.__type === 'Set') {
240+
constructed[i] = new Set();
241+
} else if (ref.__type === 'ArrayBuffer') {
242+
constructed[i] = new ArrayBuffer(ref.data.length);
243+
} else if (ref.__type === 'TypedArray') {
244+
const TypedArrayConstructor = globalThis[ref.type];
245+
constructed[i] = new TypedArrayConstructor(ref.data);
246+
} else if (ref.__type === 'URL') {
247+
constructed[i] = new URL(ref.href);
248+
} else if (!ref.__type || ref.__type === 'Object') {
249+
constructed[i] = {};
250+
} else if (ref.__type === 'Instance') {
251+
constructed[i] = {}; // Placeholder object, will be replaced later
252+
}
253+
} else {
254+
constructed[i] = ref;
255+
}
256+
}
257+
258+
// Rebuilding the actual values after references have been set
259+
for (let i = 0; i < refs.length; i++) {
260+
const ref = refs[i];
261+
const target = constructed[i];
262+
263+
if (ref && typeof ref === 'object') {
264+
if (ref.__type === 'Map') {
265+
for (const [k, v] of ref.entries) {
266+
target.set(revive(k), revive(v));
267+
}
268+
} else if (ref.__type === 'Set') {
269+
for (const v of ref.values) {
270+
target.add(revive(v));
271+
}
272+
} else if (ref.__type === 'ArrayBuffer') {
273+
target.data = revive(ref);
274+
} else if (ref.__type === 'TypedArray') {
275+
target.data = revive(ref);
276+
} else if (ref.__type === 'URL') {
277+
target.href = revive(ref);
278+
} else if (!ref.__type || ref.__type === 'Object') {
279+
for (const [key, val] of Object.entries(ref)) {
280+
target[key] = revive(val);
281+
}
282+
} else if (ref.__type === 'Instance') {
283+
for (const [key, val] of Object.entries(ref.data)) {
284+
target[key] = revive(val);
285+
}
286+
}
287+
}
288+
}
289+
290+
return revive(root);
291+
}
292+
293+
module.exports = { serialize, deserialize };

0 commit comments

Comments
 (0)