@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 +102 -0
- package/build.ts +45 -0
- package/package.json +27 -0
- package/src/index.ts +231 -0
- package/sst-env.d.ts +9 -0
- package/tsconfig.json +28 -0
- package/tsconfig.types.json +13 -0
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
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
|
+
}
|