mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-22 19:29:13 +08:00
feat(ql3): establish 3.0 incubation baseline
This commit is contained in:
@@ -0,0 +1,741 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
const { pruneRuntimeArtifact } = require('./ql3-prune-runtime-artifact.cjs');
|
||||
|
||||
const MAX_OUTPUT_BYTES = 1024 * 1024;
|
||||
const DEFAULT_MAX_ARTIFACT_FILES = 512;
|
||||
const DEFAULT_MAX_ADOPTED_ARTIFACT_FILES = 576;
|
||||
const DEFAULT_MAX_APPLICATION_ARTIFACT_FILES = 640;
|
||||
const DEFAULT_MAX_APPLICATION_AI_ARTIFACT_FILES = 768;
|
||||
const DEFAULT_MAX_APPLICATION_API_ARTIFACT_FILES = 640;
|
||||
const DEFAULT_MAX_ARTIFACT_BYTES = 4 * 1024 * 1024;
|
||||
const DEFAULT_MAX_ADOPTED_ARTIFACT_BYTES = 5 * 1024 * 1024;
|
||||
const DEFAULT_MAX_APPLICATION_ARTIFACT_BYTES = 5 * 1024 * 1024;
|
||||
const DEFAULT_MAX_APPLICATION_AI_ARTIFACT_BYTES = 6 * 1024 * 1024;
|
||||
const DEFAULT_MAX_APPLICATION_API_ARTIFACT_BYTES = 6 * 1024 * 1024;
|
||||
const MIN_APPLICATION_AI_ARTIFACT_HEADROOM_BYTES = 64 * 1024;
|
||||
const DEFAULT_MAX_MCP_ARTIFACT_FILES = 1536;
|
||||
const DEFAULT_MAX_MCP_ARTIFACT_BYTES = 16 * 1024 * 1024;
|
||||
const DEFAULT_MAX_RSS_DELTA_BYTES = 16 * 1024 * 1024;
|
||||
const DEFAULT_MAX_MCP_RSS_DELTA_BYTES = 48 * 1024 * 1024;
|
||||
const DEFAULT_MAX_APPLICATION_RSS_DELTA_BYTES = 24 * 1024 * 1024;
|
||||
const PACKAGE_CHAIN = Object.freeze([
|
||||
Object.freeze({
|
||||
name: '@qinglong/runtime-core',
|
||||
directory: 'ql3-runtime-core',
|
||||
}),
|
||||
Object.freeze({
|
||||
name: '@qinglong/local-sqlite',
|
||||
directory: 'ql3-local-sqlite',
|
||||
}),
|
||||
]);
|
||||
const BASE_EXTERNAL_PACKAGE_NAMES = Object.freeze(['semver']);
|
||||
|
||||
function fail(message) {
|
||||
throw new Error(`QingLong local Profile artifact audit failed: ${message}`);
|
||||
}
|
||||
|
||||
function boundedPositiveInteger(value, fallback, label) {
|
||||
if (value === undefined) return fallback;
|
||||
const normalized = Number(value);
|
||||
if (!Number.isSafeInteger(normalized) || normalized < 1) {
|
||||
fail(`${label} is invalid`);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function parseArguments(argv) {
|
||||
const profile = argv[0];
|
||||
if (
|
||||
profile !== 'edge' &&
|
||||
profile !== 'standalone' &&
|
||||
profile !== 'edge-adopted' &&
|
||||
profile !== 'standalone-adopted' &&
|
||||
profile !== 'edge-application' &&
|
||||
profile !== 'standalone-application' &&
|
||||
profile !== 'edge-ai' &&
|
||||
profile !== 'standalone-ai' &&
|
||||
profile !== 'edge-application-ai' &&
|
||||
profile !== 'standalone-application-ai' &&
|
||||
profile !== 'edge-application-api' &&
|
||||
profile !== 'standalone-application-api' &&
|
||||
profile !== 'edge-mcp' &&
|
||||
profile !== 'standalone-mcp'
|
||||
) {
|
||||
fail(
|
||||
'Profile must be edge, standalone, an adopted/application variant, an AI/API variant, or an MCP variant',
|
||||
);
|
||||
}
|
||||
const values = Object.fromEntries(
|
||||
argv.slice(1).map((argument) => {
|
||||
const match = /^--([a-z-]+)=(\d+)$/.exec(argument);
|
||||
if (!match) fail(`unsupported argument ${argument}`);
|
||||
return [match[1], match[2]];
|
||||
}),
|
||||
);
|
||||
return Object.freeze({
|
||||
profile,
|
||||
runtimeProfile: profile.startsWith('edge') ? 'edge' : 'standalone',
|
||||
adopted: profile.includes('-adopted') || profile.includes('-application'),
|
||||
application: profile.includes('-application'),
|
||||
ai: profile.endsWith('-ai'),
|
||||
api: profile.endsWith('-api'),
|
||||
mcp: profile.endsWith('-mcp'),
|
||||
maxArtifactFiles: boundedPositiveInteger(
|
||||
values['max-artifact-files'],
|
||||
profile.endsWith('-mcp')
|
||||
? DEFAULT_MAX_MCP_ARTIFACT_FILES
|
||||
: profile.endsWith('-api')
|
||||
? DEFAULT_MAX_APPLICATION_API_ARTIFACT_FILES
|
||||
: profile.includes('-application') && profile.endsWith('-ai')
|
||||
? DEFAULT_MAX_APPLICATION_AI_ARTIFACT_FILES
|
||||
: profile.endsWith('-application') || profile.endsWith('-ai')
|
||||
? DEFAULT_MAX_APPLICATION_ARTIFACT_FILES
|
||||
: profile.includes('-adopted')
|
||||
? DEFAULT_MAX_ADOPTED_ARTIFACT_FILES
|
||||
: DEFAULT_MAX_ARTIFACT_FILES,
|
||||
'artifact file budget',
|
||||
),
|
||||
maxArtifactBytes: boundedPositiveInteger(
|
||||
values['max-artifact-bytes'],
|
||||
profile.endsWith('-mcp')
|
||||
? DEFAULT_MAX_MCP_ARTIFACT_BYTES
|
||||
: profile.endsWith('-api')
|
||||
? DEFAULT_MAX_APPLICATION_API_ARTIFACT_BYTES
|
||||
: profile.includes('-application') && profile.endsWith('-ai')
|
||||
? DEFAULT_MAX_APPLICATION_AI_ARTIFACT_BYTES
|
||||
: profile.endsWith('-application') || profile.endsWith('-ai')
|
||||
? DEFAULT_MAX_APPLICATION_ARTIFACT_BYTES
|
||||
: profile.includes('-adopted')
|
||||
? DEFAULT_MAX_ADOPTED_ARTIFACT_BYTES
|
||||
: DEFAULT_MAX_ARTIFACT_BYTES,
|
||||
'artifact byte budget',
|
||||
),
|
||||
maxRssDeltaBytes: boundedPositiveInteger(
|
||||
values['max-rss-delta-bytes'],
|
||||
profile.endsWith('-mcp')
|
||||
? DEFAULT_MAX_MCP_RSS_DELTA_BYTES
|
||||
: profile.includes('-application')
|
||||
? DEFAULT_MAX_APPLICATION_RSS_DELTA_BYTES
|
||||
: DEFAULT_MAX_RSS_DELTA_BYTES,
|
||||
'RSS delta budget',
|
||||
),
|
||||
minimumArtifactHeadroomBytes:
|
||||
profile.includes('-application') && profile.endsWith('-ai')
|
||||
? MIN_APPLICATION_AI_ARTIFACT_HEADROOM_BYTES
|
||||
: 0,
|
||||
});
|
||||
}
|
||||
|
||||
function run(command, args, cwd) {
|
||||
const result = spawnSync(command, args, {
|
||||
cwd,
|
||||
encoding: 'utf8',
|
||||
env: { ...process.env, CI: 'true' },
|
||||
maxBuffer: MAX_OUTPUT_BYTES,
|
||||
});
|
||||
if (result.error) throw result.error;
|
||||
if (result.status !== 0) {
|
||||
const detail = (result.stderr || result.stdout).trim().slice(0, 4096);
|
||||
fail(`${command} ${args.join(' ')} exited ${result.status}: ${detail}`);
|
||||
}
|
||||
return result.stdout.trim();
|
||||
}
|
||||
|
||||
function directoryUsage(directory, maxFiles) {
|
||||
let bytes = 0;
|
||||
let files = 0;
|
||||
const pending = [directory];
|
||||
while (pending.length > 0) {
|
||||
const current = pending.pop();
|
||||
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
||||
const entryPath = path.join(current, entry.name);
|
||||
const stat = fs.lstatSync(entryPath);
|
||||
if (stat.isSymbolicLink()) fail('artifact contains a symbolic link');
|
||||
if (stat.isDirectory()) {
|
||||
pending.push(entryPath);
|
||||
} else if (stat.isFile()) {
|
||||
files += 1;
|
||||
bytes += stat.size;
|
||||
} else {
|
||||
fail('artifact contains an unsupported filesystem entry');
|
||||
}
|
||||
if (files > maxFiles) {
|
||||
fail(`artifact uses more than ${maxFiles} files`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return { bytes, files };
|
||||
}
|
||||
|
||||
function installedPackages(nodeModulesDirectory) {
|
||||
const packages = [];
|
||||
for (const entry of fs.readdirSync(nodeModulesDirectory, {
|
||||
withFileTypes: true,
|
||||
})) {
|
||||
if (!entry.isDirectory() || entry.name.startsWith('.')) continue;
|
||||
if (!entry.name.startsWith('@')) {
|
||||
packages.push(entry.name);
|
||||
continue;
|
||||
}
|
||||
for (const scoped of fs.readdirSync(
|
||||
path.join(nodeModulesDirectory, entry.name),
|
||||
{ withFileTypes: true },
|
||||
)) {
|
||||
if (scoped.isDirectory()) packages.push(`${entry.name}/${scoped.name}`);
|
||||
}
|
||||
}
|
||||
return packages.sort();
|
||||
}
|
||||
|
||||
function auditImportClosure(
|
||||
artifactDirectory,
|
||||
packageNames,
|
||||
{ allowSemver = false } = {},
|
||||
) {
|
||||
const script = `
|
||||
const before = process.memoryUsage().rss;
|
||||
for (const packageName of ${JSON.stringify(packageNames)}) {
|
||||
require(packageName);
|
||||
}
|
||||
const after = process.memoryUsage().rss;
|
||||
process.stdout.write(JSON.stringify({
|
||||
before,
|
||||
after,
|
||||
loaded: Object.keys(require.cache),
|
||||
}));
|
||||
`;
|
||||
const result = JSON.parse(
|
||||
run(process.execPath, ['-e', script], artifactDirectory),
|
||||
);
|
||||
if (
|
||||
!Number.isSafeInteger(result.before) ||
|
||||
!Number.isSafeInteger(result.after) ||
|
||||
!Array.isArray(result.loaded)
|
||||
) {
|
||||
fail('import closure report is invalid');
|
||||
}
|
||||
const forbidden = result.loaded.filter(
|
||||
(filePath) =>
|
||||
(!allowSemver &&
|
||||
/node_modules[\\/]semver(?:[\\/]|$)/i.test(filePath)) ||
|
||||
/(?:node_modules[\\/](?:croner|pg|drizzle-orm|sequelize|sqlite3)(?:[\\/]|$)|node_modules[\\/]@qinglong[\\/]cluster-|local-sqlite[\\/]dist[\\/]migration\.js$|local-sqlite[\\/]dist[\\/]migrations[\\/])/i.test(
|
||||
filePath,
|
||||
),
|
||||
);
|
||||
if (forbidden.length > 0)
|
||||
fail('runtime import closure contains forbidden modules');
|
||||
return Object.freeze({
|
||||
loadedModuleCount: result.loaded.length,
|
||||
rssDeltaBytes: Math.max(0, result.after - result.before),
|
||||
});
|
||||
}
|
||||
|
||||
function auditApplicationExecutable(artifactDirectory) {
|
||||
const catalogExport =
|
||||
'@qinglong/local-application/plugin-package-recovery-catalog';
|
||||
const packageDirectory = path.join(
|
||||
artifactDirectory,
|
||||
'node_modules',
|
||||
'@qinglong',
|
||||
'local-application',
|
||||
);
|
||||
const manifest = JSON.parse(
|
||||
fs.readFileSync(path.join(packageDirectory, 'package.json'), 'utf8'),
|
||||
);
|
||||
if (
|
||||
!manifest ||
|
||||
typeof manifest !== 'object' ||
|
||||
Array.isArray(manifest) ||
|
||||
!manifest.bin ||
|
||||
typeof manifest.bin !== 'object' ||
|
||||
Array.isArray(manifest.bin) ||
|
||||
manifest.bin['ql3-local-application'] !== 'dist/cli.js'
|
||||
) {
|
||||
fail('local application executable manifest is invalid');
|
||||
}
|
||||
const executablePath = path.join(packageDirectory, 'dist', 'cli.js');
|
||||
const executableStat = fs.lstatSync(executablePath);
|
||||
if (!executableStat.isFile() || executableStat.isSymbolicLink()) {
|
||||
fail('local application executable is not a regular file');
|
||||
}
|
||||
const output = run(
|
||||
process.execPath,
|
||||
[executablePath, '--help'],
|
||||
artifactDirectory,
|
||||
);
|
||||
if (
|
||||
output !==
|
||||
'Usage: ql3-local-application --config /absolute/private-config.json'
|
||||
) {
|
||||
fail('local application executable help output is invalid');
|
||||
}
|
||||
const catalogOutput = run(
|
||||
process.execPath,
|
||||
[
|
||||
'-e',
|
||||
`const value = require(${JSON.stringify(catalogExport)});
|
||||
if (typeof value.createLocalPluginPackageRecoveryCatalogStageProvider !== 'function') {
|
||||
process.exitCode = 1;
|
||||
}`,
|
||||
],
|
||||
artifactDirectory,
|
||||
);
|
||||
if (catalogOutput !== '') {
|
||||
fail('local Plugin Package recovery catalog import emitted output');
|
||||
}
|
||||
}
|
||||
|
||||
function auditLocalApiExecutable(artifactDirectory) {
|
||||
const packageDirectory = path.join(
|
||||
artifactDirectory,
|
||||
'node_modules',
|
||||
'@qinglong',
|
||||
'local-api',
|
||||
);
|
||||
const manifest = JSON.parse(
|
||||
fs.readFileSync(path.join(packageDirectory, 'package.json'), 'utf8'),
|
||||
);
|
||||
if (manifest.bin?.['ql3-local-api'] !== 'dist/cli.js') {
|
||||
fail('local API executable manifest is invalid');
|
||||
}
|
||||
const output = run(
|
||||
process.execPath,
|
||||
[path.join(packageDirectory, 'dist', 'cli.js'), '--help'],
|
||||
artifactDirectory,
|
||||
);
|
||||
if (
|
||||
output !==
|
||||
'Usage: ql3-local-api --config /absolute/private-config.json'
|
||||
) {
|
||||
fail('local API executable help output is invalid');
|
||||
}
|
||||
}
|
||||
|
||||
function packageRootFromEntry(entryPath, packageName) {
|
||||
let current = path.dirname(entryPath);
|
||||
while (current !== path.dirname(current)) {
|
||||
const manifestPath = path.join(current, 'package.json');
|
||||
if (fs.existsSync(manifestPath)) {
|
||||
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
||||
if (manifest.name === packageName) return current;
|
||||
}
|
||||
current = path.dirname(current);
|
||||
}
|
||||
fail(`cannot resolve package root for ${packageName}`);
|
||||
}
|
||||
|
||||
function mcpExternalPackageDirectories(root) {
|
||||
const mcpPackageDirectory = path.join(
|
||||
root,
|
||||
'packages',
|
||||
'ql3-local-mcp-server',
|
||||
);
|
||||
const serverRoot = fs.realpathSync(
|
||||
path.join(
|
||||
mcpPackageDirectory,
|
||||
'node_modules',
|
||||
'@modelcontextprotocol',
|
||||
'server',
|
||||
),
|
||||
);
|
||||
const dependencyResolutionRoot = path.dirname(path.dirname(serverRoot));
|
||||
return Object.freeze({
|
||||
'@modelcontextprotocol/server': serverRoot,
|
||||
'@modelcontextprotocol/core': packageRootFromEntry(
|
||||
require.resolve('@modelcontextprotocol/core', {
|
||||
paths: [dependencyResolutionRoot],
|
||||
}),
|
||||
'@modelcontextprotocol/core',
|
||||
),
|
||||
zod: packageRootFromEntry(
|
||||
require.resolve('zod', { paths: [dependencyResolutionRoot] }),
|
||||
'zod',
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
function pruneMcpExternalDevelopmentFiles(nodeModulesDirectory) {
|
||||
let files = 0;
|
||||
let bytes = 0;
|
||||
for (const packageName of [
|
||||
'@modelcontextprotocol/core',
|
||||
'@modelcontextprotocol/server',
|
||||
'zod',
|
||||
]) {
|
||||
const packageDirectory = path.join(nodeModulesDirectory, packageName);
|
||||
const pending = [packageDirectory];
|
||||
while (pending.length > 0) {
|
||||
const current = pending.pop();
|
||||
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
||||
const entryPath = path.join(current, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
pending.push(entryPath);
|
||||
continue;
|
||||
}
|
||||
if (!entry.isFile()) fail('MCP dependency contains unsupported data');
|
||||
if (
|
||||
entry.name.endsWith('.map') ||
|
||||
entry.name.endsWith('.d.ts') ||
|
||||
entry.name.endsWith('.d.cts') ||
|
||||
entry.name.endsWith('.d.mts') ||
|
||||
entry.name.toLowerCase() === 'readme.md'
|
||||
) {
|
||||
const stat = fs.lstatSync(entryPath);
|
||||
fs.unlinkSync(entryPath);
|
||||
files += 1;
|
||||
bytes += stat.size;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Object.freeze({ files, bytes });
|
||||
}
|
||||
|
||||
function auditMcpExecutable(artifactDirectory) {
|
||||
const packageDirectory = path.join(
|
||||
artifactDirectory,
|
||||
'node_modules',
|
||||
'@qinglong',
|
||||
'local-mcp-server',
|
||||
);
|
||||
const manifest = JSON.parse(
|
||||
fs.readFileSync(path.join(packageDirectory, 'package.json'), 'utf8'),
|
||||
);
|
||||
if (manifest.bin?.['ql3-mcp'] !== 'dist/cli.js') {
|
||||
fail('local MCP executable manifest is invalid');
|
||||
}
|
||||
const output = run(
|
||||
process.execPath,
|
||||
[path.join(packageDirectory, 'dist', 'cli.js'), '--help'],
|
||||
artifactDirectory,
|
||||
);
|
||||
if (output !== 'Usage: ql3-mcp --config /absolute/private-config.json') {
|
||||
fail('local MCP executable help output is invalid');
|
||||
}
|
||||
}
|
||||
|
||||
function main() {
|
||||
const options = parseArguments(process.argv.slice(2));
|
||||
if (process.versions.node.split('.')[0] !== '24') {
|
||||
fail('artifact audit requires Node 24');
|
||||
}
|
||||
const root = path.resolve(__dirname, '..');
|
||||
const temporaryRoot = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), `ql3-${options.profile}-artifact-`),
|
||||
);
|
||||
try {
|
||||
const packsDirectory = path.join(temporaryRoot, 'packs');
|
||||
const artifactDirectory = path.join(temporaryRoot, 'artifact');
|
||||
const cacheDirectory = path.join(temporaryRoot, 'npm-cache');
|
||||
fs.mkdirSync(packsDirectory, { recursive: true });
|
||||
fs.mkdirSync(artifactDirectory, { recursive: true });
|
||||
|
||||
const packageChain = [...PACKAGE_CHAIN];
|
||||
if (options.adopted) {
|
||||
packageChain.push(
|
||||
Object.freeze({
|
||||
name: '@qinglong/local-admin',
|
||||
directory: 'ql3-local-admin',
|
||||
}),
|
||||
Object.freeze({
|
||||
name: '@qinglong/local-secret',
|
||||
directory: 'ql3-local-secret',
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (options.application) {
|
||||
packageChain.push(
|
||||
Object.freeze({
|
||||
name: '@qinglong/local-command-file',
|
||||
directory: 'ql3-local-command-file',
|
||||
}),
|
||||
Object.freeze({
|
||||
name: '@qinglong/local-process',
|
||||
directory: 'ql3-local-process',
|
||||
}),
|
||||
Object.freeze({
|
||||
name: '@qinglong/local-execution',
|
||||
directory: 'ql3-local-execution',
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (options.ai) {
|
||||
packageChain.push(
|
||||
Object.freeze({
|
||||
name: '@qinglong/ai',
|
||||
directory: 'ql3-ai',
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (options.mcp) {
|
||||
packageChain.push(
|
||||
Object.freeze({
|
||||
name: '@qinglong/local-command-file',
|
||||
directory: 'ql3-local-command-file',
|
||||
}),
|
||||
Object.freeze({
|
||||
name: '@qinglong/local-owner-console',
|
||||
directory: 'ql3-local-owner-console',
|
||||
}),
|
||||
Object.freeze({
|
||||
name: '@qinglong/local-mcp-server',
|
||||
directory: 'ql3-local-mcp-server',
|
||||
}),
|
||||
);
|
||||
}
|
||||
const entrySpecifiers = [
|
||||
options.api
|
||||
? '@qinglong/local-api'
|
||||
: options.mcp
|
||||
? '@qinglong/local-mcp-server'
|
||||
: options.application && options.ai
|
||||
? '@qinglong/local-application/ai-feature'
|
||||
: options.application
|
||||
? '@qinglong/local-application'
|
||||
: options.adopted
|
||||
? `@qinglong/local-admin/adopted-profile/${options.runtimeProfile}`
|
||||
: `@qinglong/local-sqlite/profile/${options.runtimeProfile}`,
|
||||
...(options.ai && !options.application ? ['@qinglong/ai/profile'] : []),
|
||||
...(options.application
|
||||
? [
|
||||
'@qinglong/local-application/process',
|
||||
'@qinglong/local-application/plugin-package-recovery-catalog',
|
||||
]
|
||||
: []),
|
||||
...(options.api
|
||||
? ['@qinglong/local-api/config', '@qinglong/local-api/process']
|
||||
: []),
|
||||
...(options.mcp
|
||||
? [
|
||||
'@qinglong/local-mcp-server/config',
|
||||
'@qinglong/local-mcp-server/process',
|
||||
]
|
||||
: []),
|
||||
];
|
||||
const externalPackageNames = [
|
||||
...BASE_EXTERNAL_PACKAGE_NAMES,
|
||||
...(options.application ? ['croner'] : []),
|
||||
...(options.mcp
|
||||
? ['@modelcontextprotocol/server', '@modelcontextprotocol/core', 'zod']
|
||||
: []),
|
||||
];
|
||||
if (options.application) {
|
||||
packageChain.push(
|
||||
Object.freeze({
|
||||
name: '@qinglong/local-application',
|
||||
directory: 'ql3-local-application',
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (options.api) {
|
||||
packageChain.push(
|
||||
Object.freeze({
|
||||
name: '@qinglong/local-owner-console',
|
||||
directory: 'ql3-local-owner-console',
|
||||
}),
|
||||
Object.freeze({
|
||||
name: '@qinglong/local-api',
|
||||
directory: 'ql3-local-api',
|
||||
}),
|
||||
);
|
||||
}
|
||||
const archives = [];
|
||||
for (const packageDefinition of packageChain) {
|
||||
const packageDirectory = path.join(
|
||||
root,
|
||||
'packages',
|
||||
packageDefinition.directory,
|
||||
);
|
||||
run('pnpm', ['run', 'build'], packageDirectory);
|
||||
const archive = run(
|
||||
'pnpm',
|
||||
['pack', '--pack-destination', packsDirectory],
|
||||
packageDirectory,
|
||||
)
|
||||
.split(/\r?\n/)
|
||||
.at(-1);
|
||||
if (!archive || !path.isAbsolute(archive) || !fs.existsSync(archive)) {
|
||||
fail(`pack output for ${packageDefinition.name} is invalid`);
|
||||
}
|
||||
archives.push(archive);
|
||||
}
|
||||
const mcpExternalDirectories = options.mcp
|
||||
? mcpExternalPackageDirectories(root)
|
||||
: {};
|
||||
for (const packageName of externalPackageNames) {
|
||||
const packageDirectory =
|
||||
mcpExternalDirectories[packageName] ??
|
||||
path.dirname(
|
||||
require.resolve(`${packageName}/package.json`, {
|
||||
paths: [
|
||||
path.join(
|
||||
root,
|
||||
'packages',
|
||||
packageName === 'croner'
|
||||
? 'ql3-local-execution'
|
||||
: 'ql3-runtime-core',
|
||||
),
|
||||
],
|
||||
}),
|
||||
);
|
||||
const archive = run(
|
||||
'pnpm',
|
||||
['pack', '--pack-destination', packsDirectory],
|
||||
packageDirectory,
|
||||
)
|
||||
.split(/\r?\n/)
|
||||
.at(-1);
|
||||
if (!archive || !path.isAbsolute(archive) || !fs.existsSync(archive)) {
|
||||
fail(`pack output for ${packageName} is invalid`);
|
||||
}
|
||||
archives.push(archive);
|
||||
}
|
||||
|
||||
run(
|
||||
'npm',
|
||||
[
|
||||
'install',
|
||||
'--prefix',
|
||||
artifactDirectory,
|
||||
'--cache',
|
||||
cacheDirectory,
|
||||
'--omit=dev',
|
||||
'--ignore-scripts',
|
||||
'--no-bin-links',
|
||||
'--package-lock=false',
|
||||
'--offline',
|
||||
'--no-audit',
|
||||
'--no-fund',
|
||||
...archives,
|
||||
],
|
||||
root,
|
||||
);
|
||||
|
||||
const expectedPackages = [
|
||||
...packageChain.map(({ name }) => name),
|
||||
...externalPackageNames,
|
||||
].sort();
|
||||
const actualPackages = installedPackages(
|
||||
path.join(artifactDirectory, 'node_modules'),
|
||||
);
|
||||
if (JSON.stringify(actualPackages) !== JSON.stringify(expectedPackages)) {
|
||||
fail(
|
||||
`production package closure is invalid: ${actualPackages.join(',')}`,
|
||||
);
|
||||
}
|
||||
const prunedRuntimeArtifact = pruneRuntimeArtifact(
|
||||
path.join(artifactDirectory, 'node_modules', '@qinglong'),
|
||||
{
|
||||
entrySpecifiers,
|
||||
excludedInternalPackages:
|
||||
options.application && !options.ai ? ['@qinglong/ai'] : [],
|
||||
},
|
||||
);
|
||||
const prunedMcpExternalDevelopment = options.mcp
|
||||
? pruneMcpExternalDevelopmentFiles(
|
||||
path.join(artifactDirectory, 'node_modules'),
|
||||
)
|
||||
: Object.freeze({ files: 0, bytes: 0 });
|
||||
if (options.application) {
|
||||
auditApplicationExecutable(artifactDirectory);
|
||||
}
|
||||
if (options.api) {
|
||||
auditLocalApiExecutable(artifactDirectory);
|
||||
}
|
||||
if (options.mcp) {
|
||||
auditMcpExecutable(artifactDirectory);
|
||||
}
|
||||
const usage = directoryUsage(artifactDirectory, options.maxArtifactFiles);
|
||||
if (usage.bytes > options.maxArtifactBytes) {
|
||||
fail(
|
||||
`artifact uses ${usage.bytes} bytes, budget is ${options.maxArtifactBytes}`,
|
||||
);
|
||||
}
|
||||
const artifactHeadroomBytes = options.maxArtifactBytes - usage.bytes;
|
||||
if (artifactHeadroomBytes < options.minimumArtifactHeadroomBytes) {
|
||||
fail(
|
||||
`artifact headroom is ${artifactHeadroomBytes} bytes, minimum is ${options.minimumArtifactHeadroomBytes}`,
|
||||
);
|
||||
}
|
||||
const closure = auditImportClosure(artifactDirectory, entrySpecifiers, {
|
||||
allowSemver: options.mcp,
|
||||
});
|
||||
if (closure.rssDeltaBytes > options.maxRssDeltaBytes) {
|
||||
fail(
|
||||
`import RSS delta is ${closure.rssDeltaBytes} bytes, budget is ${options.maxRssDeltaBytes}`,
|
||||
);
|
||||
}
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({
|
||||
schemaVersion: 1,
|
||||
profile: options.profile,
|
||||
packages: actualPackages,
|
||||
artifactBytes: usage.bytes,
|
||||
artifactFiles: usage.files,
|
||||
maxArtifactFiles: options.maxArtifactFiles,
|
||||
maxArtifactBytes: options.maxArtifactBytes,
|
||||
artifactHeadroomBytes,
|
||||
minimumArtifactHeadroomBytes:
|
||||
options.minimumArtifactHeadroomBytes,
|
||||
prunedRuntimeDevelopmentFiles: prunedRuntimeArtifact.development.files,
|
||||
prunedRuntimeDevelopmentBytes: prunedRuntimeArtifact.development.bytes,
|
||||
runtimeJavaScriptFilesBefore:
|
||||
prunedRuntimeArtifact.runtimeJavaScript.filesBefore,
|
||||
runtimeJavaScriptFilesAfter:
|
||||
prunedRuntimeArtifact.runtimeJavaScript.filesAfter,
|
||||
prunedRuntimeJavaScriptFiles:
|
||||
prunedRuntimeArtifact.runtimeJavaScript.filesRemoved,
|
||||
prunedRuntimeJavaScriptBytes:
|
||||
prunedRuntimeArtifact.runtimeJavaScript.bytesRemoved,
|
||||
strippedSourceMapDirectiveFiles:
|
||||
prunedRuntimeArtifact.sourceMapDirectives.files,
|
||||
strippedSourceMapDirectiveBytes:
|
||||
prunedRuntimeArtifact.sourceMapDirectives.bytes,
|
||||
compactedPackageManifestFiles:
|
||||
prunedRuntimeArtifact.packageManifests.compactedFiles,
|
||||
compactedPackageManifestBytes:
|
||||
prunedRuntimeArtifact.packageManifests.compactedBytes,
|
||||
projectedRuntimeManifestFiles:
|
||||
prunedRuntimeArtifact.packageManifests.projectedFiles,
|
||||
projectedRuntimeManifestBytes:
|
||||
prunedRuntimeArtifact.packageManifests.projectedBytes,
|
||||
runtimeExportKeysBefore:
|
||||
prunedRuntimeArtifact.packageManifests.runtimeExports.keysBefore,
|
||||
runtimeExportKeysAfter:
|
||||
prunedRuntimeArtifact.packageManifests.runtimeExports.keysAfter,
|
||||
runtimeExportKeysRemoved:
|
||||
prunedRuntimeArtifact.packageManifests.runtimeExports.keysRemoved,
|
||||
projectedRuntimeExportBytes:
|
||||
prunedRuntimeArtifact.packageManifests.runtimeExports.bytes,
|
||||
excludedRuntimeInternalSpecifiers:
|
||||
prunedRuntimeArtifact.packageManifests.runtimeExports
|
||||
.excludedSpecifiers,
|
||||
prunedRuntimeArtifactBytes: prunedRuntimeArtifact.savedBytes,
|
||||
prunedMcpExternalDevelopmentFiles:
|
||||
prunedMcpExternalDevelopment.files,
|
||||
prunedMcpExternalDevelopmentBytes:
|
||||
prunedMcpExternalDevelopment.bytes,
|
||||
loadedModuleCount: closure.loadedModuleCount,
|
||||
rssDeltaBytes: closure.rssDeltaBytes,
|
||||
maxRssDeltaBytes: options.maxRssDeltaBytes,
|
||||
compatible: true,
|
||||
})}\n`,
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(temporaryRoot, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
main();
|
||||
} catch (error) {
|
||||
process.stderr.write(
|
||||
`${error instanceof Error ? error.message : String(error)}\n`,
|
||||
);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
Reference in New Issue
Block a user