Files
2026-07-13 13:18:05 +08:00

350 lines
12 KiB
JavaScript

import { existsSync, statSync } from 'node:fs';
import { cp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
import { createRequire } from 'node:module';
import { basename, dirname, extname, isAbsolute, relative, resolve } from 'node:path';
import { pathToFileURL } from 'node:url';
import { rollup } from '@internal/utils/dts-rollup.js';
import {
readJsonFile,
readPackageJson,
workspaceRoot,
listFiles,
resolvePackagePath,
resolvePackageRoot,
resolveWorkspacePath,
runCommandOrExit,
toPosixPath,
} from './package-utils.mjs';
const packageRoot = resolvePackageRoot();
const packageJson = readPackageJson(packageRoot);
const packageName = packageJson.name ?? packageRoot;
const packageRequire = createRequire(resolve(packageRoot, 'package.json'));
const { build } = await importPackageDependency('esbuild');
const srcEntries = [resolvePackagePath(packageRoot, 'src/index.ts', 'source entry')];
const runtimeDir = resolvePackagePath(packageRoot, 'dist', 'runtime output');
const declarationOutput = resolvePackagePath(packageRoot, 'dist/index.d.ts', 'declaration output');
const declarationDir = dirname(declarationOutput);
const tempTypesDir = resolve(packageRoot, 'build');
const egsPackagesRoot = resolveWorkspacePath('external/egs-core/packages', 'EGS packages root');
const workerBundles = [
createWorkerBundle({
entry: 'external/egs-core/packages/loaders/splat-loader/worker.ts',
entryLabel: 'splat worker entry',
fileName: 'splat-worker.js',
factorySourceFilter: /loaders[\\/]splat-loader[\\/]index\.ts$/,
factoryName: 'WorkerFactor',
blobUrlName: 'SplatWorkerBlobUrl',
nextStatement: 'const poll =',
}),
createWorkerBundle({
entry: 'external/egs-core/packages/loaders/texture-loader/ktx2/basis/worker/transcoder.worker.ts',
entryLabel: 'texture transcoder worker entry',
fileName: 'transcoder-worker.js',
factorySourceFilter: /loaders[\\/]texture-loader[\\/]ktx2[\\/]basis[\\/]worker[\\/]pool\.ts$/,
factoryName: 'WorkerFactor',
blobUrlName: 'TranscoderWorkerBlobUrl',
nextStatement: 'const pool =',
}),
createWorkerBundle({
entry: 'external/egs-core/packages/utils/splat-utils/worker.ts',
entryLabel: 'splat sort worker entry',
fileName: 'splat-sort-worker.js',
factorySourceFilter: /utils[\\/]splat-utils[\\/]sort\.ts$/,
factoryName: 'WorkerFactor',
blobUrlName: 'SplatSortWorkerBlobUrl',
nextStatement: 'const poll =',
}),
];
const declareOnlyClasses = [
{ name: 'Viewer', exported: 'export declare class Viewer', unexported: 'declare class Viewer' },
{ name: 'Viewport', exported: 'export declare class Viewport', unexported: 'declare class Viewport' },
{
name: 'Material',
exported: 'export declare abstract class Material',
unexported: 'declare abstract class Material',
},
{ name: 'Light', exported: 'export declare abstract class Light', unexported: 'declare abstract class Light' },
];
await mkdir(runtimeDir, { recursive: true });
await mkdir(declarationDir, { recursive: true });
await rm(tempTypesDir, { recursive: true, force: true });
await mkdir(tempTypesDir, { recursive: true });
await bundleRuntime();
emitPackageDeclarations();
await bundleDeclarations();
await stripPrivateTypingsImports();
await rm(tempTypesDir, { recursive: true, force: true });
console.log(`[package-build] Built ${packageName}.`);
async function importPackageDependency(name) {
try {
const resolvedPath = packageRequire.resolve(name);
return await import(pathToFileURL(resolvedPath).href);
} catch (error) {
throw new Error(`Unable to load ${name} from ${packageName}. Is it listed in that package's dependencies?`, {
cause: error,
});
}
}
async function bundleRuntime() {
const shared = {
absWorkingDir: packageRoot,
bundle: true,
format: 'esm',
logLevel: 'info',
minify: false,
platform: 'browser',
sourcemap: true,
target: 'es2020',
treeShaking: true,
loader: {
'.jpg': 'dataurl',
'.png': 'dataurl',
},
plugins: [workerBundlePatchPlugin(workerBundles), textureLoaderPatchPlugin(), dracoLoaderPatchPlugin()],
};
await build({
...shared,
entryPoints: srcEntries,
outdir: runtimeDir,
});
for (const workerBundle of workerBundles) {
await build({
...shared,
entryPoints: [workerBundle.entry],
outfile: resolve(runtimeDir, workerBundle.fileName),
});
}
}
function emitPackageDeclarations() {
runCommandOrExit(
process.execPath,
[
packageRequire.resolve('typescript/bin/tsc'),
'--emitDeclarationOnly',
'--declarationMap',
'false',
'--outDir',
toPosixPath(tempTypesDir),
],
{
cwd: packageRoot,
label: 'tsc --emitDeclarationOnly',
},
);
}
async function bundleDeclarations() {
rollup(packageRoot, {
typeOnlyExports: [
{ name: 'Viewer', type: 'class', abstract: false },
{ name: 'Viewport', type: 'class', abstract: false },
{ name: 'Material', type: 'class', abstract: true },
{ name: 'Light', type: 'class', abstract: true },
],
extractorConfig: {
bundledPackages: [
'@qunhe/egs',
'@qunhe/egs-animation',
'@qunhe/egs-texture-loader',
'@qunhe/egs-gltf-loader',
'@qunhe/egs-draco-loader',
'@qunhe/egs-splat-loader',
'@qunhe/egs-splat-utils',
'@qunhe/egs-lib',
],
},
});
await cp(resolve(packageRoot, 'build/index.d.ts'), declarationOutput, { force: true });
}
async function stripPrivateTypingsImports() {
const source = await readFile(declarationOutput, 'utf8');
const rewritten = source.replace(/^\s*import\s+type\s+\{\s*\}\s+from\s+["']@qunhe\/egs-typings["'];?\r?\n?/gm, '');
if (rewritten !== source) {
await writeFile(declarationOutput, rewritten);
}
}
function getPackageJsonPaths(dir) {
return listFiles(dir, { skipDirectories: ['build', 'dist', 'node_modules'] }).filter(
filePath => basename(filePath) === 'package.json',
);
}
function isInsidePackage(filePath) {
const relativePath = relative(packageRoot, filePath);
return relativePath === '' || (!!relativePath && !relativePath.startsWith('..') && !isAbsolute(relativePath));
}
function getInheritedConfigPaths(configPaths, fallbackPaths) {
const paths = Array.isArray(configPaths) && configPaths.length > 0 ? configPaths : fallbackPaths;
return paths.map(configPath => toRelativeConfigPath(relative(tempTypesDir, resolve(packageRoot, configPath))));
}
function toRelativeConfigPath(filePath) {
const posixPath = toPosixPath(filePath) || '.';
return posixPath.startsWith('.') ? posixPath : `./${posixPath}`;
}
function replaceExtension(filePath, replacementExtension) {
const currentExtension = extname(filePath);
return currentExtension
? `${filePath.slice(0, -currentExtension.length)}${replacementExtension}`
: `${filePath}${replacementExtension}`;
}
function sanitizePathSegment(value) {
return value
.replace(/^@/, '')
.replace(/[\\/]/g, '-')
.replace(/[^a-zA-Z0-9._-]/g, '-');
}
function splitNamedSpecifiers(specifierText) {
return specifierText
.split(',')
.map(specifier => specifier.trim())
.filter(Boolean);
}
function getNamedSpecifierName(specifier) {
return removeTypeModifier(specifier)
.split(/\s+as\s+/u)[0]
.trim();
}
function removeTypeModifier(specifier) {
return specifier.replace(/^type\s+/u, '').trim();
}
function createWorkerBundle(config) {
return {
...config,
entry: resolveWorkspacePath(config.entry, config.entryLabel),
};
}
function workerBundlePatchPlugin(workerBundles) {
return {
name: 'aholo-worker-bundle-patch',
setup(buildContext) {
for (const workerBundle of workerBundles) {
buildContext.onLoad({ filter: workerBundle.factorySourceFilter }, async args => {
const source = await readFile(args.path, 'utf8');
const contents = replaceWorkerFactory(source, workerBundle);
if (contents === source) {
throw new Error(`Unable to patch ${workerBundle.entryLabel}.`);
}
return {
contents,
loader: 'ts',
};
});
}
},
};
}
function replaceWorkerFactory(source, workerBundle) {
const factoryPattern = createWorkerFactoryPattern(workerBundle.factoryName, workerBundle.nextStatement);
return source.replace(factoryPattern, createWorkerFactoryReplacement(workerBundle));
}
function createWorkerFactoryPattern(factoryName, nextStatement) {
return new RegExp(
`let ${escapeRegExp(factoryName)}: \\(\\) => Worker;\\s*try \\{[\\s\\S]*?\\};?\\s*${escapeRegExp(nextStatement)}`,
);
}
function createWorkerFactoryReplacement({ factoryName, blobUrlName, fileName, nextStatement }) {
return `let ${factoryName}: () => Worker;
let ${blobUrlName}: string | undefined;
${factoryName} = () => {
const workerUrl = new URL("./${fileName}", import.meta.url).href;
if (!${blobUrlName}) {
const source = \`import \${JSON.stringify(workerUrl)};\`;
${blobUrlName} = URL.createObjectURL(new Blob([source], { type: "text/javascript" }));
}
return new Worker(${blobUrlName}, { type: "module" });
};
${nextStatement}`;
}
function textureLoaderPatchPlugin() {
return {
name: 'aholo-texture-loader-patch',
setup(buildContext) {
buildContext.onLoad(
{ filter: /loaders[\\/]texture-loader[\\/]ktx2[\\/]basis[\\/]wasm[\\/]basis_transcoder\.js$/ },
async args => {
const source = await readFile(args.path, 'utf8');
const nodeBranchShim =
'var fs={readFileSync:function(){throw new Error("Node file loading is not available in the browser build.");}};';
const contents = source.replace(/var fs=require\("fs"\);/, nodeBranchShim);
if (contents === source) {
throw new Error('Unable to patch basis_transcoder.js node file-system branch.');
}
return {
contents,
loader: 'js',
};
},
);
},
};
}
function dracoLoaderPatchPlugin() {
return {
name: 'aholo-draco-loader-patch',
setup(buildContext) {
buildContext.onResolve({ filter: /draco_decoder\.wasm$/ }, args => ({
path: resolve(dirname(args.importer), 'draco_decoder.wasm.js'),
}));
buildContext.onLoad({ filter: /draco_decoder_wrapper\.js$/ }, async args => {
const source = await readFile(args.path, 'utf8');
const nodeBranchShim = `var Za={readFileSync:function(){throw new Error("Node file loading is not available in the browser build.");},readFile:function(e,b,c){c(new Error("Node file loading is not available in the browser build."));}},qa={dirname:function(){return""},normalize:function(e){return e}};`;
const contents = source.replace(/var Za=require\("fs"\),qa=require\("path"\);/, nodeBranchShim);
if (contents === source) {
throw new Error('Unable to patch draco_decoder_wrapper.js node file-system branch.');
}
return {
contents,
loader: 'js',
};
});
},
};
}
function escapeRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}