@oss-idle/addon-sdk 0.5.0 → 0.6.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/README.md +42 -2
- package/bin/cli.mjs +1 -0
- package/bin/scaffold.mjs +3 -0
- package/dist/build/index.d.ts +14 -1
- package/dist/build/index.d.ts.map +1 -1
- package/dist/build/index.js +13 -10
- package/dist/build/plugins.d.ts +7 -0
- package/dist/build/plugins.d.ts.map +1 -1
- package/dist/build/plugins.js +27 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/runtime/addon.d.ts +15 -4
- package/dist/runtime/addon.d.ts.map +1 -1
- package/dist/runtime/addon.js +25 -4
- package/dist/runtime/define.d.ts +6 -3
- package/dist/runtime/define.d.ts.map +1 -1
- package/dist/runtime/define.js +7 -4
- package/dist/runtime/host.d.ts +113 -0
- package/dist/runtime/host.d.ts.map +1 -0
- package/dist/runtime/host.js +8 -0
- package/package.json +1 -1
- package/templates/common/README.md +19 -0
- package/templates/common/addon.config.json +2 -1
- package/templates/common/eslint.config.js +54 -0
package/README.md
CHANGED
|
@@ -22,8 +22,11 @@ That emits one self-contained ES module. Paste it into the in-game add-on editor
|
|
|
22
22
|
## The contract
|
|
23
23
|
|
|
24
24
|
The game loads an add-on by importing its source as a Blob ES module and calling
|
|
25
|
-
`mount` / `unmount`.
|
|
26
|
-
|
|
25
|
+
`mount` / `unmount`. `mount` is handed the game's host (`this.host` in an
|
|
26
|
+
`Addon`): its version, its React, and the editor for add-ons of the Editor type.
|
|
27
|
+
Add-ons run unsandboxed in the page realm and reach for the rest themselves. The
|
|
28
|
+
full guide, the editor API included, is at
|
|
29
|
+
https://oss.idle.rhythmgamers.net/web/docs/addons.
|
|
27
30
|
|
|
28
31
|
```ts
|
|
29
32
|
import { Addon, defineAddon } from '@oss-idle/addon-sdk';
|
|
@@ -49,6 +52,35 @@ Two things follow from that, and they shape the whole SDK:
|
|
|
49
52
|
through `this.on`, `this.interval`, `this.timeout` or `this.own` is torn down
|
|
50
53
|
for you, in reverse order, even if one teardown throws.
|
|
51
54
|
|
|
55
|
+
## Editor add-ons
|
|
56
|
+
|
|
57
|
+
An add-on of the Editor type (picked when you make it in game) reaches the map
|
|
58
|
+
editor while it is open. Everything registered through the editor's API goes when
|
|
59
|
+
the editor closes or the add-on unmounts.
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
class Mirror extends Addon {
|
|
63
|
+
constructor() { super({ name: 'Mirror' }); }
|
|
64
|
+
|
|
65
|
+
protected onMount() {
|
|
66
|
+
this.editor(editor => {
|
|
67
|
+
editor.addAction({
|
|
68
|
+
id: 'mirror-selection', label: 'Mirror the selected notes', keys: ['Alt+M'],
|
|
69
|
+
run: () => {
|
|
70
|
+
const picked = new Set(editor.selection());
|
|
71
|
+
editor.edit({
|
|
72
|
+
notes: editor.notes().map(note => picked.has(note)
|
|
73
|
+
? { ...note, column: editor.keys - 1 - note.column } : note),
|
|
74
|
+
});
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`EditorApi` and the other host types are exported from the package.
|
|
83
|
+
|
|
52
84
|
## React overlays
|
|
53
85
|
|
|
54
86
|
```tsx
|
|
@@ -197,6 +229,7 @@ moderator before they go live.
|
|
|
197
229
|
| `gameRoot` → resolves `@oss-idle/shared/*` | The game's package is unpublished, so its TypeScript is compiled from a checkout (`$OSS_IDLE_PATH`). |
|
|
198
230
|
| `dedupe` | Your sources and the game's come from different checkouts, so shared packages would otherwise be bundled twice - which silently breaks prototype patches and `instanceof`. |
|
|
199
231
|
| `stubModules` | Trims a heavy dependency a game module imports but your add-on never calls. Stubs **throw** if reached. |
|
|
232
|
+
| `plugins` | Extra esbuild plugins for the add-on and its workers, run before the SDK's own - e.g. to redirect one of the game's imports to your own module. |
|
|
200
233
|
|
|
201
234
|
### Using the game's own code
|
|
202
235
|
|
|
@@ -218,6 +251,13 @@ Doing so makes your add-on a derivative work of oss!idle, which is **AGPL-3.0**
|
|
|
218
251
|
so it must be AGPL-3.0 too. Pin the game version you built against; the sources
|
|
219
252
|
are internal and do move between releases.
|
|
220
253
|
|
|
254
|
+
### The game's React
|
|
255
|
+
|
|
256
|
+
Set `"sharedReact": true` in `addon.config.json` (the template does) and the
|
|
257
|
+
bundle reads React from the game instead of shipping its own: one React on the
|
|
258
|
+
page, and a smaller add-on. A game too old to hand out its React refuses the
|
|
259
|
+
add-on with a message; leave the option off to support those.
|
|
260
|
+
|
|
221
261
|
## Licence
|
|
222
262
|
|
|
223
263
|
AGPL-3.0-only.
|
package/bin/cli.mjs
CHANGED
package/bin/scaffold.mjs
CHANGED
|
@@ -138,6 +138,7 @@ export const scaffold = async ({
|
|
|
138
138
|
build: 'oss-idle-addon build',
|
|
139
139
|
dev: 'oss-idle-addon build --watch',
|
|
140
140
|
typecheck: 'tsc -p tsconfig.json --noEmit',
|
|
141
|
+
lint: 'eslint .',
|
|
141
142
|
},
|
|
142
143
|
dependencies: {
|
|
143
144
|
'@oss-idle/addon-sdk': await sdkVersion(),
|
|
@@ -145,7 +146,9 @@ export const scaffold = async ({
|
|
|
145
146
|
},
|
|
146
147
|
devDependencies: {
|
|
147
148
|
...(react ? { '@types/react': '^18.3.12', '@types/react-dom': '^18.3.1' } : {}),
|
|
149
|
+
eslint: '^10.13.0',
|
|
148
150
|
typescript: '^5.6.3',
|
|
151
|
+
'typescript-eslint': '^8.72.0',
|
|
149
152
|
},
|
|
150
153
|
};
|
|
151
154
|
await writeFile(join(target, 'package.json'), JSON.stringify(pkg, null, 2) + '\n', 'utf8');
|
package/dist/build/index.d.ts
CHANGED
|
@@ -13,12 +13,13 @@
|
|
|
13
13
|
* moderator before they go live, and a single minified line is a poor thing to
|
|
14
14
|
* ask anyone to review.
|
|
15
15
|
*/
|
|
16
|
+
import { type Plugin } from 'esbuild';
|
|
16
17
|
/**
|
|
17
18
|
* The individual esbuild plugins, for a build that needs to assemble its own
|
|
18
19
|
* pipeline rather than call {@link bundleAddon} - compiling a slice of an add-on
|
|
19
20
|
* for a test, say, or bundling a worker on its own.
|
|
20
21
|
*/
|
|
21
|
-
export { assetQueryPlugin, cssTextPlugin, dedupePlugin, inlineWorkerPlugin, sharedSourcePlugin, stubModulePlugin, tsExtensionPlugin, } from './plugins.js';
|
|
22
|
+
export { assetQueryPlugin, cssTextPlugin, dedupePlugin, hostReactPlugin, inlineWorkerPlugin, sharedSourcePlugin, stubModulePlugin, tsExtensionPlugin, } from './plugins.js';
|
|
22
23
|
export type BundleOptions = {
|
|
23
24
|
/** Entry module, exporting `mount` / `unmount` (usually via `defineAddon`). */
|
|
24
25
|
entry: string;
|
|
@@ -49,6 +50,18 @@ export type BundleOptions = {
|
|
|
49
50
|
dedupe?: readonly string[];
|
|
50
51
|
/** Directory the deduped packages resolve from. Defaults to the entry's directory. */
|
|
51
52
|
dedupeFrom?: string;
|
|
53
|
+
/**
|
|
54
|
+
* Extra esbuild plugins, for the add-on and every inlined worker. They run
|
|
55
|
+
* before the SDK's own, so an `onResolve` here can redirect an import -
|
|
56
|
+
* one of the game's own modules, say - before the SDK resolves it.
|
|
57
|
+
*/
|
|
58
|
+
plugins?: readonly Plugin[];
|
|
59
|
+
/**
|
|
60
|
+
* Use the game's React (`globalThis.ossIdle.react`) instead of bundling one:
|
|
61
|
+
* a much smaller add-on, and one React on the page. Needs an oss!idle that
|
|
62
|
+
* hands its React to add-ons; an older one refuses the add-on with a message.
|
|
63
|
+
*/
|
|
64
|
+
sharedReact?: boolean;
|
|
52
65
|
};
|
|
53
66
|
export type BundleResult = {
|
|
54
67
|
outfile: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/build/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/build/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAuC,KAAK,MAAM,EAAE,MAAM,SAAS,CAAC;AAe3E;;;;GAIG;AACH,OAAO,EACN,gBAAgB,EAChB,aAAa,EACb,YAAY,EACZ,eAAe,EACf,kBAAkB,EAClB,kBAAkB,EAClB,gBAAgB,EAChB,iBAAiB,GACjB,MAAM,cAAc,CAAC;AAEtB,MAAM,MAAM,aAAa,GAAG;IAC3B,+EAA+E;IAC/E,KAAK,EAAE,MAAM,CAAC;IACd,oDAAoD;IACpD,OAAO,EAAE,MAAM,CAAC;IAChB,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,8DAA8D;IAC9D,IAAI,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACnF,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CAAC;IAChD;;;;;OAKG;IACH,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,sFAAsF;IACtF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;CACtB,CAAC;AA8DF,MAAM,MAAM,YAAY,GAAG;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,oEAAoE;IACpE,KAAK,EAAE,MAAM,CAAC;CACd,CAAC;AASF,oEAAoE;AACpE,eAAO,MAAM,WAAW,GAAU,SAAS,aAAa,KAAG,OAAO,CAAC,YAAY,CAiD9E,CAAC"}
|
package/dist/build/index.js
CHANGED
|
@@ -17,16 +17,16 @@ import { build as esbuild } from 'esbuild';
|
|
|
17
17
|
import { createRequire } from 'node:module';
|
|
18
18
|
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
19
19
|
import { dirname, resolve } from 'node:path';
|
|
20
|
-
import { assetQueryPlugin, cssTextPlugin, dedupePlugin, inlineWorkerPlugin, sharedSourcePlugin, stubModulePlugin, tsExtensionPlugin, } from './plugins.js';
|
|
20
|
+
import { assetQueryPlugin, cssTextPlugin, dedupePlugin, hostReactPlugin, inlineWorkerPlugin, sharedSourcePlugin, stubModulePlugin, tsExtensionPlugin, } from './plugins.js';
|
|
21
21
|
/**
|
|
22
22
|
* The individual esbuild plugins, for a build that needs to assemble its own
|
|
23
23
|
* pipeline rather than call {@link bundleAddon} - compiling a slice of an add-on
|
|
24
24
|
* for a test, say, or bundling a worker on its own.
|
|
25
25
|
*/
|
|
26
|
-
export { assetQueryPlugin, cssTextPlugin, dedupePlugin, inlineWorkerPlugin, sharedSourcePlugin, stubModulePlugin, tsExtensionPlugin, } from './plugins.js';
|
|
26
|
+
export { assetQueryPlugin, cssTextPlugin, dedupePlugin, hostReactPlugin, inlineWorkerPlugin, sharedSourcePlugin, stubModulePlugin, tsExtensionPlugin, } from './plugins.js';
|
|
27
27
|
const sharedSrcOf = (gameRoot) => resolve(gameRoot, 'packages/shared/src');
|
|
28
28
|
/** esbuild settings shared by the add-on bundle and any inlined worker. */
|
|
29
|
-
const baseOptions = (gameRoot, minify, stubModules = {}, dedupe = [], dedupeFrom = process.cwd()) => ({
|
|
29
|
+
const baseOptions = (gameRoot, minify, stubModules = {}, dedupe = [], dedupeFrom = process.cwd(), plugins = []) => ({
|
|
30
30
|
bundle: true,
|
|
31
31
|
format: 'esm',
|
|
32
32
|
platform: 'browser',
|
|
@@ -42,6 +42,7 @@ const baseOptions = (gameRoot, minify, stubModules = {}, dedupe = [], dedupeFrom
|
|
|
42
42
|
loader: { '.wasm': 'base64' },
|
|
43
43
|
minify,
|
|
44
44
|
plugins: [
|
|
45
|
+
...plugins,
|
|
45
46
|
// Strip asset suffixes before anything tries to resolve them as modules.
|
|
46
47
|
assetQueryPlugin(),
|
|
47
48
|
// Dedupe first: it must see a bare package name before anything else
|
|
@@ -54,9 +55,9 @@ const baseOptions = (gameRoot, minify, stubModules = {}, dedupe = [], dedupeFrom
|
|
|
54
55
|
],
|
|
55
56
|
});
|
|
56
57
|
/** Build one worker entry to a self-contained string for inlining. */
|
|
57
|
-
const buildWorkerSource = async (entry, gameRoot, minify, stubModules, dedupe, dedupeFrom) => {
|
|
58
|
+
const buildWorkerSource = async (entry, gameRoot, minify, stubModules, dedupe, dedupeFrom, plugins) => {
|
|
58
59
|
const result = await esbuild({
|
|
59
|
-
...baseOptions(gameRoot, minify, stubModules, dedupe, dedupeFrom),
|
|
60
|
+
...baseOptions(gameRoot, minify, stubModules, dedupe, dedupeFrom, plugins),
|
|
60
61
|
entryPoints: [entry],
|
|
61
62
|
write: false,
|
|
62
63
|
// A worker built as ESM would need `type: 'module'` support *and* would
|
|
@@ -72,12 +73,14 @@ const buildWorkerSource = async (entry, gameRoot, minify, stubModules, dedupe, d
|
|
|
72
73
|
const sdkVersion = createRequire(import.meta.url)('@oss-idle/addon-sdk/package.json').version;
|
|
73
74
|
/** Bundle an add-on into the single ES module the runtime loads. */
|
|
74
75
|
export const bundleAddon = async (options) => {
|
|
75
|
-
const { entry, outfile, gameRoot, meta, minify = false, sourcemap = false, stubModules = {}, dedupe = [], dedupeFrom = dirname(resolve(entry)), } = options;
|
|
76
|
+
const { entry, outfile, gameRoot, meta, minify = false, sourcemap = false, stubModules = {}, dedupe = [], dedupeFrom = dirname(resolve(entry)), plugins = [], sharedReact = false, } = options;
|
|
76
77
|
// The SDK's React modules are imported from wherever npm put the SDK, so
|
|
77
78
|
// `react` resolves from there unless it is pointed back at the add-on that
|
|
78
|
-
// installed it - and two Reacts in one bundle break hooks outright.
|
|
79
|
-
|
|
80
|
-
const
|
|
79
|
+
// installed it - and two Reacts in one bundle break hooks outright. With the
|
|
80
|
+
// game's React there is none to bundle.
|
|
81
|
+
const names = [...new Set([...sharedReact ? [] : ['react', 'react-dom'], ...dedupe])];
|
|
82
|
+
const ownPlugins = sharedReact ? [hostReactPlugin(dedupeFrom), ...plugins] : plugins;
|
|
83
|
+
const base = baseOptions(gameRoot, minify, stubModules, names, dedupeFrom, ownPlugins);
|
|
81
84
|
const result = await esbuild({
|
|
82
85
|
...base,
|
|
83
86
|
// Read back at runtime by `buildMeta()`.
|
|
@@ -86,7 +89,7 @@ export const bundleAddon = async (options) => {
|
|
|
86
89
|
write: false,
|
|
87
90
|
sourcemap: sourcemap ? 'inline' : false,
|
|
88
91
|
plugins: [
|
|
89
|
-
inlineWorkerPlugin(workerEntry => buildWorkerSource(workerEntry, gameRoot, minify, stubModules, dedupe, dedupeFrom)),
|
|
92
|
+
inlineWorkerPlugin(workerEntry => buildWorkerSource(workerEntry, gameRoot, minify, stubModules, dedupe, dedupeFrom, plugins)),
|
|
90
93
|
...(base.plugins ?? []),
|
|
91
94
|
],
|
|
92
95
|
});
|
package/dist/build/plugins.d.ts
CHANGED
|
@@ -85,4 +85,11 @@ export declare const dedupePlugin: (names: readonly string[], fromDir: string) =
|
|
|
85
85
|
* (`.wasm` -> base64) applies as normal.
|
|
86
86
|
*/
|
|
87
87
|
export declare const assetQueryPlugin: () => Plugin;
|
|
88
|
+
/**
|
|
89
|
+
* React from the game instead of a bundled copy: each React import becomes a small
|
|
90
|
+
* module reading the game's own React off `globalThis.ossIdle.react`, which the game
|
|
91
|
+
* sets before it loads any add-on. Its named exports are the installed React's, read
|
|
92
|
+
* here at build time, so `import { useState } from 'react'` keeps working.
|
|
93
|
+
*/
|
|
94
|
+
export declare const hostReactPlugin: (fromDir: string) => Plugin;
|
|
88
95
|
//# sourceMappingURL=plugins.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugins.d.ts","sourceRoot":"","sources":["../../src/build/plugins.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"plugins.d.ts","sourceRoot":"","sources":["../../src/build/plugins.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAEtC;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,kBAAkB,GAAI,WAAW,MAAM,KAAG,MAgBrD,CAAC;AAEH;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,QAAO,MAanC,CAAC;AAEH;;;;;;;;;GASG;AACH,eAAO,MAAM,kBAAkB,GAAI,aAAa,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,KAAG,MA0BnF,CAAC;AAEH,oFAAoF;AACpF,eAAO,MAAM,aAAa,QAAO,MAQ/B,CAAC;AAEH;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,gBAAgB,GAAI,OAAO,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,KAAG,MAsB1E,CAAC;AAEH;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,YAAY,GAAI,OAAO,SAAS,MAAM,EAAE,EAAE,SAAS,MAAM,KAAG,MAmBvE,CAAC;AAEH;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,gBAAgB,QAAO,MAgBlC,CAAC;AAEH;;;;;GAKG;AACH,eAAO,MAAM,eAAe,GAAI,SAAS,MAAM,KAAG,MAmBhD,CAAC"}
|
package/dist/build/plugins.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
3
4
|
import { dirname, resolve } from 'node:path';
|
|
4
5
|
/**
|
|
5
6
|
* Resolve `@oss-idle/shared/*` against a checkout of the game.
|
|
@@ -211,3 +212,29 @@ export const assetQueryPlugin = () => ({
|
|
|
211
212
|
});
|
|
212
213
|
},
|
|
213
214
|
});
|
|
215
|
+
/**
|
|
216
|
+
* React from the game instead of a bundled copy: each React import becomes a small
|
|
217
|
+
* module reading the game's own React off `globalThis.ossIdle.react`, which the game
|
|
218
|
+
* sets before it loads any add-on. Its named exports are the installed React's, read
|
|
219
|
+
* here at build time, so `import { useState } from 'react'` keeps working.
|
|
220
|
+
*/
|
|
221
|
+
export const hostReactPlugin = (fromDir) => ({
|
|
222
|
+
name: 'host-react',
|
|
223
|
+
setup(build) {
|
|
224
|
+
const requireFrom = createRequire(resolve(fromDir, 'package.json'));
|
|
225
|
+
build.onResolve({ filter: /^react(-dom)?(\/(client|jsx-runtime))?$/ }, args => ({
|
|
226
|
+
path: args.path, namespace: 'oss-idle-react',
|
|
227
|
+
}));
|
|
228
|
+
build.onLoad({ filter: /.*/, namespace: 'oss-idle-react' }, args => {
|
|
229
|
+
const names = Object.keys(requireFrom(args.path))
|
|
230
|
+
.filter(name => name !== 'default' && /^[A-Za-z_$][\w$]*$/.test(name));
|
|
231
|
+
const contents = [
|
|
232
|
+
`const m = globalThis.ossIdle?.react?.[${JSON.stringify(args.path)}];`,
|
|
233
|
+
'if (!m) throw new Error("This add-on uses the game\'s React: update oss!idle to run it");',
|
|
234
|
+
'export default m;',
|
|
235
|
+
...names.map(name => `export const ${name} = m.${name};`),
|
|
236
|
+
].join('\n');
|
|
237
|
+
return { contents, loader: 'js' };
|
|
238
|
+
});
|
|
239
|
+
},
|
|
240
|
+
});
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { Addon, type AddonMeta, type Disposer } from './runtime/addon.js';
|
|
2
2
|
export { defineAddon, type AddonModule } from './runtime/define.js';
|
|
3
|
+
export { gameHost, type AddonHost, type EditorApi, type EditorMetadata, type EditorNote, type EditorRender, type EditorTimingPoint, } from './runtime/host.js';
|
|
3
4
|
export { buildMeta, type BuildMeta } from './runtime/buildMeta.js';
|
|
4
5
|
export { HotkeyManager, isolateInput, parseHotkey, type Hotkey, } from './runtime/input.js';
|
|
5
6
|
export { createSettings, type SettingsStore } from './runtime/settings.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,KAAK,SAAS,EAAE,KAAK,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC1E,OAAO,EAAE,WAAW,EAAE,KAAK,WAAW,EAAE,MAAM,qBAAqB,CAAC;AACpE,OAAO,EAAE,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnE,OAAO,EACN,aAAa,EACb,YAAY,EACZ,WAAW,EACX,KAAK,MAAM,GACX,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,cAAc,EAAE,KAAK,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3E,OAAO,EAAE,MAAM,EAAE,KAAK,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AACvD,OAAO,EACN,YAAY,EACZ,KAAK,aAAa,EAClB,KAAK,aAAa,GAClB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,aAAa,EAAE,KAAK,YAAY,EAAE,MAAM,uBAAuB,CAAC;AACzE,OAAO,EAAE,SAAS,EAAE,KAAK,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACpE,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,KAAK,SAAS,EAAE,KAAK,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC1E,OAAO,EAAE,WAAW,EAAE,KAAK,WAAW,EAAE,MAAM,qBAAqB,CAAC;AACpE,OAAO,EACN,QAAQ,EACR,KAAK,SAAS,EACd,KAAK,SAAS,EACd,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,KAAK,YAAY,EACjB,KAAK,iBAAiB,GACtB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnE,OAAO,EACN,aAAa,EACb,YAAY,EACZ,WAAW,EACX,KAAK,MAAM,GACX,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,cAAc,EAAE,KAAK,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3E,OAAO,EAAE,MAAM,EAAE,KAAK,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AACvD,OAAO,EACN,YAAY,EACZ,KAAK,aAAa,EAClB,KAAK,aAAa,GAClB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,aAAa,EAAE,KAAK,YAAY,EAAE,MAAM,uBAAuB,CAAC;AACzE,OAAO,EAAE,SAAS,EAAE,KAAK,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACpE,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { Addon } from './runtime/addon.js';
|
|
2
2
|
export { defineAddon } from './runtime/define.js';
|
|
3
|
+
export { gameHost, } from './runtime/host.js';
|
|
3
4
|
export { buildMeta } from './runtime/buildMeta.js';
|
|
4
5
|
export { HotkeyManager, isolateInput, parseHotkey, } from './runtime/input.js';
|
|
5
6
|
export { createSettings } from './runtime/settings.js';
|
package/dist/runtime/addon.d.ts
CHANGED
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* oss!idle loads an add-on by turning its `source` into a Blob URL and
|
|
5
5
|
* `import()`ing it in the page realm, then calling the module's `mount` /
|
|
6
|
-
* `unmount` exports - that pair *is* the contract
|
|
7
|
-
*
|
|
8
|
-
* the
|
|
6
|
+
* `unmount` exports - that pair *is* the contract. `mount` is handed the game's
|
|
7
|
+
* host (its React, the editor for add-ons of the Editor type: {@link host});
|
|
8
|
+
* the rest (the game's database, its beatmap store, the DOM) an add-on reaches
|
|
9
|
+
* for itself, because it runs unsandboxed on the page.
|
|
9
10
|
*
|
|
10
11
|
* Two consequences shape this class:
|
|
11
12
|
*
|
|
@@ -18,6 +19,7 @@
|
|
|
18
19
|
* hands out registration helpers that record their own teardown, and reverses
|
|
19
20
|
* them for you.
|
|
20
21
|
*/
|
|
22
|
+
import { type AddonHost, type EditorApi } from './host.js';
|
|
21
23
|
/** A teardown returned by a registration; running it undoes that registration. */
|
|
22
24
|
export type Disposer = () => void;
|
|
23
25
|
export type AddonMeta = {
|
|
@@ -70,7 +72,16 @@ export declare abstract class Addon {
|
|
|
70
72
|
* Public only because `defineAddon` lives outside the class and TypeScript
|
|
71
73
|
* has no package-private visibility; the underscore marks it as off-limits.
|
|
72
74
|
*/
|
|
73
|
-
_mount(): Promise<void>;
|
|
75
|
+
_mount(host?: AddonHost): Promise<void>;
|
|
76
|
+
private handed?;
|
|
77
|
+
/** The game's host: React, the editor. Undefined on a game too old to hand one. */
|
|
78
|
+
protected get host(): AddonHost | undefined;
|
|
79
|
+
/**
|
|
80
|
+
* Work with the editor while it is open (an add-on of the Editor type):
|
|
81
|
+
* `onOpen` runs each time it opens, with its API. What is registered through
|
|
82
|
+
* that API goes when the editor closes or this add-on unmounts.
|
|
83
|
+
*/
|
|
84
|
+
protected editor(onOpen: (editor: EditorApi) => void, onClose?: () => void): void;
|
|
74
85
|
/**
|
|
75
86
|
* Internal: called by {@link defineAddon}, not by add-on code.
|
|
76
87
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"addon.d.ts","sourceRoot":"","sources":["../../src/runtime/addon.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"addon.d.ts","sourceRoot":"","sources":["../../src/runtime/addon.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAGH,OAAO,EAEN,KAAK,SAAS,EACd,KAAK,SAAS,EACd,MAAM,WAAW,CAAC;AAEnB,kFAAkF;AAClF,MAAM,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC;AAElC,MAAM,MAAM,SAAS,GAAG;IACvB,wDAAwD;IACxD,IAAI,EAAE,MAAM,CAAC;IACb,+EAA+E;IAC/E,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF;;;;;;;GAOG;AACH,8BAAsB,KAAK;IAE1B,SAAS,CAAC,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAEnC,mEAAmE;IACnE,OAAO,CAAC,SAAS,CAAkB;IAEnC,OAAO,CAAC,OAAO,CAAS;gBAEZ,IAAI,EAAE,SAAS;IAI3B,8EAA8E;IAC9E,SAAS,CAAC,QAAQ,CAAC,OAAO,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAElD,6EAA6E;IAC7E,SAAS,CAAC,SAAS,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAE3C,oFAAoF;IACpF,SAAS,CAAC,GAAG,CAAC,CAAC,SAAS,QAAQ,EAAE,OAAO,EAAE,CAAC,GAAG,CAAC;IAKhD;;;;;OAKG;IACH,SAAS,CAAC,EAAE,CAAC,CAAC,SAAS,MAAM,cAAc,EAC1C,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,CAAC,EACP,OAAO,EAAE,CAAC,KAAK,EAAE,cAAc,CAAC,CAAC,CAAC,KAAK,IAAI,EAC3C,OAAO,CAAC,EAAE,uBAAuB,GAC/B,QAAQ;IACX,SAAS,CAAC,EAAE,CACX,MAAM,EAAE,WAAW,EACnB,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,kCAAkC,EAC3C,OAAO,CAAC,EAAE,uBAAuB,GAC/B,QAAQ;IAWX,gDAAgD;IAChD,SAAS,CAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,GAAG,QAAQ;IAK7D,uEAAuE;IACvE,SAAS,CAAC,OAAO,CAAC,OAAO,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,GAAG,QAAQ;IAK5D,8EAA8E;IAC9E,SAAS,KAAK,GAAG;wBAGC,OAAO,EAAE;wBACT,OAAO,EAAE;yBACR,OAAO,EAAE;MAE3B;IAED;;;;;OAKG;IACG,MAAM,CAAC,IAAI,CAAC,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAO7C,OAAO,CAAC,MAAM,CAAC,CAAY;IAE3B,mFAAmF;IACnF,SAAS,KAAK,IAAI,IAAI,SAAS,GAAG,SAAS,CAE1C;IAED;;;;OAIG;IACH,SAAS,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,SAAS,KAAK,IAAI,EAAE,OAAO,CAAC,EAAE,MAAM,IAAI,GAAG,IAAI;IASjF;;;;;;OAMG;IACG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;CAkB/B"}
|
package/dist/runtime/addon.js
CHANGED
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* oss!idle loads an add-on by turning its `source` into a Blob URL and
|
|
5
5
|
* `import()`ing it in the page realm, then calling the module's `mount` /
|
|
6
|
-
* `unmount` exports - that pair *is* the contract
|
|
7
|
-
*
|
|
8
|
-
* the
|
|
6
|
+
* `unmount` exports - that pair *is* the contract. `mount` is handed the game's
|
|
7
|
+
* host (its React, the editor for add-ons of the Editor type: {@link host});
|
|
8
|
+
* the rest (the game's database, its beatmap store, the DOM) an add-on reaches
|
|
9
|
+
* for itself, because it runs unsandboxed on the page.
|
|
9
10
|
*
|
|
10
11
|
* Two consequences shape this class:
|
|
11
12
|
*
|
|
@@ -19,6 +20,7 @@
|
|
|
19
20
|
* them for you.
|
|
20
21
|
*/
|
|
21
22
|
import { buildMeta } from './buildMeta.js';
|
|
23
|
+
import { gameHost, } from './host.js';
|
|
22
24
|
/**
|
|
23
25
|
* Base class for an add-on. Subclass it, implement {@link onMount}, and export
|
|
24
26
|
* the pair the runtime wants with {@link defineAddon}.
|
|
@@ -71,12 +73,31 @@ export class Addon {
|
|
|
71
73
|
* Public only because `defineAddon` lives outside the class and TypeScript
|
|
72
74
|
* has no package-private visibility; the underscore marks it as off-limits.
|
|
73
75
|
*/
|
|
74
|
-
async _mount() {
|
|
76
|
+
async _mount(host) {
|
|
75
77
|
if (this.mounted)
|
|
76
78
|
return;
|
|
77
79
|
this.mounted = true;
|
|
80
|
+
this.handed = host;
|
|
78
81
|
await this.onMount();
|
|
79
82
|
}
|
|
83
|
+
handed;
|
|
84
|
+
/** The game's host: React, the editor. Undefined on a game too old to hand one. */
|
|
85
|
+
get host() {
|
|
86
|
+
return this.handed ?? gameHost();
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Work with the editor while it is open (an add-on of the Editor type):
|
|
90
|
+
* `onOpen` runs each time it opens, with its API. What is registered through
|
|
91
|
+
* that API goes when the editor closes or this add-on unmounts.
|
|
92
|
+
*/
|
|
93
|
+
editor(onOpen, onClose) {
|
|
94
|
+
const host = this.host;
|
|
95
|
+
if (!host) {
|
|
96
|
+
this.log.warn('this game hands add-ons no editor: update oss!idle');
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
this.own(host.editor.watch(onOpen, onClose));
|
|
100
|
+
}
|
|
80
101
|
/**
|
|
81
102
|
* Internal: called by {@link defineAddon}, not by add-on code.
|
|
82
103
|
*
|
package/dist/runtime/define.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { Addon } from './addon.js';
|
|
2
|
+
import type { AddonHost } from './host.js';
|
|
2
3
|
/** The module shape oss!idle's add-on runtime imports and drives. */
|
|
3
4
|
export type AddonModule = {
|
|
4
|
-
mount: () => void;
|
|
5
|
-
unmount: () => void
|
|
5
|
+
mount: (meta?: unknown, host?: AddonHost) => void;
|
|
6
|
+
unmount: () => Promise<void>;
|
|
6
7
|
};
|
|
7
8
|
/**
|
|
8
9
|
* Turn an {@link Addon} subclass into the `mount` / `unmount` pair the runtime
|
|
@@ -12,9 +13,11 @@ export type AddonModule = {
|
|
|
12
13
|
* export const { mount, unmount } = defineAddon(() => new MyAddon());
|
|
13
14
|
* ```
|
|
14
15
|
*
|
|
15
|
-
* The runtime calls
|
|
16
|
+
* The runtime calls `mount` synchronously and ignores what it returns, so an
|
|
16
17
|
* async `onMount` is started and left to run - its failure is reported here
|
|
17
18
|
* rather than surfacing as an unhandled rejection in the player's console.
|
|
19
|
+
* `unmount` returns its teardown, which the runtime awaits before mounting an
|
|
20
|
+
* updated version.
|
|
18
21
|
*
|
|
19
22
|
* A mount still settling when `unmount` arrives (disable clicked mid-boot) is
|
|
20
23
|
* awaited first, so teardown never races a half-built overlay.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"define.d.ts","sourceRoot":"","sources":["../../src/runtime/define.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"define.d.ts","sourceRoot":"","sources":["../../src/runtime/define.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAE3C,qEAAqE;AACrE,MAAM,MAAM,WAAW,GAAG;IACzB,KAAK,EAAE,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS,KAAK,IAAI,CAAC;IAClD,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7B,CAAC;AAEF;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,WAAW,GAAI,QAAQ,MAAM,KAAK,KAAG,WAsBjD,CAAC"}
|
package/dist/runtime/define.js
CHANGED
|
@@ -6,9 +6,11 @@
|
|
|
6
6
|
* export const { mount, unmount } = defineAddon(() => new MyAddon());
|
|
7
7
|
* ```
|
|
8
8
|
*
|
|
9
|
-
* The runtime calls
|
|
9
|
+
* The runtime calls `mount` synchronously and ignores what it returns, so an
|
|
10
10
|
* async `onMount` is started and left to run - its failure is reported here
|
|
11
11
|
* rather than surfacing as an unhandled rejection in the player's console.
|
|
12
|
+
* `unmount` returns its teardown, which the runtime awaits before mounting an
|
|
13
|
+
* updated version.
|
|
12
14
|
*
|
|
13
15
|
* A mount still settling when `unmount` arrives (disable clicked mid-boot) is
|
|
14
16
|
* awaited first, so teardown never races a half-built overlay.
|
|
@@ -17,21 +19,22 @@ export const defineAddon = (create) => {
|
|
|
17
19
|
let instance;
|
|
18
20
|
let settling = Promise.resolve();
|
|
19
21
|
return {
|
|
20
|
-
mount: () => {
|
|
22
|
+
mount: (_meta, host) => {
|
|
21
23
|
instance = create();
|
|
22
24
|
const addon = instance;
|
|
23
|
-
settling = addon._mount().catch((e) => {
|
|
25
|
+
settling = addon._mount(host).catch((e) => {
|
|
24
26
|
console.error('[addon] mount failed', e);
|
|
25
27
|
});
|
|
26
28
|
},
|
|
27
29
|
unmount: () => {
|
|
28
30
|
const addon = instance;
|
|
29
31
|
if (!addon)
|
|
30
|
-
return;
|
|
32
|
+
return settling;
|
|
31
33
|
instance = undefined;
|
|
32
34
|
settling = settling
|
|
33
35
|
.then(() => addon._unmount())
|
|
34
36
|
.catch((e) => console.error('[addon] unmount failed', e));
|
|
37
|
+
return settling;
|
|
35
38
|
},
|
|
36
39
|
};
|
|
37
40
|
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the game hands an add-on: `mount(meta, host)`, and the same object on
|
|
3
|
+
* `globalThis.ossIdle` for code that runs before `mount` (the `sharedReact`
|
|
4
|
+
* build reads React there). Mirrors the game's `addons/host.ts` and
|
|
5
|
+
* `editor/editorApi.ts`; an older game hands none.
|
|
6
|
+
*/
|
|
7
|
+
/** A teardown; running it undoes what returned it. */
|
|
8
|
+
type Disposer = () => void;
|
|
9
|
+
/** A note as the editor holds it; `end` makes it a long note. */
|
|
10
|
+
export type EditorNote = {
|
|
11
|
+
time: number;
|
|
12
|
+
column: number;
|
|
13
|
+
end?: number;
|
|
14
|
+
/** the .osu hitSound bits, as written */
|
|
15
|
+
hitSound: string;
|
|
16
|
+
/** the .osu sample fields, as written */
|
|
17
|
+
sample: string;
|
|
18
|
+
};
|
|
19
|
+
export type EditorTimingPoint = {
|
|
20
|
+
time: number;
|
|
21
|
+
/** ms per beat for a BPM point; -100 / the multiplier for a scroll speed one */
|
|
22
|
+
beatLength: number;
|
|
23
|
+
meter: number;
|
|
24
|
+
sound: string;
|
|
25
|
+
effects: string;
|
|
26
|
+
};
|
|
27
|
+
export type EditorMetadata = {
|
|
28
|
+
title: string;
|
|
29
|
+
titleUnicode: string;
|
|
30
|
+
artist: string;
|
|
31
|
+
artistUnicode: string;
|
|
32
|
+
creator: string;
|
|
33
|
+
version: string;
|
|
34
|
+
source: string;
|
|
35
|
+
tags: string;
|
|
36
|
+
previewTime: number;
|
|
37
|
+
};
|
|
38
|
+
/** Draws into `container`; what it returns runs when it goes. */
|
|
39
|
+
export type EditorRender = (container: HTMLElement) => void | (() => void);
|
|
40
|
+
/**
|
|
41
|
+
* The open editor. Notes and points are the editor's own objects: read them,
|
|
42
|
+
* never change them, and hand new ones to `edit`. Everything registered here
|
|
43
|
+
* goes when the editor closes or the add-on stops.
|
|
44
|
+
*/
|
|
45
|
+
export type EditorApi = {
|
|
46
|
+
readonly keys: number;
|
|
47
|
+
notes(): readonly EditorNote[];
|
|
48
|
+
timing(): readonly EditorTimingPoint[];
|
|
49
|
+
scroll(): readonly EditorTimingPoint[];
|
|
50
|
+
metadata(): Readonly<EditorMetadata>;
|
|
51
|
+
bookmarks(): readonly number[];
|
|
52
|
+
text(): string;
|
|
53
|
+
edit(change: {
|
|
54
|
+
notes?: EditorNote[];
|
|
55
|
+
timing?: EditorTimingPoint[];
|
|
56
|
+
scroll?: EditorTimingPoint[];
|
|
57
|
+
bookmarks?: number[];
|
|
58
|
+
metadata?: Partial<EditorMetadata>;
|
|
59
|
+
}, select?: EditorNote[]): void;
|
|
60
|
+
undo(): void;
|
|
61
|
+
redo(): void;
|
|
62
|
+
save(): void;
|
|
63
|
+
selection(): EditorNote[];
|
|
64
|
+
select(notes: EditorNote[]): void;
|
|
65
|
+
now(): number;
|
|
66
|
+
seek(ms: number): void;
|
|
67
|
+
play(): void;
|
|
68
|
+
pause(): void;
|
|
69
|
+
isPlaying(): boolean;
|
|
70
|
+
snap(ms: number): number;
|
|
71
|
+
divisor(): number;
|
|
72
|
+
setDivisor(divisor: number): void;
|
|
73
|
+
onChange(callback: () => void): Disposer;
|
|
74
|
+
addAction(action: {
|
|
75
|
+
id: string;
|
|
76
|
+
label: string;
|
|
77
|
+
keys?: string[];
|
|
78
|
+
run: () => void;
|
|
79
|
+
}): Disposer;
|
|
80
|
+
addTab(tab: {
|
|
81
|
+
id: string;
|
|
82
|
+
label: string;
|
|
83
|
+
render: EditorRender;
|
|
84
|
+
}): Disposer;
|
|
85
|
+
addPanel(panel: {
|
|
86
|
+
id: string;
|
|
87
|
+
title?: string;
|
|
88
|
+
render: EditorRender;
|
|
89
|
+
}): Disposer;
|
|
90
|
+
openWindow(window: {
|
|
91
|
+
title: string;
|
|
92
|
+
width?: number;
|
|
93
|
+
height?: number;
|
|
94
|
+
render: EditorRender;
|
|
95
|
+
onClose?: () => void;
|
|
96
|
+
}): {
|
|
97
|
+
close(): void;
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
export type AddonHost = {
|
|
101
|
+
/** the game's version */
|
|
102
|
+
version: string;
|
|
103
|
+
/** the game's React modules by import path */
|
|
104
|
+
react: Record<string, unknown>;
|
|
105
|
+
editor: {
|
|
106
|
+
/** `onOpen` runs whenever the editor opens (now, if it is open), `onClose` when it closes. */
|
|
107
|
+
watch(onOpen: (editor: EditorApi) => void, onClose?: () => void): Disposer;
|
|
108
|
+
};
|
|
109
|
+
};
|
|
110
|
+
/** The game's host, if it hands one out (older games do not). */
|
|
111
|
+
export declare const gameHost: () => AddonHost | undefined;
|
|
112
|
+
export {};
|
|
113
|
+
//# sourceMappingURL=host.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"host.d.ts","sourceRoot":"","sources":["../../src/runtime/host.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the game hands an add-on: `mount(meta, host)`, and the same object on
|
|
3
|
+
* `globalThis.ossIdle` for code that runs before `mount` (the `sharedReact`
|
|
4
|
+
* build reads React there). Mirrors the game's `addons/host.ts` and
|
|
5
|
+
* `editor/editorApi.ts`; an older game hands none.
|
|
6
|
+
*/
|
|
7
|
+
/** The game's host, if it hands one out (older games do not). */
|
|
8
|
+
export const gameHost = () => globalThis.ossIdle;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oss-idle/addon-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"description": "oss!idle Addon SDK - build oss!idle add-ons in TypeScript. An Addon base class with automatic teardown, shadow-root React overlays, bridges to the game's database and beatmap store, and a bundler that emits the single-file ES module the add-on runtime loads.",
|
|
@@ -15,6 +15,25 @@ into the in-game add-on editor (Options → Add-ons → Manage) and enable it.
|
|
|
15
15
|
|
|
16
16
|
`npm run dev` rebuilds on save; re-paste to see changes.
|
|
17
17
|
|
|
18
|
+
## Publish from a repository
|
|
19
|
+
|
|
20
|
+
Instead of pasting, oss!idle can build your add-on from a public git repository
|
|
21
|
+
(any host: GitHub, GitLab, Codeberg...):
|
|
22
|
+
|
|
23
|
+
1. Push this folder with its `package-lock.json` (`dist/` stays out of git).
|
|
24
|
+
2. In game, create an add-on, choose **Built from a git repository** and give the
|
|
25
|
+
repository's https address.
|
|
26
|
+
3. Add the webhook address it shows to your repository as a push webhook.
|
|
27
|
+
4. Release by tagging: `npm version patch && git push --follow-tags`.
|
|
28
|
+
|
|
29
|
+
oss!idle refuses a built file that can run code nobody reviewed: `eval`, `new Function`,
|
|
30
|
+
`import()`, imports from an address, script elements, string timers and the like.
|
|
31
|
+
`npm run lint` flags them in your own code before you push.
|
|
32
|
+
|
|
33
|
+
Each new version tag is built by oss!idle itself (`npm ci`, then `npm run build`,
|
|
34
|
+
offline, with install scripts skipped) and goes to review; the moderator reads the
|
|
35
|
+
repository's changes, never the bundle.
|
|
36
|
+
|
|
18
37
|
## How an add-on runs
|
|
19
38
|
|
|
20
39
|
The game imports your built file as an ES module from a Blob URL and calls its
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import tseslint from 'typescript-eslint';
|
|
2
|
+
|
|
3
|
+
// oss!idle refuses an add-on whose built file can run code nobody reviewed: text turned
|
|
4
|
+
// into code, or code loaded from elsewhere. It checks the bundle when you publish;
|
|
5
|
+
// `npm run lint` tells you sooner, in your own code.
|
|
6
|
+
const NOT_REVIEWED = 'oss!idle refuses code that runs what was not reviewed';
|
|
7
|
+
|
|
8
|
+
export default tseslint.config(
|
|
9
|
+
{ ignores: ['dist/', 'node_modules/'] },
|
|
10
|
+
{
|
|
11
|
+
files: ['**/*.{js,jsx,mjs,ts,tsx,mts}'],
|
|
12
|
+
languageOptions: {
|
|
13
|
+
parser: tseslint.parser,
|
|
14
|
+
// no-implied-eval only knows the timers as globals
|
|
15
|
+
globals: Object.fromEntries(['window', 'self', 'globalThis', 'setTimeout', 'setInterval']
|
|
16
|
+
.map(name => [name, 'readonly'])),
|
|
17
|
+
},
|
|
18
|
+
rules: {
|
|
19
|
+
'no-eval': 'error',
|
|
20
|
+
'no-implied-eval': 'error',
|
|
21
|
+
'no-new-func': 'error',
|
|
22
|
+
'no-script-url': 'error',
|
|
23
|
+
'no-restricted-globals': ['error', 'eval', 'Function', 'importScripts'],
|
|
24
|
+
'no-restricted-properties': ['error',
|
|
25
|
+
{ object: 'document', property: 'write', message: NOT_REVIEWED },
|
|
26
|
+
{ object: 'document', property: 'writeln', message: NOT_REVIEWED },
|
|
27
|
+
{ property: 'eval', message: NOT_REVIEWED },
|
|
28
|
+
{ property: 'Function', message: NOT_REVIEWED },
|
|
29
|
+
{ property: 'srcdoc', message: NOT_REVIEWED },
|
|
30
|
+
{ property: 'createContextualFragment', message: NOT_REVIEWED },
|
|
31
|
+
{ property: 'serviceWorker', message: NOT_REVIEWED },
|
|
32
|
+
],
|
|
33
|
+
'no-restricted-syntax': ['error',
|
|
34
|
+
{ selector: 'ImportExpression', message: `import() loads code from elsewhere: ${NOT_REVIEWED}` },
|
|
35
|
+
{
|
|
36
|
+
selector: 'ImportDeclaration[source.value=/^([a-z]+:|\\/\\/)/i]',
|
|
37
|
+
message: `Imports from an address load code from elsewhere: ${NOT_REVIEWED}`,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
selector: 'CallExpression[callee.property.name=/^createElement(NS)?$/] > Literal[value=/^\\s*script\\s*$/i]',
|
|
41
|
+
message: `A script element loads or runs code: ${NOT_REVIEWED}`,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
selector: 'CallExpression[callee.property.name=/^setAttribute(NS)?$/] > Literal[value=/^on/i]',
|
|
45
|
+
message: `An on… attribute runs its text as code: use addEventListener. ${NOT_REVIEWED}`,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
selector: 'CallExpression[callee.property.name="constructor"], NewExpression[callee.property.name="constructor"]',
|
|
49
|
+
message: `A function's constructor builds code from text: ${NOT_REVIEWED}`,
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
);
|