@gamedevland/vite 0.0.4 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +60 -0
- package/dist/cli/GamedevlandCli.d.ts +4 -0
- package/dist/cli/GamedevlandCli.js +29 -0
- package/dist/cli/entry.d.ts +2 -0
- package/dist/cli/entry.js +14 -0
- package/dist/poki/PokiArchive.d.ts +10 -0
- package/dist/poki/PokiArchive.js +64 -0
- package/dist/poki/PokiCredentials.d.ts +5 -0
- package/dist/poki/PokiCredentials.js +34 -0
- package/dist/poki/PokiProcess.d.ts +5 -0
- package/dist/poki/PokiProcess.js +35 -0
- package/dist/poki/PokiProject.d.ts +18 -0
- package/dist/poki/PokiProject.js +80 -0
- package/dist/poki/PokiUpload.d.ts +9 -0
- package/dist/poki/PokiUpload.js +57 -0
- package/package.json +12 -2
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
- Add the `gamedevland poki pack` and `gamedevland poki upload` CLI commands.
|
|
6
|
+
- Configure game ID, build directory, npm checks/build task and archive path in `poki.json`.
|
|
7
|
+
- Package deterministic ZIPs without platform-specific shell utilities or game-specific asset assumptions.
|
|
8
|
+
- Use the official pinned Poki CLI, per-game private credentials or `POKI_UPLOAD_TOKEN`, and verified upload receipts.
|
|
9
|
+
- Keep authentication out of check/build subprocesses and reject source maps, private files, symlinks and leaked upload tokens in build output.
|
|
10
|
+
- Preserve existing Vite configuration exports and browser SDK injection.
|
package/README.md
CHANGED
|
@@ -25,6 +25,66 @@ export default ({ mode }: { readonly mode: string }) =>
|
|
|
25
25
|
The package provides the shared Gamedev Land platform configuration, development proxy setup,
|
|
26
26
|
environment resolution, and platform SDK injection used by engine-based game projects.
|
|
27
27
|
|
|
28
|
+
## Shared Poki tools (Node.js 20.19+)
|
|
29
|
+
|
|
30
|
+
The package also installs the `gamedevland` executable. No game-local upload or ZIP script is needed.
|
|
31
|
+
The CLI uses the project's `poki.json`; it never assumes a particular game name, asset format, font or directory layout.
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{
|
|
35
|
+
"game_id": "your-poki-game-uuid",
|
|
36
|
+
"build_dir": "dist/poki",
|
|
37
|
+
"tooling": {
|
|
38
|
+
"checks": ["typecheck", "lint", "test:poki"],
|
|
39
|
+
"buildScript": "compile:poki",
|
|
40
|
+
"archive": "dist/my-game-poki.zip"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"scripts": {
|
|
48
|
+
"compile:poki": "VITE_PLATFORM=poki vite build --outDir ../dist/poki",
|
|
49
|
+
"build:poki": "npm run compile:poki && gamedevland poki pack",
|
|
50
|
+
"poki:upload": "gamedevland poki upload"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
npm run build:poki
|
|
57
|
+
npm run poki:upload -- --name "Production v1" --notes "Release notes"
|
|
58
|
+
npx gamedevland poki pack --root /path/to/another-game
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`pack` validates an existing build and creates a ZIP with `index.html` at the root. It checks the
|
|
62
|
+
official SDK script and local HTML dependencies. Hidden/private files, source maps, TypeScript sources,
|
|
63
|
+
symlinks and nested archives are rejected. PNG, WAV, SVG, WebP and other game asset formats are allowed.
|
|
64
|
+
Game-specific asset/license budgets remain project tests. ZIP metadata uses a fixed timestamp for reproducibility.
|
|
65
|
+
|
|
66
|
+
`upload` validates all configured npm scripts, resolves authentication, runs checks followed by a fresh build,
|
|
67
|
+
packages it, and invokes `@poki/cli`. It preserves optimized images and does not request public release/review.
|
|
68
|
+
Only a confirmed success response for the configured game and matching Preview/Inspector version counts as success;
|
|
69
|
+
the official CLI's zero exit code alone is insufficient. Temporary upload archives are kept outside the project and cleaned.
|
|
70
|
+
A receipt `poki-upload.json` next to the local ZIP records game/version IDs, URLs, source revision/dirty state and local ZIP SHA-256.
|
|
71
|
+
|
|
72
|
+
Defaults: `checks` is `["typecheck", "lint"]`, `buildScript` is `build:poki`, and `archive` is
|
|
73
|
+
`artifacts/poki/<package-name>.zip`. Add the output directory to `.gitignore`. For projects without these checks,
|
|
74
|
+
explicitly configure the appropriate list (including `[]` when intentional). Paths must stay inside the project,
|
|
75
|
+
and the ZIP must be outside `build_dir` to prevent recursive uploads. `--root` defaults to the current directory.
|
|
76
|
+
Version names default to the git revision plus UTC timestamp; git is optional.
|
|
77
|
+
|
|
78
|
+
### Authentication
|
|
79
|
+
|
|
80
|
+
CI can set `POKI_UPLOAD_TOKEN`. For local repeated uploads, store the game's token (plain text, no shell command)
|
|
81
|
+
in `$XDG_CONFIG_HOME/poki/game-upload-tokens/<game_id>` or `~/.config/poki/game-upload-tokens/<game_id>`.
|
|
82
|
+
On POSIX systems, the token file must have permissions `0600`.
|
|
83
|
+
Never put credentials in `poki.json`, package metadata or the build. Tokens are passed only to the upload process,
|
|
84
|
+
removed from check/build environments, masked in CLI output and checked against all files being packaged.
|
|
85
|
+
|
|
86
|
+
See the [official Poki CLI](https://github.com/poki/poki-cli). The browser runtime SDK remains separate from these Node.js tools.
|
|
87
|
+
|
|
28
88
|
## License
|
|
29
89
|
|
|
30
90
|
This package is public but not open source. See [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { parseArgs } from 'node:util';
|
|
2
|
+
import { PokiArchive } from '../poki/PokiArchive.js';
|
|
3
|
+
import { PokiProject } from '../poki/PokiProject.js';
|
|
4
|
+
import { PokiUpload } from '../poki/PokiUpload.js';
|
|
5
|
+
/** Public command-line interface; no dependency on a particular game layout. */
|
|
6
|
+
export class GamedevlandCli {
|
|
7
|
+
static async run(argv, cwd) {
|
|
8
|
+
const { values, positionals } = parseArgs({ args: argv, allowPositionals: true, options: {
|
|
9
|
+
help: { type: 'boolean', short: 'h' }, root: { type: 'string' },
|
|
10
|
+
name: { type: 'string' }, notes: { type: 'string' },
|
|
11
|
+
} });
|
|
12
|
+
if (values.help || positionals.length === 0) {
|
|
13
|
+
process.stdout.write('Usage: gamedevland poki <pack|upload> [--root <project>] [--name <version>] [--notes <text>]\npack validates/packages an existing build; upload runs configured checks and a fresh build before uploading.\n');
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
if (positionals.length !== 2 || positionals[0] !== 'poki' || !['pack', 'upload'].includes(positionals[1] ?? '')) {
|
|
17
|
+
throw new Error('[gamedevland] Expected poki pack or poki upload; use --help');
|
|
18
|
+
}
|
|
19
|
+
const project = PokiProject.load(values.root ?? cwd);
|
|
20
|
+
if (positionals[1] === 'pack') {
|
|
21
|
+
const archive = await PokiArchive.pack(project, process.env.POKI_UPLOAD_TOKEN);
|
|
22
|
+
process.stdout.write(`[poki] ${project.archive}: ${archive.files} files, ${(archive.bytes / 1048576).toFixed(2)} MiB\n`);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
await PokiUpload.run(project, { ...(values.name === undefined ? {} : { name: values.name }),
|
|
26
|
+
...(values.notes === undefined ? {} : { notes: values.notes }) });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { GamedevlandCli } from './GamedevlandCli.js';
|
|
3
|
+
class GamedevlandCliEntry {
|
|
4
|
+
static async main() {
|
|
5
|
+
try {
|
|
6
|
+
await GamedevlandCli.run(process.argv.slice(2), process.cwd());
|
|
7
|
+
}
|
|
8
|
+
catch (error) {
|
|
9
|
+
process.stderr.write(`${error instanceof Error ? error.message : '[gamedevland] Command failed'}\n`);
|
|
10
|
+
process.exitCode = 1;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
void GamedevlandCliEntry.main();
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { PokiProject } from './PokiProject.js';
|
|
2
|
+
/** Validates and packages a platform build, preserving arbitrary game asset formats. */
|
|
3
|
+
export declare class PokiArchive {
|
|
4
|
+
static pack(project: PokiProject, secret?: string): Promise<{
|
|
5
|
+
readonly sha256: string;
|
|
6
|
+
readonly bytes: number;
|
|
7
|
+
readonly files: number;
|
|
8
|
+
}>;
|
|
9
|
+
private static collect;
|
|
10
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { lstat, mkdir, readFile, readdir, realpath, writeFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { zipSync } from 'fflate';
|
|
5
|
+
import { BuildPlatformSdkCatalog } from '../BuildPlatformSdkCatalog.js';
|
|
6
|
+
/** Validates and packages a platform build, preserving arbitrary game asset formats. */
|
|
7
|
+
export class PokiArchive {
|
|
8
|
+
static async pack(project, secret) {
|
|
9
|
+
if (await realpath(project.buildDir) !== project.buildDir)
|
|
10
|
+
throw new Error('[poki] Symlinked build directory is not supported');
|
|
11
|
+
const files = await PokiArchive.collect(project.buildDir);
|
|
12
|
+
if (!files.includes('index.html'))
|
|
13
|
+
throw new Error('[poki] index.html must be at the archive root');
|
|
14
|
+
const zip = {};
|
|
15
|
+
for (const file of files) {
|
|
16
|
+
const content = await readFile(path.join(project.buildDir, file));
|
|
17
|
+
if (secret && content.includes(Buffer.from(secret)))
|
|
18
|
+
throw new Error('[poki] Upload token found in build output');
|
|
19
|
+
Object.defineProperty(zip, file, { value: [content, { mtime: new Date(2000, 0, 1) }], enumerable: true });
|
|
20
|
+
}
|
|
21
|
+
const html = await readFile(path.join(project.buildDir, 'index.html'), 'utf8');
|
|
22
|
+
const sdkUrl = BuildPlatformSdkCatalog.resolveHeadTags('poki')[0]?.attrs?.src;
|
|
23
|
+
const sources = [...html.matchAll(/<script\b[^>]*\bsrc\s*=\s*["']([^"']+)["'][^>]*>/gi)].map(match => match[1]);
|
|
24
|
+
if (typeof sdkUrl !== 'string' || sources.filter(src => src === sdkUrl).length !== 1) {
|
|
25
|
+
throw new Error('[poki] Expected exactly one official Poki SDK script');
|
|
26
|
+
}
|
|
27
|
+
for (const match of html.matchAll(/\b(?:src|href)\s*=\s*["']([^"']+)["']/gi)) {
|
|
28
|
+
const url = match[1];
|
|
29
|
+
if (!url || url.startsWith('#') || /^(?:https?:|data:|blob:|\/\/)/i.test(url))
|
|
30
|
+
continue;
|
|
31
|
+
if (url.startsWith('/'))
|
|
32
|
+
throw new Error(`[poki] Root-relative HTML dependency: ${url}`);
|
|
33
|
+
const file = path.posix.normalize(decodeURIComponent(url.split(/[?#]/)[0] ?? ''));
|
|
34
|
+
if (!files.includes(file))
|
|
35
|
+
throw new Error(`[poki] Missing HTML dependency: ${url}`);
|
|
36
|
+
}
|
|
37
|
+
if (/https?:\/\/(?:localhost|127\.0\.0\.1)(?=[:/"'])|\/src\/main\.ts/i.test(html)) {
|
|
38
|
+
throw new Error('[poki] Development entry or localhost URL in HTML');
|
|
39
|
+
}
|
|
40
|
+
const archive = zipSync(zip, { level: 9 });
|
|
41
|
+
await mkdir(path.dirname(project.archive), { recursive: true });
|
|
42
|
+
await writeFile(project.archive, archive);
|
|
43
|
+
return { sha256: createHash('sha256').update(archive).digest('hex'), bytes: archive.length, files: files.length };
|
|
44
|
+
}
|
|
45
|
+
static async collect(root, prefix = '') {
|
|
46
|
+
const result = [];
|
|
47
|
+
for (const name of (await readdir(path.join(root, prefix))).sort()) {
|
|
48
|
+
const file = prefix ? `${prefix}/${name}` : name;
|
|
49
|
+
if (name.startsWith('.') || /\.(?:map|ts|tsx|tgz|pem|key|zip)$/i.test(name)) {
|
|
50
|
+
throw new Error(`[poki] Source, private or nested archive file in build: ${file}`);
|
|
51
|
+
}
|
|
52
|
+
const stat = await lstat(path.join(root, file));
|
|
53
|
+
if (stat.isSymbolicLink())
|
|
54
|
+
throw new Error(`[poki] Symlink in build: ${file}`);
|
|
55
|
+
if (stat.isDirectory())
|
|
56
|
+
result.push(...await PokiArchive.collect(root, file));
|
|
57
|
+
else if (stat.isFile())
|
|
58
|
+
result.push(file);
|
|
59
|
+
else
|
|
60
|
+
throw new Error(`[poki] Unsupported build entry: ${file}`);
|
|
61
|
+
}
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
/** Resolves a per-game secret outside the project or from the CI environment. */
|
|
5
|
+
export class PokiCredentials {
|
|
6
|
+
static resolve(gameId) {
|
|
7
|
+
const envToken = process.env.POKI_UPLOAD_TOKEN;
|
|
8
|
+
if (envToken !== undefined) {
|
|
9
|
+
if (!envToken.trim())
|
|
10
|
+
throw new Error('[poki] POKI_UPLOAD_TOKEN must not be empty');
|
|
11
|
+
return envToken.trim();
|
|
12
|
+
}
|
|
13
|
+
const file = path.join(process.env.XDG_CONFIG_HOME ?? path.join(homedir(), '.config'), 'poki', 'game-upload-tokens', gameId);
|
|
14
|
+
try {
|
|
15
|
+
if (process.platform !== 'win32' && (statSync(file).mode & 0o777) !== 0o600) {
|
|
16
|
+
throw new Error('permissions');
|
|
17
|
+
}
|
|
18
|
+
const token = readFileSync(file, 'utf8').trim();
|
|
19
|
+
if (!token)
|
|
20
|
+
throw new Error('empty');
|
|
21
|
+
return token;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
throw new Error(`[poki] Set POKI_UPLOAD_TOKEN or create a private (0600) token file: ${file}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
static withoutSecrets() {
|
|
28
|
+
const env = { ...process.env };
|
|
29
|
+
delete env.POKI_UPLOAD_TOKEN;
|
|
30
|
+
delete env.POKI_ACCESS_TOKEN;
|
|
31
|
+
delete env.API_PATH;
|
|
32
|
+
return env;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Runs npm tasks and captures CLI output without shell-interpolated arguments. */
|
|
2
|
+
export declare class PokiProcess {
|
|
3
|
+
static script(root: string, name: string): Promise<void>;
|
|
4
|
+
static capture(command: string, args: readonly string[], cwd: string, env: NodeJS.ProcessEnv, inherit?: boolean, secret?: string): Promise<string>;
|
|
5
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { PokiCredentials } from './PokiCredentials.js';
|
|
3
|
+
/** Runs npm tasks and captures CLI output without shell-interpolated arguments. */
|
|
4
|
+
export class PokiProcess {
|
|
5
|
+
static async script(root, name) {
|
|
6
|
+
const npmEntry = process.env.npm_execpath;
|
|
7
|
+
const command = npmEntry ? process.execPath : 'npm';
|
|
8
|
+
const args = npmEntry ? [npmEntry, 'run', name] : ['run', name];
|
|
9
|
+
await PokiProcess.capture(command, args, root, PokiCredentials.withoutSecrets(), true);
|
|
10
|
+
}
|
|
11
|
+
static capture(command, args, cwd, env, inherit = false, secret) {
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
const child = spawn(command, [...args], { cwd, env, stdio: inherit ? 'inherit' : 'pipe',
|
|
14
|
+
shell: process.platform === 'win32' && command === 'npm' });
|
|
15
|
+
let output = '';
|
|
16
|
+
const timer = inherit ? undefined : setTimeout(() => child.kill('SIGKILL'), 180000);
|
|
17
|
+
const capture = (data) => { output += data.toString('utf8'); };
|
|
18
|
+
child.stdout?.on('data', capture);
|
|
19
|
+
child.stderr?.on('data', capture);
|
|
20
|
+
child.once('error', () => { if (timer)
|
|
21
|
+
clearTimeout(timer); reject(new Error('[poki] Could not start required tool')); });
|
|
22
|
+
child.once('close', code => {
|
|
23
|
+
if (timer)
|
|
24
|
+
clearTimeout(timer);
|
|
25
|
+
const safe = secret ? output.replaceAll(secret, '[REDACTED]') : output;
|
|
26
|
+
if (!inherit)
|
|
27
|
+
process.stdout.write(safe);
|
|
28
|
+
if (code !== 0)
|
|
29
|
+
reject(new Error(`[poki] Tool exited with status ${code ?? 'signal'}`));
|
|
30
|
+
else
|
|
31
|
+
resolve(safe);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Decodes project-local configuration without game-specific defaults. */
|
|
2
|
+
export declare class PokiProject {
|
|
3
|
+
readonly root: string;
|
|
4
|
+
readonly gameId: string;
|
|
5
|
+
readonly buildDir: string;
|
|
6
|
+
readonly archive: string;
|
|
7
|
+
readonly buildScript: string;
|
|
8
|
+
readonly checks: readonly string[];
|
|
9
|
+
private readonly scripts;
|
|
10
|
+
private static readonly TOOLING;
|
|
11
|
+
private static readonly CONFIG;
|
|
12
|
+
private static readonly PACKAGE;
|
|
13
|
+
private constructor();
|
|
14
|
+
static load(root: string): PokiProject;
|
|
15
|
+
validateScripts(): void;
|
|
16
|
+
private static json;
|
|
17
|
+
private static projectPath;
|
|
18
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { readFileSync, realpathSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { SchemaDecoder } from '@gamedevland/engine/validation';
|
|
4
|
+
/** Decodes project-local configuration without game-specific defaults. */
|
|
5
|
+
export class PokiProject {
|
|
6
|
+
root;
|
|
7
|
+
gameId;
|
|
8
|
+
buildDir;
|
|
9
|
+
archive;
|
|
10
|
+
buildScript;
|
|
11
|
+
checks;
|
|
12
|
+
scripts;
|
|
13
|
+
static TOOLING = SchemaDecoder.object({
|
|
14
|
+
buildScript: SchemaDecoder.optional(SchemaDecoder.string({ nonEmpty: true })),
|
|
15
|
+
checks: SchemaDecoder.optional(SchemaDecoder.array(SchemaDecoder.string({ nonEmpty: true }))),
|
|
16
|
+
archive: SchemaDecoder.optional(SchemaDecoder.string({ nonEmpty: true })),
|
|
17
|
+
});
|
|
18
|
+
static CONFIG = SchemaDecoder.object({
|
|
19
|
+
game_id: SchemaDecoder.string({ nonEmpty: true }),
|
|
20
|
+
build_dir: SchemaDecoder.string({ nonEmpty: true }),
|
|
21
|
+
tooling: SchemaDecoder.optional(PokiProject.TOOLING),
|
|
22
|
+
});
|
|
23
|
+
static PACKAGE = SchemaDecoder.object({
|
|
24
|
+
name: SchemaDecoder.string({ nonEmpty: true }),
|
|
25
|
+
scripts: SchemaDecoder.record(),
|
|
26
|
+
});
|
|
27
|
+
constructor(root, gameId, buildDir, archive, buildScript, checks, scripts) {
|
|
28
|
+
this.root = root;
|
|
29
|
+
this.gameId = gameId;
|
|
30
|
+
this.buildDir = buildDir;
|
|
31
|
+
this.archive = archive;
|
|
32
|
+
this.buildScript = buildScript;
|
|
33
|
+
this.checks = checks;
|
|
34
|
+
this.scripts = scripts;
|
|
35
|
+
}
|
|
36
|
+
static load(root) {
|
|
37
|
+
root = realpathSync(root);
|
|
38
|
+
const config = SchemaDecoder.decode(PokiProject.json(path.join(root, 'poki.json')), PokiProject.CONFIG, 'poki.json');
|
|
39
|
+
const pkg = SchemaDecoder.decode(PokiProject.json(path.join(root, 'package.json')), PokiProject.PACKAGE, 'package.json');
|
|
40
|
+
const scripts = Object.fromEntries(Object.entries(pkg.scripts).map(([key, value]) => [key, SchemaDecoder.decode(value, SchemaDecoder.string(), 'package.json', `$.scripts.${key}`)]));
|
|
41
|
+
if (!/^[\da-f]{8}(?:-[\da-f]{4}){3}-[\da-f]{12}$/i.test(config.game_id)) {
|
|
42
|
+
throw new Error('[poki] game_id must be a UUID');
|
|
43
|
+
}
|
|
44
|
+
const buildDir = PokiProject.projectPath(root, config.build_dir);
|
|
45
|
+
const slug = pkg.name.replace(/^@/, '').replace(/[^\w.-]+/g, '-');
|
|
46
|
+
const archive = PokiProject.projectPath(root, config.tooling?.archive ?? `artifacts/poki/${slug}.zip`);
|
|
47
|
+
if (!path.relative(buildDir, archive).startsWith(`..${path.sep}`)) {
|
|
48
|
+
throw new Error('[poki] Archive must be outside build_dir');
|
|
49
|
+
}
|
|
50
|
+
if (!archive.endsWith('.zip'))
|
|
51
|
+
throw new Error('[poki] Archive must end with .zip');
|
|
52
|
+
return new PokiProject(root, config.game_id, buildDir, archive, config.tooling?.buildScript ?? 'build:poki', config.tooling?.checks ?? ['typecheck', 'lint'], scripts);
|
|
53
|
+
}
|
|
54
|
+
validateScripts() {
|
|
55
|
+
for (const name of [...this.checks, this.buildScript]) {
|
|
56
|
+
if (!/^[\w:.-]+$/.test(name) || !this.scripts[name]) {
|
|
57
|
+
throw new Error(`[poki] Missing or invalid package.json script: ${name}`);
|
|
58
|
+
}
|
|
59
|
+
if (/\bgamedevland\s+poki\s+upload\b/.test(this.scripts[name])) {
|
|
60
|
+
throw new Error(`[poki] Recursive upload script: ${name}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
static json(file) {
|
|
65
|
+
try {
|
|
66
|
+
return JSON.parse(readFileSync(file, 'utf8'));
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
throw new Error(`[poki] Cannot read valid JSON: ${file}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
static projectPath(root, input) {
|
|
73
|
+
const result = path.resolve(root, input);
|
|
74
|
+
const relative = path.relative(root, result);
|
|
75
|
+
if (!relative || relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
76
|
+
throw new Error('[poki] Build/archive paths must stay inside the project');
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { PokiProject } from './PokiProject.js';
|
|
2
|
+
/** Executes a fresh checked build and accepts only an API-confirmed upload. */
|
|
3
|
+
export declare class PokiUpload {
|
|
4
|
+
static run(project: PokiProject, options: {
|
|
5
|
+
readonly name?: string;
|
|
6
|
+
readonly notes?: string;
|
|
7
|
+
}): Promise<void>;
|
|
8
|
+
private static git;
|
|
9
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { PokiArchive } from './PokiArchive.js';
|
|
7
|
+
import { PokiCredentials } from './PokiCredentials.js';
|
|
8
|
+
import { PokiProcess } from './PokiProcess.js';
|
|
9
|
+
/** Executes a fresh checked build and accepts only an API-confirmed upload. */
|
|
10
|
+
export class PokiUpload {
|
|
11
|
+
static async run(project, options) {
|
|
12
|
+
project.validateScripts();
|
|
13
|
+
const secret = PokiCredentials.resolve(project.gameId);
|
|
14
|
+
const revision = PokiUpload.git(project.root, ['rev-parse', '--short', 'HEAD']);
|
|
15
|
+
const dirty = PokiUpload.git(project.root, ['status', '--porcelain']);
|
|
16
|
+
const name = options.name ?? `${revision || 'build'}-${new Date().toISOString().replace(/[-:.]/g, '')}`;
|
|
17
|
+
const notes = options.notes ?? (PokiUpload.git(project.root, ['log', '-1', '--pretty=%s']) || 'Automated game build');
|
|
18
|
+
if (!name.trim())
|
|
19
|
+
throw new Error('[poki] Version name must not be empty');
|
|
20
|
+
for (const task of [...project.checks, project.buildScript])
|
|
21
|
+
await PokiProcess.script(project.root, task);
|
|
22
|
+
const archive = await PokiArchive.pack(project, secret);
|
|
23
|
+
const scratch = await mkdtemp(path.join(tmpdir(), 'gamedevland-poki-'));
|
|
24
|
+
let output;
|
|
25
|
+
try {
|
|
26
|
+
const cli = createRequire(import.meta.url).resolve('@poki/cli/bin/index.js');
|
|
27
|
+
output = await PokiProcess.capture(process.execPath, [cli, 'upload', '--game', project.gameId,
|
|
28
|
+
'--build-dir', project.buildDir, '--name', name, '--notes', notes, '--disable-image-compression'], scratch, { ...PokiCredentials.withoutSecrets(), POKI_UPLOAD_TOKEN: secret }, false, secret);
|
|
29
|
+
}
|
|
30
|
+
finally {
|
|
31
|
+
await rm(scratch, { recursive: true, force: true });
|
|
32
|
+
}
|
|
33
|
+
// @poki/cli 0.1.19 also exits 0 on API errors; require its success response.
|
|
34
|
+
if (!output.includes('Version uploaded successfully'))
|
|
35
|
+
throw new Error('[poki] Upload not confirmed by Poki');
|
|
36
|
+
const preview = output.match(/Preview: (https:\/\/poki\.com\/en\/preview\/[^\s]+)/)?.[1];
|
|
37
|
+
const inspector = output.match(/Inspector: (https:\/\/inspector\.poki\.dev\/[^\s]+)/)?.[1];
|
|
38
|
+
const prefix = `https://poki.com/en/preview/${project.gameId}/`;
|
|
39
|
+
const versionId = preview?.startsWith(prefix) ? preview.slice(prefix.length) : '';
|
|
40
|
+
if (!/^[\da-f]{8}(?:-[\da-f]{4}){3}-[\da-f]{12}$/i.test(versionId) || inspector !== `https://inspector.poki.dev/?game=poki-${versionId}`) {
|
|
41
|
+
throw new Error('[poki] Upload response does not match the configured game/version');
|
|
42
|
+
}
|
|
43
|
+
const receipt = path.join(path.dirname(project.archive), 'poki-upload.json');
|
|
44
|
+
await writeFile(receipt, JSON.stringify({ gameId: project.gameId, versionId, name, revision,
|
|
45
|
+
dirty: Boolean(dirty), uploadedAt: new Date().toISOString(), archive: project.archive,
|
|
46
|
+
archiveSha256: archive.sha256, bytes: archive.bytes, files: archive.files, preview, inspector }, null, 2) + '\n');
|
|
47
|
+
process.stdout.write(`[poki] Receipt: ${receipt}\n`);
|
|
48
|
+
}
|
|
49
|
+
static git(root, args) {
|
|
50
|
+
try {
|
|
51
|
+
return execFileSync('git', args, { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], env: PokiCredentials.withoutSecrets() }).trim();
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return '';
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gamedevland/vite",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Vite build tooling for Gamedev Land HTML5 games.",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -15,9 +15,16 @@
|
|
|
15
15
|
"main": "./dist/index.js",
|
|
16
16
|
"types": "./dist/index.d.ts",
|
|
17
17
|
"sideEffects": false,
|
|
18
|
+
"bin": {
|
|
19
|
+
"gamedevland": "./dist/cli/entry.js"
|
|
20
|
+
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=20.19.0"
|
|
23
|
+
},
|
|
18
24
|
"files": [
|
|
19
25
|
"dist",
|
|
20
26
|
"README.md",
|
|
27
|
+
"CHANGELOG.md",
|
|
21
28
|
"LICENSE"
|
|
22
29
|
],
|
|
23
30
|
"publishConfig": {
|
|
@@ -30,13 +37,16 @@
|
|
|
30
37
|
}
|
|
31
38
|
},
|
|
32
39
|
"scripts": {
|
|
40
|
+
"test": "node --test tests/*.test.mjs",
|
|
33
41
|
"clean": "rimraf dist *.tsbuildinfo",
|
|
34
42
|
"pretypecheck": "npm run build -w @gamedevland/engine",
|
|
35
43
|
"typecheck": "tsc --noEmit",
|
|
36
44
|
"build": "npm run clean && tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json --resolve-full-paths"
|
|
37
45
|
},
|
|
38
46
|
"dependencies": {
|
|
39
|
-
"@gamedevland/engine": "0.0.5"
|
|
47
|
+
"@gamedevland/engine": "0.0.5",
|
|
48
|
+
"@poki/cli": "0.1.19",
|
|
49
|
+
"fflate": "0.8.3"
|
|
40
50
|
},
|
|
41
51
|
"peerDependencies": {
|
|
42
52
|
"vite": "^7.1.0 || ^8.0.0"
|