@oidoid/void 0.1.0-2 → 0.1.0-3

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.
Files changed (2) hide show
  1. package/package.json +2 -3
  2. package/tools/void.js +143 -0
package/package.json CHANGED
@@ -1,8 +1,7 @@
1
1
  {
2
2
  "author": "Stephen Niedzielski <stephen@oidoid.com> (https://oidoid.com)",
3
3
  "bin": {
4
- "ase": "bin/ase",
5
- "void": "bin/void"
4
+ "void": "tools/void.js"
6
5
  },
7
6
  "bugs": "https://github.com/oidoid/void/issues",
8
7
  "description": "Tiny 2D game engine.",
@@ -69,5 +68,5 @@
69
68
  },
70
69
  "type": "module",
71
70
  "types": "dist/index.d.ts",
72
- "version": "0.1.0-2"
71
+ "version": "0.1.0-3"
73
72
  }
package/tools/void.js ADDED
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env -S node --no-warnings
2
+ // Bundles sources into a single HTML file for distribution and development.
3
+ //
4
+ // void --html=file --out=dir [--watch] sprite...
5
+ // --watch Run development server. Serve on http://localhost:1234 and reload on
6
+ // code change.
7
+ //
8
+ // --no-warnings shebang works around JSON import warnings. See
9
+ // https://github.com/nodejs/node/issues/27355 and
10
+ // https://github.com/nodejs/node/issues/40940.
11
+
12
+ import {execFile} from 'child_process'
13
+ import esbuild from 'esbuild'
14
+ import {JSDOM} from 'jsdom'
15
+ import fs from 'node:fs/promises'
16
+ import path from 'node:path'
17
+ import pkg from '../package.json' assert {type: 'json'}
18
+ import {parseAtlas} from '../src/atlas/atlas-parser.js'
19
+
20
+ const args = process.argv.filter(arg => !arg.startsWith('--'))
21
+ const opts = Object.fromEntries(
22
+ process.argv.filter(arg => arg.startsWith('--')).map(arg => arg.split('='))
23
+ )
24
+
25
+ const watch = '--watch' in opts
26
+ const inFilename = opts['--html']
27
+ if (!inFilename) throw Error('missing input')
28
+ const outDir = opts['--out']
29
+ if (!outDir) throw Error('missing output')
30
+ const doc = new JSDOM(await fs.readFile(inFilename, 'utf8')).window.document
31
+ let srcFilename = /** @type {HTMLScriptElement|null} */ (
32
+ doc.querySelector('script[type="module"][src]')
33
+ )?.src
34
+ if (!srcFilename) throw Error('missing script source')
35
+ srcFilename = `${path.dirname(inFilename)}/${srcFilename}`
36
+ const sprites = args.splice(2)
37
+ if (!sprites.length) throw Error('missing sprites')
38
+
39
+ const atlasPNGFilename = `${await fs.mkdtemp('/tmp/', {encoding: 'utf8'})}/atlas.png`
40
+ const [err, stdout, stderr] = await new Promise(resolve =>
41
+ execFile(
42
+ 'aseprite',
43
+ [
44
+ '--batch',
45
+ '--color-mode=indexed',
46
+ '--filename-format={title}--{tag}--{frame}',
47
+ // '--ignore-empty', Breaks --tagname-format.
48
+ '--list-slices',
49
+ '--list-tags',
50
+ '--merge-duplicates',
51
+ `--sheet=${atlasPNGFilename}`,
52
+ '--sheet-pack',
53
+ '--tagname-format={title}--{tag}',
54
+ ...sprites
55
+ ],
56
+ (err, stdout, stderr) => resolve([err, stdout, stderr])
57
+ )
58
+ )
59
+ process.stderr.write(stderr)
60
+ if (err) throw err
61
+
62
+ const atlasJSON = JSON.stringify(parseAtlas(JSON.parse(stdout)))
63
+ const atlasURI =
64
+ await `data:image/png;base64,${(await fs.readFile(atlasPNGFilename)).toString('base64')}`
65
+
66
+ /** @type {Parameters<esbuild.PluginBuild['onEnd']>[0]} */
67
+ async function pluginOnEnd(result) {
68
+ const copy = /** @type {Document} */ (doc.cloneNode(true))
69
+ const manifestEl = /** @type {HTMLLinkElement|null} */ (
70
+ copy.querySelector('link[href][rel="manifest"]')
71
+ )
72
+ if (manifestEl) {
73
+ const manifestFilename = `${path.dirname(inFilename)}/${manifestEl.href}`
74
+ const manifest = JSON.parse(await fs.readFile(manifestFilename, 'utf8'))
75
+ for (const icon of manifest.icons) {
76
+ if (!icon.src) throw Error('missing manifest icon src')
77
+ if (!icon.type) throw Error('missing manifest icon type')
78
+ const file = await fs.readFile(
79
+ `${path.dirname(manifestFilename)}/${icon.src}`
80
+ )
81
+ icon.src = `data:${icon.type};base64,${file.toString('base64')}`
82
+ }
83
+ if (watch) manifest.start_url = 'http://localhost:1234'
84
+ manifest.version = pkg.version
85
+ manifestEl.href = `data:application/json,${encodeURIComponent(
86
+ JSON.stringify(manifest)
87
+ )}`
88
+ }
89
+ const iconEl = /** @type {HTMLLinkElement|null} */ (
90
+ copy.querySelector('link[href][rel="icon"][type]')
91
+ )
92
+ if (iconEl) {
93
+ const file = await fs.readFile(`${path.dirname(inFilename)}/${iconEl.href}`)
94
+ iconEl.href = `data:${iconEl.type};base64,${file.toString('base64')}`
95
+ }
96
+
97
+ let js = ''
98
+ if (watch)
99
+ js +=
100
+ "new globalThis.EventSource('/esbuild').addEventListener('change', () => globalThis.location.reload());"
101
+
102
+ const outFiles =
103
+ result.outputFiles?.filter(file => file.path.endsWith('.js')) ?? []
104
+ if (outFiles.length > 1) throw Error('cannot concatenate JavaScript files')
105
+ if (outFiles[0]) js += outFiles[0].text
106
+
107
+ const scriptEl = /** @type {HTMLScriptElement|null} */ (
108
+ copy.querySelector('script[type="module"][src]')
109
+ )
110
+ if (!scriptEl) throw Error('missing script')
111
+ scriptEl.removeAttribute('src')
112
+ scriptEl.textContent = `
113
+ const atlasURI = '${atlasURI}'
114
+ const atlas = ${atlasJSON}
115
+ ${js}
116
+ `
117
+ const outFilename = `${outDir}/${
118
+ watch ? 'index' : `${path.basename(inFilename, '.html')}-v${pkg.version}`
119
+ }.html`
120
+ await fs.mkdir(path.dirname(outFilename), {recursive: true})
121
+ await fs.writeFile(
122
+ outFilename,
123
+ `<!doctype html>${copy.documentElement.outerHTML}`
124
+ )
125
+ }
126
+
127
+ /** @type {esbuild.BuildOptions} */
128
+ const buildOpts = {
129
+ bundle: true,
130
+ entryPoints: [srcFilename],
131
+ format: 'esm',
132
+ logLevel: `info`, // Print the port and build demarcations.
133
+ minify: !watch,
134
+ outdir: outDir,
135
+ plugins: [{name: 'void', setup: build => build.onEnd(pluginOnEnd)}],
136
+ sourcemap: 'linked',
137
+ target: 'es2022', // https://esbuild.github.io/content-types/#tsconfig-json
138
+ write: false // Written by plugin.
139
+ }
140
+ if (watch) {
141
+ const ctx = await esbuild.context(buildOpts)
142
+ await Promise.race([ctx.watch(), ctx.serve({port: 1234, servedir: 'dist'})])
143
+ } else await esbuild.build(buildOpts)