92 lines
3.3 KiB
TypeScript
92 lines
3.3 KiB
TypeScript
/**
|
|
* Build the static solitaire site into `dist/`.
|
|
*
|
|
* Everything here is BUILD-time. The output is plain files on disk — upload them anywhere that
|
|
* serves static content and the game runs entirely in the visitor's browser. No server, no API, no
|
|
* network call at runtime.
|
|
*
|
|
* The one thing a browser cannot do is run TypeScript: Node 22's type-stripping is a Node feature.
|
|
* So `tsc` emits ES2022 modules, rewriting the `.ts` import extensions the engine uses into `.js`.
|
|
*/
|
|
|
|
import { execFileSync } from 'node:child_process';
|
|
import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
import { dirname, join } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const root = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
const dist = join(root, 'dist');
|
|
|
|
rmSync(dist, { recursive: true, force: true });
|
|
mkdirSync(dist, { recursive: true });
|
|
|
|
execFileSync(
|
|
'npx',
|
|
[
|
|
'tsc',
|
|
'--ignoreConfig',
|
|
'src/web/main.ts',
|
|
'--outDir', dist,
|
|
'--rootDir', 'src',
|
|
'--target', 'es2022',
|
|
'--module', 'es2022',
|
|
'--moduleResolution', 'bundler',
|
|
'--lib', 'es2022,dom',
|
|
'--types', '',
|
|
'--strict',
|
|
'--allowImportingTsExtensions',
|
|
'--rewriteRelativeImportExtensions',
|
|
],
|
|
{ cwd: root, stdio: 'inherit' },
|
|
);
|
|
|
|
/**
|
|
* Stamp the build into the page.
|
|
*
|
|
* Once this is published, "what is actually deployed?" stops being answerable by looking at the
|
|
* source. The stamp is written into index.html at copy time rather than generated into `src/`, so
|
|
* no generated file has to be ignored, imported, or kept in step.
|
|
*
|
|
* `-dirty` marks a build made from an uncommitted tree — the honest state of most test deploys, and
|
|
* exactly the thing you want to know when a fix appears to have had no effect.
|
|
*/
|
|
function buildStamp(): string {
|
|
const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')) as { version: string };
|
|
let git = 'nogit';
|
|
try {
|
|
const sha = execFileSync('git', ['rev-parse', '--short', 'HEAD'], { cwd: root })
|
|
.toString()
|
|
.trim();
|
|
const dirty =
|
|
execFileSync('git', ['status', '--porcelain'], { cwd: root }).toString().trim().length > 0;
|
|
git = sha + (dirty ? '-dirty' : '');
|
|
} catch {
|
|
// A build from a tarball with no git history is still a valid build.
|
|
}
|
|
const when = new Date().toISOString().replace('T', ' ').slice(0, 16);
|
|
return `v${pkg.version} · ${git} · ${when}Z`;
|
|
}
|
|
|
|
const stamp = buildStamp();
|
|
const page = readFileSync(join(root, 'src/web/index.html'), 'utf8').replaceAll('__BUILD__', stamp);
|
|
writeFileSync(join(dist, 'index.html'), page);
|
|
|
|
// A tiny note for whoever unzips this later and wonders what it needs.
|
|
writeFileSync(
|
|
join(dist, 'README.txt'),
|
|
[
|
|
'Station Master — solitaire, static build.',
|
|
'',
|
|
'Upload the contents of this folder to any static host and open index.html.',
|
|
'There is no server component. The game runs entirely in the browser and saves',
|
|
'to localStorage. Add ?seed=1234 to the URL for a reproducible deal.',
|
|
'',
|
|
'Note: ES modules require the files to be SERVED over http(s). Opening',
|
|
'index.html directly from the filesystem will be blocked by the browser.',
|
|
'To try it locally: npx http-server dist (or any static file server)',
|
|
'',
|
|
].join('\n'),
|
|
);
|
|
|
|
console.log(`built -> ${dist}\n ${stamp}`);
|