@playcademy/vite-plugin 0.0.1-beta.1

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/README.md ADDED
@@ -0,0 +1,102 @@
1
+ # @playcademy/vite-plugin
2
+
3
+ A Vite plugin to generate a `playcademy.manifest.json` for Playcademy games and optionally create a zip archive of the build output.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ # Using bun
9
+ bun add -D @playcademy/vite-plugin
10
+
11
+ # Using pnpm
12
+ pnpm add -D @playcademy/vite-plugin
13
+
14
+ # Using yarn
15
+ yarn add --dev @playcademy/vite-plugin
16
+
17
+ # Using npm
18
+ npm install --save-dev @playcademy/vite-plugin
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ Add the plugin to your `vite.config.ts` or `vite.config.js`:
24
+
25
+ ```typescript
26
+ // vite.config.ts
27
+ import { defineConfig } from 'vite'
28
+ import { playcademy } from '@playcademy/vite-plugin'
29
+
30
+ export default defineConfig({
31
+ plugins: [
32
+ playcademy({
33
+ // Optional: configure plugin options here
34
+ // bootMode: 'direct',
35
+ // entryPoint: 'my-game.html',
36
+ // styles: ['src/main.css'],
37
+ // engine: 'phaser',
38
+ // autoZip: true, // Set to true to enable zipping locally
39
+ }),
40
+ ],
41
+ // ...other Vite config
42
+ })
43
+ ```
44
+
45
+ ## What it does
46
+
47
+ 1. **Generates `playcademy.manifest.json`**:
48
+ This file is created in your Vite build's output directory (`dist` by default). It contains metadata about your game, such as its entry point, boot mode, and engine type, which is used by the Playcademy platform.
49
+
50
+ 2. **Creates Output Zip Archive (Optional)**:
51
+ If `autoZip` is enabled, the plugin will create a zip file named `{your-project-name}.zip` inside a `.playcademy` directory at the root of your project (e.g., `my-game/.playcademy/my-game.zip`).
52
+ This archive contains all the contents of your build output directory and can be uploaded directly to the Playcademy platform.
53
+
54
+ 3. **Sets Vite `base` Configuration (if not set by user)**:
55
+ The plugin defaults Vite's `base` configuration to `'./'`. This is necessary for games to correctly reference assets when deployed on the Playcademy platform. If you explicitly set a `base` path in your `vite.config.ts`, the plugin will respect your configuration and not override it.
56
+
57
+ ## Options (`PlaycademyPluginOptions`)
58
+
59
+ The `playcademy()` plugin function accepts an optional options object:
60
+
61
+ - **`bootMode`**:
62
+
63
+ - Type: `'iframe' | 'module'`
64
+ - Default: `'iframe'`
65
+ - Specifies how the game should be launched.
66
+
67
+ - **`entryPoint`**:
68
+
69
+ - Type: `string`
70
+ - Default: `'index.html'`
71
+ - The main HTML file for your game.
72
+
73
+ - **`styles`**:
74
+
75
+ - Type: `string[]`
76
+ - Default: `[]`
77
+ - An array of CSS file paths (relative to the project root) that should be loaded by the Playcademy platform.
78
+
79
+ - **`engine`**:
80
+
81
+ - Type: `'three' | 'phaser' | 'godot' | 'unity' | 'custom'`
82
+ - Default: `'custom'`
83
+ - Specifies the game engine used.
84
+
85
+ - **`autoZip`**:
86
+ - Type: `boolean`
87
+ - Default: `false`
88
+ - Controls whether to automatically create a zip archive of the build output.
89
+
90
+ ## Development
91
+
92
+ To install dependencies for this package:
93
+
94
+ ```bash
95
+ bun install
96
+ ```
97
+
98
+ To build the plugin:
99
+
100
+ ```bash
101
+ bun run build
102
+ ```
package/build.ts ADDED
@@ -0,0 +1,45 @@
1
+ import { $ } from 'bun'
2
+ import packageJson from './package.json' assert { type: 'json' }
3
+ import yoctoSpinner from 'yocto-spinner'
4
+
5
+ const startTime = performance.now()
6
+ const spinner = yoctoSpinner({ text: 'Building JavaScript...' }).start()
7
+
8
+ const buildDir = './dist'
9
+ const entrypoints = ['./src/index.ts']
10
+
11
+ try {
12
+ await $`rm -rf ${buildDir}`
13
+
14
+ await Bun.build({
15
+ entrypoints,
16
+ external: [...Object.keys(packageJson.peerDependencies)],
17
+ outdir: buildDir,
18
+ format: 'esm',
19
+ target: 'node',
20
+ })
21
+
22
+ spinner.text = 'Generating types...'
23
+
24
+ const { stderr } =
25
+ await $`tsc --emitDeclarationOnly --declaration --project tsconfig.types.json --outDir ${buildDir}`
26
+
27
+ if (stderr.toString().length) {
28
+ spinner.error(`Type generation failed:\n${stderr.toString()}`)
29
+ process.exit(1) // Exit with error code
30
+ } else {
31
+ const duration = ((performance.now() - startTime) / 1000).toFixed(2)
32
+ spinner.success(`Build complete in ${duration}s!`)
33
+
34
+ // Log built entrypoints
35
+ for (const entry of entrypoints) {
36
+ console.log(` - ${entry}`)
37
+ }
38
+ }
39
+ process.exit(0)
40
+ } catch (error) {
41
+ const duration = ((performance.now() - startTime) / 1000).toFixed(2)
42
+ console.log({ error })
43
+ spinner.error(`Build failed in ${duration}s: ${error}`)
44
+ process.exit(1) // Exit with error code
45
+ }
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@playcademy/vite-plugin",
3
+ "type": "module",
4
+ "version": "0.0.1-beta.1",
5
+ "module": "src/index.ts",
6
+ "exports": {
7
+ ".": {
8
+ "import": "./dist/index.js",
9
+ "types": "./dist/index.d.ts"
10
+ }
11
+ },
12
+ "scripts": {
13
+ "build": "bun build.ts",
14
+ "pub": "bun run build && bunx bumpp --no-tag --no-push && bun publish --access public"
15
+ },
16
+ "devDependencies": {
17
+ "@types/archiver": "^6.0.3",
18
+ "@types/bun": "latest"
19
+ },
20
+ "peerDependencies": {
21
+ "typescript": "^5"
22
+ },
23
+ "dependencies": {
24
+ "archiver": "^7.0.1",
25
+ "picocolors": "^1.1.1"
26
+ }
27
+ }
package/src/index.ts ADDED
@@ -0,0 +1,231 @@
1
+ import path from 'node:path'
2
+ import fs from 'node:fs/promises'
3
+ import { createWriteStream } from 'node:fs'
4
+ import type { Plugin, ResolvedConfig } from 'vite'
5
+ import { ManifestV1Schema, type ManifestV1 } from '@playcademy/data/schemas'
6
+ import archiver from 'archiver'
7
+ import pc from 'picocolors'
8
+
9
+ export interface PlaycademyPluginOptions {
10
+ bootMode?: ManifestV1['bootMode']
11
+ entryPoint?: string
12
+ styles?: string[]
13
+ engine?: ManifestV1['engine']
14
+ autoZip?: boolean
15
+ }
16
+
17
+ const LOG_LINE_TOTAL_WIDTH = 60
18
+
19
+ interface PlaycademyOutputData {
20
+ manifestPath?: string
21
+ manifestSizeKb?: string
22
+ zipPath?: string
23
+ zipSizeKb?: string
24
+ }
25
+
26
+ function formatNumberWithCommas(numStr: string): string {
27
+ if (!numStr) return numStr
28
+ const parts = numStr.split('.')
29
+ if (parts[0] === undefined) return numStr
30
+ parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',')
31
+ return parts.join('.')
32
+ }
33
+
34
+ async function generatePlaycademyManifest(
35
+ config: ResolvedConfig,
36
+ options: Required<Omit<PlaycademyPluginOptions, 'autoZip'>>,
37
+ outDir: string,
38
+ buildOutputs: PlaycademyOutputData,
39
+ ) {
40
+ const manifestData: ManifestV1 = {
41
+ version: '1',
42
+ bootMode: options.bootMode,
43
+ entryPoint: options.entryPoint,
44
+ styles: options.styles || [],
45
+ engine: options.engine || 'custom',
46
+ createdAt: new Date().toISOString(),
47
+ }
48
+
49
+ const validation = ManifestV1Schema.safeParse(manifestData)
50
+
51
+ if (!validation.success) {
52
+ console.error(
53
+ '[Playcademy] Error validating generated manifest data:',
54
+ validation.error.flatten(),
55
+ )
56
+ config.logger.error(
57
+ '[Playcademy] Failed to generate valid manifest file.',
58
+ )
59
+ throw new Error('[Playcademy] Manifest validation failed')
60
+ }
61
+
62
+ const manifestPath = path.resolve(outDir, 'playcademy.manifest.json')
63
+ const manifestJson = JSON.stringify(validation.data, null, 2)
64
+
65
+ await fs.writeFile(manifestPath, manifestJson)
66
+
67
+ try {
68
+ const stats = await fs.stat(manifestPath)
69
+ buildOutputs.manifestPath = path.relative(config.root, manifestPath)
70
+ buildOutputs.manifestSizeKb = (stats.size / 1024).toFixed(2)
71
+ } catch (statError) {
72
+ config.logger.warn(
73
+ `[Playcademy] Could not get stats for manifest file: ${statError}`,
74
+ )
75
+ }
76
+ }
77
+
78
+ async function createOutputZipArchive(
79
+ config: ResolvedConfig,
80
+ outDir: string,
81
+ buildOutputs: PlaycademyOutputData,
82
+ ) {
83
+ const projectRoot = path.resolve(config.root)
84
+ const projectName = path.basename(projectRoot)
85
+ const playcademyDir = path.resolve(projectRoot, '.playcademy')
86
+
87
+ await fs.mkdir(playcademyDir, { recursive: true })
88
+
89
+ const zipName = `${projectName}.zip`
90
+ const zipOutPath = path.resolve(playcademyDir, zipName)
91
+
92
+ const output = createWriteStream(zipOutPath)
93
+ const archive = archiver('zip', {
94
+ zlib: { level: 9 },
95
+ })
96
+
97
+ await new Promise<void>((resolve, reject) => {
98
+ output.on('close', () => {
99
+ buildOutputs.zipPath = path.relative(config.root, zipOutPath)
100
+ buildOutputs.zipSizeKb = (archive.pointer() / 1024).toFixed(2)
101
+ resolve()
102
+ })
103
+ output.on('error', reject)
104
+ archive.pipe(output)
105
+ archive.directory(outDir, false)
106
+ archive.finalize()
107
+ })
108
+ }
109
+
110
+ function performPlaycademyLogging(
111
+ config: ResolvedConfig,
112
+ buildOutputs: PlaycademyOutputData,
113
+ ) {
114
+ if (!buildOutputs.manifestPath && !buildOutputs.zipSizeKb) {
115
+ return
116
+ }
117
+
118
+ config.logger.info('') // newline
119
+ config.logger.info(pc.magenta('[Playcademy]'))
120
+
121
+ if (buildOutputs.manifestPath && buildOutputs.manifestSizeKb) {
122
+ const dir = path.dirname(buildOutputs.manifestPath)
123
+ const file = path.basename(buildOutputs.manifestPath)
124
+ const uncoloredPathLength =
125
+ (dir === '.' ? 0 : dir.length + 1) + file.length
126
+ const coloredPath = `${pc.dim(dir === '.' ? '' : dir + '/')}${pc.green(file)}`
127
+
128
+ const formattedNumber = formatNumberWithCommas(
129
+ buildOutputs.manifestSizeKb!,
130
+ )
131
+ const sizeString = `${formattedNumber} kB`
132
+ const formattedSize = pc.bold(pc.yellow(sizeString))
133
+
134
+ const paddingNeeded = Math.max(
135
+ 2,
136
+ LOG_LINE_TOTAL_WIDTH - uncoloredPathLength - sizeString.length,
137
+ )
138
+ const paddingSpaces = ' '.repeat(paddingNeeded)
139
+
140
+ config.logger.info(`${coloredPath}${paddingSpaces}${formattedSize}`)
141
+ }
142
+
143
+ if (buildOutputs.zipPath && buildOutputs.zipSizeKb) {
144
+ let dir = path.dirname(buildOutputs.zipPath)
145
+ const file = path.basename(buildOutputs.zipPath)
146
+ if (dir === '.') dir = ''
147
+ const uncoloredPathLength = (dir ? dir.length + 1 : 0) + file.length
148
+ const coloredPath = `${dir ? pc.dim(dir + '/') : ''}${pc.green(file)}`
149
+
150
+ const formattedNumber = formatNumberWithCommas(buildOutputs.zipSizeKb!)
151
+ const sizeString = `${formattedNumber} kB`
152
+ const formattedSize = pc.bold(pc.yellow(sizeString))
153
+
154
+ const paddingNeeded = Math.max(
155
+ 2,
156
+ LOG_LINE_TOTAL_WIDTH - uncoloredPathLength - sizeString.length,
157
+ )
158
+ const paddingSpaces = ' '.repeat(paddingNeeded)
159
+
160
+ config.logger.info(`${coloredPath}${paddingSpaces}${formattedSize}`)
161
+ }
162
+
163
+ config.logger.info('') // newline
164
+ }
165
+
166
+ export function playcademy(options: PlaycademyPluginOptions = {}): Plugin {
167
+ let viteConfig: ResolvedConfig
168
+ let currentBuildOutputs: PlaycademyOutputData = {}
169
+
170
+ const finalOptions = {
171
+ bootMode: options.bootMode ?? 'iframe',
172
+ entryPoint: options.entryPoint ?? 'index.html',
173
+ styles: options.styles ?? [],
174
+ engine: options.engine ?? 'custom',
175
+ autoZip: process.env.CI === 'true' ? false : (options.autoZip ?? false),
176
+ }
177
+
178
+ return {
179
+ name: 'vite-plugin-playcademy',
180
+
181
+ config(userConfig) {
182
+ if (userConfig.base === undefined) {
183
+ return {
184
+ base: './',
185
+ }
186
+ }
187
+ return {}
188
+ },
189
+
190
+ configResolved(resolvedConfig) {
191
+ viteConfig = resolvedConfig
192
+ currentBuildOutputs = {}
193
+ },
194
+
195
+ async writeBundle() {
196
+ const outDir =
197
+ viteConfig.build.outDir || path.join(process.cwd(), 'dist')
198
+
199
+ try {
200
+ await generatePlaycademyManifest(
201
+ viteConfig,
202
+ finalOptions,
203
+ outDir,
204
+ currentBuildOutputs,
205
+ )
206
+ if (finalOptions.autoZip) {
207
+ await createOutputZipArchive(
208
+ viteConfig,
209
+ outDir,
210
+ currentBuildOutputs,
211
+ )
212
+ }
213
+ } catch (error) {
214
+ if (
215
+ error instanceof Error &&
216
+ error.message.includes('[Playcademy]')
217
+ ) {
218
+ viteConfig.logger.error(error.message)
219
+ } else {
220
+ viteConfig.logger.error(
221
+ `[Playcademy] An unexpected error occurred during writeBundle: ${error instanceof Error ? error.message : String(error)}`,
222
+ )
223
+ }
224
+ }
225
+ },
226
+
227
+ closeBundle() {
228
+ performPlaycademyLogging(viteConfig, currentBuildOutputs)
229
+ },
230
+ }
231
+ }
package/sst-env.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ /* This file is auto-generated by SST. Do not edit. */
2
+ /* tslint:disable */
3
+ /* eslint-disable */
4
+ /* deno-fmt-ignore-file */
5
+
6
+ /// <reference path="../../sst-env.d.ts" />
7
+
8
+ import "sst"
9
+ export {}
package/tsconfig.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "compilerOptions": {
3
+ // Environment setup & latest features
4
+ "lib": ["ESNext"],
5
+ "target": "ESNext",
6
+ "module": "ESNext",
7
+ "moduleDetection": "force",
8
+ "jsx": "react-jsx",
9
+ "allowJs": true,
10
+
11
+ // Bundler mode
12
+ "moduleResolution": "bundler",
13
+ "allowImportingTsExtensions": true,
14
+ "verbatimModuleSyntax": true,
15
+ "noEmit": true,
16
+
17
+ // Best practices
18
+ "strict": true,
19
+ "skipLibCheck": true,
20
+ "noFallthroughCasesInSwitch": true,
21
+ "noUncheckedIndexedAccess": true,
22
+
23
+ // Some stricter flags (disabled by default)
24
+ "noUnusedLocals": false,
25
+ "noUnusedParameters": false,
26
+ "noPropertyAccessFromIndexSignature": false
27
+ }
28
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "noEmit": false,
5
+ "emitDeclarationOnly": true,
6
+ "declaration": true,
7
+ "outDir": "./dist",
8
+ "rootDir": "./src",
9
+ "skipLibCheck": true
10
+ },
11
+ "include": ["src/**/*.ts"],
12
+ "exclude": ["node_modules", "**/*.test.ts"]
13
+ }