'use strict'; /** * DC-119: frontend build determinism across line endings. * * The frontend build (status/build.js) concatenates raw source files and * minifies them with esbuild using sourcemap:'both' — the inline map * base64-embeds the raw source bytes (sourcesContent), CRs included. A CRLF * working copy (Windows dev tree, core.autocrlf=true) vs an LF working copy * (DNS2 Linux checkout) of the SAME commit therefore produces different * dist bytes and a different sw.js cache tag — so the committed dist could * never be reproduced on the deploy host, showing up as permanent phantom * drift on `git pull` in /opt/dashcaddy (the recurring "pre-pull drift" * stashes). * * build.js now normalizes every source read to LF (\r\n -> \n) before * concatenation. This test pins that behavior at the transform level: the * SAME input, CRLF vs LF, must produce byte-identical minified output, and * the normalization regex used by build.js must strip all CR bytes. * * It deliberately does NOT shell out to `node build.js` (slow, writes * dist/) — it exercises the exact transform + normalization logic inline. */ const test = require('node:test'); const assert = require('node:assert'); // Same devDependency esbuild the build itself uses. const esbuild = require('esbuild'); // DC-119: import THE ACTUAL normalization from build.js — not a local // re-implementation — so this test fails if the build's regex ever changes. const { normalizeSource: normalize } = require('../build.js'); // Representative source: top-level names, nested scopes, strings with // escapes, template literals, regex literals, comments — the constructs // whose minified renames shifted pre-fix. const SAMPLE = `// feature module const logoCustomization = { position: 'left', cacheTag: 'dashcaddy-shell-abc123', }; function applyPosition(position, elem) { const normalized = position || logoCustomization.position; elem.setAttribute('data-logo-pos', normalized); return normalized.replace(/_/g, ' '); } const summarize = (items) => { let total = 0; for (const item of items) { total += item.count ?? 0; } return \`total: \${total} (\${items.length} items)\`; }; async function loadConfig(url) { try { const res = await fetch(url); if (!res.ok) throw new Error('HTTP ' + res.status); return await res.json(); } catch (err) { console.warn('load failed:', err.message); return null; } } module.exports = { applyPosition, summarize, loadConfig }; `; // Production transform options — MUST mirror build.js. The CRLF divergence // lives in the INLINE SOURCEMAP: sourcemap:'both' embeds the raw source as // base64 sourcesContent, so CRLF bytes survive into dist and shift both the // bundle bytes and the sw.js content-hash cache tag. async function minify(source) { const { code } = await esbuild.transform(source, { minify: true, target: 'es2020', sourcemap: 'both', }); return code; } test('DC-119: CRLF and LF inputs produce byte-identical minified output', async () => { const lf = SAMPLE; const crlf = SAMPLE.replace(/\n/g, '\r\n'); // Sanity: the two raw inputs really do differ. assert.notEqual(lf, crlf, 'fixture setup: CRLF variant must differ from LF'); const outLf = await minify(normalize(lf)); const outCrlf = await minify(normalize(crlf)); assert.strictEqual( outCrlf, outLf, 'minified output must be byte-identical after CRLF->LF normalization' ); }); test('DC-119: without normalization, CRLF vs LF differ (documents the bug)', async () => { const lf = SAMPLE; const crlf = SAMPLE.replace(/\n/g, '\r\n'); const outLf = await minify(lf); const outCrlf = await minify(crlf); // Documents WHY the normalization exists: the inline sourcemap's // sourcesContent base64-encodes the raw bytes, CRs included. If esbuild // ever normalizes sourcesContent itself, this may flip to equal — then // the normalization is redundant but harmless; update the DC-119 comment // in build.js when that happens. assert.notEqual( outCrlf, outLf, 'expected CRLF/LF divergence pre-normalization (inline sourcemap sourcesContent); if equal, esbuild changed behavior — update the DC-119 comment in build.js' ); }); test('DC-119: normalization strips every CR from CRLF input and leaves LF untouched', () => { const crlf = 'line1\r\nline2\r\n'; const lf = 'line1\nline2\n'; // Lone \r (old-Mac style) is NOT produced by git autocrlf and is NOT // claimed to be handled — assert only the CRLF contract. assert.strictEqual(normalize(crlf), 'line1\nline2\n'); assert.strictEqual(normalize(lf), 'line1\nline2\n'); assert.ok(!normalize(crlf).includes('\r'), 'no CR may survive normalization'); });