SOURCE / PINNED RELEASE
Made of little things.
Supersonic RC Revive
- Release
- 1ba42f1ca1d6…
- Author-recorded commit
- baecad10b1cd…
- License
- LICENSE
- Author’s source reference
- nostr://npub1ye5ptcxfyyxl5vjvdjar2ua3f0hynkjzpx552mu5snj3qmx5pzjscpknpr/wss%3A%2F%2Fgit.napplet.soy%2F/n-143146b0d6f
Archive hash verified: 90d22b206672eba4…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
import { existsSync } from 'node:fs';
import { gzipSync } from 'node:zlib';
import { fileURLToPath } from 'node:url';
import { defineConfig, loadEnv } from 'vite';
import { nip5aManifest } from '@napplet/vite-plugin';
import type { Plugin } from 'vite';
import { assetPack, CUSTOM_PACK } from './tools/build/asset-pack';
/**
* rapier3d-compat inlines its WASM as plain base64 (4.1 MB of the bundle).
* Re-embed it gzip-compressed and inflate it in memory with
* DecompressionStream at init, as soyLI's own WASM recipe does. Bindings the
* game never calls are stripped first (tools/build/rapier-slim.ts): ≈1.1 MB.
*
* wasm-bindgen's loader also keeps a dormant `fetch(url)` branch for URL
* inputs. Napplets have no network authority, so replace that branch with an
* explicit error.
*/
function rapierWasm(full: boolean): Plugin {
return {
name: 'ssrc-rapier-wasm',
async transform(code, id) {
if (!id.includes('rapier3d-compat')) return null;
this.addWatchFile(fileURLToPath(new URL('./tools/build/rapier-exports.json', import.meta.url)));
const embedded = /[\w$]+\.toByteArray\("(AGFzbQ[A-Za-z0-9+/=]+)"\)/.exec(code);
let wasm: Uint8Array | null = embedded ? Buffer.from(embedded[1], 'base64') : null;
if (wasm && !full) {
// binaryen is large; load it only when there is a module to strip.
const { rapierKeepList, slimRapierWasm } = await import('./tools/build/rapier-slim');
wasm = slimRapierWasm(wasm, rapierKeepList(fileURLToPath(new URL('.', import.meta.url))));
}
const out = code
.replace(/\(A=fetch\(A\)\)/g, '(A=(()=>{throw new Error("network WASM loading is not available")})())')
.replace(/[\w$]+\.toByteArray\("(AGFzbQ[A-Za-z0-9+/=]+)"\)/, () => {
const packed = gzipSync(wasm!, { level: 9 }).toString('base64');
return (
// The caller reads `.buffer` off the decoded bytes, so yield a Uint8Array.
'new Uint8Array(yield (typeof DecompressionStream>"u"' +
'?Promise.reject(new Error("This browser is too old to run the physics engine (no DecompressionStream)."))' +
`:new Response(new Blob([Uint8Array.from(atob("${packed}"),c=>c.charCodeAt(0))]).stream()` +
'.pipeThrough(new DecompressionStream("gzip"))).arrayBuffer()))'
);
});
return out === code ? null : { code: out, map: null };
},
};
}
export default defineConfig(({ command, mode }) => {
// Original SuperSonic RC assets (ref-out/game, built by tools/extract) are
// copyrighted reference material. They are bundled only when explicitly
// requested for local development; any other build gets the custom bundle
// (src/game/assets-custom, generated by tools/assets).
// Request them with SSRC_ORIGINAL_ASSETS=1 in the shell, or in the gitignored
// .env.local for `soyli dev`: soyli runs Vite with a scrubbed environment, so
// only the file reaches it. The file is honoured only by the dev server and watch
// builds (how soyli dev builds), never by the one-shot builds behind soyli
// build/publish.
const fileEnv = loadEnv(mode, process.cwd(), 'SSRC_');
const env = { ...fileEnv, ...process.env };
const watching = command === 'serve' || process.argv.includes('--watch') || process.argv.includes('-w');
const originals = fileURLToPath(new URL('./ref-out/game', import.meta.url));
const requested = process.env.SSRC_ORIGINAL_ASSETS === '1' || (watching && fileEnv.SSRC_ORIGINAL_ASSETS === '1');
const useOriginals = requested && existsSync(originals);
if (useOriginals) {
console.warn('\n⚠ SSRC_ORIGINAL_ASSETS=1: bundling ORIGINAL game assets. Do not publish this build.\n');
} else if (requested) {
console.warn('\n⚠ SSRC_ORIGINAL_ASSETS=1 but ref-out/game is missing (see tools/README.md); using the custom bundle.\n');
} else if (fileEnv.SSRC_ORIGINAL_ASSETS === '1') {
console.warn('\n.env.local asks for the original assets; ignored outside watch builds (soyli dev). This build gets the custom bundle.\n');
}
// Release builds leave the custom pack out of index.html: it is a registered
// soyli asset (pnpm assets:register) that soyli publish uploads to Blossom,
// and the napplet downloads it at runtime by hash. Dev and
// watch builds (soyli dev serves only registered soyli assets) and builds
// with the originals embed it; SSRC_EMBED_ASSETS=1 forces a single file.
// SSRC_EXTERNAL_ASSETS=1 (shell or .env.local) tries the download/side-load
// path in a watch build.
const external = process.env.SSRC_EXTERNAL_ASSETS === '1' || (watching && fileEnv.SSRC_EXTERNAL_ASSETS === '1');
const embed = useOriginals || (watching && !external) || env.SSRC_EMBED_ASSETS === '1';
return {
build: {
// Vite's module-preload polyfill calls `fetch`; one inlined entry needs no
// preload graph, and NIP-5D napplet code has no ambient network authority.
modulePreload: { polyfill: false },
},
plugins: [
rapierWasm(env.SSRC_RAPIER_FULL === '1'),
assetPack({
dir: useOriginals ? originals : fileURLToPath(new URL(`./${CUSTOM_PACK.dir}`, import.meta.url)),
meta: useOriginals ? { title: 'SuperSonic RC (original assets)', original: true } : CUSTOM_PACK.meta,
embed,
outDir: fileURLToPath(new URL('./dist-pack', import.meta.url)),
inventory: watching ? null : fileURLToPath(new URL('./napplet.assets.json', import.meta.url)),
levels: fileURLToPath(new URL('./napplet.assets.json', import.meta.url)),
}),
// Produce one self-contained `/index.html` for NIP-5D `srcdoc` loading,
// then content-address it with the NIP-5A tag/hash schema.
nip5aManifest({
nappletType: 'my-napplet',
artifactMode: 'single-file',
// NAP-INTENT: other napplets open this one by role to play a linked
// asset pack. Keep in step with GAME_ARCHETYPE and PLAY_CONVENTION in
// src/napplet-intent.ts.
archetypes: [{ slug: 'supersonicrc', naps: ['napplet:supersonicrc/play'] }],
}),
],
};
});
