import { describe, it, before, after } from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import path from 'node:path'; import os from 'node:os'; import { rewriteAssetFields, checkAssetFieldsExist } from './rewrite.ts'; let tmpRoot; let entryDir; let cmsRoot; function setup() { tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cms-rewrite-')); cmsRoot = tmpRoot; entryDir = path.join(tmpRoot, 'data', 'entry'); fs.mkdirSync(entryDir, { recursive: true }); fs.mkdirSync(path.join(tmpRoot, 'assets'), { recursive: true }); fs.mkdirSync(path.join(tmpRoot, 'media'), { recursive: true }); fs.writeFileSync(path.join(tmpRoot, 'assets', 'x.png'), 'png'); fs.writeFileSync(path.join(tmpRoot, 'media', 'logo.png'), 'png'); } function teardown() { fs.rmSync(tmpRoot, { recursive: true, force: true }); } function ctx(extras = {}) { return { env: 'pre', cdnBase: 'https://cdn.example.com', version: 'abc12345', entryDir, cmsRoot, ...extras, }; } describe('rewriteAssetFields - absolute refs preserved', () => { before(() => setup()); after(() => teardown()); const cases = [ 'https://example.com/x.png', 'http://example.com/x.png', 'cloud://bucket/path/x.zip', 'data:image/png;base64,iVBORw0KGgo=', 'mailto:a@b.com', '//cdn.example.com/x.png', ]; for (const v of cases) { it(`preserves: ${v.slice(0, 40)}`, () => { assert.equal(rewriteAssetFields(v, ctx()), v); }); } }); describe('rewriteAssetFields - relative refs rewritten', () => { before(() => setup()); after(() => teardown()); it('../../assets/x.png → CDN URL', () => { assert.equal( rewriteAssetFields('../../assets/x.png', ctx()), 'https://cdn.example.com/cms/pre/assets/x.png?v=abc12345', ); }); it('@/assets/x.png → CDN URL (alias)', () => { assert.equal( rewriteAssetFields('@/assets/x.png', ctx()), 'https://cdn.example.com/cms/pre/assets/x.png?v=abc12345', ); }); it('@/media/logo.png → CDN URL (outside assets/)', () => { assert.equal( rewriteAssetFields('@/media/logo.png', ctx()), 'https://cdn.example.com/cms/pre/media/logo.png?v=abc12345', ); }); }); describe('rewriteAssetFields - non-ref strings untouched', () => { before(() => setup()); after(() => teardown()); const cases = ['some title', '使用 npm/yarn', 'react-starter', '']; for (const v of cases) { it(`untouched: ${JSON.stringify(v)}`, () => { assert.equal(rewriteAssetFields(v, ctx()), v); }); } }); describe('checkAssetFieldsExist', () => { before(() => setup()); after(() => teardown()); it('existing relative ref → no error', () => { const errors = []; checkAssetFieldsExist({ icon: '../../assets/x.png' }, entryDir, cmsRoot, errors, 'zh'); assert.deepEqual(errors, []); }); // isResourceRef 只识别 @/ 开头或含 /assets 的路径,故越界/缺失用例需带 /assets 才能命中候选判定 it('outside cms/ → error', () => { const errors = []; checkAssetFieldsExist({ icon: '../../../assets/passwd' }, entryDir, cmsRoot, errors, 'zh'); assert.equal(errors.length, 1); assert.match(errors[0], /越出 cms\/ 目录/); }); it('missing file → error', () => { const errors = []; checkAssetFieldsExist({ icon: './assets/missing.png' }, entryDir, cmsRoot, errors, 'zh'); assert.equal(errors.length, 1); assert.match(errors[0], /资源文件不存在/); }); it('absolute refs → no error', () => { const errors = []; checkAssetFieldsExist( { a: 'https://x.com/y.png', b: 'cloud://b/z.zip', c: 'data:image/png;base64,xx' }, entryDir, cmsRoot, errors, 'zh', ); assert.deepEqual(errors, []); }); it('plain strings → no error', () => { const errors = []; checkAssetFieldsExist({ t: '使用 npm/yarn', u: 'react-starter' }, entryDir, cmsRoot, errors, 'zh'); assert.deepEqual(errors, []); }); }); describe('onAsset callback', () => { before(() => setup()); after(() => teardown()); it('invoked on rewrite with absInRepo + relFromCms', () => { const seen = []; const c = ctx({ onAsset: (abs, rel) => seen.push({ abs, rel }) }); rewriteAssetFields('@/media/logo.png', c); assert.equal(seen.length, 1); assert.equal(seen[0].rel, 'media/logo.png'); assert.equal(seen[0].abs, path.join(cmsRoot, 'media', 'logo.png')); }); it('assets/ ref also reported (build decides to skip)', () => { const seen = []; const c = ctx({ onAsset: (abs, rel) => seen.push(rel) }); rewriteAssetFields('../../assets/x.png', c); assert.deepEqual(seen, ['assets/x.png']); }); it('not invoked for absolute refs', () => { const seen = []; const c = ctx({ onAsset: () => seen.push(true) }); rewriteAssetFields('https://x.com/y.png', c); rewriteAssetFields('cloud://b/z.zip', c); assert.equal(seen.length, 0); }); it('not invoked for plain strings', () => { const seen = []; const c = ctx({ onAsset: () => seen.push(true) }); rewriteAssetFields('使用 npm/yarn', c); assert.equal(seen.length, 0); }); });