@rpgjs/vite 5.0.0-alpha.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.
@@ -0,0 +1 @@
1
+ export declare const rpgjsModuleViteConfig: () => import('vite').UserConfigFnPromise;
@@ -0,0 +1,36 @@
1
+ import { Plugin } from 'vite';
2
+ export interface RemoveImportsPluginOptions {
3
+ /**
4
+ * Array of patterns to match against import sources
5
+ * Can be strings (exact match) or RegExp objects
6
+ */
7
+ patterns: (string | RegExp)[];
8
+ }
9
+ /**
10
+ * Vite plugin that replaces import statements with const declarations set to null
11
+ *
12
+ * This plugin analyzes JavaScript/TypeScript files and replaces import statements
13
+ * whose source matches any of the provided patterns with const declarations set to null.
14
+ * This is useful for removing dependencies while maintaining variable declarations.
15
+ *
16
+ * @param options - Configuration options for the plugin
17
+ * @returns Vite plugin object
18
+ *
19
+ * @example
20
+ * ```typescript
21
+ * // Replace specific imports
22
+ * removeImportsPlugin({
23
+ * patterns: ['react', 'vue', '@types/node']
24
+ * })
25
+ *
26
+ * // Replace imports using regex patterns
27
+ * removeImportsPlugin({
28
+ * patterns: [/^@types\//, /^react-/, 'lodash']
29
+ * })
30
+ *
31
+ * // Transform example:
32
+ * // import React, { useState } from 'react';
33
+ * // becomes: const React = null; const useState = null;
34
+ * ```
35
+ */
36
+ export declare function removeImportsPlugin(options: RemoveImportsPluginOptions): Plugin;
@@ -0,0 +1,48 @@
1
+ import { Plugin } from 'vite';
2
+ export interface DataFolderPluginOptions {
3
+ /**
4
+ * Source folder containing the data files (TMX, TSX, images)
5
+ */
6
+ sourceFolder: string;
7
+ /**
8
+ * Public path prefix for accessing the data files
9
+ * @default '/data'
10
+ */
11
+ publicPath?: string;
12
+ /**
13
+ * Target folder in build output for the data files
14
+ * @default 'assets/data'
15
+ */
16
+ buildOutputPath?: string;
17
+ /**
18
+ * File extensions to include
19
+ * @default ['.tmx', '.tsx', '.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg']
20
+ */
21
+ allowedExtensions?: string[];
22
+ }
23
+ /**
24
+ * Vite plugin that serves a data folder in development mode and copies it to assets during build
25
+ *
26
+ * This plugin allows serving game data files (TMX maps, TSX tilesets, images) during development
27
+ * and automatically includes them in the build output for production deployment.
28
+ *
29
+ * @param options - Configuration options for the plugin
30
+ *
31
+ * @example
32
+ * ```js
33
+ * // In vite.config.ts
34
+ * import { defineConfig } from 'vite';
35
+ * import { dataFolderPlugin } from '@rpgjs/vite';
36
+ *
37
+ * export default defineConfig({
38
+ * plugins: [
39
+ * dataFolderPlugin({
40
+ * sourceFolder: './game-data',
41
+ * publicPath: '/data',
42
+ * buildOutputPath: 'assets/data'
43
+ * })
44
+ * ]
45
+ * });
46
+ * ```
47
+ */
48
+ export declare function tiledMapFolderPlugin(options: DataFolderPluginOptions): Plugin;
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@rpgjs/vite",
3
+ "version": "5.0.0-alpha.0",
4
+ "main": "dist/index.js",
5
+ "types": "dist/index.d.ts",
6
+ "keywords": [],
7
+ "author": "",
8
+ "license": "MIT",
9
+ "description": "Vite plugins for RPGJS",
10
+ "devDependencies": {
11
+ "@types/node": "^20.0.0",
12
+ "acorn": "^8.14.1",
13
+ "acorn-walk": "^8.3.4",
14
+ "magic-string": "^0.30.0",
15
+ "typescript": "^5.0.0",
16
+ "vite": "^6.2.5",
17
+ "vite-plugin-dts": "^4.5.3",
18
+ "vitest": "^3.1.1"
19
+ },
20
+ "type": "module",
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "dependencies": {
25
+ "@canvasengine/compiler": "2.0.0-beta.21"
26
+ },
27
+ "scripts": {
28
+ "build": "vite build",
29
+ "dev": "vite build --watch",
30
+ "test": "vitest run"
31
+ }
32
+ }
@@ -0,0 +1,80 @@
1
+ import { Plugin } from 'vite';
2
+ import MagicString from 'magic-string';
3
+ import { parse } from 'acorn';
4
+ import * as walk from 'acorn-walk';
5
+
6
+ export interface DirectivePluginOptions {
7
+ side: 'client' | 'server';
8
+ }
9
+
10
+ function removeNode(s: MagicString, start: number, end: number) {
11
+ s.remove(start, end);
12
+ }
13
+
14
+ function isDirective(node: any): node is any {
15
+ return node.type === 'ExpressionStatement' &&
16
+ node.expression.type === 'Literal' &&
17
+ (node.expression.value === 'use client' || node.expression.value === 'use server');
18
+ }
19
+
20
+ export function directivePlugin(options: DirectivePluginOptions): Plugin {
21
+ return {
22
+ name: 'rpgjs-directive',
23
+ transform(code, id) {
24
+ const ast = parse(code, { ecmaVersion: 'latest', sourceType: 'module' }) as any;
25
+ const s = new MagicString(code);
26
+
27
+ let fileDirective: 'client' | 'server' | null = null;
28
+ if (ast.body.length && isDirective(ast.body[0])) {
29
+ fileDirective = (ast.body[0].expression.value as string).split(' ')[1] as 'client' | 'server';
30
+ if (fileDirective !== options.side) {
31
+ return {
32
+ code: 'export default null;',
33
+ map: { mappings: '' }
34
+ };
35
+ }
36
+ removeNode(s, ast.body[0].start, ast.body[0].end);
37
+ }
38
+
39
+ walk.ancestor(ast, {
40
+ FunctionDeclaration(node: any, ancestors: any[]) {
41
+ if (node.body && node.body.body && node.body.body.length && isDirective(node.body.body[0])) {
42
+ const directive = (node.body.body[0].expression.value as string).split(' ')[1] as 'client' | 'server';
43
+ if (directive !== options.side) {
44
+ removeNode(s, node.start, node.end);
45
+ } else {
46
+ removeNode(s, node.body.body[0].start, node.body.body[0].end);
47
+ }
48
+ }
49
+ },
50
+ FunctionExpression(node: any, ancestors: any[]) {
51
+ if (node.body && node.body.body && node.body.body.length && isDirective(node.body.body[0])) {
52
+ const directive = (node.body.body[0].expression.value as string).split(' ')[1] as 'client' | 'server';
53
+ if (directive !== options.side) {
54
+ removeNode(s, node.start, node.end);
55
+ } else {
56
+ removeNode(s, node.body.body[0].start, node.body.body[0].end);
57
+ }
58
+ }
59
+ },
60
+ ArrowFunctionExpression(node: any, ancestors: any[]) {
61
+ if (node.body && node.body.body && node.body.body.length && isDirective(node.body.body[0])) {
62
+ const directive = (node.body.body[0].expression.value as string).split(' ')[1] as 'client' | 'server';
63
+ if (directive !== options.side) {
64
+ const decl = ancestors.slice().reverse().find((n: any) => n.type === 'VariableDeclaration');
65
+ if (decl) {
66
+ removeNode(s, decl.start, decl.end);
67
+ } else {
68
+ removeNode(s, node.start, node.end);
69
+ }
70
+ } else {
71
+ removeNode(s, node.body.body[0].start, node.body.body[0].end);
72
+ }
73
+ }
74
+ }
75
+ });
76
+
77
+ return { code: s.toString(), map: s.generateMap({ hires: true }) };
78
+ }
79
+ };
80
+ }
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export { tiledMapFolderPlugin, type DataFolderPluginOptions } from './tiled-map-folder-plugin';
2
+ export { rpgjsModuleViteConfig } from './module-config';
3
+ export { directivePlugin, type DirectivePluginOptions } from './directive-plugin';
4
+ export { removeImportsPlugin, type RemoveImportsPluginOptions } from './remove-imports-plugin';
@@ -0,0 +1,122 @@
1
+ import { defineConfig, build } from "vite";
2
+ import canvasengine from "@canvasengine/compiler";
3
+ import dts from "vite-plugin-dts";
4
+ import { directivePlugin, removeImportsPlugin } from "./index";
5
+
6
+ /**
7
+ * Creates a build configuration for client or server side
8
+ *
9
+ * This function generates a standardized Vite build configuration that can be used
10
+ * for both client and server builds with different plugins and output directories.
11
+ *
12
+ * @param {Object} options - Build configuration options
13
+ * @param {string} options.side - The build side ('client' or 'server')
14
+ * @param {boolean} options.watch - Whether to enable watch mode
15
+ * @returns {Object} Vite build configuration object
16
+ *
17
+ * @example
18
+ * ```typescript
19
+ * const clientConfig = createBuildConfig({ side: 'client', watch: false });
20
+ * const serverConfig = createBuildConfig({ side: 'server', watch: true });
21
+ * ```
22
+ */
23
+ function createBuildConfig({ side, watch }: { side: 'client' | 'server', watch: boolean }) {
24
+ const isClient = side === 'client';
25
+
26
+ const plugins = isClient
27
+ ? [
28
+ canvasengine(),
29
+ removeImportsPlugin({ patterns: [/server/] }),
30
+ directivePlugin({ side: "client" }),
31
+ ]
32
+ : [
33
+ removeImportsPlugin({ patterns: [/\.ce$/, /client/] }),
34
+ directivePlugin({ side: "server" }),
35
+ ];
36
+
37
+ return {
38
+ configFile: false as const, // Prevent using this config file
39
+ plugins: [
40
+ ...plugins,
41
+ dts({
42
+ include: ['src/**/*.ts'],
43
+ outDir: 'dist'
44
+ })
45
+ ],
46
+ build: {
47
+ watch: watch ? {} : undefined,
48
+ outDir: `dist/${side}`,
49
+ minify: false,
50
+ lib: {
51
+ entry: {
52
+ index: "src/index.ts",
53
+ },
54
+ fileName: "index",
55
+ formats: ["es" as const],
56
+ },
57
+ rollupOptions: {
58
+ external: [
59
+ /@rpgjs/,
60
+ "canvasengine",
61
+ "esbuild",
62
+ "@canvasengine/presets",
63
+ "rxjs",
64
+ ],
65
+ output: {
66
+ preserveModules: true,
67
+ preserveModulesRoot: "src",
68
+ },
69
+ },
70
+ },
71
+ };
72
+ }
73
+
74
+ /**
75
+ * Builds both client and server configurations in parallel
76
+ *
77
+ * This function creates and executes build configurations for both client and server
78
+ * sides simultaneously using Promise.all for better performance.
79
+ *
80
+ * @param {boolean} watch - Whether to enable watch mode for both builds
81
+ * @returns {Promise<void[]>} Promise that resolves when both builds are complete
82
+ *
83
+ * @example
84
+ * ```typescript
85
+ * // Build once
86
+ * await buildClientAndServer(false);
87
+ *
88
+ * // Build with watch mode
89
+ * await buildClientAndServer(true);
90
+ * ```
91
+ */
92
+ async function buildClientAndServer(watch: boolean = false) {
93
+ const clientBuild = build(createBuildConfig({ side: 'client', watch }));
94
+ const serverBuild = build(createBuildConfig({ side: 'server', watch }));
95
+
96
+ await Promise.all([clientBuild, serverBuild]);
97
+
98
+ console.log("✅ Build complete");
99
+ }
100
+
101
+ const isWatchMode = process.argv.includes("--watch");
102
+ const isBuildCommand = process.argv.includes("build");
103
+ const isManualControl = isWatchMode && isBuildCommand;
104
+
105
+ export const rpgjsModuleViteConfig = () => {
106
+ return defineConfig(async ({ command }) => {
107
+ if (isManualControl) {
108
+ buildClientAndServer(true);
109
+ return {};
110
+ }
111
+ if (command === "build") {
112
+ console.log("👀 Building...");
113
+ await buildClientAndServer();
114
+ // Return empty config to prevent default build
115
+ process.exit(0);
116
+ } else {
117
+ return {
118
+ plugins: [],
119
+ };
120
+ }
121
+ });
122
+ };
@@ -0,0 +1,164 @@
1
+ import { Plugin } from 'vite';
2
+ import MagicString from 'magic-string';
3
+ import { parse } from 'acorn';
4
+ import * as walk from 'acorn-walk';
5
+
6
+ export interface RemoveImportsPluginOptions {
7
+ /**
8
+ * Array of patterns to match against import sources
9
+ * Can be strings (exact match) or RegExp objects
10
+ */
11
+ patterns: (string | RegExp)[];
12
+ }
13
+
14
+ /**
15
+ * Replaces a node in the source code using MagicString
16
+ * @param s - MagicString instance
17
+ * @param start - Start position of the node
18
+ * @param end - End position of the node
19
+ * @param replacement - The replacement string
20
+ */
21
+ function replaceNode(s: MagicString, start: number, end: number, replacement: string) {
22
+ s.overwrite(start, end, replacement);
23
+ }
24
+
25
+ /**
26
+ * Checks if an import source matches any of the provided patterns
27
+ * @param source - The import source string
28
+ * @param patterns - Array of string or RegExp patterns
29
+ * @returns True if the source matches any pattern
30
+ *
31
+ * @example
32
+ * ```typescript
33
+ * matchesPattern('react', ['react', /^@types/]) // true
34
+ * matchesPattern('@types/node', ['react', /^@types/]) // true
35
+ * matchesPattern('lodash', ['react', /^@types/]) // false
36
+ * ```
37
+ */
38
+ function matchesPattern(source: string, patterns: (string | RegExp)[]): boolean {
39
+ return patterns.some(pattern => {
40
+ if (typeof pattern === 'string') {
41
+ return source === pattern;
42
+ } else if (pattern instanceof RegExp) {
43
+ return pattern.test(source);
44
+ }
45
+ return false;
46
+ });
47
+ }
48
+
49
+ /**
50
+ * Generates const declarations from import specifiers
51
+ * @param node - The import declaration node
52
+ * @returns String with const declarations set to null
53
+ *
54
+ * @example
55
+ * ```typescript
56
+ * // import React, { useState, useEffect } from 'react'
57
+ * // becomes: const React = null; const useState = null; const useEffect = null;
58
+ *
59
+ * // import * as utils from 'utils'
60
+ * // becomes: const utils = null;
61
+ *
62
+ * // import 'side-effect-module'
63
+ * // becomes: // removed import: side-effect-module
64
+ * ```
65
+ */
66
+ function generateConstDeclarations(node: any): string {
67
+ const declarations: string[] = [];
68
+
69
+ if (node.specifiers && node.specifiers.length > 0) {
70
+ for (const specifier of node.specifiers) {
71
+ if (specifier.type === 'ImportDefaultSpecifier') {
72
+ // import React from 'react' -> const React = null;
73
+ declarations.push(`const ${specifier.local.name} = null;`);
74
+ } else if (specifier.type === 'ImportSpecifier') {
75
+ // import { useState } from 'react' -> const useState = null;
76
+ declarations.push(`const ${specifier.local.name} = null;`);
77
+ } else if (specifier.type === 'ImportNamespaceSpecifier') {
78
+ // import * as React from 'react' -> const React = null;
79
+ declarations.push(`const ${specifier.local.name} = null;`);
80
+ }
81
+ }
82
+ } else {
83
+ // Side-effect import like import 'module' -> comment
84
+ declarations.push(`// removed import: ${node.source.value}`);
85
+ }
86
+
87
+ return declarations.join(' ');
88
+ }
89
+
90
+ /**
91
+ * Vite plugin that replaces import statements with const declarations set to null
92
+ *
93
+ * This plugin analyzes JavaScript/TypeScript files and replaces import statements
94
+ * whose source matches any of the provided patterns with const declarations set to null.
95
+ * This is useful for removing dependencies while maintaining variable declarations.
96
+ *
97
+ * @param options - Configuration options for the plugin
98
+ * @returns Vite plugin object
99
+ *
100
+ * @example
101
+ * ```typescript
102
+ * // Replace specific imports
103
+ * removeImportsPlugin({
104
+ * patterns: ['react', 'vue', '@types/node']
105
+ * })
106
+ *
107
+ * // Replace imports using regex patterns
108
+ * removeImportsPlugin({
109
+ * patterns: [/^@types\//, /^react-/, 'lodash']
110
+ * })
111
+ *
112
+ * // Transform example:
113
+ * // import React, { useState } from 'react';
114
+ * // becomes: const React = null; const useState = null;
115
+ * ```
116
+ */
117
+ export function removeImportsPlugin(options: RemoveImportsPluginOptions): Plugin {
118
+ return {
119
+ name: 'rpgjs-remove-imports',
120
+ transform(code, id) {
121
+ // Skip non-JS/TS files
122
+ if (!id.match(/\.(js|ts|jsx|tsx)$/)) {
123
+ return null;
124
+ }
125
+
126
+ try {
127
+ const ast = parse(code, {
128
+ ecmaVersion: 'latest',
129
+ sourceType: 'module'
130
+ }) as any;
131
+
132
+ const s = new MagicString(code);
133
+ let hasChanges = false;
134
+
135
+ // Walk through the AST to find import declarations
136
+ walk.simple(ast, {
137
+ ImportDeclaration(node: any) {
138
+ const importSource = node.source.value;
139
+
140
+ if (matchesPattern(importSource, options.patterns)) {
141
+ const replacement = generateConstDeclarations(node);
142
+ replaceNode(s, node.start, node.end, replacement);
143
+ hasChanges = true;
144
+ }
145
+ }
146
+ });
147
+
148
+ // Only return transformed code if changes were made
149
+ if (hasChanges) {
150
+ return {
151
+ code: s.toString(),
152
+ map: s.generateMap({ hires: true })
153
+ };
154
+ }
155
+
156
+ return null;
157
+ } catch (error) {
158
+ // If parsing fails, return the original code
159
+ console.warn(`Failed to parse ${id}:`, error);
160
+ return null;
161
+ }
162
+ }
163
+ };
164
+ }