From 4632fd764e116239a96829768cbf50428972dc5e Mon Sep 17 00:00:00 2001 From: YamadaBlog Date: Thu, 11 Jun 2026 22:52:54 +0200 Subject: [PATCH 1/2] fix(packaging+perf): rc.2 rebuilt packages + consumer smoke + demo perf (round 9) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audit #5/#6 closure — the npm rc.0/rc.1 publishes shipped dist/ built before the @pulse/* -> @pulse-music/* rename (unusable for consumers). - bump buildable packages to 3.0.0-rc.2, add prepublishOnly builds - scripts/consumer-smoke.mjs + CI workflow: pack -> install into clean consumers (wc/react/svelte/vue) -> vite build -> stale-scope assert - demo perf: async chunks + staged idle mounting for below-fold sections (entry 130.94 -> 81.04 kB gzip), transform-only hero animation (LCP), idle early-bail in audio backdrop - AudioBars: one cached canvas-space gradient + single fill per frame (was 64 createLinearGradient/frame) Co-Authored-By: Claude Fable 5 --- .github/workflows/consumer-smoke.yml | 43 ++++++++ package.json | 3 +- packages/core/package.json | 5 +- packages/react/package.json | 5 +- packages/svelte/package.json | 5 +- packages/tokens/package.json | 3 +- packages/types/package.json | 3 +- packages/web-component/package.json | 5 +- scripts/consumer-smoke.mjs | 153 +++++++++++++++++++++++++++ src/App.vue | 89 +++++++++++----- src/components/AudioBars.vue | 31 ++++-- src/composables/usePremiumMotion.ts | 11 +- 12 files changed, 307 insertions(+), 49 deletions(-) create mode 100644 .github/workflows/consumer-smoke.yml create mode 100644 scripts/consumer-smoke.mjs diff --git a/.github/workflows/consumer-smoke.yml b/.github/workflows/consumer-smoke.yml new file mode 100644 index 0000000..e4423f9 --- /dev/null +++ b/.github/workflows/consumer-smoke.yml @@ -0,0 +1,43 @@ +name: Consumer smoke (packed artefacts) + +# Audit №5 root-cause closure : the rc.0/rc.1 npm publish shipped +# stale dist/ files importing the pre-rename @pulse/* scope. Every CI +# suite was green because everything ran on workspace SOURCES — no job +# ever installed a tarball the way a real user does. This workflow +# packs each buildable package (plus the root Vue lib), installs the +# tarballs into throwaway consumer projects, vite-builds an entry that +# imports the public surface, and asserts no stale-scope strings in +# the output bundles. If this is red, DO NOT PUBLISH. + +on: + push: + branches: [main, 'feat/**'] + pull_request: + branches: [main] + +concurrency: + group: consumer-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + smoke: + name: Pack → install → build (4 consumers) + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v6 + + - name: Setup Node 20 + uses: actions/setup-node@v6 + with: + node-version: 20 + cache: 'npm' + + - name: Install dependencies + run: npm ci --no-audit --prefer-offline + + - name: Build workspace packages + run: npm run build:packages + + - name: Consumer smoke + run: npm run test:consumer diff --git a/package.json b/package.json index 451093a..44b023e 100644 --- a/package.json +++ b/package.json @@ -89,7 +89,8 @@ "check:lib-identical": "node scripts/check-lib-byte-identical.mjs", "ci": "npm run type-check && npm run lint && npm run test && npm run test:packages && npm run build && npm run audit && npm run check:lib-identical", "prepublishOnly": "npm run ci && npm run build:lib", - "prepare": "husky 2>/dev/null || true" + "prepare": "husky 2>/dev/null || true", + "test:consumer": "node scripts/consumer-smoke.mjs" }, "peerDependencies": { "pinia": "^2.1.0", diff --git a/packages/core/package.json b/packages/core/package.json index f127f7d..069424f 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@pulse-music/core", - "version": "3.0.0-rc.0", + "version": "3.0.0-rc.2", "description": "Framework-agnostic audio engine for pulse-player — Web Audio + state machine + typed event bus. Pure TypeScript, no DOM, no framework imports.", "license": "MIT", "type": "module", @@ -22,7 +22,8 @@ "scripts": { "build": "tsup", "test": "vitest run", - "test:watch": "vitest" + "test:watch": "vitest", + "prepublishOnly": "npm run build" }, "dependencies": { "@pulse-music/types": "*" diff --git a/packages/react/package.json b/packages/react/package.json index 3e67af4..49a761e 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@pulse-music/react", - "version": "3.0.0-rc.0", + "version": "3.0.0-rc.2", "description": "React 18 / 19 wrapper for pulse-player — hooks + JSX components built on @pulse-music/web-component.", "license": "MIT", "type": "module", @@ -22,7 +22,8 @@ "scripts": { "build": "tsup", "test": "vitest run", - "test:watch": "vitest" + "test:watch": "vitest", + "prepublishOnly": "npm run build" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", diff --git a/packages/svelte/package.json b/packages/svelte/package.json index b053975..66b23e9 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -1,6 +1,6 @@ { "name": "@pulse-music/svelte", - "version": "3.0.0-rc.0", + "version": "3.0.0-rc.2", "description": "Svelte 5 wrapper for pulse-player. Plain TypeScript hook + Svelte classic-store contract over @pulse-music/web-component.", "license": "MIT", "type": "module", @@ -22,7 +22,8 @@ "scripts": { "build": "tsup", "test": "vitest run", - "test:watch": "vitest" + "test:watch": "vitest", + "prepublishOnly": "npm run build" }, "peerDependencies": { "svelte": "^5.56.3" diff --git a/packages/tokens/package.json b/packages/tokens/package.json index b4453e3..2cf5e46 100644 --- a/packages/tokens/package.json +++ b/packages/tokens/package.json @@ -29,7 +29,8 @@ "scripts": { "build": "tsup", "test": "vitest run", - "test:watch": "vitest" + "test:watch": "vitest", + "prepublishOnly": "npm run build" }, "publishConfig": { "access": "public" diff --git a/packages/types/package.json b/packages/types/package.json index 3ca4182..fc882af 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -20,7 +20,8 @@ "README.md" ], "scripts": { - "build": "tsup" + "build": "tsup", + "prepublishOnly": "npm run build" }, "publishConfig": { "access": "public" diff --git a/packages/web-component/package.json b/packages/web-component/package.json index 8d844c8..702cb31 100644 --- a/packages/web-component/package.json +++ b/packages/web-component/package.json @@ -1,6 +1,6 @@ { "name": "@pulse-music/web-component", - "version": "3.0.0-rc.1", + "version": "3.0.0-rc.2", "description": "Universal Web Component renderer for pulse-player — Lit-based `` and ``. Works in React, Vue, Angular, Svelte, Solid, vanilla JS, or plain HTML.", "license": "MIT", "type": "module", @@ -22,7 +22,8 @@ "scripts": { "build": "tsup", "test": "vitest run", - "test:watch": "vitest" + "test:watch": "vitest", + "prepublishOnly": "npm run build" }, "dependencies": { "@pulse-music/core": "*", diff --git a/scripts/consumer-smoke.mjs b/scripts/consumer-smoke.mjs new file mode 100644 index 0000000..18d9365 --- /dev/null +++ b/scripts/consumer-smoke.mjs @@ -0,0 +1,153 @@ +#!/usr/bin/env node +/** + * consumer-smoke.mjs — prove the PACKED artefacts work for a real + * consumer, not just the workspace sources. + * + * Audit №5 root-cause : the rc.0/rc.1 npm publish shipped dist/ files + * built BEFORE the @pulse/* → @pulse-music/* scope rename. Every + * repo test passed (they run on sources via workspaces) while every + * actual `npm install @pulse-music/web-component && vite build` + * failed with "Rollup failed to resolve import '@pulse/core'". + * Nothing in CI consumed a tarball — this script closes that hole. + * + * What it does, per buildable package + the root Vue lib : + * 1. `npm pack` the package (after the workspace build). + * 2. Scaffold a THROWAWAY consumer project in a temp dir + * (real package.json, no workspace links). + * 3. Install the tarball(s) from disk. + * 4. Vite-build an entry that imports the public surface. + * 5. Assert the bundle contains no stale-scope imports. + * + * Exit codes : 0 = all consumers build ; 1 = a consumer failed ; + * the failing package and the build output are printed. + * + * Usage : node scripts/consumer-smoke.mjs + * (CI: .github/workflows/consumer-smoke.yml) + */ + +import { execSync } from 'node:child_process' +import { mkdtempSync, writeFileSync, mkdirSync, readdirSync, rmSync, readFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { fileURLToPath } from 'node:url' + +const ROOT = join(fileURLToPath(import.meta.url), '..', '..') +const sh = (cmd, cwd) => execSync(cmd, { cwd, stdio: 'pipe', encoding: 'utf-8' }) + +/** Pack a workspace package, return absolute tarball path. */ +function pack(dir) { + const out = sh('npm pack --silent', dir).trim().split('\n').pop() + return join(dir, out) +} + +function scaffold(name) { + const dir = mkdtempSync(join(tmpdir(), `pulse-smoke-${name}-`)) + mkdirSync(join(dir, 'src'), { recursive: true }) + return dir +} + +function buildConsumer(dir, entryCode, deps, extraPkg = {}) { + writeFileSync( + join(dir, 'package.json'), + JSON.stringify( + { + name: 'smoke', + private: true, + type: 'module', + scripts: { build: 'vite build' }, + devDependencies: { vite: '^5.0.0', ...(extraPkg.devDependencies ?? {}) }, + dependencies: extraPkg.dependencies ?? {}, + }, + null, + 2, + ), + ) + writeFileSync(join(dir, 'index.html'), '
') + writeFileSync(join(dir, 'src/main.js'), entryCode) + if (extraPkg.files) for (const [p, c] of Object.entries(extraPkg.files)) writeFileSync(join(dir, p), c) + sh('npm install --no-audit --silent --fund=false', dir) + if (deps.length) sh(`npm install --no-audit --silent --fund=false ${deps.map((d) => JSON.stringify(d)).join(' ')}`, dir) + sh('npm run build', dir) + // Stale-scope assertion on the produced bundle. + const assets = join(dir, 'dist', 'assets') + for (const f of readdirSync(assets)) { + const body = readFileSync(join(assets, f), 'utf-8') + if (/@pulse\//.test(body)) throw new Error(`stale @pulse/ scope found in built ${f}`) + } +} + +const failures = [] + +function run(name, fn) { + process.stdout.write(`▶ consumer: ${name} … `) + try { + fn() + console.log('✅') + } catch (err) { + console.log('❌') + failures.push({ name, err: String(err.stdout || err.message || err).slice(-2000) }) + } +} + +// ─── 1. Web Component chain (types + tokens + core + web-component) ── +run('web-component (vanilla)', () => { + const tarballs = ['types', 'tokens', 'core', 'web-component'].map((p) => pack(join(ROOT, 'packages', p))) + const dir = scaffold('wc') + buildConsumer(dir, `import '@pulse-music/web-component'\nif(!customElements.get('pulse-player')) throw new Error('element not registered')\n`, tarballs) + rmSync(dir, { recursive: true, force: true }) +}) + +// ─── 2. React wrapper chain ────────────────────────────────────────── +run('react wrapper', () => { + const tarballs = ['types', 'tokens', 'core', 'web-component', 'react'].map((p) => pack(join(ROOT, 'packages', p))) + const dir = scaffold('react') + buildConsumer( + dir, + `import { PulsePlayer } from '@pulse-music/react'\nimport React from 'react'\nimport { createRoot } from 'react-dom/client'\ncreateRoot(document.getElementById('app')).render(React.createElement(PulsePlayer, { variant: 'aurora' }))\n`, + tarballs, + { dependencies: { react: '^18.3.1', 'react-dom': '^18.3.1' } }, + ) + rmSync(dir, { recursive: true, force: true }) +}) + +// ─── 3. Svelte wrapper chain (plain TS hook — no compiler needed) ──── +run('svelte wrapper', () => { + const tarballs = ['types', 'tokens', 'core', 'web-component', 'svelte'].map((p) => pack(join(ROOT, 'packages', p))) + const dir = scaffold('svelte') + buildConsumer(dir, `import { usePulseAudio, ALL_VARIANTS } from '@pulse-music/svelte'\nconsole.log(typeof usePulseAudio, ALL_VARIANTS.length)\n`, tarballs) + rmSync(dir, { recursive: true, force: true }) +}) + +// ─── 4. Root Vue library tarball ───────────────────────────────────── +run('vue root lib', () => { + sh('npm run build:lib', ROOT) + const tarball = pack(ROOT) + const dir = scaffold('vue') + buildConsumer( + dir, + `import { createApp, h } from 'vue'\nimport { createPinia } from 'pinia'\nimport { MusicPlayer, setAudioTracks } from 'pulse-player'\nimport 'pulse-player/style.css'\nsetAudioTracks([{ title: 'T', src: '/x.webm', cover: '/x.webp', coverPos: '50% 50%' }])\ncreateApp({ render: () => h(MusicPlayer, { variant: 'midnight' }) }).use(createPinia()).mount('#app')\n`, + [tarball], + { + dependencies: { vue: '^3.4.0', pinia: '^2.1.0' }, + devDependencies: { '@vitejs/plugin-vue': '^5.0.0' }, + files: { + 'vite.config.js': `import { defineConfig } from 'vite'\nimport vue from '@vitejs/plugin-vue'\nexport default defineConfig({ plugins: [vue()] })\n`, + }, + }, + ) + rmSync(dir, { recursive: true, force: true }) + rmSync(tarball, { force: true }) +}) + +// Clean package tarballs left in workspace dirs. +for (const p of ['types', 'tokens', 'core', 'web-component', 'react', 'svelte']) { + const dir = join(ROOT, 'packages', p) + for (const f of readdirSync(dir)) if (f.endsWith('.tgz')) rmSync(join(dir, f), { force: true }) +} + +if (failures.length) { + console.error(`\n✗ ${failures.length} consumer build(s) FAILED :`) + for (const f of failures) console.error(`\n── ${f.name} ──\n${f.err}`) + process.exit(1) +} +console.log('\n✅ every packed artefact builds in a clean consumer project.') diff --git a/src/App.vue b/src/App.vue index bb04797..537e5e3 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,5 +1,5 @@