hard-cut batch D: enforce worker bundle isolation and CI guard
This commit is contained in:
parent
f77ee8cfd4
commit
fb0c95dfb1
4 changed files with 59 additions and 35 deletions
4
.github/workflows/playwright.yml
vendored
4
.github/workflows/playwright.yml
vendored
|
|
@ -36,6 +36,10 @@ jobs:
|
||||||
weed version
|
weed version
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
- name: Build app and enforce bundle guard
|
||||||
|
run: |
|
||||||
|
pnpm build
|
||||||
|
pnpm build:bundle-guard
|
||||||
- name: Verify ffprobe
|
- name: Verify ffprobe
|
||||||
run: ffprobe -version
|
run: ffprobe -version
|
||||||
- name: Install Playwright Browsers
|
- name: Install Playwright Browsers
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,4 @@
|
||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from "next";
|
||||||
import path from "node:path";
|
|
||||||
import { createRequire } from "node:module";
|
|
||||||
|
|
||||||
type DefinePluginCtor = new (defs: Record<string, string>) => unknown;
|
|
||||||
const require = createRequire(import.meta.url);
|
|
||||||
const { DefinePlugin } = require('webpack') as { DefinePlugin: DefinePluginCtor };
|
|
||||||
|
|
||||||
const securityHeaders = [
|
const securityHeaders = [
|
||||||
{ key: 'X-Content-Type-Options', value: 'nosniff' },
|
{ key: 'X-Content-Type-Options', value: 'nosniff' },
|
||||||
|
|
@ -21,11 +15,7 @@ const securityHeaders = [
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const computeModeRaw = (process.env.COMPUTE_MODE || 'local').trim().toLowerCase();
|
const bundleWorkerCompute = true;
|
||||||
const computeMode = computeModeRaw === 'none' || computeModeRaw === 'worker' || computeModeRaw === 'local'
|
|
||||||
? computeModeRaw
|
|
||||||
: 'local';
|
|
||||||
const bundleWorkerCompute = computeMode === 'worker';
|
|
||||||
const serverExternalPackages = [
|
const serverExternalPackages = [
|
||||||
'@napi-rs/canvas',
|
'@napi-rs/canvas',
|
||||||
'better-sqlite3',
|
'better-sqlite3',
|
||||||
|
|
@ -48,7 +38,7 @@ const nextConfig: NextConfig = {
|
||||||
canvas: '@napi-rs/canvas',
|
canvas: '@napi-rs/canvas',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
transpilePackages: !bundleWorkerCompute ? ['@openreader/compute-core'] : [],
|
transpilePackages: [],
|
||||||
serverExternalPackages,
|
serverExternalPackages,
|
||||||
outputFileTracingIncludes: {
|
outputFileTracingIncludes: {
|
||||||
'/api/audiobook': [
|
'/api/audiobook': [
|
||||||
|
|
@ -73,35 +63,14 @@ const nextConfig: NextConfig = {
|
||||||
outputFileTracingExcludes: {
|
outputFileTracingExcludes: {
|
||||||
'/*': [
|
'/*': [
|
||||||
'./docstore/**/*',
|
'./docstore/**/*',
|
||||||
...(bundleWorkerCompute
|
'./node_modules/onnxruntime-node/**/*',
|
||||||
? [
|
'./node_modules/@huggingface/tokenizers/**/*',
|
||||||
'./node_modules/onnxruntime-node/**/*',
|
|
||||||
'./node_modules/@huggingface/tokenizers/**/*',
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
webpack: (config, { isServer }) => {
|
webpack: (config, { isServer }) => {
|
||||||
if (isServer && bundleWorkerCompute) {
|
if (isServer && bundleWorkerCompute) {
|
||||||
config.plugins = config.plugins || [];
|
|
||||||
config.plugins.push(
|
|
||||||
new DefinePlugin({
|
|
||||||
__OPENREADER_COMPUTE_MODE__: JSON.stringify('worker'),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (isServer && bundleWorkerCompute) {
|
|
||||||
const workerComputeEntry = path.resolve(__dirname, 'src/lib/server/compute/index.worker.ts');
|
|
||||||
const computeIndexTs = path.resolve(__dirname, 'src/lib/server/compute/index.ts');
|
|
||||||
const computeIndexNoExt = path.resolve(__dirname, 'src/lib/server/compute/index');
|
|
||||||
const computeDir = path.resolve(__dirname, 'src/lib/server/compute');
|
|
||||||
config.resolve.alias = {
|
config.resolve.alias = {
|
||||||
...(config.resolve.alias || {}),
|
...(config.resolve.alias || {}),
|
||||||
'@/lib/server/compute$': workerComputeEntry,
|
|
||||||
'@/lib/server/compute/index$': workerComputeEntry,
|
|
||||||
[`${computeIndexTs}$`]: workerComputeEntry,
|
|
||||||
[`${computeIndexNoExt}$`]: workerComputeEntry,
|
|
||||||
[`${computeDir}$`]: workerComputeEntry,
|
|
||||||
'@openreader/compute-core/local-runtime$': false,
|
'@openreader/compute-core/local-runtime$': false,
|
||||||
'onnxruntime-node$': false,
|
'onnxruntime-node$': false,
|
||||||
'@huggingface/tokenizers$': false,
|
'@huggingface/tokenizers$': false,
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
"dev": "node scripts/openreader-entrypoint.mjs -- pnpm dev:raw",
|
"dev": "node scripts/openreader-entrypoint.mjs -- pnpm dev:raw",
|
||||||
"dev:raw": "next dev --turbopack -p 3003",
|
"dev:raw": "next dev --turbopack -p 3003",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
|
"build:bundle-guard": "node scripts/check-next-server-bundle.mjs",
|
||||||
"start": "node scripts/openreader-entrypoint.mjs -- pnpm start:raw",
|
"start": "node scripts/openreader-entrypoint.mjs -- pnpm start:raw",
|
||||||
"start:raw": "next start -p 3003",
|
"start:raw": "next start -p 3003",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
|
|
|
||||||
50
scripts/check-next-server-bundle.mjs
Normal file
50
scripts/check-next-server-bundle.mjs
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
const root = process.cwd();
|
||||||
|
const serverDir = path.join(root, '.next', 'server');
|
||||||
|
|
||||||
|
if (!fs.existsSync(serverDir)) {
|
||||||
|
console.error('[bundle-guard] Missing .next/server. Run `pnpm build` first.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const forbidden = [
|
||||||
|
'onnxruntime-node',
|
||||||
|
'@huggingface/tokenizers',
|
||||||
|
'@openreader/compute-core/local-runtime',
|
||||||
|
];
|
||||||
|
|
||||||
|
const includeExt = new Set(['.js', '.mjs', '.cjs']);
|
||||||
|
const failures = [];
|
||||||
|
|
||||||
|
function walk(dir) {
|
||||||
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||||
|
const fullPath = path.join(dir, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
walk(fullPath);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!entry.isFile()) continue;
|
||||||
|
const ext = path.extname(entry.name).toLowerCase();
|
||||||
|
if (!includeExt.has(ext)) continue;
|
||||||
|
const text = fs.readFileSync(fullPath, 'utf8');
|
||||||
|
for (const pattern of forbidden) {
|
||||||
|
if (text.includes(pattern)) {
|
||||||
|
failures.push({ file: fullPath, pattern });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
walk(serverDir);
|
||||||
|
|
||||||
|
if (failures.length > 0) {
|
||||||
|
console.error('[bundle-guard] Forbidden compute deps detected in Next server bundle:');
|
||||||
|
for (const failure of failures) {
|
||||||
|
console.error(`- ${failure.pattern} in ${path.relative(root, failure.file)}`);
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.info('[bundle-guard] OK: no forbidden compute deps in .next/server');
|
||||||
Loading…
Reference in a new issue