@fate-app/mod-build 2.0.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/LICENSE +21 -0
- package/README.md +28 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +42 -0
- package/dist/dev.d.ts +22 -0
- package/dist/dev.js +99 -0
- package/dist/devCli.d.ts +1 -0
- package/dist/devCli.js +13 -0
- package/dist/fateSdkShims.d.ts +17 -0
- package/dist/fateSdkShims.js +39 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +57 -0
- package/dist/manifestChecks.d.ts +3 -0
- package/dist/manifestChecks.js +44 -0
- package/dist/sdkExports.d.ts +1 -0
- package/dist/sdkExports.js +2171 -0
- package/dist/testing.d.ts +62 -0
- package/dist/testing.js +183 -0
- package/package.json +70 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Stanislav Sonder
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# @fate-app/mod-build
|
|
2
|
+
|
|
3
|
+
Vite build preset + dev-mode server for authoring [Assistant for Fate](https://github.com/Stanislavsonder/fate)
|
|
4
|
+
mods — externalizes host-shared libraries (`vue`, `vue-i18n`, `@ionic/vue`,
|
|
5
|
+
`ionicons`) against `window.FateSDK` instead of bundling them, inlines CSS,
|
|
6
|
+
and enforces bundle size limits.
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
// vite.config.ts
|
|
10
|
+
import { defineModConfig } from '@fate-app/mod-build'
|
|
11
|
+
|
|
12
|
+
export default defineModConfig()
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`./testing` exposes `stubFateSDK()`/`smokeLoad()` for headlessly verifying a
|
|
16
|
+
built bundle's shape outside a real app (used by the registry's CI).
|
|
17
|
+
|
|
18
|
+
Full author-facing documentation lives in the app repo's
|
|
19
|
+
[`docs/MOD_API.md`](https://github.com/Stanislavsonder/fate/blob/main/docs/MOD_API.md).
|
|
20
|
+
Scaffold a new mod project with `pnpm create fate-mod` rather than hand-rolling
|
|
21
|
+
this config.
|
|
22
|
+
|
|
23
|
+
## Version discipline
|
|
24
|
+
|
|
25
|
+
This package's version tracks `SDK_VERSION` (the `FateSDK` ABI, defined in
|
|
26
|
+
the app's `src/mods/sdk.ts`) — same major.minor, patch is free. Don't pin a
|
|
27
|
+
version here that doesn't correspond to a real `SDK_VERSION` the app has
|
|
28
|
+
shipped.
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { startDevServer } from "./dev.js";
|
|
4
|
+
/**
|
|
5
|
+
* Published as the `fate-mod-build` binary (package.json `bin`). A real mod
|
|
6
|
+
* project (outside this monorepo, e.g. one scaffolded by `create-fate-mod`)
|
|
7
|
+
* runs this via its own `package.json` scripts: `"dev": "fate-mod-build dev"`,
|
|
8
|
+
* `"build": "fate-mod-build build"`. Inside this monorepo, `example-mod`
|
|
9
|
+
* still invokes `dev.ts`'s `startDevServer` directly via `devCli.ts` — kept
|
|
10
|
+
* as-is rather than migrated, since it predates this bin and works fine.
|
|
11
|
+
*/
|
|
12
|
+
async function main() {
|
|
13
|
+
const [, , command] = process.argv;
|
|
14
|
+
const root = process.cwd();
|
|
15
|
+
if (command === 'dev') {
|
|
16
|
+
const port = Number(process.env.PORT) || 5199;
|
|
17
|
+
await startDevServer({ root, port });
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (command === 'build') {
|
|
21
|
+
await runBuild(root);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
console.error(`Unknown command "${command ?? ''}". Usage: fate-mod-build <dev|build>`);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Shells `vite build` (no --watch) once. See dev.ts's startDevServer for why
|
|
29
|
+
* this spawns the CLI (shell:true, a fixed non-interpolated command string)
|
|
30
|
+
* rather than Vite's programmatic build() API or an argv array.
|
|
31
|
+
*/
|
|
32
|
+
function runBuild(root) {
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
const child = spawn('npx vite build', { cwd: root, stdio: 'inherit', shell: true });
|
|
35
|
+
child.on('exit', code => (code === 0 ? resolve() : reject(new Error(`vite build exited with code ${code}`))));
|
|
36
|
+
child.on('error', reject);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
main().catch((e) => {
|
|
40
|
+
console.error(e);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
});
|
package/dist/dev.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface DevServerOptions {
|
|
2
|
+
/** The mod project root (where manifest.json / translations/ / vite.config.ts live). */
|
|
3
|
+
root: string;
|
|
4
|
+
/** Where the watched build writes bundle.mjs — defaults to <root>/dist. */
|
|
5
|
+
distDir?: string;
|
|
6
|
+
port?: number;
|
|
7
|
+
}
|
|
8
|
+
export interface DevServerHandle {
|
|
9
|
+
close(): Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The author-side half of Developer Mode live reload (see src/mods/devMode.ts
|
|
13
|
+
* for the app-side client). Spawns `vite build --watch` as a child process —
|
|
14
|
+
* deliberately NOT Vite's programmatic build() API: at the time this was
|
|
15
|
+
* written (Vite 8 / rolldown-vite), build({ watch: {} }) produced a
|
|
16
|
+
* corrupted/incomplete build (dropped modules, SFC parse errors) that the
|
|
17
|
+
* plain CLI command does not, in this exact project. Spawning the CLI
|
|
18
|
+
* reuses the code path that's actually proven to work, and decouples "run
|
|
19
|
+
* the watched build" from "serve files + notify" — a plain fs.watch on the
|
|
20
|
+
* output directory drives the SSE `/events` endpoint the app subscribes to.
|
|
21
|
+
*/
|
|
22
|
+
export declare function startDevServer(options: DevServerOptions): Promise<DevServerHandle>;
|
package/dist/dev.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { createServer as createHttpServer } from 'node:http';
|
|
2
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
3
|
+
import { existsSync, watch as watchFs } from 'node:fs';
|
|
4
|
+
import { join, extname } from 'node:path';
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
const MIME = {
|
|
7
|
+
'.json': 'application/json',
|
|
8
|
+
'.mjs': 'text/javascript',
|
|
9
|
+
'.js': 'text/javascript'
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* The author-side half of Developer Mode live reload (see src/mods/devMode.ts
|
|
13
|
+
* for the app-side client). Spawns `vite build --watch` as a child process —
|
|
14
|
+
* deliberately NOT Vite's programmatic build() API: at the time this was
|
|
15
|
+
* written (Vite 8 / rolldown-vite), build({ watch: {} }) produced a
|
|
16
|
+
* corrupted/incomplete build (dropped modules, SFC parse errors) that the
|
|
17
|
+
* plain CLI command does not, in this exact project. Spawning the CLI
|
|
18
|
+
* reuses the code path that's actually proven to work, and decouples "run
|
|
19
|
+
* the watched build" from "serve files + notify" — a plain fs.watch on the
|
|
20
|
+
* output directory drives the SSE `/events` endpoint the app subscribes to.
|
|
21
|
+
*/
|
|
22
|
+
export async function startDevServer(options) {
|
|
23
|
+
const { root } = options;
|
|
24
|
+
const port = options.port ?? 5199;
|
|
25
|
+
const distDir = options.distDir ?? join(root, 'dist');
|
|
26
|
+
const clients = new Set();
|
|
27
|
+
function notifyRebuild() {
|
|
28
|
+
for (const res of clients) {
|
|
29
|
+
res.write('data: rebuild\n\n');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// Windows needs shell:true to run npx (a .cmd file) at all; combining shell:true
|
|
33
|
+
// with an argv array triggers Node's DEP0190 warning (args aren't escaped for
|
|
34
|
+
// the shell), so pass one fixed, non-interpolated command string instead —
|
|
35
|
+
// nothing here is ever built from user input.
|
|
36
|
+
const child = spawn('npx vite build --watch', {
|
|
37
|
+
cwd: root,
|
|
38
|
+
stdio: 'inherit',
|
|
39
|
+
shell: true
|
|
40
|
+
});
|
|
41
|
+
// Watches `root` (not distDir directly) because distDir may not exist yet at
|
|
42
|
+
// startup (e.g. right after `rm -rf dist`) — fs.watch on a not-yet-existing
|
|
43
|
+
// path never fires for anything created later, silently going dead for the
|
|
44
|
+
// whole session. `root` always exists; filter to changes under dist/.
|
|
45
|
+
// Debounced: a rebuild touches multiple files in dist/ in quick succession.
|
|
46
|
+
let debounceTimer;
|
|
47
|
+
const fsWatcher = watchFs(root, { recursive: true }, (_event, filename) => {
|
|
48
|
+
if (!filename || !filename.split(/[\\/]/).includes('dist')) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
clearTimeout(debounceTimer);
|
|
52
|
+
debounceTimer = setTimeout(notifyRebuild, 150);
|
|
53
|
+
});
|
|
54
|
+
const server = createHttpServer(async (req, res) => {
|
|
55
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
56
|
+
const url = new URL(req.url ?? '/', `http://localhost:${port}`);
|
|
57
|
+
if (url.pathname === '/events') {
|
|
58
|
+
res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive' });
|
|
59
|
+
res.write('\n');
|
|
60
|
+
clients.add(res);
|
|
61
|
+
req.on('close', () => clients.delete(res));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const relative = decodeURIComponent(url.pathname.replace(/^\/+/, ''));
|
|
65
|
+
// dist/ (the built bundle) takes priority; everything else (manifest.json,
|
|
66
|
+
// translations/) is served straight from the project root.
|
|
67
|
+
const distPath = join(distDir, relative);
|
|
68
|
+
const filePath = existsSync(distPath) ? distPath : join(root, relative);
|
|
69
|
+
try {
|
|
70
|
+
const stats = await stat(filePath);
|
|
71
|
+
if (!stats.isFile()) {
|
|
72
|
+
throw new Error('not a file');
|
|
73
|
+
}
|
|
74
|
+
const contents = await readFile(filePath);
|
|
75
|
+
res.writeHead(200, { 'Content-Type': MIME[extname(filePath)] ?? 'application/octet-stream' });
|
|
76
|
+
res.end(contents);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
res.writeHead(404);
|
|
80
|
+
res.end('Not found');
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
await new Promise((resolve, reject) => {
|
|
84
|
+
server.once('error', reject);
|
|
85
|
+
server.listen(port, resolve);
|
|
86
|
+
});
|
|
87
|
+
console.log(`[mod-build dev] serving ${root} at http://localhost:${port} (SSE: /events)`);
|
|
88
|
+
return {
|
|
89
|
+
async close() {
|
|
90
|
+
for (const res of clients) {
|
|
91
|
+
res.end();
|
|
92
|
+
}
|
|
93
|
+
clients.clear();
|
|
94
|
+
fsWatcher?.close();
|
|
95
|
+
child.kill();
|
|
96
|
+
await new Promise((resolve, reject) => server.close(err => (err ? reject(err) : resolve())));
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
}
|
package/dist/devCli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/devCli.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal CLI entry for `pnpm dev` in a mod project. Not a published binary
|
|
3
|
+
* in this phase (Decision #6 in the Phase 2 plan — mod-build only needs to
|
|
4
|
+
* work from inside this monorepo). A real mod project runs this directly:
|
|
5
|
+
* `node --experimental-transform-types <path-to>/devCli.ts`.
|
|
6
|
+
*/
|
|
7
|
+
import { startDevServer } from "./dev.js";
|
|
8
|
+
const root = process.cwd();
|
|
9
|
+
const port = Number(process.env.PORT) || 5199;
|
|
10
|
+
startDevServer({ root, port }).catch((e) => {
|
|
11
|
+
console.error(e);
|
|
12
|
+
process.exit(1);
|
|
13
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Plugin } from 'vite';
|
|
2
|
+
/**
|
|
3
|
+
* Rewrites imports of host-shared libraries (vue, vue-i18n, @ionic/vue,
|
|
4
|
+
* ionicons/icons — decision D2 in README.md) into virtual modules that
|
|
5
|
+
* re-export named bindings from `globalThis.FateSDK.*` instead of bundling
|
|
6
|
+
* the library. This is what keeps the mod using the HOST's Vue instance
|
|
7
|
+
* (required for reactivity/provide-inject to work across the bundle
|
|
8
|
+
* boundary) while still letting mod authors write plain
|
|
9
|
+
* `import { ref } from 'vue'`.
|
|
10
|
+
*
|
|
11
|
+
* Export lists come from the pinned `sdkExports.ts` (see
|
|
12
|
+
* scripts/generateSdkExports.ts), not a live `import()` at build time — the
|
|
13
|
+
* latter resolves Node's CJS-interop build of each package (via the "node"
|
|
14
|
+
* export condition) rather than the browser ESM build Vite/the app actually
|
|
15
|
+
* use, producing a polluted/incomplete export list.
|
|
16
|
+
*/
|
|
17
|
+
export declare function fateSdkShims(externals: Record<string, string>): Plugin;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { SDK_EXPORTS } from "./sdkExports.js";
|
|
2
|
+
const VIRTUAL_PREFIX = '\0fate-sdk-shim:';
|
|
3
|
+
/**
|
|
4
|
+
* Rewrites imports of host-shared libraries (vue, vue-i18n, @ionic/vue,
|
|
5
|
+
* ionicons/icons — decision D2 in README.md) into virtual modules that
|
|
6
|
+
* re-export named bindings from `globalThis.FateSDK.*` instead of bundling
|
|
7
|
+
* the library. This is what keeps the mod using the HOST's Vue instance
|
|
8
|
+
* (required for reactivity/provide-inject to work across the bundle
|
|
9
|
+
* boundary) while still letting mod authors write plain
|
|
10
|
+
* `import { ref } from 'vue'`.
|
|
11
|
+
*
|
|
12
|
+
* Export lists come from the pinned `sdkExports.ts` (see
|
|
13
|
+
* scripts/generateSdkExports.ts), not a live `import()` at build time — the
|
|
14
|
+
* latter resolves Node's CJS-interop build of each package (via the "node"
|
|
15
|
+
* export condition) rather than the browser ESM build Vite/the app actually
|
|
16
|
+
* use, producing a polluted/incomplete export list.
|
|
17
|
+
*/
|
|
18
|
+
export function fateSdkShims(externals) {
|
|
19
|
+
return {
|
|
20
|
+
name: 'fate-sdk-shims',
|
|
21
|
+
enforce: 'pre',
|
|
22
|
+
resolveId(id) {
|
|
23
|
+
return id in externals ? VIRTUAL_PREFIX + id : null;
|
|
24
|
+
},
|
|
25
|
+
load(id) {
|
|
26
|
+
if (!id.startsWith(VIRTUAL_PREFIX)) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
const specifier = id.slice(VIRTUAL_PREFIX.length);
|
|
30
|
+
const globalPath = externals[specifier];
|
|
31
|
+
const names = SDK_EXPORTS[specifier];
|
|
32
|
+
if (!names) {
|
|
33
|
+
this.error(`No pinned export list for "${specifier}" — add it in sdkExports.ts (see generateSdkExports.ts).`);
|
|
34
|
+
}
|
|
35
|
+
const lines = [`const m = globalThis.${globalPath};`, ...names.map(name => `export const ${name} = m.${name};`)];
|
|
36
|
+
return lines.join('\n');
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type UserConfig } from 'vite';
|
|
2
|
+
/**
|
|
3
|
+
* Host-shared libraries a mod must not bundle its own copy of (decision D2).
|
|
4
|
+
* Keys are the import specifiers a mod author writes; values are the
|
|
5
|
+
* `globalThis.FateSDK` path the host installs before loading any mod.
|
|
6
|
+
*/
|
|
7
|
+
export declare const EXTERNALS: Record<string, string>;
|
|
8
|
+
/**
|
|
9
|
+
* The Vite config a mod project's own vite.config.ts hands back:
|
|
10
|
+
* `export default defineModConfig()`. Produces a single-file ESM bundle
|
|
11
|
+
* (`bundle.mjs`) with CSS inlined and injected at load, and the host's
|
|
12
|
+
* shared libraries externalized via fateSdkShims instead of bundled.
|
|
13
|
+
*/
|
|
14
|
+
export declare function defineModConfig(overrides?: UserConfig): UserConfig;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import vue from '@vitejs/plugin-vue';
|
|
2
|
+
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js';
|
|
3
|
+
import { defineConfig } from 'vite';
|
|
4
|
+
import { fateSdkShims } from "./fateSdkShims.js";
|
|
5
|
+
import { manifestChecks } from "./manifestChecks.js";
|
|
6
|
+
/**
|
|
7
|
+
* Host-shared libraries a mod must not bundle its own copy of (decision D2).
|
|
8
|
+
* Keys are the import specifiers a mod author writes; values are the
|
|
9
|
+
* `globalThis.FateSDK` path the host installs before loading any mod.
|
|
10
|
+
*/
|
|
11
|
+
export const EXTERNALS = {
|
|
12
|
+
vue: 'FateSDK.vue',
|
|
13
|
+
'vue-i18n': 'FateSDK.vueI18n',
|
|
14
|
+
'@ionic/vue': 'FateSDK.ionicVue',
|
|
15
|
+
'ionicons/icons': 'FateSDK.ionicons',
|
|
16
|
+
// Experimental (dice capability) — see docs/MOD_API.md. `Dice`/`DiceMaterial`
|
|
17
|
+
// themselves are NOT externalized: import them normally from
|
|
18
|
+
// @fate-app/mod-types, they bundle directly (see that package's dice.ts).
|
|
19
|
+
three: 'FateSDK.dice.three',
|
|
20
|
+
'cannon-es': 'FateSDK.dice.cannonEs'
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* The Vite config a mod project's own vite.config.ts hands back:
|
|
24
|
+
* `export default defineModConfig()`. Produces a single-file ESM bundle
|
|
25
|
+
* (`bundle.mjs`) with CSS inlined and injected at load, and the host's
|
|
26
|
+
* shared libraries externalized via fateSdkShims instead of bundled.
|
|
27
|
+
*/
|
|
28
|
+
export function defineModConfig(overrides = {}) {
|
|
29
|
+
return defineConfig({
|
|
30
|
+
plugins: [vue(), fateSdkShims(EXTERNALS), cssInjectedByJsPlugin(), manifestChecks()],
|
|
31
|
+
define: {
|
|
32
|
+
'process.env.NODE_ENV': '"production"',
|
|
33
|
+
__VUE_OPTIONS_API__: 'true',
|
|
34
|
+
__VUE_PROD_DEVTOOLS__: 'false',
|
|
35
|
+
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: 'false'
|
|
36
|
+
},
|
|
37
|
+
build: {
|
|
38
|
+
lib: {
|
|
39
|
+
entry: 'bundle.ts',
|
|
40
|
+
formats: ['es'],
|
|
41
|
+
fileName: () => 'bundle.mjs'
|
|
42
|
+
},
|
|
43
|
+
// Deliberately NOT `rollupOptions.external`: that tells the bundler to
|
|
44
|
+
// leave `import ... from 'vue'` as a literal, unresolvable bare
|
|
45
|
+
// specifier in the output (blob-URL `import()` has no bare-specifier
|
|
46
|
+
// resolution). fateSdkShims virtualizes these specifiers into local
|
|
47
|
+
// modules instead, which get bundled normally — the opposite of external.
|
|
48
|
+
rollupOptions: {
|
|
49
|
+
output: { codeSplitting: false }
|
|
50
|
+
},
|
|
51
|
+
cssCodeSplit: false,
|
|
52
|
+
assetsInlineLimit: 1024 * 1024,
|
|
53
|
+
emptyOutDir: true
|
|
54
|
+
},
|
|
55
|
+
...overrides
|
|
56
|
+
});
|
|
57
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
const SOFT_LIMIT_BYTES = 1 * 1024 * 1024;
|
|
4
|
+
const HARD_LIMIT_BYTES = 3 * 1024 * 1024;
|
|
5
|
+
const RECOMMENDED_FIELDS = ['id', 'version', 'name', 'author', 'description', 'loadPriority'];
|
|
6
|
+
/** Validates manifest.json presence/shape and warns/errors on bundle size (README.md §4 limits). */
|
|
7
|
+
export function manifestChecks() {
|
|
8
|
+
return {
|
|
9
|
+
name: 'fate-mod-manifest-checks',
|
|
10
|
+
buildStart() {
|
|
11
|
+
const manifestPath = resolve(process.cwd(), 'manifest.json');
|
|
12
|
+
if (!existsSync(manifestPath)) {
|
|
13
|
+
this.error('manifest.json not found in the mod project root — every mod must ship one alongside its bundle.');
|
|
14
|
+
}
|
|
15
|
+
let manifest;
|
|
16
|
+
try {
|
|
17
|
+
manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
18
|
+
}
|
|
19
|
+
catch (e) {
|
|
20
|
+
this.error(`manifest.json is not valid JSON: ${e instanceof Error ? e.message : String(e)}`);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
for (const field of RECOMMENDED_FIELDS) {
|
|
24
|
+
if (!(field in manifest)) {
|
|
25
|
+
this.warn(`manifest.json is missing recommended field "${field}"`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
writeBundle(_options, bundle) {
|
|
30
|
+
for (const [fileName, output] of Object.entries(bundle)) {
|
|
31
|
+
if (!fileName.endsWith('.mjs') && !fileName.endsWith('.js')) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const size = 'code' in output ? Buffer.byteLength(output.code, 'utf-8') : 0;
|
|
35
|
+
if (size > HARD_LIMIT_BYTES) {
|
|
36
|
+
this.error(`${fileName} is ${(size / 1024 / 1024).toFixed(2)}MB, over the 3MB hard limit for a mod bundle.`);
|
|
37
|
+
}
|
|
38
|
+
else if (size > SOFT_LIMIT_BYTES) {
|
|
39
|
+
this.warn(`${fileName} is ${(size / 1024).toFixed(0)}KB, over the 1MB soft limit for a mod bundle — consider trimming assets.`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const SDK_EXPORTS: Record<string, string[]>;
|