mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-10 01:09:43 +02:00
feat(preview): render XLSX spreadsheets in the file-preview overlay
xlsx files were download-only. Add a read-only, virtualized preview (sheet tabs, number formats, merges, theme colours) parsed entirely in a browser Web Worker with exceljs and fflate, loaded only when a spreadsheet is opened. The workbook is checked against ZIP-bomb, entry and cell limits before exceljs loads; cell text is written with textContent, formulas are never evaluated and nothing referenced by the workbook is fetched. On the server xlsx only joins the existing allowlist and classification, with a 10 MB cap on ?preview=true. xls and ods stay download-only.
This commit is contained in:
@@ -49,6 +49,9 @@ run('xterm-addon-serialize', 'npx esbuild node_modules/@xterm/addon-serialize/li
|
||||
run('xterm-addon-webgl', 'cp node_modules/@xterm/addon-webgl/lib/addon-webgl.js dist/web/public/vendor/xterm-addon-webgl.min.js');
|
||||
run('xterm-addon-unicode11', 'npx esbuild node_modules/@xterm/addon-unicode11/lib/addon-unicode11.js --minify --outfile=dist/web/public/vendor/xterm-addon-unicode11.min.js');
|
||||
run('xterm-zerolag-input', 'npx esbuild packages/xterm-zerolag-input/src/zerolag-input-addon.ts --bundle --minify --format=iife --global-name=XtermZerolagInput --outfile=dist/web/public/vendor/xterm-zerolag-input.js');
|
||||
// XLSX preview parser bundles: loaded only inside spreadsheet-preview-worker.js,
|
||||
// never by the page (see scripts/prepare-spreadsheet-assets.mjs).
|
||||
run('spreadsheet preview vendors', 'node scripts/prepare-spreadsheet-assets.mjs dist/web/public/vendor');
|
||||
|
||||
// Append global aliases so app.js can use `new LocalEchoOverlay(terminal)`
|
||||
appendFileSync(
|
||||
@@ -125,6 +128,7 @@ console.log('\n[build] content-hash cache busting');
|
||||
'api-client.js',
|
||||
'subagent-windows.js',
|
||||
'image-input.js',
|
||||
'spreadsheet-preview.js',
|
||||
'vendor/xterm-zerolag-input.js',
|
||||
'vendor/xterm-predictive-echo.js',
|
||||
];
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { readdirSync, readFileSync } from 'node:fs';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
||||
import { dirname, extname, join, relative, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
@@ -9,6 +10,10 @@ const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const publicRoot = resolve(repoRoot, 'src/web/public');
|
||||
const prettierBin = resolve(repoRoot, 'node_modules/.bin/prettier');
|
||||
const checkedExtensions = new Set(['.js', '.css', '.html', '.json']);
|
||||
// Combined budget for the two XLSX-preview vendor bundles (exceljs + fflate).
|
||||
// They load only inside the spreadsheet worker, but a dependency bump that
|
||||
// balloons them should be a deliberate decision, not a silent one.
|
||||
const SPREADSHEET_VENDOR_MAX_BYTES = 1_100_000;
|
||||
|
||||
function collectTextAssets(dir) {
|
||||
const files = [];
|
||||
@@ -35,6 +40,39 @@ function findNullByte(buffer) {
|
||||
const files = collectTextAssets(publicRoot);
|
||||
const failures = [];
|
||||
|
||||
// The spreadsheet worker is a stable (unhashed) URL, cache-busted by the
|
||||
// SPREADSHEET_ASSET_VERSION token in spreadsheet-preview.js. That token must be
|
||||
// the content hash of everything the worker loads, or a deploy can pair a new
|
||||
// worker with a stale cached core/vendor file (static assets are cached 1y).
|
||||
const spreadsheetWorker = join(publicRoot, 'spreadsheet-preview-worker.js');
|
||||
const spreadsheetCore = join(publicRoot, 'spreadsheet-xlsx-core.js');
|
||||
const spreadsheetEntry = join(publicRoot, 'spreadsheet-preview.js');
|
||||
const spreadsheetVendors = [join(publicRoot, 'vendor', 'exceljs.min.js'), join(publicRoot, 'vendor', 'fflate.min.js')];
|
||||
|
||||
if ([spreadsheetWorker, spreadsheetCore, spreadsheetEntry, ...spreadsheetVendors].every(existsSync)) {
|
||||
const vendorBytes = spreadsheetVendors.reduce((total, file) => total + readFileSync(file).length, 0);
|
||||
if (vendorBytes > SPREADSHEET_VENDOR_MAX_BYTES) {
|
||||
failures.push(`Spreadsheet vendor bundles exceed ${SPREADSHEET_VENDOR_MAX_BYTES} bytes (${vendorBytes} bytes)`);
|
||||
}
|
||||
|
||||
const expectedVersion = createHash('sha256')
|
||||
.update(readFileSync(spreadsheetWorker))
|
||||
.update(readFileSync(spreadsheetCore))
|
||||
.update(readFileSync(spreadsheetVendors[0]))
|
||||
.update(readFileSync(spreadsheetVendors[1]))
|
||||
.digest('hex')
|
||||
.slice(0, 12);
|
||||
const entrySource = readFileSync(spreadsheetEntry, 'utf8');
|
||||
const actualVersion = entrySource.match(/const SPREADSHEET_ASSET_VERSION = '([a-f0-9]+)'/)?.[1];
|
||||
if (actualVersion !== expectedVersion) {
|
||||
failures.push(
|
||||
`SPREADSHEET_ASSET_VERSION mismatch: expected ${expectedVersion}, found ${actualVersion || 'missing'}`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
failures.push('Spreadsheet preview assets are missing; run `node scripts/prepare-spreadsheet-assets.mjs`');
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
const rel = relative(repoRoot, file);
|
||||
const data = readFileSync(file);
|
||||
|
||||
@@ -341,6 +341,22 @@ if (isGlobalInstall) {
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// 4a. Copy the XLSX preview's browser bundles (exceljs, fflate) into
|
||||
// src/web/public/vendor/ for dev mode. The build does the same into dist/.
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
if (!isGlobalInstall) {
|
||||
try {
|
||||
execSync(`node "${join(import.meta.dirname, 'prepare-spreadsheet-assets.mjs')}"`, { stdio: 'pipe' });
|
||||
console.log(colors.green('✓ Spreadsheet preview vendor files prepared'));
|
||||
} catch (err) {
|
||||
hasWarnings = true;
|
||||
console.log(colors.yellow('⚠ Failed to prepare spreadsheet preview vendor files'));
|
||||
console.log(colors.dim(` ${err.message}`));
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// 4b. Fetch gesture-overlay runtime assets (MediaPipe wasm + model) for dev mode
|
||||
// (src/web/public/gesture/). Opt-in feature (CODEMAN_GESTURE=1); non-fatal.
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Copy the XLSX preview's browser bundles (exceljs, fflate) into a public vendor
|
||||
* dir. Run by postinstall for dev (src/web/public/vendor, gitignored) and by
|
||||
* build.mjs for prod (dist/web/public/vendor). Both packages are pinned exactly
|
||||
* in package.json, and check-public-assets.mjs hashes the output into
|
||||
* SPREADSHEET_ASSET_VERSION (the worker's cache-bust token), so a version bump
|
||||
* that changes the bytes fails that check until the token is refreshed.
|
||||
* Source-map comments are stripped: the maps are not shipped.
|
||||
*/
|
||||
|
||||
import { createRequire } from 'node:module';
|
||||
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { dirname, join, resolve } from 'node:path';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const outputDir = resolve(process.argv[2] || join(import.meta.dirname, '..', 'src', 'web', 'public', 'vendor'));
|
||||
const excelSource = require.resolve('exceljs/dist/exceljs.min.js');
|
||||
const fflateSource = join(dirname(require.resolve('fflate')), '..', 'umd', 'index.js');
|
||||
|
||||
function copyBrowserBundle(source, outputName) {
|
||||
const content = readFileSync(source, 'utf8').replace(/\n?\/\/# sourceMappingURL=.*(?:\n|$)/g, '\n');
|
||||
if (/sourceMappingURL/.test(content)) {
|
||||
throw new Error(`Failed to strip sourceMappingURL from ${outputName}`);
|
||||
}
|
||||
writeFileSync(join(outputDir, outputName), content, 'utf8');
|
||||
}
|
||||
|
||||
mkdirSync(outputDir, { recursive: true });
|
||||
copyBrowserBundle(excelSource, 'exceljs.min.js');
|
||||
copyBrowserBundle(fflateSource, 'fflate.min.js');
|
||||
Reference in New Issue
Block a user