@vylos/cli 0.5.3 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vylos/cli",
3
- "version": "0.5.3",
3
+ "version": "0.6.0",
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.5.3"
20
+ "@vylos/core": "0.6.0"
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
- * Vite plugin that resolves virtual module `vylos:project` to the project's config.
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.replace(/\\/g, '/')}';\n`;
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.replace(/\\/g, '/')}';\n`;
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(server) {
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
- if (existsSync(assetPath)) {
49
- req.url = '/@fs/' + assetPath.replace(/\\/g, '/');
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() {
@@ -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
+ }
@@ -0,0 +1,6 @@
1
+ import type { LocationManager } from '@vylos/core';
2
+
3
+ export default function (lm: LocationManager) {
4
+ lm.link('room', ['hallway']);
5
+ lm.link('hallway', ['room']);
6
+ }
package/templates/main.ts CHANGED
@@ -1,121 +1,6 @@
1
- import 'reflect-metadata';
2
1
  import './style.css';
3
- import { createApp, watch } from 'vue';
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 { useGameStore } from '@game';
4
+ import * as project from 'virtual:vylos-project';
20
5
 
21
- // Locations
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 });