@caperjs/core 0.1.1 → 0.2.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 +1 -1
- package/{config → build}/assetpack.mjs +103 -45
- package/build/defaults.mjs +157 -0
- package/build/index.mjs +96 -0
- package/build/internal/ast.mjs +255 -0
- package/build/internal/buildFlags.mjs +50 -0
- package/build/internal/discovery.mjs +402 -0
- package/build/internal/manifest.mjs +38 -0
- package/build/internal/schema.mjs +65 -0
- package/build/internal/util.mjs +30 -0
- package/build/internal/validate.mjs +186 -0
- package/build/plugins/assetTypes.mjs +460 -0
- package/build/plugins/caperConfig.mjs +489 -0
- package/build/plugins/devHelper.mjs +31 -0
- package/build/plugins/lists.mjs +286 -0
- package/build/plugins/pruneFallbacks.mjs +150 -0
- package/build/plugins/pwa.mjs +240 -0
- package/build/plugins/runtime.mjs +129 -0
- package/cli.mjs +9 -6
- package/extras/llms.txt +23 -17
- package/lib/{CaptionsPlugin-Dc8fIrjy.js → CaptionsPlugin-DKhbmJvH.js} +15 -15
- package/lib/{CaptionsPlugin-Dc8fIrjy.js.map → CaptionsPlugin-DKhbmJvH.js.map} +1 -1
- package/lib/{DataAdapter-BY2114sp.js → DataAdapter-BZFTkFQ4.js} +4 -4
- package/lib/{DataAdapter-BY2114sp.js.map → DataAdapter-BZFTkFQ4.js.map} +1 -1
- package/lib/{DebugRenderer-BJMLILqI.js → DebugRenderer-C7X-nlXE.js} +2 -2
- package/lib/{DebugRenderer-BJMLILqI.js.map → DebugRenderer-C7X-nlXE.js.map} +1 -1
- package/lib/{DevToolsPlugin-wQ5rOkn0.js → DevToolsPlugin-P-atJ-5U.js} +2 -2
- package/lib/{DevToolsPlugin-wQ5rOkn0.js.map → DevToolsPlugin-P-atJ-5U.js.map} +1 -1
- package/lib/{GSAPPlugin-DHhSZKv8.js → GSAPPlugin-CeH9275z.js} +2 -2
- package/lib/{GSAPPlugin-DHhSZKv8.js.map → GSAPPlugin-CeH9275z.js.map} +1 -1
- package/lib/{LayoutPlugin-B-DF9SYM.js → LayoutPlugin-iZ3k6RsK.js} +4 -4
- package/lib/{LayoutPlugin-B-DF9SYM.js.map → LayoutPlugin-iZ3k6RsK.js.map} +1 -1
- package/lib/{SpinePlugin-Dz_3dljz.js → SpinePlugin-BWiavCpO.js} +2 -2
- package/lib/{SpinePlugin-Dz_3dljz.js.map → SpinePlugin-BWiavCpO.js.map} +1 -1
- package/lib/{StatsPlugin-Cg82JMBD.js → StatsPlugin-CBpzDJL0.js} +4 -4
- package/lib/{StatsPlugin-Cg82JMBD.js.map → StatsPlugin-CBpzDJL0.js.map} +1 -1
- package/lib/{VoiceOverPlugin-BR3ImT8C.js → VoiceOverPlugin-YaiKJtJ6.js} +26 -26
- package/lib/{VoiceOverPlugin-BR3ImT8C.js.map → VoiceOverPlugin-YaiKJtJ6.js.map} +1 -1
- package/lib/caper.mjs +95 -95
- package/lib/caper.mjs.map +1 -1
- package/lib/core/Application.d.ts +1 -1
- package/lib/core/Application.d.ts.map +1 -1
- package/lib/core/interfaces/IApplicationOptions.d.ts +2 -1
- package/lib/core/interfaces/IApplicationOptions.d.ts.map +1 -1
- package/lib/core/interfaces/ICoreSignals.d.ts +5 -1
- package/lib/core/interfaces/ICoreSignals.d.ts.map +1 -1
- package/lib/display/Container.d.ts +1 -1
- package/lib/display/Container.d.ts.map +1 -1
- package/lib/display/ParticleContainer.d.ts +1 -1
- package/lib/display/ParticleContainer.d.ts.map +1 -1
- package/lib/plugins/Plugin.d.ts +1 -1
- package/lib/plugins/Plugin.d.ts.map +1 -1
- package/lib/plugins/defaults.d.ts.map +1 -1
- package/lib/plugins/gesture/GesturePlugin.d.ts +58 -0
- package/lib/plugins/gesture/GesturePlugin.d.ts.map +1 -0
- package/lib/plugins/gesture/gestureMath.d.ts +32 -0
- package/lib/plugins/gesture/gestureMath.d.ts.map +1 -0
- package/lib/plugins/gesture/gestureMath.test.d.ts +2 -0
- package/lib/plugins/gesture/gestureMath.test.d.ts.map +1 -0
- package/lib/plugins/gesture/index.d.ts +3 -0
- package/lib/plugins/gesture/index.d.ts.map +1 -0
- package/lib/plugins/gesture/types.d.ts +45 -0
- package/lib/plugins/gesture/types.d.ts.map +1 -0
- package/lib/plugins/index.d.ts +1 -0
- package/lib/plugins/index.d.ts.map +1 -1
- package/lib/plugins/input/Controls.d.ts +5 -2
- package/lib/plugins/input/Controls.d.ts.map +1 -1
- package/lib/plugins/input/InputPlugin.d.ts +9 -2
- package/lib/plugins/input/InputPlugin.d.ts.map +1 -1
- package/lib/plugins/input/interfaces.d.ts +5 -3
- package/lib/plugins/input/interfaces.d.ts.map +1 -1
- package/lib/plugins/input/touch/{TouchControls.d.ts → VirtualControls.d.ts} +5 -4
- package/lib/plugins/input/touch/VirtualControls.d.ts.map +1 -0
- package/lib/plugins/input/touch/index.d.ts +1 -1
- package/lib/plugins/input/touch/index.d.ts.map +1 -1
- package/lib/{registries-CYCaYKZI.js → registries-qQmLeN0b.js} +505 -354
- package/lib/registries-qQmLeN0b.js.map +1 -0
- package/lib/types/caper-app.d.ts +1 -0
- package/lib/utils/bind.test.d.ts +2 -0
- package/lib/utils/bind.test.d.ts.map +1 -0
- package/package.json +33 -33
- package/src/core/Application.ts +1 -1
- package/src/core/interfaces/IApplicationOptions.ts +2 -0
- package/src/core/interfaces/ICoreSignals.ts +9 -0
- package/src/display/Container.ts +1 -1
- package/src/display/ParticleContainer.ts +1 -1
- package/src/plugins/Plugin.ts +1 -1
- package/src/plugins/defaults.ts +6 -0
- package/src/plugins/gesture/GesturePlugin.ts +215 -0
- package/src/plugins/gesture/gestureMath.test.ts +75 -0
- package/src/plugins/gesture/gestureMath.ts +59 -0
- package/src/plugins/gesture/index.ts +2 -0
- package/src/plugins/gesture/types.ts +54 -0
- package/src/plugins/index.ts +1 -0
- package/src/plugins/input/Controls.ts +9 -3
- package/src/plugins/input/InputPlugin.ts +61 -14
- package/src/plugins/input/interfaces.ts +5 -3
- package/src/plugins/input/touch/{TouchControls.ts → VirtualControls.ts} +4 -1
- package/src/plugins/input/touch/index.ts +1 -1
- package/src/types/caper-app.d.ts +1 -0
- package/src/utils/bind.test.ts +99 -0
- package/src/version.ts +1 -1
- package/templates/app/default/package.template.json +8 -4
- package/templates/app/default/vite.config.ts +15 -0
- package/LICENSE +0 -22
- package/cli/vite.mjs +0 -55
- package/config/vite.mjs +0 -2464
- package/lib/plugins/input/touch/TouchControls.d.ts.map +0 -1
- package/lib/registries-CYCaYKZI.js.map +0 -1
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads AssetPack's generated `assets.json`.
|
|
3
|
+
*
|
|
4
|
+
* Lives in `internal/` rather than beside the asset-types plugin because
|
|
5
|
+
* build-time validation needs it too, and `internal/` must not depend on
|
|
6
|
+
* `plugins/` — that dependency ran the wrong way and hid a missing import until
|
|
7
|
+
* a real build failed.
|
|
8
|
+
*/
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { cwd, logger } from './util.mjs';
|
|
12
|
+
|
|
13
|
+
export function loadManifestBundleNames(manifestUrl = 'assets.json') {
|
|
14
|
+
const manifestPath = path.join(process.cwd(), 'public', 'assets', manifestUrl);
|
|
15
|
+
if (!fs.existsSync(manifestPath)) return null;
|
|
16
|
+
try {
|
|
17
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
18
|
+
const names = new Set();
|
|
19
|
+
for (const bundle of manifest.bundles || []) {
|
|
20
|
+
if (bundle?.name) names.add(bundle.name);
|
|
21
|
+
}
|
|
22
|
+
return names;
|
|
23
|
+
} catch (e) {
|
|
24
|
+
logger.warn(`[caper] Could not parse manifest for build-time validation: ${e.message}`);
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Extract `defaultScene` string and the `plugins: [...]` id list from an
|
|
31
|
+
* already-parsed `defineConfig({...})` ObjectExpression AST node. Returns
|
|
32
|
+
* `{ defaultScene, pluginIds }` where either may be undefined if absent.
|
|
33
|
+
*
|
|
34
|
+
* Plugin entries can be either a string literal or a tuple literal whose
|
|
35
|
+
* first element is a string literal — anything else (dynamic expressions,
|
|
36
|
+
* spreads, non-literal identifiers) is skipped silently, since we can't
|
|
37
|
+
* statically know the ID.
|
|
38
|
+
*/
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zod schema for `caper.config.ts`, strict by design: an unknown key is almost
|
|
3
|
+
* always a typo or a stale option, and failing loudly beats silently ignoring it.
|
|
4
|
+
*/
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
|
|
7
|
+
export const pluginConfigSchema = z.union([
|
|
8
|
+
z.string(),
|
|
9
|
+
z.tuple([
|
|
10
|
+
z.string(),
|
|
11
|
+
z
|
|
12
|
+
.object({
|
|
13
|
+
autoLoad: z.boolean().optional(),
|
|
14
|
+
options: z.any().optional(),
|
|
15
|
+
})
|
|
16
|
+
.loose(),
|
|
17
|
+
]),
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
export const caperConfigSchema = z
|
|
21
|
+
.object({
|
|
22
|
+
id: z.string().min(1).optional(),
|
|
23
|
+
application: z.any().optional(),
|
|
24
|
+
defaultScene: z.string().min(1).optional(),
|
|
25
|
+
defaultSceneLoadMethod: z.string().optional(),
|
|
26
|
+
plugins: z.array(pluginConfigSchema).optional(),
|
|
27
|
+
scenes: z.any().optional(),
|
|
28
|
+
assets: z
|
|
29
|
+
.object({
|
|
30
|
+
manifest: z.any().optional(),
|
|
31
|
+
preload: z
|
|
32
|
+
.object({
|
|
33
|
+
bundles: z.array(z.string()).optional(),
|
|
34
|
+
})
|
|
35
|
+
.loose()
|
|
36
|
+
.optional(),
|
|
37
|
+
background: z
|
|
38
|
+
.object({
|
|
39
|
+
bundles: z.array(z.string()).optional(),
|
|
40
|
+
})
|
|
41
|
+
.loose()
|
|
42
|
+
.optional(),
|
|
43
|
+
})
|
|
44
|
+
.loose()
|
|
45
|
+
.optional(),
|
|
46
|
+
useStore: z.boolean().optional(),
|
|
47
|
+
useSpine: z.boolean().optional(),
|
|
48
|
+
useLayout: z.boolean().optional(),
|
|
49
|
+
useVoiceover: z.boolean().optional(),
|
|
50
|
+
useHash: z.boolean().optional(),
|
|
51
|
+
// Build-time only — read by readCaperBuildFlags(), no runtime effect.
|
|
52
|
+
useWasm: z.boolean().optional(),
|
|
53
|
+
showStats: z.boolean().optional(),
|
|
54
|
+
showSceneDebugMenu: z.boolean().optional(),
|
|
55
|
+
resizeToContainer: z.boolean().optional(),
|
|
56
|
+
logger: z.string().optional(),
|
|
57
|
+
sceneGroupOrder: z.array(z.string()).optional(),
|
|
58
|
+
})
|
|
59
|
+
.loose();
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Evaluate the user's `caper.config.ts` through Vite's own module
|
|
63
|
+
* graph and validate the default export. Dev-only — requires a live
|
|
64
|
+
* `server` (i.e. `vite dev`). Returns `true` on success.
|
|
65
|
+
*/
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small shared pieces for caper's build plugins: the logger they all report
|
|
3
|
+
* through, and two timing helpers used by the file watchers.
|
|
4
|
+
*
|
|
5
|
+
* `cwd` is read once at module load, which is fine for what uses it (logging and
|
|
6
|
+
* relative-path trimming) but not for anything that resolves project files —
|
|
7
|
+
* those take vite's resolved `root` instead. See `build/defaults.mjs`.
|
|
8
|
+
*/
|
|
9
|
+
import process from 'node:process';
|
|
10
|
+
import { createLogger } from 'vite';
|
|
11
|
+
|
|
12
|
+
export const env = process.env.NODE_ENV;
|
|
13
|
+
export const cwd = process.cwd();
|
|
14
|
+
|
|
15
|
+
export const logger = createLogger('caper-config');
|
|
16
|
+
|
|
17
|
+
export const DTS_FILE_NAME = 'caper-app.d.ts';
|
|
18
|
+
export const ASSET_DTS_FILE_NAME = 'caper-assets.d.ts';
|
|
19
|
+
|
|
20
|
+
export const debounce = (func, wait) => {
|
|
21
|
+
let timeout;
|
|
22
|
+
return (...args) => {
|
|
23
|
+
clearTimeout(timeout);
|
|
24
|
+
timeout = setTimeout(() => func.apply(this, args), wait);
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
29
|
+
|
|
30
|
+
// Function to generate TypeScript types from the manifest
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build-time checks over what discovery found: that every scene/plugin/popup id
|
|
3
|
+
* is unique, that referenced asset bundles exist in the manifest, and that
|
|
4
|
+
* plugin dependencies form no cycle.
|
|
5
|
+
*
|
|
6
|
+
* These run before the app ever boots, so a typo in a bundle name fails the
|
|
7
|
+
* build instead of throwing at runtime in front of a player.
|
|
8
|
+
*/
|
|
9
|
+
import { loadManifestBundleNames } from './manifest.mjs';
|
|
10
|
+
import { AST_NODE_TYPES, extractConfigReferences } from './ast.mjs';
|
|
11
|
+
import { logger } from './util.mjs';
|
|
12
|
+
|
|
13
|
+
export function runBuildTimeValidation({
|
|
14
|
+
server,
|
|
15
|
+
configPath,
|
|
16
|
+
configObject,
|
|
17
|
+
scenes,
|
|
18
|
+
plugins,
|
|
19
|
+
popups,
|
|
20
|
+
entities,
|
|
21
|
+
breakpointsName,
|
|
22
|
+
}) {
|
|
23
|
+
const warnings = [];
|
|
24
|
+
const bundleNames = loadManifestBundleNames();
|
|
25
|
+
|
|
26
|
+
// 1. Scene bundle references
|
|
27
|
+
if (bundleNames && bundleNames.size > 0) {
|
|
28
|
+
for (const scene of scenes) {
|
|
29
|
+
for (const kind of ['preload', 'background']) {
|
|
30
|
+
const bundlesField = scene.assets?.[kind]?.bundles;
|
|
31
|
+
if (!bundlesField) continue;
|
|
32
|
+
const list = Array.isArray(bundlesField) ? bundlesField : [bundlesField];
|
|
33
|
+
for (const bundle of list) {
|
|
34
|
+
if (typeof bundle !== 'string') continue;
|
|
35
|
+
if (!bundleNames.has(bundle)) {
|
|
36
|
+
warnings.push(
|
|
37
|
+
`Scene '${scene.id}' references ${kind} bundle '${bundle}' which is not in the assetpack manifest. ` +
|
|
38
|
+
`Known bundles: ${[...bundleNames].join(', ') || '(none)'}.`,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 2 + 3. caper.config.ts cross-references
|
|
47
|
+
const { defaultScene, pluginIds: configPluginIds } = extractConfigReferences(configObject);
|
|
48
|
+
const discoveredPluginIds = new Set(plugins.map((p) => p.id));
|
|
49
|
+
const discoveredSceneIds = new Set(scenes.map((s) => s.id));
|
|
50
|
+
|
|
51
|
+
for (const id of configPluginIds) {
|
|
52
|
+
if (!discoveredPluginIds.has(id)) {
|
|
53
|
+
warnings.push(
|
|
54
|
+
`caper.config.ts references plugin '${id}' which was not discovered. ` +
|
|
55
|
+
`Known plugin IDs: ${[...discoveredPluginIds].join(', ') || '(none)'}.`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (defaultScene && !discoveredSceneIds.has(defaultScene)) {
|
|
60
|
+
warnings.push(
|
|
61
|
+
`caper.config.ts defaultScene is '${defaultScene}' but no scene with that id was discovered. ` +
|
|
62
|
+
`Known scene IDs: ${[...discoveredSceneIds].join(', ') || '(none)'}.`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 4. Plugin `requires` cross-references (local plugins only — npm
|
|
67
|
+
// plugins can declare requires on the class itself, which the runtime
|
|
68
|
+
// topo-sort handles, but the AST can't see).
|
|
69
|
+
for (const p of plugins) {
|
|
70
|
+
if (!p.isLocal || !Array.isArray(p.requires) || p.requires.length === 0) continue;
|
|
71
|
+
for (const req of p.requires) {
|
|
72
|
+
if (!discoveredPluginIds.has(req)) {
|
|
73
|
+
warnings.push(
|
|
74
|
+
`Plugin '${p.id}' requires '${req}' which is not a discovered plugin. ` +
|
|
75
|
+
`Known plugin IDs: ${[...discoveredPluginIds].join(', ') || '(none)'}.`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 4b. Plugin `requires` cycle detection (build-time, so the dev sees the
|
|
82
|
+
// cycle in the terminal/overlay before bootstrap even tries to run).
|
|
83
|
+
const cycleEdges = new Map();
|
|
84
|
+
for (const p of plugins) {
|
|
85
|
+
cycleEdges.set(
|
|
86
|
+
p.id,
|
|
87
|
+
(p.requires ?? []).filter((r) => discoveredPluginIds.has(r)),
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
const cycle = detectCycle(cycleEdges);
|
|
91
|
+
if (cycle) {
|
|
92
|
+
warnings.push(`Plugin dependency cycle: ${cycle.join(' → ')}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 5. Duplicate-id detection
|
|
96
|
+
const checkDuplicates = (label, list) => {
|
|
97
|
+
const seen = new Map();
|
|
98
|
+
for (const item of list) {
|
|
99
|
+
if (seen.has(item.id)) {
|
|
100
|
+
warnings.push(`Duplicate ${label} id '${item.id}' — multiple files export the same id.`);
|
|
101
|
+
} else {
|
|
102
|
+
seen.set(item.id, true);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
checkDuplicates('scene', scenes);
|
|
107
|
+
checkDuplicates('plugin', plugins);
|
|
108
|
+
checkDuplicates('popup', popups);
|
|
109
|
+
checkDuplicates('entity', entities);
|
|
110
|
+
|
|
111
|
+
// 6. Breakpoints declared in config but not via defineBreakpoints() — the
|
|
112
|
+
// names will work at runtime but get no intellisense.
|
|
113
|
+
if (!breakpointsName && configObject?.type === AST_NODE_TYPES.ObjectExpression) {
|
|
114
|
+
const hasKey = configObject.properties.some(
|
|
115
|
+
(p) => p.type === AST_NODE_TYPES.Property && p.key?.name === 'breakpoints',
|
|
116
|
+
);
|
|
117
|
+
if (hasKey) {
|
|
118
|
+
warnings.push(
|
|
119
|
+
`caper.config.ts sets \`breakpoints\` but no \`defineBreakpoints()\` export was found — breakpoint names will not be type-checked. Wrap the object: \`export const breakpoints = defineBreakpoints({ ... })\`.`,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (warnings.length === 0) return;
|
|
125
|
+
|
|
126
|
+
for (const msg of warnings) {
|
|
127
|
+
// Vite's createLogger is suppressed inside the dts plugin chain during
|
|
128
|
+
// builds, so go straight to console.warn — yellow ANSI so it stands out
|
|
129
|
+
// amid Vite's own output.
|
|
130
|
+
console.warn(`\x1b[33m[caper] ${msg}\x1b[0m`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (server?.ws) {
|
|
134
|
+
// Surface the first warning in the browser overlay so a dev in the
|
|
135
|
+
// tab notices. Subsequent ones are in the terminal — don't spam the
|
|
136
|
+
// overlay with a stack of them.
|
|
137
|
+
server.ws.send({
|
|
138
|
+
type: 'error',
|
|
139
|
+
err: {
|
|
140
|
+
message:
|
|
141
|
+
`caper build-time validation (${warnings.length} warning${warnings.length === 1 ? '' : 's'}):\n` +
|
|
142
|
+
warnings.map((w) => ' • ' + w).join('\n'),
|
|
143
|
+
id: configPath,
|
|
144
|
+
plugin: 'vite-plugin-caper-config',
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* DFS-based cycle detection over a `Map<id, dependencyIds[]>` graph.
|
|
152
|
+
* Returns the cycle path (e.g. `['A','B','A']`) on the first cycle found,
|
|
153
|
+
* or `null` if the graph is acyclic. Used by build-time validation so a
|
|
154
|
+
* plugin requires-cycle surfaces in the terminal before bootstrap runs.
|
|
155
|
+
*/
|
|
156
|
+
export function detectCycle(edges) {
|
|
157
|
+
const WHITE = 0;
|
|
158
|
+
const GRAY = 1;
|
|
159
|
+
const BLACK = 2;
|
|
160
|
+
const color = new Map();
|
|
161
|
+
for (const id of edges.keys()) color.set(id, WHITE);
|
|
162
|
+
const stack = [];
|
|
163
|
+
|
|
164
|
+
function visit(id) {
|
|
165
|
+
if (color.get(id) === GRAY) {
|
|
166
|
+
const startIdx = stack.indexOf(id);
|
|
167
|
+
return [...stack.slice(startIdx), id];
|
|
168
|
+
}
|
|
169
|
+
if (color.get(id) === BLACK) return null;
|
|
170
|
+
color.set(id, GRAY);
|
|
171
|
+
stack.push(id);
|
|
172
|
+
for (const dep of edges.get(id) ?? []) {
|
|
173
|
+
const found = visit(dep);
|
|
174
|
+
if (found) return found;
|
|
175
|
+
}
|
|
176
|
+
stack.pop();
|
|
177
|
+
color.set(id, BLACK);
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
for (const id of edges.keys()) {
|
|
182
|
+
const found = visit(id);
|
|
183
|
+
if (found) return found;
|
|
184
|
+
}
|
|
185
|
+
return null;
|
|
186
|
+
}
|