Files
QZMusic-Web/src/plugins/nodePolyfills.ts

523 lines
21 KiB
TypeScript
Raw Normal View History

// 浏览器端 Node.js 内置模块 polyfill
// 用于加载 webpack/ncc 打包的官方音源插件
// 提供 http/https (基于 fetch)、stream、util、events、crypto、url、zlib、path、fs、buffer 等
function createEventEmitter(): any {
const listeners: Record<string, Function[]> = {};
const self: any = {
on(event: string, fn: Function) {
(listeners[event] = listeners[event] || []).push(fn);
return self;
},
once(event: string, fn: Function) {
const wrapper = (...args: any[]) => {
self.off(event, wrapper);
fn(...args);
};
return self.on(event, wrapper);
},
off(event: string, fn: Function) {
if (listeners[event]) {
listeners[event] = listeners[event].filter((f: Function) => f !== fn);
}
return self;
},
emit(event: string, ...args: any[]) {
const fns = listeners[event] || [];
for (const fn of fns) {
try { fn.apply(null, args); } catch (_) {}
}
return true;
},
addListener: function() { return self.on.apply(self, arguments as any); },
removeListener: function() { return self.off.apply(self, arguments as any); },
removeAllListeners(event?: string) {
if (event) delete listeners[event];
else { for (const k in listeners) delete listeners[k]; }
return self;
},
listeners(event: string) { return listeners[event] || []; },
};
return self;
}
function createStreamBase(): any {
function Stream() {}
Stream.prototype.pipe = function(dest: any) { return dest; };
Stream.prototype.on = function(_e: string, _fn: Function) { return this; };
Stream.prototype.emit = function() { return true; };
return Stream;
}
function createReadable(): any {
function Readable() {}
Readable.prototype.pipe = function(dest: any) { return dest; };
Readable.prototype.on = function() { return this; };
Readable.prototype.once = function() { return this; };
Readable.prototype.emit = function() { return true; };
Readable.prototype.read = function() { return null; };
Readable.prototype.pause = function() { return this; };
Readable.prototype.resume = function() { return this; };
Readable.prototype.push = function() { return this; };
return Readable;
}
function createWritable(): any {
function Writable() {}
Writable.prototype.write = function() { return true; };
Writable.prototype.end = function() { return this; };
Writable.prototype.on = function() { return this; };
Writable.prototype.once = function() { return this; };
Writable.prototype.emit = function() { return true; };
return Writable;
}
function createTransform(): any {
function Transform() {}
Transform.prototype.pipe = function(dest: any) { return dest; };
Transform.prototype.write = function() { return true; };
Transform.prototype.end = function() { return this; };
Transform.prototype.on = function() { return this; };
Transform.prototype.once = function() { return this; };
Transform.prototype.emit = function() { return true; };
return Transform;
}
function createPassThrough(): any {
function PassThrough() {}
PassThrough.prototype.pipe = function(dest: any) { return dest; };
PassThrough.prototype.write = function() { return true; };
PassThrough.prototype.end = function() { return this; };
PassThrough.prototype.on = function() { return this; };
PassThrough.prototype.once = function() { return this; };
PassThrough.prototype.emit = function() { return true; };
return PassThrough;
}
function parseUrl(u: string): any {
try {
const url = new URL(u, typeof window !== 'undefined' ? window.location.href : 'http://localhost/');
return {
protocol: url.protocol,
hostname: url.hostname,
host: url.host,
port: url.port || (url.protocol === 'https:' ? '443' : '80'),
pathname: url.pathname,
path: url.pathname + url.search,
search: url.search,
query: Object.fromEntries(url.searchParams.entries()),
hash: url.hash,
href: url.href,
auth: url.username ? url.username + (url.password ? ':' + url.password : '') : '',
};
} catch {
return { protocol: '', hostname: '', host: '', pathname: '', path: '', href: u, port: '80' };
}
}
// http/https 模块 - 用 fetch 实现
function createHttpModule(isHttps: boolean): any {
function request(options: any, callback?: (res: any) => void): any {
let url: string;
let opts: any = typeof options === 'string' ? {} : { ...options };
if (typeof options === 'string') {
url = options;
} else if (options && typeof options === 'object') {
const protocol = opts.protocol || (isHttps ? 'https:' : 'http:');
const host = opts.hostname || opts.host || 'localhost';
const port = opts.port ? ':' + opts.port : '';
const path = opts.path || opts.pathname || '/';
url = protocol + '//' + host + port + path;
} else {
url = String(options);
}
const method = opts.method || 'GET';
const headers = opts.headers || {};
const req: any = createEventEmitter();
let requestBodyChunks: Uint8Array[] = [];
let ended = false;
req.write = function(chunk: any) {
if (chunk) requestBodyChunks.push(typeof chunk === 'string' ? new TextEncoder().encode(chunk) : chunk);
return true;
};
req.end = function(chunk: any) {
if (chunk) req.write(chunk);
if (ended) return;
ended = true;
let body: any = undefined;
if (method !== 'GET' && method !== 'HEAD' && requestBodyChunks.length > 0) {
if (requestBodyChunks.length === 1) body = requestBodyChunks[0];
else {
const total = requestBodyChunks.reduce((a, b) => a + b.length, 0);
const merged = new Uint8Array(total);
let offset = 0;
for (const c of requestBodyChunks) { merged.set(c, offset); offset += c.length; }
body = merged;
}
}
fetch(url, {
method,
headers,
body,
signal: opts.signal,
mode: 'cors',
credentials: 'omit',
} as any).then((resp: any) => {
const res: any = createEventEmitter();
res.statusCode = resp.status;
res.statusMessage = resp.statusText;
res.headers = {};
try {
resp.headers.forEach((v: string, k: string) => { res.headers[k.toLowerCase()] = v; });
} catch {}
res.setEncoding = function() { return res; };
res.pipe = function(dest: any) {
resp.arrayBuffer().then((buf: ArrayBuffer) => {
if (dest && typeof dest.write === 'function') dest.write(new Uint8Array(buf));
if (dest && typeof dest.end === 'function') dest.end();
res.emit('end');
}).catch((err: Error) => res.emit('error', err));
return dest;
};
res.text = () => resp.text();
res.json = () => resp.json();
res.body = resp.body;
if (callback) callback(res);
req.emit('response', res);
// 如果没有 callback等待 body 然后发射 data/end 事件
if (!callback) {
resp.arrayBuffer().then((buf: ArrayBuffer) => {
res.emit('data', new Uint8Array(buf));
res.emit('end');
}).catch((err: Error) => res.emit('error', err));
}
}).catch((err: Error) => {
req.emit('error', err);
});
return req;
};
req.abort = function() { req.emit('abort'); };
req.setTimeout = function() { return req; };
req.setHeader = function(name: string, value: string) { (headers as any)[name.toLowerCase()] = value; return req; };
req.getHeader = function(name: string) { return (headers as any)[name.toLowerCase()]; };
req.removeHeader = function(name: string) { delete (headers as any)[name.toLowerCase()]; };
// 如果用户立即调用 req.end(),我们需要在 next tick 开始 - 但实际上用户必须自己调用 end
return req;
}
function get(options: any, callback?: (res: any) => void): any {
const req = request(options, callback);
req.end();
return req;
}
return {
request,
get,
createServer: () => ({ listen: () => {}, close: () => {} }),
Agent: function() { this.maxSockets = 50; },
globalAgent: { maxSockets: 50 },
STATUS_CODES: {},
};
}
// util 模块
const utilModule: any = {
inspect: (obj: any) => {
try { return JSON.stringify(obj); } catch { return String(obj); }
},
format: function() { return Array.prototype.slice.call(arguments).join(' '); },
inherits: function(ctor: any, superCtor: any) {
if (!superCtor || !superCtor.prototype) return;
ctor.super_ = superCtor;
const origProto = ctor.prototype;
ctor.prototype = Object.create(superCtor.prototype, {
constructor: { value: ctor, enumerable: false, writable: true, configurable: true },
});
// 保留原型上原有的属性
if (origProto) {
for (const k in origProto) {
if (Object.prototype.hasOwnProperty.call(origProto, k)) {
ctor.prototype[k] = origProto[k];
}
}
}
},
promisify: function(fn: any) {
return function() {
const args = Array.prototype.slice.call(arguments);
return new Promise((resolve, reject) => {
args.push((err: any, ...rest: any[]) => {
if (err) reject(err);
else resolve(rest.length === 1 ? rest[0] : rest);
});
try { fn.apply(null, args); } catch (e) { reject(e); }
});
};
},
callbackify: function(fn: any) {
return function() {
const args = Array.prototype.slice.call(arguments);
const cb = args.pop();
fn.apply(null, args).then((r: any) => cb(null, r)).catch((e: Error) => cb(e));
};
},
TextDecoder: typeof (globalThis as any).TextDecoder !== 'undefined' ? (globalThis as any).TextDecoder : function(this: any) { this.decode = function() { return ''; }; },
TextEncoder: typeof (globalThis as any).TextEncoder !== 'undefined' ? (globalThis as any).TextEncoder : function(this: any) { this.encode = function(s: string) { return s; }; },
types: { isDate: (x: any) => x instanceof Date, isRegExp: (x: any) => x instanceof RegExp, isError: (x: any) => x instanceof Error, isArray: Array.isArray, isFunction: (x: any) => typeof x === 'function' },
};
// crypto 模块
const cryptoModule: any = {
createHash: function(algo: string) {
const chunks: string[] = [];
return {
update: function(chunk: any) { chunks.push(String(chunk)); return this; },
digest: function(encoding?: string) {
// 简化:不做真正的哈希,返回一个稳定的伪随机值
const s = algo + ':' + chunks.join('');
let h = 0;
for (let i = 0; i < s.length; i++) h = ((h << 5) - h + s.charCodeAt(i)) | 0;
const hex = Math.abs(h).toString(16).padStart(16, '0') + Math.abs(h * 31).toString(16).padStart(16, '0');
if (encoding === 'hex') return hex;
if (encoding === 'base64') return btoa(hex);
return hex;
},
};
},
createHmac: function(algo: string, key: any) {
const chunks: string[] = [];
return {
update: function(chunk: any) { chunks.push(String(chunk)); return this; },
digest: function(encoding?: string) {
const s = 'hmac:' + algo + ':' + key + ':' + chunks.join('');
let h = 0;
for (let i = 0; i < s.length; i++) h = ((h << 5) - h + s.charCodeAt(i)) | 0;
const hex = Math.abs(h).toString(16).padStart(16, '0') + Math.abs(h * 17).toString(16).padStart(16, '0');
if (encoding === 'hex') return hex;
if (encoding === 'base64') return btoa(hex);
return hex;
},
};
},
randomBytes: function(n: number, cb?: (err: Error | null, buf: any) => void) {
const arr = new Uint8Array(n);
if (typeof (globalThis as any).crypto !== 'undefined' && (globalThis as any).crypto.getRandomValues) {
(globalThis as any).crypto.getRandomValues(arr);
} else {
for (let i = 0; i < n; i++) arr[i] = Math.floor(Math.random() * 256);
}
const buf: any = {
buffer: arr,
length: n,
toString: function(enc?: string) {
if (enc === 'hex') {
let s = '';
for (let i = 0; i < n; i++) s += arr[i].toString(16).padStart(2, '0');
return s;
}
if (enc === 'base64') {
let s = '';
for (let i = 0; i < n; i++) s += String.fromCharCode(arr[i]);
return btoa(s);
}
return String.fromCharCode.apply(null, arr as any);
},
};
if (cb) { setTimeout(() => cb(null, buf), 0); return undefined; }
return buf;
},
randomUUID: function() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
},
};
// 完整的 polyfill 注册表
export function createNodePolyfills(): Record<string, any> {
const streamMod: any = {
Stream: createStreamBase(),
Readable: createReadable(),
Writable: createWritable(),
Duplex: createTransform(),
Transform: createTransform(),
PassThrough: createPassThrough(),
pipeline: (_a: any, b: any, cb?: () => void) => { if (cb) cb(); return b; },
finished: (_s: any, cb: () => void) => { cb(); },
};
streamMod.Stream.prototype = Object.create(createEventEmitter());
streamMod.Readable.prototype = Object.create(streamMod.Stream.prototype);
streamMod.Writable.prototype = Object.create(streamMod.Stream.prototype);
streamMod.Duplex.prototype = Object.create(streamMod.Stream.prototype);
streamMod.Transform.prototype = Object.create(streamMod.Duplex.prototype);
streamMod.PassThrough.prototype = Object.create(streamMod.Transform.prototype);
const bufferModule: any = {
from: (x: any) => {
if (x instanceof Uint8Array) return x;
if (typeof x === 'string') return new TextEncoder().encode(x);
if (Array.isArray(x)) return new Uint8Array(x);
return new Uint8Array(x || 0);
},
alloc: (n: number) => new Uint8Array(n),
allocUnsafe: (n: number) => new Uint8Array(n),
isBuffer: () => false,
concat: (arrs: any[]) => {
const total = arrs.reduce((a, b) => a + (b?.length || 0), 0);
const merged = new Uint8Array(total);
let offset = 0;
for (const c of arrs) { if (c) { merged.set(c, offset); offset += c.length; } }
return merged;
},
byteLength: (x: any) => typeof x === 'string' ? new TextEncoder().encode(x).length : (x?.length || 0),
compare: (_a: any, _b: any) => 0,
isEncoding: () => true,
};
return {
http: createHttpModule(false),
https: createHttpModule(true),
http2: createHttpModule(true),
stream: streamMod,
util: utilModule,
events: { EventEmitter: function() { return createEventEmitter(); } },
crypto: cryptoModule,
url: {
parse: parseUrl,
format: (o: any) => o.href || `${o.protocol}//${o.host}${o.path || o.pathname || ''}`,
resolve: (_from: string, to: string) => { try { return new URL(to, _from).href; } catch { return to; } },
URL: typeof (globalThis as any).URL !== 'undefined' ? (globalThis as any).URL : function() {},
URLSearchParams: typeof (globalThis as any).URLSearchParams !== 'undefined' ? (globalThis as any).URLSearchParams : function() {},
},
zlib: {
gunzipSync: (d: any) => d,
inflateSync: (d: any) => d,
deflateSync: (d: any) => d,
gzipSync: (d: any) => d,
gunzip: (d: any, cb: (err: Error | null, out: any) => void) => { setTimeout(() => cb(null, d), 0); },
inflate: (d: any, cb: (err: Error | null, out: any) => void) => { setTimeout(() => cb(null, d), 0); },
createGunzip: () => { const p = Object.create(streamMod.Transform.prototype); p.write = () => true; p.end = function() { this.emit && this.emit('end'); return this; }; return p; },
createInflate: () => { const p = Object.create(streamMod.Transform.prototype); p.write = () => true; p.end = function() { this.emit && this.emit('end'); return this; }; return p; },
createGzip: () => { const p = Object.create(streamMod.Transform.prototype); p.write = () => true; p.end = function() { this.emit && this.emit('end'); return this; }; return p; },
},
path: {
join: function() { return Array.prototype.slice.call(arguments).filter(Boolean).join('/').replace(/\/+/g, '/'); },
resolve: function() { return Array.prototype.slice.call(arguments).filter(Boolean).join('/'); },
dirname: (p: string) => String(p).split('/').slice(0, -1).join('/') || '/',
basename: (p: string, ext?: string) => { const base = String(p).split('/').pop() || ''; return ext && base.endsWith(ext) ? base.slice(0, -ext.length) : base; },
extname: (p: string) => { const m = String(p).match(/\.[^.]*$/); return m ? m[0] : ''; },
sep: '/', delimiter: ':',
parse: (p: string) => { const parts = String(p).split('/'); return { root: '/', dir: parts.slice(0, -1).join('/'), base: parts[parts.length - 1], ext: '', name: parts[parts.length - 1].replace(/\.[^.]*$/, '') }; },
normalize: (p: string) => p,
isAbsolute: (p: string) => p && p[0] === '/',
relative: (_from: string, to: string) => to,
},
fs: {
readFileSync: () => '',
writeFileSync: () => {},
existsSync: () => false,
createWriteStream: () => { const s = Object.create(streamMod.Writable.prototype); s.write = () => true; s.end = function() { this.emit && this.emit('finish'); return this; }; return s; },
createReadStream: () => { const s = Object.create(streamMod.Readable.prototype); s.pipe = (d: any) => { s.emit && s.emit('end'); return d; }; return s; },
readFile: (_path: any, enc: any, cb: (err: Error | null, data: any) => void) => { if (typeof enc === 'function') cb = enc; setTimeout(() => cb(null, ''), 0); },
writeFile: (_p: any, _d: any, cb?: (err: Error | null) => void) => { if (typeof cb === 'function') setTimeout(() => cb(null), 0); },
mkdirSync: () => {},
statSync: () => ({ isDirectory: () => false, isFile: () => true, size: 0, mtime: new Date() }),
unlinkSync: () => {},
promises: {
readFile: async () => '',
writeFile: async () => {},
mkdir: async () => {},
},
},
os: {
platform: () => 'browser',
type: () => 'Browser',
release: () => '1.0',
tmpdir: () => '/tmp',
homedir: () => '/',
EOL: '\n',
arch: () => 'x64',
cpus: () => [],
totalmem: () => 0,
freemem: () => 0,
},
assert: {
ok: function() {},
equal: function() {},
deepEqual: function() {},
strictEqual: function() {},
throws: function() {},
notEqual: function() {},
ifError: function() {},
},
dns: {
lookup: (_host: string, cb: (err: Error | null, addr: string, family: number) => void) => { setTimeout(() => cb(null, '127.0.0.1', 4), 0); },
resolve: (_host2: string, cb: (err: Error | null, addrs: string[]) => void) => { setTimeout(() => cb(null, ['127.0.0.1']), 0); },
},
buffer: bufferModule,
Buffer: bufferModule,
string_decoder: {
StringDecoder: function() {
this.write = function(x: any) { return typeof x === 'string' ? x : new TextDecoder().decode(x); };
this.end = function(x: any) { return x ? this.write(x) : ''; };
},
},
querystring: {
parse: (s: string) => {
const out: Record<string, string> = {};
const q = String(s || '').replace(/^\?/, '');
if (!q) return out;
for (const pair of q.split('&')) {
const [k, v] = pair.split('=');
if (k) out[decodeURIComponent(k)] = v !== undefined ? decodeURIComponent(v) : '';
}
return out;
},
stringify: (obj: any) => Object.entries(obj || {}).map(([k, v]) => encodeURIComponent(String(k)) + '=' + encodeURIComponent(String(v))).join('&'),
},
tty: { isatty: () => false, ReadStream: function() {}, WriteStream: function() {} },
net: {
createServer: () => ({ listen: () => {}, close: () => {}, on: function() { return this; } }),
createConnection: () => createEventEmitter(),
Socket: function() { return createEventEmitter(); },
},
tls: {
connect: () => createEventEmitter(),
TLSSocket: function() { return createEventEmitter(); },
createSecureContext: () => ({}),
},
child_process: {
spawn: () => createEventEmitter(),
exec: (_cmd: string, cb: (err: Error | null, stdout: string, stderr: string) => void) => setTimeout(() => cb(null, '', ''), 0),
execSync: () => '',
},
cluster: { isWorker: false, isMaster: true, fork: function() {}, on: function() { return this; } },
module: { Module: function() {} },
vm: { runInNewContext: (code: string, sandbox: any) => { const fn = new Function(...Object.keys(sandbox || {}), code); return fn(...Object.values(sandbox || {})); } },
perf_hooks: { performance: typeof (globalThis as any).performance !== 'undefined' ? (globalThis as any).performance : { now: () => Date.now() } },
console,
};
}
// 为给定的 webpack bundle 代码提供完整的沙箱环境
export function createSandboxRequire(): (id: string) => any {
const polyfills = createNodePolyfills();
return function(id: string): any {
if (polyfills[id]) return polyfills[id];
// 处理子路径,如 stream/transform, crypto/hash 等
const base = id.split('/')[0];
if (polyfills[base]) return polyfills[base];
return {};
};
}