@rpgjs/vite 5.0.0-beta.8 → 5.0.0-beta.9

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.
Files changed (36) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/compatibility-v4/flag-transform.d.ts +6 -0
  3. package/dist/compatibility-v4/index.d.ts +22 -0
  4. package/dist/compatibility-v4/load-config-file.d.ts +2 -0
  5. package/dist/compatibility-v4/require-transform.d.ts +2 -0
  6. package/dist/compatibility-v4/utils.d.ts +55 -0
  7. package/dist/index.d.ts +1 -0
  8. package/dist/index.js +1329 -164
  9. package/dist/index.js.map +1 -1
  10. package/dist/rpgjs-plugin.d.ts +1 -0
  11. package/package.json +17 -9
  12. package/src/compatibility-v4/flag-transform.ts +61 -0
  13. package/src/compatibility-v4/index.ts +713 -0
  14. package/src/compatibility-v4/load-config-file.ts +38 -0
  15. package/src/compatibility-v4/require-transform.ts +67 -0
  16. package/src/compatibility-v4/utils.ts +170 -0
  17. package/src/index.ts +2 -1
  18. package/tests/compatibility-v4.spec.ts +105 -0
  19. package/tests/fixtures/v4-game/rpg.toml +11 -0
  20. package/tests/fixtures/v4-game/src/modules/main/characters/assets/hero.svg +2 -0
  21. package/tests/fixtures/v4-game/src/modules/main/characters/hero.ts +2 -0
  22. package/tests/fixtures/v4-game/src/modules/main/client.ts +4 -0
  23. package/tests/fixtures/v4-game/src/modules/main/database/potion.ts +6 -0
  24. package/tests/fixtures/v4-game/src/modules/main/events/npc.ts +4 -0
  25. package/tests/fixtures/v4-game/src/modules/main/gui/menu.vue +2 -0
  26. package/tests/fixtures/v4-game/src/modules/main/maps/map.tmx +2 -0
  27. package/tests/fixtures/v4-game/src/modules/main/maps/map.ts +7 -0
  28. package/tests/fixtures/v4-game/src/modules/main/player.ts +4 -0
  29. package/tests/fixtures/v4-game/src/modules/main/scene-map.ts +4 -0
  30. package/tests/fixtures/v4-game/src/modules/main/server.ts +4 -0
  31. package/tests/fixtures/v4-game/src/modules/main/sounds/theme.ogg +2 -0
  32. package/tests/fixtures/v4-game/src/modules/main/sounds/theme.ts +5 -0
  33. package/tests/fixtures/v4-game/src/modules/main/sprite.ts +4 -0
  34. package/tests/fixtures/v4-game/src/modules/main/worlds/maps/world-map.tmx +6 -0
  35. package/tests/fixtures/v4-game/src/modules/main/worlds/world.world +13 -0
  36. package/vite.config.ts +7 -1
@@ -0,0 +1,38 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { loadEnv } from "vite";
4
+ import toml from "@iarna/toml";
5
+ import { Config, replaceEnvVars } from "./utils";
6
+
7
+ export function loadConfigFileSync(mode = "development", root = process.cwd()): Config {
8
+ const tomlFile = path.resolve(root, "rpg.toml");
9
+ const jsonFile = path.resolve(root, "rpg.json");
10
+ let config: any = {};
11
+
12
+ if (fs.existsSync(tomlFile)) {
13
+ config = toml.parse(fs.readFileSync(tomlFile, "utf8"));
14
+ } else if (fs.existsSync(jsonFile)) {
15
+ config = JSON.parse(fs.readFileSync(jsonFile, "utf8"));
16
+ }
17
+
18
+ config = replaceEnvVars(config, loadEnv(mode, root, ""));
19
+ config.autostart = config.autostart ?? true;
20
+ config.modulesRoot = config.modulesRoot ?? "";
21
+ config.compilerOptions ??= {};
22
+ config.compilerOptions.build ??= {};
23
+ config.compilerOptions.build.pwaEnabled ??= true;
24
+ config.compilerOptions.build.outputDir ??= "dist";
25
+
26
+ if (config.modules) {
27
+ config.modules = config.modules.map((module: string) => {
28
+ if (module.startsWith(".")) {
29
+ return "./" + path.join(config.modulesRoot, module);
30
+ }
31
+ return module;
32
+ });
33
+ }
34
+
35
+ config.startMap = config.startMap || config.start?.map;
36
+ return config as Config;
37
+ }
38
+
@@ -0,0 +1,67 @@
1
+ import * as parser from "@babel/parser";
2
+ import _traverse from "@babel/traverse";
3
+ import _generate from "@babel/generator";
4
+ import { importDeclaration, importDefaultSpecifier, identifier, stringLiteral } from "@babel/types";
5
+ import type { Plugin } from "vite";
6
+
7
+ const traverse = (_traverse as any).default ?? _traverse;
8
+ const generate = (_generate as any).default ?? _generate;
9
+
10
+ function readStaticRequireArg(arg: any, ast: any): string {
11
+ if (!arg) return "";
12
+ if (arg.type === "StringLiteral") return arg.value;
13
+ if (arg.type === "Identifier") {
14
+ let value = "";
15
+ traverse(ast, {
16
+ VariableDeclarator(path: any) {
17
+ if (path.node.id?.name === arg.name && path.node.init?.type === "StringLiteral") {
18
+ value = path.node.init.value;
19
+ }
20
+ },
21
+ });
22
+ return value;
23
+ }
24
+ if (arg.type === "BinaryExpression" && arg.operator === "+") {
25
+ const left = readStaticRequireArg(arg.left, ast);
26
+ const right = readStaticRequireArg(arg.right, ast);
27
+ return left && right ? left + right : "";
28
+ }
29
+ return "";
30
+ }
31
+
32
+ export default function vitePluginRequire(): Plugin {
33
+ return {
34
+ name: "rpgjs-v4-require-transform",
35
+ transform(code, id) {
36
+ const fileRegex = /(.jsx?|.tsx?)(\?.*)?$/;
37
+ const allowRegex = /^(?!.*node_modules(?:\/|\\)(?!rpgjs-|@rpgjs)).*$/;
38
+ if (!fileRegex.test(id) || !allowRegex.test(id)) {
39
+ return { code, map: null };
40
+ }
41
+
42
+ const ast = parser.parse(code, {
43
+ sourceType: "module",
44
+ plugins: ["typescript", "jsx", "decorators-legacy", "classProperties"],
45
+ });
46
+
47
+ let changed = false;
48
+ traverse(ast, {
49
+ CallExpression(path: any) {
50
+ if (!path.node.callee || path.node.callee.type !== "Identifier" || path.node.callee.name !== "require") return;
51
+
52
+ const request = readStaticRequireArg(path.node.arguments[0], ast);
53
+ if (!request) return;
54
+
55
+ const variableName = `__rpgjs_v4_require_${path.scope.generateUidIdentifier("asset").name}`;
56
+ ast.program.body.unshift(importDeclaration([importDefaultSpecifier(identifier(variableName))], stringLiteral(request)));
57
+ path.replaceWith(identifier(variableName));
58
+ changed = true;
59
+ },
60
+ });
61
+
62
+ if (!changed) return { code, map: null };
63
+ return { code: generate(ast, {}).code, map: null };
64
+ },
65
+ };
66
+ }
67
+
@@ -0,0 +1,170 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+
4
+ export interface ClientBuildConfigOptions {
5
+ serveMode?: boolean;
6
+ side?: "client" | "server";
7
+ type?: "rpg" | "mmorpg";
8
+ mode?: string;
9
+ config?: Config;
10
+ tiledMapBasePath?: string;
11
+ }
12
+
13
+ export interface Config {
14
+ modules?: string[];
15
+ modulesRoot?: string;
16
+ startMap?: string;
17
+ start?: {
18
+ map?: string;
19
+ graphic?: string;
20
+ hitbox?: [number, number];
21
+ };
22
+ inputs?: Record<string, { bind: string | string[] }>;
23
+ spritesheetDirectories?: string[];
24
+ compilerOptions?: {
25
+ alias?: Record<string, string>;
26
+ build?: {
27
+ outputDir?: string;
28
+ pwaEnabled?: boolean;
29
+ assetsPath?: string;
30
+ serverUrl?: string;
31
+ };
32
+ };
33
+ autostart?: boolean;
34
+ type?: "rpg" | "mmorpg";
35
+ vite?: any;
36
+ [key: string]: any;
37
+ }
38
+
39
+ export interface ImportObject {
40
+ importString: string;
41
+ variablesString: string;
42
+ folder: string;
43
+ relativePath: string;
44
+ }
45
+
46
+ export function dedent(strings: TemplateStringsArray, ...values: unknown[]) {
47
+ const fullString = strings.reduce((acc, str, i) => acc + str + (values[i] ?? ""), "");
48
+ const lines = fullString.split("\n");
49
+ let minIndent = Infinity;
50
+
51
+ for (const line of lines) {
52
+ if (!line.trim()) continue;
53
+ minIndent = Math.min(minIndent, line.match(/^\s*/)?.[0].length ?? 0);
54
+ }
55
+
56
+ if (minIndent === Infinity) return fullString.trim();
57
+
58
+ return lines
59
+ .map((line) => line.trim() ? line.slice(minIndent) : line)
60
+ .join("\n")
61
+ .trim();
62
+ }
63
+
64
+ export function warn(message: string) {
65
+ console.warn(`[RPG-JS v4 compatibility] ${message}`);
66
+ }
67
+
68
+ export function toPosix(value: string) {
69
+ return value.replace(/\\/g, "/");
70
+ }
71
+
72
+ export function formatVariableName(value: string) {
73
+ return value.replace(/\./g, "").replace(/[.@/\\ -]/g, "_").replace(/[^A-Za-z0-9_$]/g, "_");
74
+ }
75
+
76
+ export function transformPathIfModule(moduleName: string) {
77
+ if (moduleName.startsWith("@rpgjs") || moduleName.startsWith("rpgjs-")) {
78
+ return path.join("node_modules", moduleName);
79
+ }
80
+ return moduleName;
81
+ }
82
+
83
+ export function resolveModuleImport(moduleName: string) {
84
+ return moduleName.replace(/^\.\//, "");
85
+ }
86
+
87
+ export function getAllFiles(dirPath: string): string[] {
88
+ if (!fs.existsSync(dirPath)) return [];
89
+ const files: string[] = [];
90
+ const dirents = fs.readdirSync(dirPath, { withFileTypes: true });
91
+
92
+ for (const dirent of dirents) {
93
+ const fullPath = path.join(dirPath, dirent.name);
94
+ if (dirent.isDirectory()) {
95
+ files.push(...getAllFiles(fullPath));
96
+ } else {
97
+ files.push(fullPath);
98
+ }
99
+ }
100
+
101
+ return files;
102
+ }
103
+
104
+ export function importPathForFile(file: string, root: string) {
105
+ const srcPath = path.join(root, "src");
106
+ if (file.startsWith(srcPath + path.sep)) {
107
+ return `@/${toPosix(path.relative(srcPath, file))}`;
108
+ }
109
+ return `./${toPosix(path.relative(root, file))}`;
110
+ }
111
+
112
+ export function importString(modulePath: string, fileName: string, variableName = fileName, projectRoot = process.cwd()) {
113
+ const file = path.resolve(projectRoot, transformPathIfModule(modulePath), `${fileName}.ts`);
114
+ if (!fs.existsSync(file)) return "";
115
+ return `import ${variableName} from '${importPathForFile(file, projectRoot)}'`;
116
+ }
117
+
118
+ export function searchFolderAndTransformToImportString(
119
+ folderPath: string,
120
+ modulePath: string,
121
+ extensionFilter: string | string[],
122
+ returnCb?: (file: string, variableName: string, absoluteFile: string) => string,
123
+ options?: {
124
+ customFilter?: (file: string) => boolean;
125
+ },
126
+ projectRoot = process.cwd()
127
+ ): ImportObject {
128
+ const folder = path.resolve(projectRoot, transformPathIfModule(modulePath), folderPath);
129
+ if (!fs.existsSync(folder)) {
130
+ return { variablesString: "", importString: "", folder: "", relativePath: "" };
131
+ }
132
+
133
+ const extensions = Array.isArray(extensionFilter) ? extensionFilter : [extensionFilter];
134
+ let importString = "";
135
+ let relativePath = "";
136
+ const variablesString = getAllFiles(folder)
137
+ .filter((file) => extensions.some((ext) => file.endsWith(ext)))
138
+ .filter((file) => options?.customFilter ? options.customFilter(file) : true)
139
+ .map((file) => {
140
+ const importPath = importPathForFile(file, projectRoot);
141
+ const variableName = formatVariableName(importPath);
142
+ relativePath = importPath;
143
+ importString += `\nimport ${variableName} from '${importPath}'`;
144
+ return returnCb ? returnCb(importPath, variableName, file) : variableName;
145
+ })
146
+ .join(",");
147
+
148
+ return {
149
+ variablesString,
150
+ importString,
151
+ folder,
152
+ relativePath,
153
+ };
154
+ }
155
+
156
+ export function replaceEnvVars(obj: any, envs: Record<string, string | undefined>): any {
157
+ if (obj == null) return obj;
158
+ if (typeof obj === "string" && obj.startsWith("$ENV:")) {
159
+ return envs[obj.slice(5)];
160
+ }
161
+ if (Array.isArray(obj)) return obj.map((item) => replaceEnvVars(item, envs));
162
+ if (typeof obj === "object") {
163
+ return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, replaceEnvVars(value, envs)]));
164
+ }
165
+ return obj;
166
+ }
167
+
168
+ export function assetsFolder(outputDir: string) {
169
+ return path.join(outputDir, "assets");
170
+ }
package/src/index.ts CHANGED
@@ -5,4 +5,5 @@ export { removeImportsPlugin, type RemoveImportsPluginOptions } from './remove-i
5
5
  export { replaceConfigImport } from './replace-config-import';
6
6
  export { rpgjs } from './rpgjs-plugin';
7
7
  export { serverPlugin } from './server-plugin';
8
- export { entryPointPlugin, type EntryPointPluginOptions } from './entry-point-plugin';
8
+ export { entryPointPlugin, type EntryPointPluginOptions } from './entry-point-plugin';
9
+ export { default as compatibilityV4Plugin } from './compatibility-v4';
@@ -0,0 +1,105 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import path from "path";
3
+ import { flagTransform } from "../src/compatibility-v4/flag-transform";
4
+ import { createClientConfigLoad, createModulesLoad, createTiledMapEntries, createWorldMapEntries, loadClientFiles, loadServerFiles, loadSpriteSheet } from "../src/compatibility-v4";
5
+ import { loadConfigFileSync } from "../src/compatibility-v4/load-config-file";
6
+
7
+ const fixtureRoot = path.resolve(__dirname, "fixtures/v4-game");
8
+
9
+ describe("compatibilityV4Plugin", () => {
10
+ it("loads and normalizes a v4 rpg.toml", () => {
11
+ const config = loadConfigFileSync("development", fixtureRoot);
12
+
13
+ expect(config.modules).toEqual(["./src/modules/main"]);
14
+ expect(config.startMap).toBe("map");
15
+ expect(config.compilerOptions?.build?.outputDir).toBe("dist");
16
+ });
17
+
18
+ it("generates a v5 server from a v4 module layout", () => {
19
+ const config = loadConfigFileSync("development", fixtureRoot);
20
+ const code = loadServerFiles("./src/modules/main", { modulesCreated: [], type: "rpg", serveMode: true, config, tiledMapBasePath: "map" }, config, fixtureRoot);
21
+
22
+ expect(code).toContain("createServer");
23
+ expect(code).toContain("provideServerModules");
24
+ expect(code).toContain("provideTiledMap()");
25
+ expect(code).toContain("player.setGraphic('hero')");
26
+ expect(code).toContain("player.setHitbox(32, 32)");
27
+ expect(code).toContain("await player.changeMap('map')");
28
+ expect(code).toContain("events:");
29
+ expect(code).toContain("database:");
30
+ expect(code).toContain("worldMaps:");
31
+ });
32
+
33
+ it("generates a v5 client from a v4 module layout", () => {
34
+ const config = loadConfigFileSync("development", fixtureRoot);
35
+ const code = loadClientFiles("./src/modules/main", { type: "rpg", serveMode: true, config }, config, fixtureRoot);
36
+
37
+ expect(code).toContain("spritesheets:");
38
+ expect(code).toContain("sprite,");
39
+ expect(code).toContain("engine,");
40
+ expect(code).toContain("sceneMap:");
41
+ expect(code).toContain("gui:");
42
+ expect(code).toContain("sounds:");
43
+ expect(code).toContain("prototype.width = 32");
44
+ expect(code).toContain("prototype.height = 32");
45
+ });
46
+
47
+ it("registers v4 spritesheet images by file basename", () => {
48
+ const config = loadConfigFileSync("development", fixtureRoot);
49
+ const spritesheet = loadSpriteSheet("characters", "./src/modules/main", { type: "rpg", serveMode: true, config }, fixtureRoot);
50
+
51
+ expect(spritesheet.variablesString).toContain("...");
52
+ expect(spritesheet.propImagesString).toContain("hero.svg?url");
53
+ expect(spritesheet.propImagesString).toContain('"hero":');
54
+ expect(spritesheet.propImagesString).toContain("id,");
55
+ expect(spritesheet.propImagesString).toContain("image,");
56
+ });
57
+
58
+ it("generates a client config with Tiled as the default map loader", () => {
59
+ const config = loadConfigFileSync("development", fixtureRoot);
60
+ const code = createClientConfigLoad(config);
61
+
62
+ expect(code).toContain("from '@rpgjs/tiledmap/client'");
63
+ expect(code).toContain("provideTiledMap({ basePath: 'map' })");
64
+ expect(code).toContain("provideClientModules(modules)");
65
+ });
66
+
67
+ it("autoloads Tiled maps from v4 map roots with public paths", () => {
68
+ const config = loadConfigFileSync("development", fixtureRoot);
69
+ const code = createTiledMapEntries("./src/modules/main", { type: "rpg", serveMode: true, config, tiledMapBasePath: "map" }, fixtureRoot);
70
+
71
+ expect(code).toContain("{ id: 'world-map', file: '/map/world-map.tmx' }");
72
+ expect(code).not.toContain("{ id: 'map'");
73
+ });
74
+
75
+ it("normalizes Tiled world files for the v5 world map manager", () => {
76
+ const code = createWorldMapEntries("./src/modules/main", fixtureRoot);
77
+
78
+ expect(code).toContain('"id":"world"');
79
+ expect(code).toContain('"id":"world-map"');
80
+ expect(code).toContain('"worldX":64');
81
+ expect(code).toContain('"worldY":-160');
82
+ });
83
+
84
+ it("maps RPGJS v4 starter legacy modules to virtual v5 modules", () => {
85
+ const code = createModulesLoad(["./main", "@rpgjs/mobile-gui", "@rpgjs/default-gui", "@rpgjs/gamepad"]);
86
+
87
+ expect(code).toContain("virtual:rpgjs-v4-legacy-mobile-gui");
88
+ expect(code).toContain("virtual:rpgjs-v4-legacy-default-gui");
89
+ expect(code).toContain("virtual:rpgjs-v4-legacy-gamepad");
90
+ });
91
+
92
+ it("removes flagged imports for the opposite side", async () => {
93
+ const plugin = flagTransform({ side: "client", type: "mmorpg", mode: "development" });
94
+ const result = await plugin.transform?.call({} as any, "export default 1", "/tmp/mod.ts?server");
95
+
96
+ expect(result).toEqual({ code: "export default null;", map: null });
97
+ });
98
+
99
+ it("keeps server flagged imports when the import chain is server-side", async () => {
100
+ const plugin = flagTransform({ side: "client", type: "mmorpg", mode: "development" });
101
+ const result = await plugin.transform?.call({} as any, "export default 1", "/tmp/mod.ts?server&side=server");
102
+
103
+ expect(result).toEqual({ code: "export default 1", map: null });
104
+ });
105
+ });
@@ -0,0 +1,11 @@
1
+ modulesRoot = './src/modules'
2
+ modules = ['./main']
3
+
4
+ [start]
5
+ map = 'map'
6
+ graphic = 'hero'
7
+ hitbox = [32, 32]
8
+
9
+ [compilerOptions.build]
10
+ pwaEnabled = false
11
+
@@ -0,0 +1,2 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32"><rect width="32" height="32"/></svg>
2
+
@@ -0,0 +1,2 @@
1
+ export default class HeroSpritesheet {}
2
+
@@ -0,0 +1,4 @@
1
+ export default {
2
+ onStart() {}
3
+ }
4
+
@@ -0,0 +1,6 @@
1
+ export default {
2
+ Potion: {
3
+ name: 'Potion'
4
+ }
5
+ }
6
+
@@ -0,0 +1,4 @@
1
+ export default {
2
+ name: 'EV-1'
3
+ }
4
+
@@ -0,0 +1,2 @@
1
+ <template><div>Menu</div></template>
2
+
@@ -0,0 +1,2 @@
1
+ <map version="1.10" tiledversion="1.10.2" orientation="orthogonal" renderorder="right-down" width="1" height="1" tilewidth="32" tileheight="32" infinite="0"></map>
2
+
@@ -0,0 +1,7 @@
1
+ export default {
2
+ id: 'script-map',
3
+ width: 320,
4
+ height: 320,
5
+ events: []
6
+ }
7
+
@@ -0,0 +1,4 @@
1
+ export default {
2
+ onConnected() {}
3
+ }
4
+
@@ -0,0 +1,4 @@
1
+ export default {
2
+ onAfterLoading() {}
3
+ }
4
+
@@ -0,0 +1,4 @@
1
+ export default {
2
+ onStart() {}
3
+ }
4
+
@@ -0,0 +1,5 @@
1
+ export default {
2
+ id: 'theme',
3
+ src: 'theme.ogg'
4
+ }
5
+
@@ -0,0 +1,4 @@
1
+ export default {
2
+ onInit() {}
3
+ }
4
+
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <map version="1.9" tiledversion="1.9.2" orientation="orthogonal" renderorder="right-down" width="1" height="1" tilewidth="32" tileheight="32" infinite="0" nextlayerid="2" nextobjectid="1">
3
+ <layer id="1" name="Ground" width="1" height="1">
4
+ <data encoding="csv">0</data>
5
+ </layer>
6
+ </map>
@@ -0,0 +1,13 @@
1
+ {
2
+ "maps": [
3
+ {
4
+ "fileName": "maps/world-map.tmx",
5
+ "height": 640,
6
+ "width": 800,
7
+ "x": 64,
8
+ "y": -160
9
+ }
10
+ ],
11
+ "onlyShowAdjacentMaps": false,
12
+ "type": "world"
13
+ }
package/vite.config.ts CHANGED
@@ -36,9 +36,15 @@ export default defineConfig({
36
36
  'vite',
37
37
  'vite-plugin-dts',
38
38
  '@canvasengine/compiler',
39
+ '@iarna/toml',
40
+ 'image-size',
41
+ '@babel/parser',
42
+ '@babel/traverse',
43
+ '@babel/generator',
44
+ '@babel/types',
39
45
  'chokidar',
40
46
  'ws'
41
47
  ]
42
48
  }
43
49
  },
44
- })
50
+ })