63 lines
3.8 KiB
JavaScript
63 lines
3.8 KiB
JavaScript
import { test, expect } from 'bun:test';
|
|
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { createHash } from 'node:crypto';
|
|
import { createAssetManifest } from './asset-manifest.js';
|
|
|
|
const hash = value => createHash('sha256').update(value).digest('hex').slice(0, 10);
|
|
function fixture(fn) {
|
|
const dir = mkdtempSync(join(tmpdir(), 'ytp-manifest-'));
|
|
try {
|
|
writeFileSync(join(dir, 'index.html'), '<meta name="ytp-build" content="__BUILD_TAG__"><link href="/app.css"><script src="app.js?old=1"></script><script src="https://example.com/x.js"></script>');
|
|
writeFileSync(join(dir, 'app.js'), 'window.app=1');
|
|
writeFileSync(join(dir, 'app.css'), 'body{}');
|
|
writeFileSync(join(dir, 'sw.js'), "const BUILD_TAG = typeof __BUILD_TAG__ !== 'undefined' ? __BUILD_TAG__ : 'old';");
|
|
writeFileSync(join(dir, 'assets.json'), JSON.stringify({ groups: { core: { contract: 1, eager: true, files: ['/index.html', '/app.js', '/app.css'] } } }));
|
|
fn(dir);
|
|
} finally { rmSync(dir, { recursive: true, force: true }); }
|
|
}
|
|
test('manifest determinism, final served byte hashes and one-file URL isolation', () => fixture(dir => {
|
|
const first = createAssetManifest(dir);
|
|
expect(createAssetManifest(dir)).toEqual(first);
|
|
expect(first.manifest.buildTag).toMatch(/^[a-f0-9]{12}$/);
|
|
expect(first.manifest.files['/app.js']).toEqual({ h: hash('window.app=1'), s: 12, g: 'core' });
|
|
expect(first.manifest.contracts.core).toBe(1);
|
|
expect(first.manifest.groups.core.eager).toBe(true);
|
|
expect(first.index).toContain(`/app.css?v=${first.manifest.files['/app.css'].h}`);
|
|
expect(first.index).toContain(`app.js?old=1&v=${first.manifest.files['/app.js'].h}`);
|
|
expect(first.index).toContain('https://example.com/x.js');
|
|
expect(first.index).toContain(`content="${first.manifest.buildTag}"`);
|
|
expect(first.manifest.files['/index.html'].h).toBe(hash(first.index));
|
|
expect(first.manifest.files['/sw.js'].h).toBe(hash(first.sw));
|
|
writeFileSync(join(dir, 'app.js'), 'window.app=2');
|
|
const second = createAssetManifest(dir);
|
|
expect(second.manifest.buildTag).not.toBe(first.manifest.buildTag);
|
|
for (const path of ['/app.css', '/assets.json']) expect(second.manifest.files[path]).toEqual(first.manifest.files[path]);
|
|
expect(second.manifest.files['/app.js'].h).not.toBe(first.manifest.files['/app.js'].h);
|
|
}));
|
|
test('unlisted files affect build identity; group contracts and order affect identity', () => fixture(dir => {
|
|
const first = createAssetManifest(dir);
|
|
writeFileSync(join(dir, 'other.bin'), 'shipped');
|
|
const second = createAssetManifest(dir);
|
|
expect(second.manifest.buildTag).not.toBe(first.manifest.buildTag);
|
|
expect(second.manifest.files['/other.bin'].h).toBe(hash('shipped'));
|
|
writeFileSync(join(dir, 'assets.json'), JSON.stringify({ groups: { core: { contract: 2, eager: true, files: ['/app.css', '/app.js', '/index.html'] } } }));
|
|
expect(createAssetManifest(dir).manifest.buildTag).not.toBe(second.manifest.buildTag);
|
|
}));
|
|
test('invalid group definitions fail visibly', () => fixture(dir => {
|
|
writeFileSync(join(dir, 'assets.json'), JSON.stringify({ groups: { core: { contract: 1, eager: true, files: ['/missing.js'] } } }));
|
|
expect(() => createAssetManifest(dir)).toThrow('missing');
|
|
}));
|
|
test('rollback retains legacy tag algorithm and relative-only single-tag stamping', () => fixture(dir => {
|
|
const result = createAssetManifest(dir, { hashing: false });
|
|
const expected = createHash('sha256');
|
|
for (const name of ['app.css', 'app.js', 'assets.json', 'index.html', 'sw.js']) {
|
|
expected.update(`./public/${name}`);
|
|
const { readFileSync } = require('node:fs');
|
|
expected.update(readFileSync(join(dir, name)));
|
|
}
|
|
expect(result.manifest.buildTag).toBe(expected.digest('hex').slice(0, 12));
|
|
expect(result.index).toContain('href="/app.css"');
|
|
}));
|