@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
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { type FateModCapability } from '@fate-app/mod-types';
|
|
2
|
+
import type * as VueModule from 'vue';
|
|
3
|
+
import type * as VueI18nModule from 'vue-i18n';
|
|
4
|
+
import type * as ThreeModule from 'three';
|
|
5
|
+
import type * as CannonEsModule from 'cannon-es';
|
|
6
|
+
/**
|
|
7
|
+
* `fate-core-mods`'s CI smoke-load check (see the registry repo's
|
|
8
|
+
* `validate-pr.yml`) imports a freshly built `bundle.mjs` in plain Node and
|
|
9
|
+
* runs it through the exact same `validateBundleShape` gate the app's real
|
|
10
|
+
* loader uses (`src/mods/loader.ts` in the app repo), then mounts every
|
|
11
|
+
* declared sheet component to catch render-time crashes before a human
|
|
12
|
+
* reviewer ever runs the mod. This file is intentionally NOT imported by
|
|
13
|
+
* `./index.ts` (the build-preset entry) — it pulls in `vue`/`vue-i18n`/
|
|
14
|
+
* `jsdom`/`@vue/test-utils`, none of which a mod author's `vite.config.ts`
|
|
15
|
+
* needs, so it's a separate `@fate-app/mod-build/testing` subpath export.
|
|
16
|
+
*
|
|
17
|
+
* `vue`/`vue-i18n` are deliberately imported *dynamically*, never at module
|
|
18
|
+
* top level: `@vue/runtime-dom` captures a reference to `document` the
|
|
19
|
+
* moment it's first evaluated (`const doc = typeof document !== 'undefined'
|
|
20
|
+
* ? document : null`, module-scope, computed once). A static top-level
|
|
21
|
+
* `import 'vue'` in this file would run before `ensureDom()` ever gets a
|
|
22
|
+
* chance to install jsdom's `document` onto `globalThis`, permanently baking
|
|
23
|
+
* in `doc = null` for this module instance — reassigning `globalThis.document`
|
|
24
|
+
* afterwards doesn't retroactively fix an already-captured reference. Every
|
|
25
|
+
* function here that touches Vue imports it lazily, after `ensureDom()`.
|
|
26
|
+
*/
|
|
27
|
+
/** Loose, duck-typed stand-in for the app's real `FateSDK` shape (`src/mods/sdk.ts`)
|
|
28
|
+
* — deliberately not imported from the app, which isn't reachable from here. */
|
|
29
|
+
export interface StubFateSDK {
|
|
30
|
+
version: string;
|
|
31
|
+
vue: typeof VueModule;
|
|
32
|
+
vueI18n: typeof VueI18nModule;
|
|
33
|
+
ionicVue: Record<string, unknown>;
|
|
34
|
+
ionicons: Record<string, unknown>;
|
|
35
|
+
dice: {
|
|
36
|
+
three: typeof ThreeModule;
|
|
37
|
+
cannonEs: typeof CannonEsModule;
|
|
38
|
+
};
|
|
39
|
+
api: {
|
|
40
|
+
toast: {
|
|
41
|
+
error(key: string, opts?: Record<string, unknown>): Promise<void>;
|
|
42
|
+
success(key: string, opts?: Record<string, unknown>): Promise<void>;
|
|
43
|
+
};
|
|
44
|
+
getModData<T>(character: unknown, key: string): T | undefined;
|
|
45
|
+
setModData<T>(character: unknown, key: string, value: T): void;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export declare function stubFateSDK(): Promise<StubFateSDK>;
|
|
49
|
+
export type SmokeLoadResult = {
|
|
50
|
+
ok: true;
|
|
51
|
+
} | {
|
|
52
|
+
ok: false;
|
|
53
|
+
error: string;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Imports a built `bundle.mjs`, validates its shape, and mounts every
|
|
57
|
+
* declared sheet component. Returns a result instead of throwing so the CI
|
|
58
|
+
* script can print a clean pass/fail per mod rather than a raw stack trace.
|
|
59
|
+
*/
|
|
60
|
+
export declare function smokeLoad(bundlePath: string, manifest: {
|
|
61
|
+
capabilities?: FateModCapability[];
|
|
62
|
+
}): Promise<SmokeLoadResult>;
|
package/dist/testing.js
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
import { pathToFileURL } from 'node:url';
|
|
10
|
+
import { resolve } from 'node:path';
|
|
11
|
+
import { validateBundleShape, DiceMaterial } from '@fate-app/mod-types';
|
|
12
|
+
/**
|
|
13
|
+
* Every property access on the returned object resolves to *something* valid
|
|
14
|
+
* (a trivial passthrough component, or the property name itself for icons)
|
|
15
|
+
* instead of `undefined` — real `@ionic/vue`/`ionicons` Web Components aren't
|
|
16
|
+
* registered in a plain Node process, and failing a smoke test purely because
|
|
17
|
+
* of that would be a false positive. Genuine bugs in a mod's own render logic
|
|
18
|
+
* still throw normally.
|
|
19
|
+
*/
|
|
20
|
+
function stubModule(vue, kind) {
|
|
21
|
+
return new Proxy({}, {
|
|
22
|
+
get(_target, prop) {
|
|
23
|
+
if (typeof prop !== 'string')
|
|
24
|
+
return undefined;
|
|
25
|
+
if (kind === 'icon')
|
|
26
|
+
return prop;
|
|
27
|
+
return vue.defineComponent({
|
|
28
|
+
name: `Stub_${prop}`,
|
|
29
|
+
setup(_props, { slots }) {
|
|
30
|
+
return () => vue.h('div', slots.default?.());
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
export async function stubFateSDK() {
|
|
37
|
+
const vue = await import('vue');
|
|
38
|
+
const vueI18n = await import('vue-i18n');
|
|
39
|
+
const three = await import('three');
|
|
40
|
+
const cannonEs = await import('cannon-es');
|
|
41
|
+
return {
|
|
42
|
+
version: '0.0.0-smoke-test',
|
|
43
|
+
vue,
|
|
44
|
+
vueI18n,
|
|
45
|
+
ionicVue: stubModule(vue, 'component'),
|
|
46
|
+
ionicons: stubModule(vue, 'icon'),
|
|
47
|
+
dice: { three, cannonEs },
|
|
48
|
+
api: {
|
|
49
|
+
toast: { error: async () => { }, success: async () => { } },
|
|
50
|
+
getModData: () => undefined,
|
|
51
|
+
setModData: () => { }
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function stubContext() {
|
|
56
|
+
return { modules: {}, constants: {}, templates: { character: {} }, shared: {}, components: [] };
|
|
57
|
+
}
|
|
58
|
+
/** Sheet components receive the character as `defineModel<Character>({ required: true })`
|
|
59
|
+
* — a real mod legitimately expects a non-empty `modelValue` prop, so mounting
|
|
60
|
+
* without one is a smoke-load false positive/negative, not a real bug to catch. */
|
|
61
|
+
function stubCharacter() {
|
|
62
|
+
return { id: 0, name: 'Smoke Test', avatar: '', _modules: {} };
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Constructor-level check for a dice-capability bundle: instantiates each
|
|
66
|
+
* declared shape with a real (headless) three/cannon-es material and physics
|
|
67
|
+
* world, catching crashes in createMesh()/createBody(). Deliberately shallow
|
|
68
|
+
* — it never renders (no WebGLRenderer, no canvas) or steps physics; real
|
|
69
|
+
* visual/gameplay behavior is reviewed by a human, same as sheet components
|
|
70
|
+
* are only mounted here, not interacted with.
|
|
71
|
+
*/
|
|
72
|
+
async function smokeLoadDice(dice) {
|
|
73
|
+
const three = await import('three');
|
|
74
|
+
const cannonEs = await import('cannon-es');
|
|
75
|
+
for (const material of (dice.materials ?? [])) {
|
|
76
|
+
if (typeof material.name !== 'string' || typeof material.previewColor !== 'string' || !material.faceMaterial || !material.symbolMaterial) {
|
|
77
|
+
throw new Error(`Invalid dice material shape: ${JSON.stringify(Object.keys(material))}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const stubMaterial = new DiceMaterial('smoke-test', new three.MeshStandardMaterial(), new three.MeshStandardMaterial(), '#ffffff');
|
|
81
|
+
const world = new cannonEs.World();
|
|
82
|
+
for (const Shape of (dice.shapes ?? [])) {
|
|
83
|
+
const instance = new Shape(stubMaterial, 1, 1, 1, world, () => { });
|
|
84
|
+
instance.getResult();
|
|
85
|
+
instance.formatResult(0);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Vue's runtime-dom + Ionic-style components reach for a range of DOM
|
|
89
|
+
// constructors, not just document/navigator. Deliberately a curated list,
|
|
90
|
+
// not "copy every jsdom window property" — jsdom's window has circular
|
|
91
|
+
// self-references (window.self/top/parent all alias back to window itself),
|
|
92
|
+
// and eagerly reading those as plain values produces a genuinely circular
|
|
93
|
+
// object graph that overflows the stack the moment anything tries to
|
|
94
|
+
// traverse it (Vue's reactivity, console.log, etc.).
|
|
95
|
+
const DOM_GLOBALS = [
|
|
96
|
+
'document',
|
|
97
|
+
'navigator',
|
|
98
|
+
'Element',
|
|
99
|
+
'HTMLElement',
|
|
100
|
+
'SVGElement',
|
|
101
|
+
'Node',
|
|
102
|
+
'Text',
|
|
103
|
+
'Comment',
|
|
104
|
+
'DocumentFragment',
|
|
105
|
+
'Event',
|
|
106
|
+
'CustomEvent',
|
|
107
|
+
'MouseEvent',
|
|
108
|
+
'KeyboardEvent',
|
|
109
|
+
'MutationObserver',
|
|
110
|
+
'customElements',
|
|
111
|
+
'getComputedStyle',
|
|
112
|
+
'requestAnimationFrame',
|
|
113
|
+
'cancelAnimationFrame'
|
|
114
|
+
];
|
|
115
|
+
/**
|
|
116
|
+
* `@vue/test-utils`'s `mount()` needs `document`/`window` — present under a
|
|
117
|
+
* test runner's own environment (Vitest/Jest), but not in a bare `node
|
|
118
|
+
* scripts/ci/smoke-load.ts` process. Bootstraps a minimal jsdom window onto
|
|
119
|
+
* `globalThis` the first time it's needed so `smokeLoad()` works standalone;
|
|
120
|
+
* a no-op if a DOM global already exists (e.g. this ever runs inside Vitest).
|
|
121
|
+
* Must run — and complete — before `vue` is imported anywhere (see the
|
|
122
|
+
* module-level comment above).
|
|
123
|
+
*/
|
|
124
|
+
async function ensureDom() {
|
|
125
|
+
if (typeof document !== 'undefined')
|
|
126
|
+
return;
|
|
127
|
+
const { JSDOM } = await import('jsdom');
|
|
128
|
+
const dom = new JSDOM('<!doctype html><html><body></body></html>');
|
|
129
|
+
const globalTarget = globalThis;
|
|
130
|
+
const domWindow = dom.window;
|
|
131
|
+
globalTarget.window = dom.window;
|
|
132
|
+
for (const key of DOM_GLOBALS) {
|
|
133
|
+
// Node defines a few of these itself (e.g. `navigator`, read-only since
|
|
134
|
+
// Node 21) — defineProperty with configurable:true overrides those too.
|
|
135
|
+
Object.defineProperty(globalTarget, key, { value: domWindow[key], configurable: true, writable: true });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Imports a built `bundle.mjs`, validates its shape, and mounts every
|
|
140
|
+
* declared sheet component. Returns a result instead of throwing so the CI
|
|
141
|
+
* script can print a clean pass/fail per mod rather than a raw stack trace.
|
|
142
|
+
*/
|
|
143
|
+
export async function smokeLoad(bundlePath, manifest) {
|
|
144
|
+
await ensureDom();
|
|
145
|
+
const globalTarget = globalThis;
|
|
146
|
+
const previousSDK = globalTarget.FateSDK;
|
|
147
|
+
globalTarget.FateSDK = await stubFateSDK();
|
|
148
|
+
try {
|
|
149
|
+
const imported = (await import(__rewriteRelativeImportExtension(pathToFileURL(resolve(bundlePath)).href)));
|
|
150
|
+
const bundle = imported.default;
|
|
151
|
+
validateBundleShape(bundle, manifest.capabilities);
|
|
152
|
+
if (manifest.capabilities?.includes('sheetComponents') && Array.isArray(bundle.components)) {
|
|
153
|
+
const { mount } = await import('@vue/test-utils');
|
|
154
|
+
for (const entry of bundle.components) {
|
|
155
|
+
// The component's real prop shape isn't known statically (it comes
|
|
156
|
+
// from a dynamically imported, untyped bundle) — `as never` opts out
|
|
157
|
+
// of mount()'s prop-shape checking for both arguments deliberately,
|
|
158
|
+
// not just the component itself.
|
|
159
|
+
mount(entry.component, {
|
|
160
|
+
props: { modelValue: stubCharacter() },
|
|
161
|
+
global: {
|
|
162
|
+
provide: { context: stubContext() },
|
|
163
|
+
// Real mods use $t() in templates via the host's real vue-i18n
|
|
164
|
+
// instance (FateSDK.vueI18n) — no i18n plugin is installed here,
|
|
165
|
+
// so stub $t as an identity function rather than failing a
|
|
166
|
+
// legitimate call with "$t is not a function".
|
|
167
|
+
mocks: { $t: (key) => key }
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (manifest.capabilities?.includes('dice') && bundle.dice) {
|
|
173
|
+
await smokeLoadDice(bundle.dice);
|
|
174
|
+
}
|
|
175
|
+
return { ok: true };
|
|
176
|
+
}
|
|
177
|
+
catch (e) {
|
|
178
|
+
return { ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
179
|
+
}
|
|
180
|
+
finally {
|
|
181
|
+
globalTarget.FateSDK = previousSDK;
|
|
182
|
+
}
|
|
183
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fate-app/mod-build",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Vite build preset + dev-mode server for authoring Assistant for Fate mods — externalizes host-shared libraries against window.FateSDK, inlines CSS, and enforces bundle size limits.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/Stanislavsonder/fate.git",
|
|
9
|
+
"directory": "packages/mod-build"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"bin": {
|
|
14
|
+
"fate-mod-build": "./dist/cli.js"
|
|
15
|
+
},
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"import": "./dist/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./testing": {
|
|
22
|
+
"types": "./dist/testing.d.ts",
|
|
23
|
+
"import": "./dist/testing.js"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist"
|
|
28
|
+
],
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@fate-app/mod-types": "^2.0.0",
|
|
34
|
+
"vite-plugin-css-injected-by-js": "^3.5.2"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/jsdom": "^21.1.0",
|
|
38
|
+
"cannon-es": "^0.20.0",
|
|
39
|
+
"es-module-lexer": "^2.3.1",
|
|
40
|
+
"jsdom": "^29.1.1",
|
|
41
|
+
"three": "^0.185.0"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"@vitejs/plugin-vue": "^6.0.0",
|
|
45
|
+
"@vue/test-utils": "^2.4.0",
|
|
46
|
+
"cannon-es": "^0.20.0",
|
|
47
|
+
"jsdom": "^25.0.0 || ^26.0.0 || ^27.0.0 || ^28.0.0 || ^29.0.0",
|
|
48
|
+
"three": "^0.185.0",
|
|
49
|
+
"vite": "^8.0.0",
|
|
50
|
+
"vue": "^3.5.0"
|
|
51
|
+
},
|
|
52
|
+
"peerDependenciesMeta": {
|
|
53
|
+
"@vue/test-utils": {
|
|
54
|
+
"optional": true
|
|
55
|
+
},
|
|
56
|
+
"cannon-es": {
|
|
57
|
+
"optional": true
|
|
58
|
+
},
|
|
59
|
+
"jsdom": {
|
|
60
|
+
"optional": true
|
|
61
|
+
},
|
|
62
|
+
"three": {
|
|
63
|
+
"optional": true
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
"scripts": {
|
|
67
|
+
"build": "tsc -p tsconfig.build.json",
|
|
68
|
+
"generate-sdk-exports": "node --experimental-transform-types scripts/generateSdkExports.ts"
|
|
69
|
+
}
|
|
70
|
+
}
|