Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions scripts/add-candidate-metadata.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ function main() {
throw new Error('add-candidate-metadata: esm-chunked candidate is missing cycle_hoists field (audit incomplete)');
}
versionEntry.cycle_hoists = offsets.cycle_hoists;
if (Object.prototype.hasOwnProperty.call(offsets, 'cycle_hoists_skipped_assets')) {
versionEntry.cycle_hoists_skipped_assets = offsets.cycle_hoists_skipped_assets;
}
} else {
if (!(offsets.entry_js_offset > 0) || !(offsets.entry_end_offset > offsets.entry_js_offset)) {
throw new Error('legacy-cjs offsets missing entry_js_offset/entry_end_offset');
Expand Down
202 changes: 202 additions & 0 deletions scripts/add-candidate-metadata.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
#!/usr/bin/env node
'use strict';

const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const os = require('os');
const path = require('path');
const cp = require('child_process');

function makeTempDir(prefix) {
const baseDir = process.env.TMPDIR || (process.env.PREFIX ? path.join(process.env.PREFIX, 'tmp') : os.tmpdir());
return fs.mkdtempSync(path.join(baseDir, prefix));
}

test('add-candidate-metadata: esm-chunked with cycle_hoists_skipped_assets', () => {
const tempRoot = makeTempDir('add-candidate-metadata-test-');
try {
// Copy script to temp directory
const scriptSourcePath = path.join(__dirname, 'add-candidate-metadata.js');
const scriptTempDir = path.join(tempRoot, 'scripts');
fs.mkdirSync(scriptTempDir, { recursive: true });
const scriptDestPath = path.join(scriptTempDir, 'add-candidate-metadata.js');
fs.copyFileSync(scriptSourcePath, scriptDestPath);

// Create directory structure
const configDir = path.join(tempRoot, 'config');
const packageConfigDir = path.join(tempRoot, 'packages', 'claude-code', 'config');
fs.mkdirSync(configDir, { recursive: true });
fs.mkdirSync(packageConfigDir, { recursive: true });

// Create initial config files (both must have identical version keys)
const initialConfig = { versions: {} };
fs.writeFileSync(path.join(configDir, 'claude-native-audited-versions.json'), JSON.stringify(initialConfig, null, 2) + '\n');
fs.writeFileSync(path.join(packageConfigDir, 'claude-native-audited-versions.json'), JSON.stringify(initialConfig, null, 2) + '\n');

// Create package.json
fs.mkdirSync(path.join(tempRoot, 'packages', 'claude-code'), { recursive: true });
fs.writeFileSync(path.join(tempRoot, 'packages', 'claude-code', 'package.json'), JSON.stringify({ version: '0.0.0', name: '@anthropic-ai/claude-code' }, null, 2) + '\n');

// Create offset file with cycle_hoists_skipped_assets
const offsetFile = path.join(tempRoot, 'offsets.json');
const offsets = {
entry_format: 'esm-chunked',
tarball_integrity: 'sha512-x',
tarball_sha256: 'y',
num_modules: 100,
byte_count: 1000,
cycle_hoists: [],
cycle_hoists_skipped_assets: ['vendor1.js', 'vendor2.js'],
};
fs.writeFileSync(offsetFile, JSON.stringify(offsets, null, 2) + '\n');

// Run add-candidate-metadata
const result = cp.spawnSync('node', [scriptDestPath, '9.9.9', offsetFile], {
cwd: tempRoot,
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
});

assert.equal(result.status, 0, `Script failed: ${result.stderr}`);

// Verify both config files have cycle_hoists_skipped_assets
const configRoot = JSON.parse(fs.readFileSync(path.join(configDir, 'claude-native-audited-versions.json'), 'utf8'));
const configPackage = JSON.parse(fs.readFileSync(path.join(packageConfigDir, 'claude-native-audited-versions.json'), 'utf8'));

assert.deepEqual(configRoot.versions['9.9.9'].cycle_hoists_skipped_assets, ['vendor1.js', 'vendor2.js']);
assert.deepEqual(configPackage.versions['9.9.9'].cycle_hoists_skipped_assets, ['vendor1.js', 'vendor2.js']);
} finally {
fs.rmSync(tempRoot, { recursive: true, force: true });
}
});

test('add-candidate-metadata: esm-chunked without cycle_hoists_skipped_assets', () => {
const tempRoot = makeTempDir('add-candidate-metadata-test-');
try {
// Copy script to temp directory
const scriptSourcePath = path.join(__dirname, 'add-candidate-metadata.js');
const scriptTempDir = path.join(tempRoot, 'scripts');
fs.mkdirSync(scriptTempDir, { recursive: true });
const scriptDestPath = path.join(scriptTempDir, 'add-candidate-metadata.js');
fs.copyFileSync(scriptSourcePath, scriptDestPath);

// Create directory structure
const configDir = path.join(tempRoot, 'config');
const packageConfigDir = path.join(tempRoot, 'packages', 'claude-code', 'config');
fs.mkdirSync(configDir, { recursive: true });
fs.mkdirSync(packageConfigDir, { recursive: true });

// Create initial config files (both must have identical version keys)
const initialConfig = { versions: {} };
fs.writeFileSync(path.join(configDir, 'claude-native-audited-versions.json'), JSON.stringify(initialConfig, null, 2) + '\n');
fs.writeFileSync(path.join(packageConfigDir, 'claude-native-audited-versions.json'), JSON.stringify(initialConfig, null, 2) + '\n');

// Create package.json
fs.mkdirSync(path.join(tempRoot, 'packages', 'claude-code'), { recursive: true });
fs.writeFileSync(path.join(tempRoot, 'packages', 'claude-code', 'package.json'), JSON.stringify({ version: '0.0.0', name: '@anthropic-ai/claude-code' }, null, 2) + '\n');

// Create offset file WITHOUT cycle_hoists_skipped_assets
const offsetFile = path.join(tempRoot, 'offsets.json');
const offsets = {
entry_format: 'esm-chunked',
tarball_integrity: 'sha512-x',
tarball_sha256: 'y',
num_modules: 100,
byte_count: 1000,
cycle_hoists: [],
};
fs.writeFileSync(offsetFile, JSON.stringify(offsets, null, 2) + '\n');

// Run add-candidate-metadata
const result = cp.spawnSync('node', [scriptDestPath, '9.9.9', offsetFile], {
cwd: tempRoot,
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
});

assert.equal(result.status, 0, `Script failed: ${result.stderr}`);

// Verify both config files do NOT have cycle_hoists_skipped_assets key
const configRoot = JSON.parse(fs.readFileSync(path.join(configDir, 'claude-native-audited-versions.json'), 'utf8'));
const configPackage = JSON.parse(fs.readFileSync(path.join(packageConfigDir, 'claude-native-audited-versions.json'), 'utf8'));

assert.equal(
Object.prototype.hasOwnProperty.call(configRoot.versions['9.9.9'], 'cycle_hoists_skipped_assets'),
false,
'cycle_hoists_skipped_assets should not exist in root config'
);
assert.equal(
Object.prototype.hasOwnProperty.call(configPackage.versions['9.9.9'], 'cycle_hoists_skipped_assets'),
false,
'cycle_hoists_skipped_assets should not exist in package config'
);
} finally {
fs.rmSync(tempRoot, { recursive: true, force: true });
}
});

test('add-candidate-metadata: legacy-cjs ignores cycle_hoists_skipped_assets', () => {
const tempRoot = makeTempDir('add-candidate-metadata-test-');
try {
// Copy script to temp directory
const scriptSourcePath = path.join(__dirname, 'add-candidate-metadata.js');
const scriptTempDir = path.join(tempRoot, 'scripts');
fs.mkdirSync(scriptTempDir, { recursive: true });
const scriptDestPath = path.join(scriptTempDir, 'add-candidate-metadata.js');
fs.copyFileSync(scriptSourcePath, scriptDestPath);

// Create directory structure
const configDir = path.join(tempRoot, 'config');
const packageConfigDir = path.join(tempRoot, 'packages', 'claude-code', 'config');
fs.mkdirSync(configDir, { recursive: true });
fs.mkdirSync(packageConfigDir, { recursive: true });

// Create initial config files (both must have identical version keys)
const initialConfig = { versions: {} };
fs.writeFileSync(path.join(configDir, 'claude-native-audited-versions.json'), JSON.stringify(initialConfig, null, 2) + '\n');
fs.writeFileSync(path.join(packageConfigDir, 'claude-native-audited-versions.json'), JSON.stringify(initialConfig, null, 2) + '\n');

// Create package.json
fs.mkdirSync(path.join(tempRoot, 'packages', 'claude-code'), { recursive: true });
fs.writeFileSync(path.join(tempRoot, 'packages', 'claude-code', 'package.json'), JSON.stringify({ version: '0.0.0', name: '@anthropic-ai/claude-code' }, null, 2) + '\n');

// Create offset file for legacy-cjs WITH cycle_hoists_skipped_assets (should be ignored)
const offsetFile = path.join(tempRoot, 'offsets.json');
const offsets = {
entry_format: 'legacy-cjs',
tarball_integrity: 'sha512-x',
tarball_sha256: 'y',
entry_js_offset: 10,
entry_end_offset: 20,
cycle_hoists_skipped_assets: ['vendor1.js'],
};
fs.writeFileSync(offsetFile, JSON.stringify(offsets, null, 2) + '\n');

// Run add-candidate-metadata
const result = cp.spawnSync('node', [scriptDestPath, '9.9.9', offsetFile], {
cwd: tempRoot,
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
});

assert.equal(result.status, 0, `Script failed: ${result.stderr}`);

// Verify both config files do NOT have cycle_hoists_skipped_assets key (legacy-cjs should ignore it)
const configRoot = JSON.parse(fs.readFileSync(path.join(configDir, 'claude-native-audited-versions.json'), 'utf8'));
const configPackage = JSON.parse(fs.readFileSync(path.join(packageConfigDir, 'claude-native-audited-versions.json'), 'utf8'));

assert.equal(
Object.prototype.hasOwnProperty.call(configRoot.versions['9.9.9'], 'cycle_hoists_skipped_assets'),
false,
'legacy-cjs should not have cycle_hoists_skipped_assets'
);
assert.equal(
Object.prototype.hasOwnProperty.call(configPackage.versions['9.9.9'], 'cycle_hoists_skipped_assets'),
false,
'legacy-cjs should not have cycle_hoists_skipped_assets'
);
} finally {
fs.rmSync(tempRoot, { recursive: true, force: true });
}
});
14 changes: 11 additions & 3 deletions scripts/termux-prepare-claude-native-version.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,13 +129,20 @@ function analyzeCycleHoists(ownedDirForAnalysis, options = {}) {
const asts = new Map();
let parseFailureCount = 0;
const parseFailureFiles = [];
const skippedAssets = [];

for (const f of files) {
const src = fs.readFileSync(path.join(ownedDirForAnalysis, f), 'utf8');
const buf = fs.readFileSync(path.join(ownedDirForAnalysis, f));
const src = buf.toString('utf8');
let ast;
try {
ast = acorn.parse(src, { ecmaVersion: 'latest', sourceType: 'module', allowImportExportEverywhere: true });
} catch (e) {
const ZSTD_MAGIC = Buffer.from([0x28, 0xb5, 0x2f, 0xfd]);
if (!f.startsWith('chunk-') && buf.length >= 4 && buf.subarray(0, 4).equals(ZSTD_MAGIC)) {
skippedAssets.push(f);
continue;
}
parseFailureCount += 1;
parseFailureFiles.push(f);
continue;
Expand Down Expand Up @@ -280,7 +287,7 @@ function analyzeCycleHoists(ownedDirForAnalysis, options = {}) {
}
}

return cycleHoists;
return { cycleHoists, skippedAssets: skippedAssets.sort() };
}

function discoverCycleHoists(binary, ownedDirForAnalysis) {
Expand Down Expand Up @@ -310,12 +317,13 @@ function discoverEsmChunkedOffsets(binary, packDir) {
throw new Error('entry module is legacy-cjs wrapped, not esm-chunked');
}
const cycleAnalysisDir = path.join(packDir, 'cycle-analysis');
const cycleHoists = discoverCycleHoists(binary, cycleAnalysisDir);
const { cycleHoists, skippedAssets } = discoverCycleHoists(binary, cycleAnalysisDir);
return {
entry_format: 'esm-chunked',
num_modules: graph.numModules,
byte_count: graph.byteCount,
cycle_hoists: cycleHoists,
cycle_hoists_skipped_assets: skippedAssets,
};
} finally {
fs.closeSync(graph.fd);
Expand Down
64 changes: 57 additions & 7 deletions scripts/termux-prepare-claude-native-version.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,15 @@ test('analyzeCycleHoists: structural cycle + eager call', () => {
'var x = import.meta.require("/$bunfs/root/A.js").someExport;\nexport const y = "B";\n'
);

const result = analyzeCycleHoists(tempDir);
assert.equal(result.length, 1);
assert.deepEqual(result[0], {
const { cycleHoists, skippedAssets } = analyzeCycleHoists(tempDir);
assert.equal(cycleHoists.length, 1);
assert.deepEqual(cycleHoists[0], {
file: 'B.js',
targetModule: 'A.js',
expectedOccurrences: 1,
assertProperties: ['someExport'],
});
assert.deepEqual(skippedAssets, []);
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
Expand All @@ -57,8 +58,9 @@ test('analyzeCycleHoists: structural cycle + all-delayed calls', () => {
'function f() { var x = import.meta.require("/$bunfs/root/A.js").someExport; }\nexport const y = "B";\n'
);

const result = analyzeCycleHoists(tempDir);
assert.equal(result.length, 0);
const { cycleHoists, skippedAssets } = analyzeCycleHoists(tempDir);
assert.equal(cycleHoists.length, 0);
assert.deepEqual(skippedAssets, []);
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
Expand All @@ -79,8 +81,9 @@ test('analyzeCycleHoists: no cycle', () => {
'export const z = "B";\n'
);

const result = analyzeCycleHoists(tempDir);
assert.equal(result.length, 0);
const { cycleHoists, skippedAssets } = analyzeCycleHoists(tempDir);
assert.equal(cycleHoists.length, 0);
assert.deepEqual(skippedAssets, []);
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
Expand Down Expand Up @@ -109,6 +112,53 @@ test('analyzeCycleHoists: parse failure throws', () => {
}
});

test('analyzeCycleHoists: chunk file parse failure still throws', () => {
const tempDir = makeTempDir('cycle-hoist-test-');
try {
fs.writeFileSync(path.join(tempDir, 'A.js'), 'export const a = 1;\n');
fs.writeFileSync(path.join(tempDir, 'chunk-bad.js'), 'this is {{{ invalid syntax');
assert.throws(() => analyzeCycleHoists(tempDir), /failed to parse/);
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
});

test('analyzeCycleHoists: non-chunk zstd assets are skipped and sorted', () => {
const tempDir = makeTempDir('cycle-hoist-test-');
try {
fs.writeFileSync(path.join(tempDir, 'A.js'), 'export const a = 1;\n');
const zstd = Buffer.concat([Buffer.from([0x28, 0xb5, 0x2f, 0xfd]), Buffer.from('xxxxxx')]);
fs.writeFileSync(path.join(tempDir, 'z.js'), zstd);
fs.writeFileSync(path.join(tempDir, 'a.js'), zstd);
const { skippedAssets } = analyzeCycleHoists(tempDir);
assert.deepEqual(skippedAssets, ['a.js', 'z.js']);
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
});

test('analyzeCycleHoists: non-chunk non-zstd parse failure still throws', () => {
const tempDir = makeTempDir('cycle-hoist-test-');
try {
fs.writeFileSync(path.join(tempDir, 'A.js'), 'export const a = 1;\n');
fs.writeFileSync(path.join(tempDir, 'g.js'), Buffer.concat([Buffer.from([0x1f, 0x8b, 0x08, 0x00]), Buffer.from('data')]));
assert.throws(() => analyzeCycleHoists(tempDir), /failed to parse/);
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
});

test('analyzeCycleHoists: chunk file with zstd magic still throws', () => {
const tempDir = makeTempDir('cycle-hoist-test-');
try {
fs.writeFileSync(path.join(tempDir, 'A.js'), 'export const a = 1;\n');
fs.writeFileSync(path.join(tempDir, 'chunk-z.js'), Buffer.concat([Buffer.from([0x28, 0xb5, 0x2f, 0xfd]), Buffer.from('xxxxxx')]));
assert.throws(() => analyzeCycleHoists(tempDir), /failed to parse/);
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
});

test('analyzeCycleHoists: acorn version mismatch throws', () => {
const tempDir = makeTempDir('cycle-hoist-test-');
try {
Expand Down