474 lines
14 KiB
JavaScript
474 lines
14 KiB
JavaScript
import { defineConfig } from 'astro/config';
|
|
import { spawn } from 'node:child_process';
|
|
import { createReadStream, existsSync } from 'node:fs';
|
|
import { copyFile, mkdir, readdir, rm, stat } from 'node:fs/promises';
|
|
import { dirname, extname, isAbsolute, relative, resolve } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import checker from 'vite-plugin-checker';
|
|
import {
|
|
assetsPrefix,
|
|
astroAssetsDir,
|
|
buildHashLength,
|
|
getManualAssetOutputPath,
|
|
manualAssetsDir,
|
|
manualAssetPath,
|
|
} from './src/utils/manual-assets.js';
|
|
|
|
const workspaceRoot = fileURLToPath(new URL('../', import.meta.url));
|
|
const manualContentRoot = fileURLToPath(new URL('./src/content/manual', import.meta.url));
|
|
const manualAssetSourceRoot = fileURLToPath(new URL('./src/content/manual/assets', import.meta.url));
|
|
const manualBuildAssetRoot = fileURLToPath(new URL(`./dist/${astroAssetsDir}/${manualAssetsDir}`, import.meta.url));
|
|
const manualDevAssetPath = manualAssetPath;
|
|
const examplesContentRoot = fileURLToPath(new URL('./src/content/examples', import.meta.url));
|
|
const rolldownOutputFileNames = {
|
|
entryFileNames: `${astroAssetsDir}/[name].[hash:${buildHashLength}].js`,
|
|
chunkFileNames: `${astroAssetsDir}/[name].[hash:${buildHashLength}].js`,
|
|
assetFileNames: `${astroAssetsDir}/[name].[hash:${buildHashLength}][extname]`,
|
|
};
|
|
const egsCoreWatchRoots = [
|
|
fileURLToPath(new URL('../external/egs-core/packages', import.meta.url)),
|
|
fileURLToPath(new URL('../external/egs-core/tools/utils', import.meta.url)),
|
|
];
|
|
const contentReloadEvents = ['add', 'change', 'unlink'];
|
|
const manualInvalidationRules = {
|
|
includes: ['/src/content/manual/', '/src/pages/[lang]/manual/'],
|
|
endsWith: ['/src/utils/manual.ts'],
|
|
};
|
|
const examplesInvalidationRules = {
|
|
includes: ['/src/content/examples/', '/src/pages/[lang]/examples/'],
|
|
endsWith: [
|
|
'/src/components/PlaygroundShell.astro',
|
|
'/src/pages/[lang]/index.astro',
|
|
'/src/pages/[lang]/playground.astro',
|
|
'/src/utils/examples.ts',
|
|
],
|
|
};
|
|
const rendererInvalidationRules = {
|
|
includes: ['/packages/renderer/dist/', '/node_modules/@manycore/aholo-viewer/', '@manycore/aholo-viewer'],
|
|
endsWith: ['/packages/renderer/dist/index.js', '/packages/renderer/dist/splat-worker.js'],
|
|
};
|
|
|
|
const isProd = process.env.NODE_ENV === 'production';
|
|
|
|
export default defineConfig({
|
|
output: 'static',
|
|
trailingSlash: 'always',
|
|
build: {
|
|
assets: astroAssetsDir,
|
|
assetsPrefix,
|
|
},
|
|
vite: {
|
|
build: {
|
|
rolldownOptions: {
|
|
output: rolldownOutputFileNames,
|
|
},
|
|
},
|
|
worker: {
|
|
rolldownOptions: {
|
|
output: rolldownOutputFileNames,
|
|
},
|
|
},
|
|
server: {
|
|
watch: {
|
|
interval: 250,
|
|
usePolling: true,
|
|
},
|
|
},
|
|
optimizeDeps: {
|
|
exclude: ['monaco-editor', '@manycore/aholo-viewer'],
|
|
},
|
|
environments: {
|
|
client: {
|
|
build: {
|
|
rolldownOptions: {
|
|
output: rolldownOutputFileNames,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
plugins: [
|
|
!isProd &&
|
|
checker({
|
|
typescript: true,
|
|
}),
|
|
manualAssetsPlugin(),
|
|
manualContentReloadPlugin(),
|
|
examplesContentReloadPlugin(),
|
|
egsCoreReloadPlugin(),
|
|
],
|
|
},
|
|
});
|
|
|
|
function manualAssetsPlugin() {
|
|
return {
|
|
name: 'aholo-manual-assets',
|
|
configureServer(server) {
|
|
server.watcher.add(manualAssetSourceRoot);
|
|
server.middlewares.use(manualDevAssetPath, async (request, response, next) => {
|
|
try {
|
|
const requestedPath = getManualAssetRequestPath(request.url);
|
|
|
|
if (!requestedPath) {
|
|
next();
|
|
return;
|
|
}
|
|
|
|
const assetPath = resolve(manualAssetSourceRoot, requestedPath);
|
|
|
|
if (!isWithinRoot(manualAssetSourceRoot, assetPath)) {
|
|
next();
|
|
return;
|
|
}
|
|
|
|
const assetStat = await stat(assetPath);
|
|
|
|
if (!assetStat.isFile()) {
|
|
next();
|
|
return;
|
|
}
|
|
|
|
response.statusCode = 200;
|
|
response.setHeader('Content-Type', getContentType(assetPath));
|
|
createReadStream(assetPath).pipe(response);
|
|
} catch {
|
|
next();
|
|
}
|
|
});
|
|
},
|
|
async closeBundle() {
|
|
if (!existsSync(manualAssetSourceRoot)) {
|
|
return;
|
|
}
|
|
|
|
await rm(manualBuildAssetRoot, { recursive: true, force: true });
|
|
await copyManualBuildAssets(manualAssetSourceRoot);
|
|
},
|
|
};
|
|
}
|
|
|
|
async function copyManualBuildAssets(root, currentDir = root) {
|
|
const entries = await readdir(currentDir, { withFileTypes: true });
|
|
|
|
for (const entry of entries) {
|
|
const sourcePath = resolve(currentDir, entry.name);
|
|
|
|
if (entry.isDirectory()) {
|
|
await copyManualBuildAssets(root, sourcePath);
|
|
continue;
|
|
}
|
|
|
|
if (!entry.isFile()) {
|
|
continue;
|
|
}
|
|
|
|
const relativeAssetPath = toPosixPath(relative(root, sourcePath));
|
|
const outputAssetPath = getManualAssetOutputPath(relativeAssetPath, sourcePath, { hash: true });
|
|
const buildAssetPath = resolve(manualBuildAssetRoot, outputAssetPath);
|
|
|
|
await mkdir(dirname(buildAssetPath), { recursive: true });
|
|
await copyFile(sourcePath, buildAssetPath);
|
|
}
|
|
}
|
|
|
|
function manualContentReloadPlugin() {
|
|
return contentReloadPlugin({
|
|
name: 'aholo-manual-content-reload',
|
|
root: manualContentRoot,
|
|
shouldReload: isManualMarkdown,
|
|
invalidate: invalidateManualModules,
|
|
notify: [sendFullReload],
|
|
});
|
|
}
|
|
|
|
function examplesContentReloadPlugin() {
|
|
return contentReloadPlugin({
|
|
name: 'aholo-examples-content-reload',
|
|
root: examplesContentRoot,
|
|
shouldReload: isExampleContent,
|
|
invalidate: invalidateExampleModules,
|
|
notify: [sendContentChanged, sendFullReload],
|
|
});
|
|
}
|
|
|
|
function egsCoreReloadPlugin() {
|
|
let rebuildTimer;
|
|
let rebuildRunning = false;
|
|
let rebuildQueued = false;
|
|
|
|
return {
|
|
name: 'aholo-egs-core-reload',
|
|
apply: 'serve',
|
|
configureServer(server) {
|
|
server.watcher.setMaxListeners(Math.max(server.watcher.getMaxListeners(), 20));
|
|
server.watcher.add(egsCoreWatchRoots.filter(root => existsSync(root)));
|
|
|
|
const scheduleRebuild = file => {
|
|
if (!isEgsCoreDevSource(file)) {
|
|
return;
|
|
}
|
|
|
|
clearTimeout(rebuildTimer);
|
|
rebuildTimer = setTimeout(() => {
|
|
rebuildTimer = undefined;
|
|
void rebuildRendererFromEgs(server);
|
|
}, 250);
|
|
};
|
|
|
|
for (const eventName of contentReloadEvents) {
|
|
server.watcher.on(eventName, scheduleRebuild);
|
|
}
|
|
},
|
|
};
|
|
|
|
async function rebuildRendererFromEgs(server) {
|
|
if (rebuildRunning) {
|
|
rebuildQueued = true;
|
|
return;
|
|
}
|
|
|
|
rebuildRunning = true;
|
|
|
|
try {
|
|
do {
|
|
rebuildQueued = false;
|
|
console.log('[egs-dev] EGS source changed. Rebuilding renderer.');
|
|
await runWorkspaceScript('.renderer:build');
|
|
} while (rebuildQueued);
|
|
|
|
invalidateRendererModules(server);
|
|
sendFullReload(server);
|
|
console.log('[egs-dev] Renderer rebuild complete. Site reloaded.');
|
|
} catch (error) {
|
|
console.error('[egs-dev] Renderer rebuild failed. Fix the error and save again to retry.');
|
|
console.error(error);
|
|
} finally {
|
|
rebuildRunning = false;
|
|
|
|
if (rebuildQueued) {
|
|
void rebuildRendererFromEgs(server);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function contentReloadPlugin({ name, root, shouldReload, invalidate, notify }) {
|
|
return {
|
|
name,
|
|
configureServer(server) {
|
|
server.watcher.setMaxListeners(Math.max(server.watcher.getMaxListeners(), 20));
|
|
server.watcher.add(root);
|
|
|
|
const reload = file => {
|
|
if (!shouldReload(file)) {
|
|
return;
|
|
}
|
|
|
|
invalidate(server);
|
|
|
|
for (const send of notify) {
|
|
send(server);
|
|
}
|
|
};
|
|
|
|
for (const eventName of contentReloadEvents) {
|
|
server.watcher.on(eventName, reload);
|
|
}
|
|
},
|
|
};
|
|
}
|
|
|
|
function invalidateManualModules(server) {
|
|
invalidateModules(server, id => matchesRule(id, manualInvalidationRules));
|
|
}
|
|
|
|
function invalidateExampleModules(server) {
|
|
invalidateModules(server, id => matchesRule(id, examplesInvalidationRules));
|
|
}
|
|
|
|
function invalidateRendererModules(server) {
|
|
invalidateModules(server, id => matchesRule(id, rendererInvalidationRules));
|
|
}
|
|
|
|
function invalidateModules(server, shouldInvalidate) {
|
|
const invalidatedModules = new Set();
|
|
const timestamp = Date.now();
|
|
|
|
for (const moduleGraph of getModuleGraphs(server)) {
|
|
for (const moduleNode of getModuleNodes(moduleGraph)) {
|
|
const id = toPosixPath(moduleNode.id ?? moduleNode.url ?? '');
|
|
|
|
if (!id || !shouldInvalidate(id) || invalidatedModules.has(moduleNode)) {
|
|
continue;
|
|
}
|
|
|
|
moduleGraph.invalidateModule(moduleNode, undefined, timestamp, true);
|
|
invalidatedModules.add(moduleNode);
|
|
}
|
|
}
|
|
}
|
|
|
|
function getModuleGraphs(server) {
|
|
const moduleGraphs = [];
|
|
const environments = server.environments ? Object.values(server.environments) : [];
|
|
|
|
for (const environment of environments) {
|
|
if (environment?.moduleGraph && !moduleGraphs.includes(environment.moduleGraph)) {
|
|
moduleGraphs.push(environment.moduleGraph);
|
|
}
|
|
}
|
|
|
|
if (server.moduleGraph && !moduleGraphs.includes(server.moduleGraph)) {
|
|
moduleGraphs.push(server.moduleGraph);
|
|
}
|
|
|
|
return moduleGraphs;
|
|
}
|
|
|
|
function getModuleNodes(moduleGraph) {
|
|
if (moduleGraph.idToModuleMap) {
|
|
return moduleGraph.idToModuleMap.values();
|
|
}
|
|
|
|
if (moduleGraph.urlToModuleMap) {
|
|
return moduleGraph.urlToModuleMap.values();
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
function matchesRule(value, rule) {
|
|
const matchesInclude = rule.includes.some(pattern => value.includes(pattern));
|
|
const matchesSuffix = rule.endsWith.some(pattern => value.endsWith(pattern));
|
|
|
|
return matchesInclude || matchesSuffix;
|
|
}
|
|
|
|
function sendContentChanged(server) {
|
|
server.environments?.ssr?.hot?.send('astro:content-changed', {});
|
|
}
|
|
|
|
function sendFullReload(server) {
|
|
const payload = { type: 'full-reload', path: '*' };
|
|
|
|
if (server.environments?.client?.hot) {
|
|
server.environments.client.hot.send(payload);
|
|
return;
|
|
}
|
|
|
|
server.ws.send(payload);
|
|
}
|
|
|
|
function isManualMarkdown(file) {
|
|
const normalized = toPosixPath(file);
|
|
|
|
return normalized.includes('/src/content/manual/') && normalized.endsWith('.md');
|
|
}
|
|
|
|
function isExampleContent(file) {
|
|
const normalized = toPosixPath(file);
|
|
|
|
return normalized.includes('/src/content/examples/') && ['.json', '.ts'].includes(extname(normalized));
|
|
}
|
|
|
|
function isEgsCoreDevSource(file) {
|
|
if (!file) {
|
|
return false;
|
|
}
|
|
|
|
const normalized = toPosixPath(file);
|
|
|
|
if (
|
|
normalized.includes('/build/') ||
|
|
normalized.includes('/dist/') ||
|
|
normalized.includes('/node_modules/') ||
|
|
!egsCoreWatchRoots.some(root => isWithinRoot(root, file))
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
return (
|
|
normalized.endsWith('.ts') ||
|
|
normalized.endsWith('.tsx') ||
|
|
normalized.endsWith('/package.json') ||
|
|
normalized.endsWith('/tsconfig.json')
|
|
);
|
|
}
|
|
|
|
function runWorkspaceScript(scriptName) {
|
|
return new Promise((resolveRun, rejectRun) => {
|
|
const child = spawn(getPnpmCommand(), ['run', scriptName], {
|
|
cwd: workspaceRoot,
|
|
shell: process.platform === 'win32',
|
|
stdio: 'inherit',
|
|
});
|
|
let settled = false;
|
|
|
|
const finish = error => {
|
|
if (settled) {
|
|
return;
|
|
}
|
|
|
|
settled = true;
|
|
|
|
if (error) {
|
|
rejectRun(error);
|
|
return;
|
|
}
|
|
|
|
resolveRun();
|
|
};
|
|
|
|
child.on('error', finish);
|
|
child.on('close', (code, signal) => {
|
|
if (code === 0) {
|
|
finish();
|
|
return;
|
|
}
|
|
|
|
finish(new Error(`pnpm run ${scriptName} failed with ${signal ?? `exit code ${code}`}.`));
|
|
});
|
|
});
|
|
}
|
|
|
|
function getPnpmCommand() {
|
|
return process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm';
|
|
}
|
|
|
|
function toPosixPath(value) {
|
|
return value.replace(/\\/g, '/');
|
|
}
|
|
|
|
function getManualAssetRequestPath(url) {
|
|
let pathname = new URL(url ?? '/', 'http://localhost').pathname;
|
|
|
|
if (pathname.startsWith(`${manualDevAssetPath}/`)) {
|
|
pathname = pathname.slice(manualDevAssetPath.length);
|
|
}
|
|
|
|
return decodeURIComponent(pathname).replace(/^\/+/, '') || undefined;
|
|
}
|
|
|
|
function isWithinRoot(root, filePath) {
|
|
const relativePath = relative(root, filePath);
|
|
|
|
return relativePath === '' || (!!relativePath && !relativePath.startsWith('..') && !isAbsolute(relativePath));
|
|
}
|
|
|
|
function getContentType(filePath) {
|
|
switch (extname(filePath).toLowerCase()) {
|
|
case '.gif':
|
|
return 'image/gif';
|
|
case '.jpg':
|
|
case '.jpeg':
|
|
return 'image/jpeg';
|
|
case '.png':
|
|
return 'image/png';
|
|
case '.svg':
|
|
return 'image/svg+xml';
|
|
case '.webp':
|
|
return 'image/webp';
|
|
default:
|
|
return 'application/octet-stream';
|
|
}
|
|
}
|