350 lines
12 KiB
JavaScript
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, '\\$&');
|
|
}
|