@moku-labs/game 0.4.4 → 0.4.6

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 CHANGED
@@ -192,8 +192,30 @@ createApp({ plugins: [...screen, effectsPlugin, audioPlugin, platformPlugin] });
192
192
  | `@moku-labs/game/assets` | Node and Bun | Asset keys, the manifest, strings, the pack |
193
193
  | `@moku-labs/game/inspect` | anywhere | Read a running game |
194
194
  | `@moku-labs/game/control` | dev builds | Drive a running game |
195
+ | `@moku-labs/game/lint` | oxlint | The game lint rules, as an oxlint JS plugin |
195
196
  | `@moku-labs/game/jsx-runtime`, `/jsx-dev-runtime` | anywhere | The JSX runtime. A game never imports it by hand. |
196
197
 
198
+ ### Lint for games
199
+
200
+ `@moku-labs/game/lint` is an oxlint JS plugin, `moku-game`. It checks the engine rules in a game: lazy Pixi and Yoga, no native package in the logic, the editor only in dev files, no module-scope state, determinism and pure rules.
201
+
202
+ ```json
203
+ // .oxlintrc.json
204
+ {
205
+ "jsPlugins": ["@moku-labs/game/lint"],
206
+ "rules": {
207
+ "moku-game/lazy-imports": "error",
208
+ "moku-game/native-imports": "error",
209
+ "moku-game/dev-imports": "error",
210
+ "moku-game/no-module-state": "error",
211
+ "moku-game/determinism": "error",
212
+ "moku-game/rules-siblings": "error"
213
+ }
214
+ }
215
+ ```
216
+
217
+ Each rule takes `{ "files": [...], "ignores": [...] }`. The defaults follow the template layout. See [Lint for games](./docs/lint.md).
218
+
197
219
  ## Docs
198
220
 
199
221
  | Page | What is inside |
@@ -204,6 +226,7 @@ createApp({ plugins: [...screen, effectsPlugin, audioPlugin, platformPlugin] });
204
226
  | [Doors for the editor](./docs/doors.md) | `/inspect` and `/control`, base sources and commands |
205
227
  | [Events](./docs/events.md) | Every event and its payload |
206
228
  | [Configuration](./docs/configuration.md) | Every config field and its default |
229
+ | [Lint for games](./docs/lint.md) | The `moku-game` oxlint rules, their options and defaults |
207
230
  | [Testing](./docs/testing.md) | Headless and visual tests, all scripts, test layout, lint rules |
208
231
  | [`llms.txt`](./llms.txt) | The engine in one page, for an AI that writes a game |
209
232
 
@@ -0,0 +1,79 @@
1
+ //#region src/lint.d.ts
2
+ /** The options every rule takes: which files it checks. A key given replaces the rule's default. */
3
+ type GameLintOptions = {
4
+ /** Globs of the files the rule checks, relative to the directory oxlint runs in. */files?: string[]; /** Globs of the files the rule skips, even when `files` matches them. */
5
+ ignores?: string[];
6
+ };
7
+ /** The part of an ESTree node the rules read. oxlint and ESLint hand over full nodes. */
8
+ type GameLintNode = {
9
+ readonly type: string;
10
+ readonly name?: string;
11
+ readonly value?: unknown;
12
+ readonly kind?: string;
13
+ readonly computed?: boolean;
14
+ readonly importKind?: string;
15
+ readonly exportKind?: string;
16
+ readonly source?: GameLintNode | null;
17
+ readonly body?: readonly GameLintNode[];
18
+ readonly declarations?: readonly GameLintNode[];
19
+ readonly declaration?: GameLintNode | null;
20
+ readonly init?: GameLintNode | null;
21
+ readonly callee?: GameLintNode;
22
+ readonly arguments?: readonly GameLintNode[];
23
+ readonly object?: GameLintNode;
24
+ readonly property?: GameLintNode;
25
+ };
26
+ /** The part of the rule context the rules read: where the file is, the options, the report. */
27
+ type GameLintContext = {
28
+ readonly cwd: string;
29
+ readonly filename: string;
30
+ readonly options: readonly unknown[];
31
+ report(descriptor: {
32
+ node: GameLintNode;
33
+ message: string;
34
+ }): void;
35
+ };
36
+ /** A node visitor per ESTree node type. */
37
+ type GameLintListeners = Record<string, (node: GameLintNode) => void>;
38
+ /** One rule in the ESLint rule shape oxlint `jsPlugins` load. */
39
+ type GameLintRule = {
40
+ readonly meta: {
41
+ readonly type: "problem";
42
+ readonly docs: {
43
+ readonly description: string;
44
+ };
45
+ readonly schema: readonly unknown[];
46
+ };
47
+ create(context: GameLintContext): GameLintListeners;
48
+ };
49
+ /** The plugin object: `moku-game` and its six rules. */
50
+ type GameLintPlugin = {
51
+ readonly meta: {
52
+ readonly name: "moku-game";
53
+ };
54
+ readonly rules: Readonly<Record<GameLintRuleName, GameLintRule>>;
55
+ };
56
+ /** The rule names, without the `moku-game/` prefix. */
57
+ type GameLintRuleName = "lazy-imports" | "native-imports" | "dev-imports" | "no-module-state" | "determinism" | "rules-siblings";
58
+ /**
59
+ * Turn a glob into an anchored regular expression: `**`, `*`, `?` and one level of `{a,b}`.
60
+ *
61
+ * @param glob - A POSIX glob relative to the lint root.
62
+ * @returns The expression a relative POSIX path is tested against.
63
+ * @example
64
+ * ```ts
65
+ * globToRegExp("{nodes,rules}/*.ts").test("rules/merge.ts"); // true
66
+ * ```
67
+ */
68
+ declare function globToRegExp(glob: string): RegExp;
69
+ /**
70
+ * The `moku-game` oxlint plugin. A game loads it by package name and turns its rules on.
71
+ *
72
+ * @example
73
+ * ```json
74
+ * { "jsPlugins": ["@moku-labs/game/lint"], "rules": { "moku-game/lazy-imports": "error" } }
75
+ * ```
76
+ */
77
+ declare const plugin: GameLintPlugin;
78
+ //#endregion
79
+ export { GameLintContext, GameLintListeners, GameLintNode, GameLintOptions, GameLintPlugin, GameLintRule, GameLintRuleName, plugin as default, globToRegExp };
package/dist/lint.mjs ADDED
@@ -0,0 +1,360 @@
1
+ import path from "node:path";
2
+ //#region src/lint.ts
3
+ /**
4
+ * @file The lint door of the engine (subpath `./lint`): an oxlint JS plugin named `moku-game` with
5
+ * the rules a game carries. A game loads it by package name in `.oxlintrc.json`
6
+ * (`"jsPlugins": ["@moku-labs/game/lint"]`) and turns the rules on as `moku-game/<rule>`. The rules
7
+ * use the ESLint rule API, so ESLint 9 loads the same object. Every rule takes
8
+ * `[{ files, ignores }]`: globs relative to the directory oxlint runs in; a key given replaces the
9
+ * default. Only `node:path` is imported; the file is erasable TypeScript, so Node loads it unbuilt.
10
+ */
11
+ /** Tests and test helpers: no rule checks them by default. */
12
+ const TESTS = [
13
+ "tests/**",
14
+ "**/__tests__/**",
15
+ "**/*.{test,spec}.{ts,tsx}"
16
+ ];
17
+ /** Dev modules: a `.dev` module is wired into the editor only. */
18
+ const DEV_MODULES = ["**/*.dev.{ts,tsx}"];
19
+ /** The logic of a game: state, tables, nodes, flows, rules, features. `**` so `src/` matches too. */
20
+ const LOGIC = [
21
+ "**/state.ts",
22
+ "**/tables.ts",
23
+ "**/{nodes,flows,rules,features}/**"
24
+ ];
25
+ /** The options schema every rule shares; oxlint refuses options a rule has no schema for. */
26
+ const SCHEMA = [{
27
+ type: "object",
28
+ properties: {
29
+ files: {
30
+ type: "array",
31
+ items: { type: "string" }
32
+ },
33
+ ignores: {
34
+ type: "array",
35
+ items: { type: "string" }
36
+ }
37
+ },
38
+ additionalProperties: false
39
+ }];
40
+ /** Bases of a timer call that still reach the global timer. */
41
+ const GLOBAL_OBJECT = /^(?:globalThis|window|self)$/;
42
+ /** The global timers of L3. */
43
+ const TIMER = /^set(?:Timeout|Interval)$/;
44
+ /** The forbidden member reads of L3: object, property and the fix. */
45
+ const CLOCK_READS = [
46
+ {
47
+ object: "Math",
48
+ property: "random",
49
+ message: "L3: use an rng stream: rng.stream(id)."
50
+ },
51
+ {
52
+ object: "Date",
53
+ property: "now",
54
+ message: "L3: use `now` from the node context."
55
+ },
56
+ {
57
+ object: "performance",
58
+ property: "now",
59
+ message: "L3: use `now` from the node context."
60
+ }
61
+ ];
62
+ /** The collections a module may not hold at module scope (L5). */
63
+ const COLLECTION = /^(?:Map|Set|WeakMap|WeakSet)$/;
64
+ /**
65
+ * Turn a glob into an anchored regular expression: `**`, `*`, `?` and one level of `{a,b}`.
66
+ *
67
+ * @param glob - A POSIX glob relative to the lint root.
68
+ * @returns The expression a relative POSIX path is tested against.
69
+ * @example
70
+ * ```ts
71
+ * globToRegExp("{nodes,rules}/*.ts").test("rules/merge.ts"); // true
72
+ * ```
73
+ */
74
+ function globToRegExp(glob) {
75
+ let source = "";
76
+ let index = 0;
77
+ while (index < glob.length) {
78
+ const char = glob.charAt(index);
79
+ if (glob.startsWith("**/", index)) {
80
+ source += "(?:.*/)?";
81
+ index += 3;
82
+ } else if (glob.startsWith("**", index)) {
83
+ source += ".*";
84
+ index += 2;
85
+ } else {
86
+ source += globChar(char);
87
+ index += 1;
88
+ }
89
+ }
90
+ return new RegExp(`^${source}$`);
91
+ }
92
+ /**
93
+ * The expression for one glob character other than `**`.
94
+ *
95
+ * @param char - The character.
96
+ * @returns Its regular-expression source.
97
+ */
98
+ function globChar(char) {
99
+ switch (char) {
100
+ case "*": return "[^/]*";
101
+ case "?": return "[^/]";
102
+ case "{": return "(?:";
103
+ case "}": return ")";
104
+ case ",": return "|";
105
+ default: return char.replaceAll(/[$()+.[\\\]^|]/g, String.raw`\$&`);
106
+ }
107
+ }
108
+ /**
109
+ * Whether the rule checks the file the context lints: `files` match and no `ignores` match.
110
+ *
111
+ * @param context - The rule context.
112
+ * @param defaults - The rule's default globs.
113
+ * @returns True when the rule applies.
114
+ */
115
+ function inScope(context, defaults) {
116
+ const [given] = context.options;
117
+ const files = given?.files ?? defaults.files;
118
+ const ignores = given?.ignores ?? defaults.ignores;
119
+ const file = path.relative(context.cwd, context.filename).split(path.sep).join("/");
120
+ const matches = (globs) => globs.some((glob) => globToRegExp(glob).test(file));
121
+ return matches(files) && !matches(ignores);
122
+ }
123
+ /**
124
+ * The specifier of an import node, when it is a string literal.
125
+ *
126
+ * @param node - An import, export-from or `import()` node.
127
+ * @returns The specifier, or undefined for a computed `import()` or a local export.
128
+ */
129
+ function specifierOf(node) {
130
+ const value = node.source?.type === "Literal" ? node.source.value : void 0;
131
+ return typeof value === "string" ? value : void 0;
132
+ }
133
+ /**
134
+ * Whether a static import or export carries types only: `import type` or `export type`.
135
+ * `import { type A }` keeps a side-effect import under `verbatimModuleSyntax`, so it counts as a value.
136
+ *
137
+ * @param node - A static import or export node.
138
+ * @returns True for a type-only statement.
139
+ */
140
+ function isTypeOnly(node) {
141
+ return node.importKind === "type" || node.exportKind === "type";
142
+ }
143
+ /**
144
+ * Build a rule that reports imports of forbidden specifiers.
145
+ *
146
+ * @param description - What the rule enforces.
147
+ * @param defaults - The rule's default globs.
148
+ * @param checks - The forbidden patterns and their messages.
149
+ * @param lazy - True for L2: `import()` and type-only statements pass.
150
+ * @returns The rule.
151
+ */
152
+ function importRule(description, defaults, checks, lazy = false) {
153
+ return {
154
+ meta: {
155
+ type: "problem",
156
+ docs: { description },
157
+ schema: SCHEMA
158
+ },
159
+ /**
160
+ * Visit the file when the rule's globs match it.
161
+ *
162
+ * @param context - The rule context.
163
+ * @returns The node visitors, none for a file out of scope.
164
+ */
165
+ create(context) {
166
+ if (!inScope(context, defaults)) return {};
167
+ const check = (node) => {
168
+ const name = specifierOf(node);
169
+ const hit = name === void 0 ? void 0 : checks.find((item) => item.pattern.test(name));
170
+ if (hit === void 0) return;
171
+ if (lazy && isTypeOnly(node)) return;
172
+ context.report({
173
+ node,
174
+ message: hit.message
175
+ });
176
+ };
177
+ const listeners = {
178
+ ImportDeclaration: check,
179
+ ExportAllDeclaration: check,
180
+ ExportNamedDeclaration: check
181
+ };
182
+ if (!lazy) listeners.ImportExpression = check;
183
+ return listeners;
184
+ }
185
+ };
186
+ }
187
+ /**
188
+ * The name of a member's property: `a.b` and `a["b"]`.
189
+ *
190
+ * @param node - A MemberExpression.
191
+ * @returns The property name, or undefined for a computed non-literal key.
192
+ */
193
+ function propertyName(node) {
194
+ const key = node.property;
195
+ if (key === void 0) return void 0;
196
+ if (!node.computed) return key.name;
197
+ return typeof key.value === "string" ? key.value : void 0;
198
+ }
199
+ /**
200
+ * Report the module-scope state of one top-level statement (L5).
201
+ *
202
+ * @param context - The rule context.
203
+ * @param statement - A statement of the Program body.
204
+ */
205
+ function reportModuleState(context, statement) {
206
+ const declaration = statement.type === "ExportNamedDeclaration" ? statement.declaration : statement;
207
+ if (declaration?.type !== "VariableDeclaration") return;
208
+ if (declaration.kind === "let" || declaration.kind === "var") {
209
+ context.report({
210
+ node: declaration,
211
+ message: "L5: no module-scope state. State lives in player and session."
212
+ });
213
+ return;
214
+ }
215
+ for (const declarator of declaration.declarations ?? []) {
216
+ const init = declarator.init;
217
+ if (init?.type === "NewExpression" && COLLECTION.test(init.callee?.name ?? "")) context.report({
218
+ node: init,
219
+ message: "L5: no module-scope collections. Create them inside a function."
220
+ });
221
+ }
222
+ }
223
+ /**
224
+ * Whether a call's callee reaches a global timer: `setTimeout(...)`, `globalThis.setInterval(...)`.
225
+ *
226
+ * @param callee - The callee of a CallExpression.
227
+ * @returns True for a timer.
228
+ */
229
+ function isTimer(callee) {
230
+ if (callee?.type === "Identifier") return TIMER.test(callee.name ?? "");
231
+ if (callee?.type !== "MemberExpression") return false;
232
+ return GLOBAL_OBJECT.test(callee.object?.name ?? "") && TIMER.test(propertyName(callee) ?? "");
233
+ }
234
+ /** L2: static value imports of the packages the engine loads lazily. */
235
+ const lazyImports = importRule("No static import of pixi.js or yoga-layout; import() and import type pass.", {
236
+ files: ["**"],
237
+ ignores: TESTS
238
+ }, [{
239
+ pattern: /^pixi\.js(?:\/|$)/,
240
+ message: "L2: the renderer loads Pixi lazily. Draw with the engine's tags, use import() or import type."
241
+ }, {
242
+ pattern: /^yoga-layout(?:\/|$)/,
243
+ message: "L2: the ui plugin loads Yoga lazily. Use import() or import type."
244
+ }], true);
245
+ /** L13: native packages stay out of the engine-facing layers. */
246
+ const nativeImports = importRule("No @moku-labs/system, @moku-labs/native or @tauri-apps/* in the engine-facing layers.", {
247
+ files: [
248
+ ...LOGIC,
249
+ "**/kit.ts",
250
+ "**/game.ts"
251
+ ],
252
+ ignores: [...DEV_MODULES, ...TESTS]
253
+ }, [{
254
+ pattern: /^(?:@moku-labs\/(?:system|native)(?:\/|$)|@tauri-apps\/)/,
255
+ message: "L13: only platform-bridge.ts, native.ts and web/ import a native package. Pass a PlatformProvider."
256
+ }]);
257
+ /** Dev only: the editor and the control door stay in dev files. */
258
+ const developmentImports = importRule("@moku-labs/editor and @moku-labs/game/control only in dev files and tests.", {
259
+ files: ["**"],
260
+ ignores: [
261
+ "web/main.ts",
262
+ "web/dev*.ts",
263
+ "web/editor*.ts",
264
+ ...DEV_MODULES,
265
+ ...TESTS
266
+ ]
267
+ }, [{
268
+ pattern: /^@moku-labs\/(?:editor(?:\/|$)|game\/control$)/,
269
+ message: "Dev only: web/main.ts, web/dev*.ts, web/editor*.ts, .dev modules and tests import the editor and /control."
270
+ }]);
271
+ /** L4: a rule imports only its siblings. */
272
+ const rulesSiblings = importRule("A file under rules/ imports only its siblings.", {
273
+ files: ["**/rules/**"],
274
+ ignores: TESTS
275
+ }, [{
276
+ pattern: /^(?!\.\/)/,
277
+ message: "L4: a rule imports only its siblings in rules/: (state, input, tables) => result."
278
+ }]);
279
+ /**
280
+ * The `moku-game` oxlint plugin. A game loads it by package name and turns its rules on.
281
+ *
282
+ * @example
283
+ * ```json
284
+ * { "jsPlugins": ["@moku-labs/game/lint"], "rules": { "moku-game/lazy-imports": "error" } }
285
+ * ```
286
+ */
287
+ const plugin = {
288
+ meta: { name: "moku-game" },
289
+ rules: {
290
+ "lazy-imports": lazyImports,
291
+ "native-imports": nativeImports,
292
+ "dev-imports": developmentImports,
293
+ "no-module-state": {
294
+ meta: {
295
+ type: "problem",
296
+ docs: { description: "No module-scope let or var and no module-scope Map, Set, WeakMap, WeakSet." },
297
+ schema: SCHEMA
298
+ },
299
+ /**
300
+ * Visit the file when the rule's globs match it.
301
+ *
302
+ * @param context - The rule context.
303
+ * @returns The node visitors, none for a file out of scope.
304
+ */
305
+ create(context) {
306
+ if (!inScope(context, {
307
+ files: ["**"],
308
+ ignores: TESTS
309
+ })) return {};
310
+ return { Program: (program) => {
311
+ for (const statement of program.body ?? []) reportModuleState(context, statement);
312
+ } };
313
+ }
314
+ },
315
+ determinism: {
316
+ meta: {
317
+ type: "problem",
318
+ docs: { description: "No Math.random, Date.now, performance.now, new Date(), setTimeout or setInterval in the logic." },
319
+ schema: SCHEMA
320
+ },
321
+ /**
322
+ * Visit the file when the rule's globs match it.
323
+ *
324
+ * @param context - The rule context.
325
+ * @returns The node visitors, none for a file out of scope.
326
+ */
327
+ create(context) {
328
+ if (!inScope(context, {
329
+ files: LOGIC,
330
+ ignores: [...DEV_MODULES, ...TESTS]
331
+ })) return {};
332
+ return {
333
+ MemberExpression: (node) => {
334
+ const property = propertyName(node);
335
+ const read = CLOCK_READS.find((item) => item.object === node.object?.name && item.property === property);
336
+ if (read !== void 0) context.report({
337
+ node,
338
+ message: read.message
339
+ });
340
+ },
341
+ NewExpression: (node) => {
342
+ if (node.callee?.name === "Date" && (node.arguments ?? []).length === 0) context.report({
343
+ node,
344
+ message: "L3: use `now` from the node context."
345
+ });
346
+ },
347
+ CallExpression: (node) => {
348
+ if (isTimer(node.callee)) context.report({
349
+ node,
350
+ message: "L3: store the moment in player, await fx(schedule(moment))."
351
+ });
352
+ }
353
+ };
354
+ }
355
+ },
356
+ "rules-siblings": rulesSiblings
357
+ }
358
+ };
359
+ //#endregion
360
+ export { plugin as default, globToRegExp };
package/llms.txt CHANGED
@@ -11,6 +11,7 @@ Facts: Bun only. ESM only. `"sideEffects": false`. `pixi.js` `^8.0.0` is a peer
11
11
  - `@moku-labs/game/assets` (node and bun only): the asset key scanner, the production packer and the string compiler. The package bin `moku-game-assets` runs it.
12
12
  - `@moku-labs/game/inspect`: the read door of the editor, safe in a production build. See "Doors for the editor".
13
13
  - `@moku-labs/game/control`: the write door of the editor, dev builds only. See "Doors for the editor".
14
+ - `@moku-labs/game/lint`: the oxlint JS plugin `moku-game` with the game lint rules. See "Lint for games".
14
15
  - `@moku-labs/game/fonts/*`: the shipped MSDF body font and its licence. See "Fonts".
15
16
  - `@moku-labs/game/jsx-runtime` and `@moku-labs/game/jsx-dev-runtime`: what `"jsxImportSource": "@moku-labs/game"` resolves to. A game never imports them by hand.
16
17
 
@@ -130,6 +131,33 @@ type NodeContext = {
130
131
  - A node never calls `app.model`, `app.clock` or `app.time`. Everything outside logic goes through `fx`.
131
132
  - A timer is a moment stored in player state. How long is left is computed from `now`.
132
133
 
134
+ ## Lint for games
135
+
136
+ `.oxlintrc.json` of a game (oxlint 1.86.0 or later):
137
+
138
+ ```json
139
+ {
140
+ "jsPlugins": ["@moku-labs/game/lint"],
141
+ "rules": {
142
+ "moku-game/lazy-imports": "error",
143
+ "moku-game/native-imports": "error",
144
+ "moku-game/dev-imports": "error",
145
+ "moku-game/no-module-state": "error",
146
+ "moku-game/determinism": "error",
147
+ "moku-game/rules-siblings": "error"
148
+ }
149
+ }
150
+ ```
151
+
152
+ - `moku-game/lazy-imports` (L2): no static value import of `pixi.js` or `yoga-layout`. `import type` and `import()` pass. Default files `**`.
153
+ - `moku-game/native-imports` (L13): no `@moku-labs/system`, `@moku-labs/native`, `@tauri-apps/*` in the logic, `kit.ts` and `game.ts`. Use `platform-bridge.ts`.
154
+ - `moku-game/dev-imports`: `@moku-labs/editor` and `@moku-labs/game/control` only in `web/main.ts`, `web/dev*.ts`, `web/editor*.ts`, `*.dev.ts(x)` and tests.
155
+ - `moku-game/no-module-state` (L5): no module-scope `let` or `var`, no module-scope `new Map/Set/WeakMap/WeakSet`. Default files `**`.
156
+ - `moku-game/determinism` (L3): no `Math.random`, `Date.now`, `performance.now`, `new Date()`, `setTimeout`, `setInterval` in the logic. `new Date(now)` passes.
157
+ - `moku-game/rules-siblings` (L4): a file under `rules/` imports only `./` siblings.
158
+ - Logic = `**/state.ts`, `**/tables.ts`, `**/{nodes,flows,rules,features}/**`. Tests (`tests/**`, `**/__tests__/**`, `*.test.ts`) are skipped by every rule.
159
+ - Every rule takes `["error", { "files": [...], "ignores": [...] }]`. Globs are relative to the directory oxlint runs in; a key given replaces the default.
160
+
133
161
  ## Minimal game
134
162
 
135
163
  ```ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moku-labs/game",
3
- "version": "0.4.4",
3
+ "version": "0.4.6",
4
4
  "description": "2D puzzle game engine on TypeScript and PixiJS, built on @moku-labs/core.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -27,6 +27,10 @@
27
27
  "types": "./dist/control.d.mts",
28
28
  "default": "./dist/control.mjs"
29
29
  },
30
+ "./lint": {
31
+ "types": "./dist/lint.d.mts",
32
+ "default": "./dist/lint.mjs"
33
+ },
30
34
  "./jsx-runtime": {
31
35
  "types": "./dist/jsx-runtime.d.mts",
32
36
  "default": "./dist/jsx-runtime.mjs"
@@ -124,6 +128,7 @@
124
128
  "globals": "17.4.0",
125
129
  "jiti": "2.6.1",
126
130
  "lefthook": "2.1.1",
131
+ "oxlint": "1.86.0",
127
132
  "pixi.js": "8.21.0",
128
133
  "playwright-core": "1.60.0",
129
134
  "publint": "0.3.21",