This is an automated email from the ASF dual-hosted git repository.

lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git


The following commit(s) were added to refs/heads/rocketmq-studio by this push:
     new b1144f43c fix(build): hash finalized frontend artifacts in the license 
manifest (#5019)
b1144f43c is described below

commit b1144f43c1cc9f5f45dea65e963a09e88fe5fc8f
Author: zmuxuny <[email protected]>
AuthorDate: Fri Oct 9 23:39:21 2026 -0700

    fix(build): hash finalized frontend artifacts in the license manifest 
(#5019)
    
    The license gate hashed the in-memory chunk code, while Vite's 
`buildImportAnalysisPlugin` prepends
    `__vite__mapDeps` to the chunk that actually lands on disk - so one 
manifest entry always mismatched and
    `npm run license:check`, `npm run build` and with them the whole frontend 
CI job failed. `writeBundle`
    now recomputes the hash from the bytes written. The tamper detection itself 
is unchanged.
    
    `npm run license:test` 5/5 green. This was the only surviving fix for that 
trunk red light.
---
 web/scripts/licenses.mjs      | 13 +++++++++++--
 web/scripts/licenses.test.mjs | 41 ++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 51 insertions(+), 3 deletions(-)

diff --git a/web/scripts/licenses.mjs b/web/scripts/licenses.mjs
index d48d68857..42278db04 100644
--- a/web/scripts/licenses.mjs
+++ b/web/scripts/licenses.mjs
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 import { createHash } from 'node:crypto';
-import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
+import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from 
'node:fs';
 import path from 'node:path';
 import { fileURLToPath } from 'node:url';
 
@@ -175,6 +175,7 @@ export function collectLicenses(moduleIds, base = root) {
 
 export function distributionLicenses() {
   let base;
+  let manifest;
   return {
     name: 'distribution-licenses',
     apply: 'build',
@@ -195,13 +196,21 @@ export function distributionLicenses() {
       for (const [name, item] of Object.entries(bundle)) {
         outputFiles[name] = sha(item.type === 'chunk' ? item.code : 
item.source);
       }
-      const manifest = { modules: result.modules, components: 
result.components, outputFiles, files: {} };
+      manifest = { modules: result.modules, components: result.components, 
outputFiles, files: {} };
       for (const [name, data] of result.files) {
         manifest.files[name] = sha(data);
         this.emitFile({ type: 'asset', fileName: name, source: data });
       }
       this.emitFile({ type: 'asset', fileName: 'legal/manifest.json', source: 
`${JSON.stringify(manifest, null, 2)}\n` });
     },
+    writeBundle({ dir }) {
+      // Vite may rewrite the entry chunk after this plugin's generateBundle 
hook.
+      // Record the bytes that were actually distributed, before the 
post-build gate runs.
+      for (const name of Object.keys(manifest.outputFiles)) {
+        manifest.outputFiles[name] = sha(read(path.join(dir, name)));
+      }
+      writeFileSync(path.join(dir, 'legal/manifest.json'), 
`${JSON.stringify(manifest, null, 2)}\n`);
+    },
   };
 }
 
diff --git a/web/scripts/licenses.test.mjs b/web/scripts/licenses.test.mjs
index 75bb3188d..35c100e87 100644
--- a/web/scripts/licenses.test.mjs
+++ b/web/scripts/licenses.test.mjs
@@ -64,6 +64,7 @@ test('missingOrUnknownLicenseFailsTest', (t) => {
 
 test('vitePackagingAndTamperGateTest', async (t) => {
   const directory = temporary(t);
+  const licensePlugin = distributionLicenses();
   // Build only fixtures for React, CSS, SVG and small dependencies; do not 
build the app or run app tests.
   const result = await build({
     root,
@@ -77,7 +78,7 @@ test('vitePackagingAndTamperGateTest', async (t) => {
           if (id === '\0license-fixture') return `import React from 
'${root}node_modules/react/index.js'; import logo from 
'${root}src/assets/model-logos/openai.svg'; import '${root}src/index.css'; 
import toggle from '${root}node_modules/toggle-selection/index.js'; 
console.log(React, logo, toggle);`;
         },
       },
-      distributionLicenses(),
+      licensePlugin,
     ],
     build: { write: false, minify: false, rollupOptions: { input: 
'license-fixture' } },
   });
@@ -93,6 +94,44 @@ test('vitePackagingAndTamperGateTest', async (t) => {
   assert.throws(() => checkDistribution(directory), /modified or missing/);
   write(directory, 'NOTICE', result.output.find((item) => item.fileName === 
'NOTICE').source);
   const output = Object.keys(manifest.outputFiles)[0];
+  write(directory, output, Buffer.concat([readFileSync(path.join(directory, 
output)), Buffer.from('\n// late build rewrite')]));
+  assert.throws(() => checkDistribution(directory), /build artifact 
verification failed/);
+  licensePlugin.writeBundle({ dir: directory });
+  checkDistribution(directory);
   write(directory, output, 'tampered');
   assert.throws(() => checkDistribution(directory), /build artifact 
verification failed/);
 });
+
+test('viteWriteLifecycleHashesTheFinalChunkTest', async (t) => {
+  const directory = temporary(t);
+  const source = mkdtempSync(path.join(root, 'src/.license-test-'));
+  t.after(() => rmSync(source, { recursive: true, force: true }));
+  write(source, 'index.html', '<script type="module" 
src="./main.js"></script>');
+  write(source, 'main.js', "import React from 'react'; 
console.log(React.version);");
+  const output = path.join(directory, 'dist');
+
+  await build({
+    root,
+    configFile: false,
+    logLevel: 'error',
+    plugins: [
+      distributionLicenses(),
+      {
+        name: 'late-chunk-rewrite',
+        enforce: 'post',
+        generateBundle(_options, bundle) {
+          const entry = Object.values(bundle).find((item) => item.type === 
'chunk' && item.isEntry);
+          assert(entry, 'fixture must emit an entry chunk');
+          entry.code += '\n// rewritten after the license manifest was 
generated\n';
+        },
+      },
+    ],
+    build: { outDir: output, emptyOutDir: false, rollupOptions: { input: 
path.join(source, 'index.html') } },
+  });
+
+  checkDistribution(output);
+  const manifest = JSON.parse(readFileSync(path.join(output, 
'legal/manifest.json')));
+  const entry = Object.keys(manifest.outputFiles).find((name) => 
name.endsWith('.js'));
+  assert(entry, 'manifest must include the emitted JavaScript');
+  assert.match(readFileSync(path.join(output, entry), 'utf8'), /rewritten 
after the license manifest/);
+});

Reply via email to