@vylos/cli 0.5.4 → 0.6.1
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/package.json +2 -2
- package/src/vite/projectPlugin.ts +197 -9
- package/templates/env.d.ts +8 -0
- package/templates/links.ts +6 -0
- package/templates/main.ts +3 -118
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vylos/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/DevOpsBenjamin/Vylos"
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"tailwindcss": "^4.0.0",
|
|
18
18
|
"@tailwindcss/vite": "^4.0.0",
|
|
19
19
|
"tsx": "^4.19.2",
|
|
20
|
-
"@vylos/core": "0.
|
|
20
|
+
"@vylos/core": "0.6.1"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"typescript": "^5.7.2"
|
|
@@ -1,15 +1,150 @@
|
|
|
1
|
-
import type { Plugin } from 'vite';
|
|
1
|
+
import type { Plugin, ViteDevServer } from 'vite';
|
|
2
2
|
import { resolve } from 'path';
|
|
3
|
-
import { existsSync, cpSync } from 'fs';
|
|
3
|
+
import { existsSync, cpSync, readdirSync } from 'fs';
|
|
4
|
+
|
|
5
|
+
/** Normalize a path to forward slashes (Windows compatibility). */
|
|
6
|
+
function normalizePath(p: string): string {
|
|
7
|
+
return p.replace(/\\/g, '/');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Escape a string for safe inclusion in a single-quoted JavaScript string literal. */
|
|
11
|
+
function escapeForStringLiteral(s: string): string {
|
|
12
|
+
return s.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
13
|
+
}
|
|
4
14
|
|
|
5
15
|
/**
|
|
6
|
-
*
|
|
16
|
+
* Recursively collect all `.ts` files under `dir`.
|
|
17
|
+
* Returns absolute paths with forward slashes.
|
|
18
|
+
*/
|
|
19
|
+
function collectTsFiles(dir: string): string[] {
|
|
20
|
+
if (!existsSync(dir)) return [];
|
|
21
|
+
const results: string[] = [];
|
|
22
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
23
|
+
const full = resolve(dir, entry.name);
|
|
24
|
+
if (entry.isDirectory()) {
|
|
25
|
+
results.push(...collectTsFiles(full));
|
|
26
|
+
} else if (entry.isFile() && entry.name.endsWith('.ts')) {
|
|
27
|
+
results.push(normalizePath(full));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return results;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** An import alias and its normalized absolute path. */
|
|
34
|
+
interface ImportEntry {
|
|
35
|
+
alias: string;
|
|
36
|
+
path: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** List subdirectories under `dir` (returns empty array if dir doesn't exist). */
|
|
40
|
+
function listSubdirs(dir: string): string[] {
|
|
41
|
+
if (!existsSync(dir)) return [];
|
|
42
|
+
return readdirSync(dir, { withFileTypes: true })
|
|
43
|
+
.filter((e) => e.isDirectory())
|
|
44
|
+
.map((e) => resolve(dir, e.name));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Collect imports from per-location subdirectories and a global directory.
|
|
49
|
+
* For each location subdirectory, scans `<locationDir>/<subdir>` for .ts files.
|
|
50
|
+
* Then scans `<globalDir>/<subdir>` for additional .ts files.
|
|
51
|
+
*/
|
|
52
|
+
function collectImports(
|
|
53
|
+
locationsDir: string,
|
|
54
|
+
globalDir: string,
|
|
55
|
+
subdir: string,
|
|
56
|
+
prefix: string,
|
|
57
|
+
): ImportEntry[] {
|
|
58
|
+
const imports: ImportEntry[] = [];
|
|
59
|
+
for (const locDir of listSubdirs(locationsDir)) {
|
|
60
|
+
for (const file of collectTsFiles(resolve(locDir, subdir))) {
|
|
61
|
+
imports.push({ alias: `${prefix}${imports.length}`, path: file });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
for (const file of collectTsFiles(resolve(globalDir, subdir))) {
|
|
65
|
+
imports.push({ alias: `${prefix}${imports.length}`, path: file });
|
|
66
|
+
}
|
|
67
|
+
return imports;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Scan the project root and generate the code for the `virtual:vylos-project`
|
|
72
|
+
* module. Returns eagerly-imported locations, events, actions, and optionally
|
|
73
|
+
* initLinks.
|
|
74
|
+
*/
|
|
75
|
+
function generateAutoDiscoveryCode(projectRoot: string): string {
|
|
76
|
+
const locationsDir = resolve(projectRoot, 'locations');
|
|
77
|
+
const globalDir = resolve(projectRoot, 'global');
|
|
78
|
+
|
|
79
|
+
// Discover location definition files
|
|
80
|
+
const locationImports: ImportEntry[] = [];
|
|
81
|
+
for (const locDir of listSubdirs(locationsDir)) {
|
|
82
|
+
const locationFile = resolve(locDir, 'location.ts');
|
|
83
|
+
if (existsSync(locationFile)) {
|
|
84
|
+
locationImports.push({
|
|
85
|
+
alias: `_loc${locationImports.length}`,
|
|
86
|
+
path: normalizePath(locationFile),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Discover event and action files (per-location + global)
|
|
92
|
+
const eventImports = collectImports(locationsDir, globalDir, 'events', '_evt');
|
|
93
|
+
const actionImports = collectImports(locationsDir, globalDir, 'actions', '_act');
|
|
94
|
+
|
|
95
|
+
// Optional links file
|
|
96
|
+
const linksPath = resolve(projectRoot, 'links.ts');
|
|
97
|
+
const hasLinks = existsSync(linksPath);
|
|
98
|
+
|
|
99
|
+
// --- Generate code ---
|
|
100
|
+
const allImports = [...locationImports, ...eventImports, ...actionImports];
|
|
101
|
+
const lines: string[] = allImports.map(
|
|
102
|
+
(imp) => `import * as ${imp.alias}_mod from '${escapeForStringLiteral(imp.path)}';`,
|
|
103
|
+
);
|
|
104
|
+
if (hasLinks) {
|
|
105
|
+
lines.push(`import * as _links_mod from '${escapeForStringLiteral(normalizePath(linksPath))}';`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
lines.push('');
|
|
109
|
+
lines.push(`function _unwrap(mod, path) {`);
|
|
110
|
+
lines.push(` if (mod.default === undefined || mod.default === null) {`);
|
|
111
|
+
lines.push(` console.error('[vylos] File "' + path + '" does not have a default export. Add "export default { ... }" to include it in auto-discovery. Skipping this file.');`);
|
|
112
|
+
lines.push(` }`);
|
|
113
|
+
lines.push(` return mod.default;`);
|
|
114
|
+
lines.push(`}`);
|
|
115
|
+
lines.push('');
|
|
116
|
+
|
|
117
|
+
function toUnwrapList(imports: ImportEntry[]): string {
|
|
118
|
+
return imports.map((imp) => `_unwrap(${imp.alias}_mod, '${escapeForStringLiteral(imp.path)}')`).join(', ');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
lines.push(`export const locations = [${toUnwrapList(locationImports)}].filter(Boolean);`);
|
|
122
|
+
lines.push(`export const events = [${toUnwrapList(eventImports)}].filter(Boolean);`);
|
|
123
|
+
lines.push(`export const actions = [${toUnwrapList(actionImports)}].filter(Boolean);`);
|
|
124
|
+
|
|
125
|
+
if (hasLinks) {
|
|
126
|
+
lines.push(
|
|
127
|
+
`export const initLinks = _unwrap(_links_mod, '${escapeForStringLiteral(normalizePath(linksPath))}');`,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return lines.join('\n') + '\n';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Vite plugin that resolves virtual module `vylos:project` to the project's config,
|
|
136
|
+
* and `virtual:vylos-project` to auto-discovered locations/events/actions.
|
|
7
137
|
* Also serves project assets in dev and copies them in build.
|
|
8
138
|
*/
|
|
9
139
|
export function vylosProjectPlugin(projectRoot: string): Plugin {
|
|
10
140
|
const virtualModuleId = 'vylos:project';
|
|
11
141
|
const resolvedVirtualModuleId = '\0' + virtualModuleId;
|
|
12
142
|
|
|
143
|
+
const autoDiscoveryId = 'virtual:vylos-project';
|
|
144
|
+
const resolvedAutoDiscoveryId = '\0' + autoDiscoveryId;
|
|
145
|
+
|
|
146
|
+
let server: ViteDevServer | undefined;
|
|
147
|
+
|
|
13
148
|
return {
|
|
14
149
|
name: 'vylos-project',
|
|
15
150
|
|
|
@@ -17,26 +152,34 @@ export function vylosProjectPlugin(projectRoot: string): Plugin {
|
|
|
17
152
|
if (id === virtualModuleId) {
|
|
18
153
|
return resolvedVirtualModuleId;
|
|
19
154
|
}
|
|
155
|
+
if (id === autoDiscoveryId) {
|
|
156
|
+
return resolvedAutoDiscoveryId;
|
|
157
|
+
}
|
|
20
158
|
},
|
|
21
159
|
|
|
22
160
|
load(id) {
|
|
23
161
|
if (id === resolvedVirtualModuleId) {
|
|
24
|
-
const configPath = resolve(projectRoot, 'vylos.config.ts');
|
|
162
|
+
const configPath = normalizePath(resolve(projectRoot, 'vylos.config.ts'));
|
|
25
163
|
const setupPath = resolve(projectRoot, 'setup.ts');
|
|
26
164
|
|
|
27
|
-
let code = `export { default as config } from '${configPath
|
|
165
|
+
let code = `export { default as config } from '${configPath}';\n`;
|
|
28
166
|
|
|
29
167
|
if (existsSync(setupPath)) {
|
|
30
|
-
code += `export { default as plugin } from '${setupPath
|
|
168
|
+
code += `export { default as plugin } from '${normalizePath(setupPath)}';\n`;
|
|
31
169
|
} else {
|
|
32
170
|
code += `export const plugin = undefined;\n`;
|
|
33
171
|
}
|
|
34
172
|
|
|
35
173
|
return code;
|
|
36
174
|
}
|
|
175
|
+
|
|
176
|
+
if (id === resolvedAutoDiscoveryId) {
|
|
177
|
+
return generateAutoDiscoveryCode(projectRoot);
|
|
178
|
+
}
|
|
37
179
|
},
|
|
38
180
|
|
|
39
|
-
configureServer(
|
|
181
|
+
configureServer(devServer) {
|
|
182
|
+
server = devServer;
|
|
40
183
|
const assetsDir = resolve(projectRoot, 'assets');
|
|
41
184
|
|
|
42
185
|
server.middlewares.use((req, _res, next) => {
|
|
@@ -45,13 +188,58 @@ export function vylosProjectPlugin(projectRoot: string): Plugin {
|
|
|
45
188
|
// Rewrite /assets/... to project assets dir
|
|
46
189
|
if (req.url.startsWith('/assets/')) {
|
|
47
190
|
const assetPath = resolve(assetsDir, req.url.slice('/assets/'.length));
|
|
48
|
-
|
|
49
|
-
|
|
191
|
+
const normalizedAssetPath = normalizePath(assetPath);
|
|
192
|
+
const normalizedAssetsDir = normalizePath(assetsDir);
|
|
193
|
+
if (normalizedAssetPath.startsWith(normalizedAssetsDir + '/') && existsSync(assetPath)) {
|
|
194
|
+
req.url = '/@fs/' + normalizedAssetPath;
|
|
50
195
|
}
|
|
51
196
|
}
|
|
52
197
|
|
|
53
198
|
next();
|
|
54
199
|
});
|
|
200
|
+
|
|
201
|
+
// HMR: watch for file add/remove in scanned directories
|
|
202
|
+
const locationsDir = normalizePath(resolve(projectRoot, 'locations'));
|
|
203
|
+
const globalEventsDir = normalizePath(
|
|
204
|
+
resolve(projectRoot, 'global', 'events'),
|
|
205
|
+
);
|
|
206
|
+
const globalActionsDir = normalizePath(
|
|
207
|
+
resolve(projectRoot, 'global', 'actions'),
|
|
208
|
+
);
|
|
209
|
+
const linksPath = normalizePath(resolve(projectRoot, 'links.ts'));
|
|
210
|
+
|
|
211
|
+
function isInWatchedDir(filePath: string): boolean {
|
|
212
|
+
const normalized = normalizePath(filePath);
|
|
213
|
+
return (
|
|
214
|
+
normalized.startsWith(locationsDir + '/') ||
|
|
215
|
+
normalized.startsWith(globalEventsDir + '/') ||
|
|
216
|
+
normalized.startsWith(globalActionsDir + '/')
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function isLinksFile(filePath: string): boolean {
|
|
221
|
+
return normalizePath(filePath) === linksPath;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function invalidateAutoDiscovery() {
|
|
225
|
+
const mod = server?.moduleGraph.getModuleById(resolvedAutoDiscoveryId);
|
|
226
|
+
if (mod) {
|
|
227
|
+
server!.moduleGraph.invalidateModule(mod);
|
|
228
|
+
server!.ws.send({ type: 'full-reload' });
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
server.watcher.on('add', (filePath) => {
|
|
233
|
+
if (isLinksFile(filePath) || (filePath.endsWith('.ts') && isInWatchedDir(filePath))) {
|
|
234
|
+
invalidateAutoDiscovery();
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
server.watcher.on('unlink', (filePath) => {
|
|
239
|
+
if (isLinksFile(filePath) || (filePath.endsWith('.ts') && isInWatchedDir(filePath))) {
|
|
240
|
+
invalidateAutoDiscovery();
|
|
241
|
+
}
|
|
242
|
+
});
|
|
55
243
|
},
|
|
56
244
|
|
|
57
245
|
writeBundle() {
|
package/templates/env.d.ts
CHANGED
|
@@ -4,3 +4,11 @@ declare module '*.vue' {
|
|
|
4
4
|
const component: DefineComponent<{}, {}, any>;
|
|
5
5
|
export default component;
|
|
6
6
|
}
|
|
7
|
+
|
|
8
|
+
declare module 'virtual:vylos-project' {
|
|
9
|
+
import type { VylosLocation, VylosEvent, VylosAction, LocationManager } from '@vylos/core';
|
|
10
|
+
export const locations: VylosLocation[];
|
|
11
|
+
export const events: VylosEvent[];
|
|
12
|
+
export const actions: VylosAction[];
|
|
13
|
+
export const initLinks: ((lm: LocationManager) => void) | undefined;
|
|
14
|
+
}
|
package/templates/main.ts
CHANGED
|
@@ -1,121 +1,6 @@
|
|
|
1
|
-
import 'reflect-metadata';
|
|
2
1
|
import './style.css';
|
|
3
|
-
import {
|
|
4
|
-
import { createPinia } from 'pinia';
|
|
5
|
-
import {
|
|
6
|
-
GameShell,
|
|
7
|
-
createEngine,
|
|
8
|
-
useEngineStateStore,
|
|
9
|
-
ENGINE_INJECT_KEY,
|
|
10
|
-
CONFIG_INJECT_KEY,
|
|
11
|
-
EnginePhase,
|
|
12
|
-
LocationManager,
|
|
13
|
-
ActionManager,
|
|
14
|
-
type EventRunnerCallbacks,
|
|
15
|
-
type TextEntry,
|
|
16
|
-
type VylosCharacter,
|
|
17
|
-
} from '@vylos/core';
|
|
2
|
+
import { setupVylos } from '@vylos/core';
|
|
18
3
|
import config from './vylos.config';
|
|
19
|
-
import
|
|
4
|
+
import * as project from 'virtual:vylos-project';
|
|
20
5
|
|
|
21
|
-
|
|
22
|
-
import home from './locations/home/location';
|
|
23
|
-
|
|
24
|
-
// Events
|
|
25
|
-
import intro from './global/events/intro';
|
|
26
|
-
|
|
27
|
-
// Actions
|
|
28
|
-
import wait from './global/actions/wait';
|
|
29
|
-
|
|
30
|
-
const app = createApp(GameShell);
|
|
31
|
-
const pinia = createPinia();
|
|
32
|
-
app.use(pinia);
|
|
33
|
-
|
|
34
|
-
const engineState = useEngineStateStore(pinia);
|
|
35
|
-
const gameState = useGameStore(pinia);
|
|
36
|
-
|
|
37
|
-
// Location manager
|
|
38
|
-
const locationManager = new LocationManager();
|
|
39
|
-
locationManager.registerAll([home]);
|
|
40
|
-
|
|
41
|
-
// Action manager
|
|
42
|
-
const actionManager = new ActionManager();
|
|
43
|
-
actionManager.registerAll([wait]);
|
|
44
|
-
|
|
45
|
-
const callbacks: EventRunnerCallbacks = {
|
|
46
|
-
onSay(text: string, speaker: VylosCharacter | null) {
|
|
47
|
-
engineState.setDialogue({ text, speaker, isNarration: !speaker });
|
|
48
|
-
},
|
|
49
|
-
onChoice(options) {
|
|
50
|
-
engineState.setChoices({ prompt: null, options });
|
|
51
|
-
},
|
|
52
|
-
onSetBackground(path) { engineState.setBackground(path); },
|
|
53
|
-
onSetForeground(path) { engineState.setForeground(path); },
|
|
54
|
-
onShowOverlay() {},
|
|
55
|
-
onHideOverlay() {},
|
|
56
|
-
onSetLocation(id) {
|
|
57
|
-
gameState.state.locationId = id;
|
|
58
|
-
engineState.setLocation(id);
|
|
59
|
-
const bg = locationManager.resolveBackground(id, gameState.state.gameTime);
|
|
60
|
-
if (bg) engineState.setBackground(bg);
|
|
61
|
-
},
|
|
62
|
-
onClear() {
|
|
63
|
-
engineState.setDialogue(null);
|
|
64
|
-
engineState.setChoices(null);
|
|
65
|
-
engineState.setForeground(null);
|
|
66
|
-
engineState.setOverlay(null);
|
|
67
|
-
},
|
|
68
|
-
resolveText(entry: string | TextEntry) {
|
|
69
|
-
return typeof entry === 'string' ? entry : entry['en'] ?? Object.values(entry)[0] ?? '';
|
|
70
|
-
},
|
|
71
|
-
getState() { return gameState.state; },
|
|
72
|
-
setState(s) { gameState.setState(s); },
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
const engine = createEngine({ callbacks, projectId: config.id });
|
|
76
|
-
app.provide(ENGINE_INJECT_KEY, engine);
|
|
77
|
-
app.provide(CONFIG_INJECT_KEY, config);
|
|
78
|
-
app.mount('#app');
|
|
79
|
-
|
|
80
|
-
engineState.setPhase(EnginePhase.MainMenu);
|
|
81
|
-
|
|
82
|
-
const stopWatch = watch(() => engineState.phase, (newPhase) => {
|
|
83
|
-
if (newPhase === EnginePhase.Running) {
|
|
84
|
-
stopWatch();
|
|
85
|
-
startGame();
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
function startGame() {
|
|
90
|
-
gameState.state.locationId = 'home';
|
|
91
|
-
gameState.state.gameTime = 12;
|
|
92
|
-
engineState.setLocation('home');
|
|
93
|
-
|
|
94
|
-
engine.run([intro], () => gameState.state, {
|
|
95
|
-
onTick(state) {
|
|
96
|
-
const locations = locationManager.getAccessibleFrom(state.locationId, state);
|
|
97
|
-
engineState.setLocations(locations.map(l => ({
|
|
98
|
-
id: l.id,
|
|
99
|
-
name: typeof l.name === 'string' ? l.name : l.name['en'] ?? l.id,
|
|
100
|
-
accessible: true,
|
|
101
|
-
})));
|
|
102
|
-
|
|
103
|
-
const actions = actionManager.getAvailable(state.locationId, state);
|
|
104
|
-
engineState.setActions(actions.map(a => ({
|
|
105
|
-
id: a.id,
|
|
106
|
-
label: typeof a.label === 'string' ? a.label : a.label['en'] ?? a.id,
|
|
107
|
-
locationId: a.locationId ?? '',
|
|
108
|
-
})));
|
|
109
|
-
|
|
110
|
-
const resolveText = (t: string | Record<string, string>) =>
|
|
111
|
-
typeof t === 'string' ? t : t['en'] ?? Object.values(t)[0] ?? '';
|
|
112
|
-
engineState.setDrawableEvents(engine.eventManager.getDrawableEvents(state, resolveText));
|
|
113
|
-
|
|
114
|
-
const bg = locationManager.resolveBackground(state.locationId, state.gameTime);
|
|
115
|
-
if (bg) engineState.setBackground(bg);
|
|
116
|
-
},
|
|
117
|
-
onAction(actionId, state) {
|
|
118
|
-
actionManager.execute(actionId, state);
|
|
119
|
-
},
|
|
120
|
-
}).catch(console.error);
|
|
121
|
-
}
|
|
6
|
+
setupVylos({ config, ...project });
|