@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,224 @@
1
+ import { Plugin } from 'vite';
2
+ import { readFileSync, existsSync, statSync, readdirSync, copyFileSync, mkdirSync } from 'fs';
3
+ import { join, extname, relative, dirname } from 'path';
4
+
5
+ export interface DataFolderPluginOptions {
6
+ /**
7
+ * Source folder containing the data files (TMX, TSX, images)
8
+ */
9
+ sourceFolder: string;
10
+
11
+ /**
12
+ * Public path prefix for accessing the data files
13
+ * @default '/data'
14
+ */
15
+ publicPath?: string;
16
+
17
+ /**
18
+ * Target folder in build output for the data files
19
+ * @default 'assets/data'
20
+ */
21
+ buildOutputPath?: string;
22
+
23
+ /**
24
+ * File extensions to include
25
+ * @default ['.tmx', '.tsx', '.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg']
26
+ */
27
+ allowedExtensions?: string[];
28
+ }
29
+
30
+ /**
31
+ * Vite plugin that serves a data folder in development mode and copies it to assets during build
32
+ *
33
+ * This plugin allows serving game data files (TMX maps, TSX tilesets, images) during development
34
+ * and automatically includes them in the build output for production deployment.
35
+ *
36
+ * @param options - Configuration options for the plugin
37
+ *
38
+ * @example
39
+ * ```js
40
+ * // In vite.config.ts
41
+ * import { defineConfig } from 'vite';
42
+ * import { dataFolderPlugin } from '@rpgjs/vite';
43
+ *
44
+ * export default defineConfig({
45
+ * plugins: [
46
+ * dataFolderPlugin({
47
+ * sourceFolder: './game-data',
48
+ * publicPath: '/data',
49
+ * buildOutputPath: 'assets/data'
50
+ * })
51
+ * ]
52
+ * });
53
+ * ```
54
+ */
55
+ export function tiledMapFolderPlugin(options: DataFolderPluginOptions): Plugin {
56
+ const {
57
+ sourceFolder,
58
+ publicPath = '/data',
59
+ buildOutputPath = 'assets/data',
60
+ allowedExtensions = ['.tmx', '.tsx', '.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg']
61
+ } = options;
62
+
63
+ let isBuild = false;
64
+ let outputDir = 'dist';
65
+
66
+ /**
67
+ * Get MIME type based on file extension
68
+ */
69
+ const getMimeType = (filePath: string): string => {
70
+ const ext = extname(filePath).toLowerCase();
71
+ const mimeTypes: Record<string, string> = {
72
+ '.tmx': 'application/xml',
73
+ '.tsx': 'application/xml',
74
+ '.png': 'image/png',
75
+ '.jpg': 'image/jpeg',
76
+ '.jpeg': 'image/jpeg',
77
+ '.gif': 'image/gif',
78
+ '.webp': 'image/webp',
79
+ '.svg': 'image/svg+xml'
80
+ };
81
+ return mimeTypes[ext] || 'application/octet-stream';
82
+ };
83
+
84
+ /**
85
+ * Check if file extension is allowed
86
+ */
87
+ const isAllowedFile = (filePath: string): boolean => {
88
+ const ext = extname(filePath).toLowerCase();
89
+ return allowedExtensions.includes(ext);
90
+ };
91
+
92
+ /**
93
+ * Recursively get all files from a directory
94
+ */
95
+ const getAllFiles = (dirPath: string, basePath: string = dirPath): string[] => {
96
+ const files: string[] = [];
97
+
98
+ if (!existsSync(dirPath)) {
99
+ return files;
100
+ }
101
+
102
+ const items = readdirSync(dirPath);
103
+
104
+ for (const item of items) {
105
+ const fullPath = join(dirPath, item);
106
+ const stat = statSync(fullPath);
107
+
108
+ if (stat.isDirectory()) {
109
+ files.push(...getAllFiles(fullPath, basePath));
110
+ } else if (isAllowedFile(fullPath)) {
111
+ files.push(fullPath);
112
+ }
113
+ }
114
+
115
+ return files;
116
+ };
117
+
118
+ /**
119
+ * Copy files to build output directory
120
+ */
121
+ const copyFilesToBuild = (outputPath: string) => {
122
+ const files = getAllFiles(sourceFolder);
123
+
124
+ for (const filePath of files) {
125
+ const relativePath = relative(sourceFolder, filePath);
126
+ const targetPath = join(outputPath, buildOutputPath, relativePath);
127
+ const targetDir = dirname(targetPath);
128
+
129
+ // Create target directory if it doesn't exist
130
+ mkdirSync(targetDir, { recursive: true });
131
+
132
+ // Copy file
133
+ copyFileSync(filePath, targetPath);
134
+ console.log(`📁 Copied data file: ${relativePath}`);
135
+ }
136
+ };
137
+
138
+ return {
139
+ name: 'data-folder',
140
+ enforce: 'pre',
141
+
142
+ configResolved(config) {
143
+ isBuild = config.command === 'build';
144
+ outputDir = config.build.outDir || 'dist';
145
+ },
146
+
147
+ // Handle build mode - copy files to output directory
148
+ generateBundle() {
149
+ if (isBuild) {
150
+ console.log(`📦 Copying data files from ${sourceFolder} to ${buildOutputPath}...`);
151
+ copyFilesToBuild(outputDir);
152
+ console.log('✅ Data files copied successfully');
153
+ }
154
+ },
155
+
156
+ // Handle development mode - serve files via middleware
157
+ configureServer(server) {
158
+ if (!existsSync(sourceFolder)) {
159
+ console.warn(`⚠️ Data folder not found: ${sourceFolder}`);
160
+ return;
161
+ }
162
+
163
+ console.log(`📁 Serving data folder: ${sourceFolder} at ${publicPath}`);
164
+
165
+ server.middlewares.use((req: any, res: any, next: any) => {
166
+ if (!req.url?.startsWith(publicPath)) {
167
+ return next();
168
+ }
169
+
170
+ // Remove public path prefix to get relative file path
171
+ const relativePath = req.url.slice(publicPath.length);
172
+
173
+ // Remove leading slash if present
174
+ const cleanPath = relativePath.startsWith('/') ? relativePath.slice(1) : relativePath;
175
+
176
+ // Construct full file path
177
+ const filePath = join(sourceFolder, cleanPath);
178
+
179
+ // Security check - ensure file is within source folder
180
+ const cwd = typeof process !== 'undefined' ? process.cwd() : '';
181
+ const resolvedFilePath = join(cwd, filePath);
182
+ const resolvedSourceFolder = join(cwd, sourceFolder);
183
+
184
+ if (!resolvedFilePath.startsWith(resolvedSourceFolder)) {
185
+ res.statusCode = 403;
186
+ res.end('Forbidden');
187
+ return;
188
+ }
189
+
190
+ // Check if file exists and is allowed
191
+ if (!existsSync(filePath) || !isAllowedFile(filePath)) {
192
+ res.statusCode = 404;
193
+ res.end('Not Found');
194
+ return;
195
+ }
196
+
197
+ // Check if it's a file (not directory)
198
+ const stat = statSync(filePath);
199
+ if (!stat.isFile()) {
200
+ res.statusCode = 404;
201
+ res.end('Not Found');
202
+ return;
203
+ }
204
+
205
+ try {
206
+ // Read and serve the file
207
+ const fileContent = readFileSync(filePath);
208
+ const mimeType = getMimeType(filePath);
209
+
210
+ res.setHeader('Content-Type', mimeType);
211
+ res.setHeader('Cache-Control', 'no-cache');
212
+ res.setHeader('Access-Control-Allow-Origin', '*');
213
+ res.end(fileContent);
214
+
215
+ console.log(`📄 Served data file: ${cleanPath}`);
216
+ } catch (error) {
217
+ console.error(`❌ Error serving file ${filePath}:`, error);
218
+ res.statusCode = 500;
219
+ res.end('Internal Server Error');
220
+ }
221
+ });
222
+ }
223
+ };
224
+ }
@@ -0,0 +1,34 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { directivePlugin } from '../src/directive-plugin'
3
+
4
+ const pluginClient = directivePlugin({ side: 'client' })
5
+ const pluginServer = directivePlugin({ side: 'server' })
6
+
7
+ const fileSource = `'use client';\nexport function hello(){ return 'hi' }`
8
+ const funcSource = `export function foo(){\n 'use server';\n return 1;\n}\nexport const bar = () => {\n 'use client';\n return 2;\n}`
9
+
10
+ describe('directivePlugin', () => {
11
+ it('keeps file for matching side', async () => {
12
+ const res = await pluginClient.transform(fileSource, 'file.ts') as any
13
+ expect(res.code).toContain("export function hello")
14
+ expect(res.code).not.toContain('use client')
15
+ })
16
+
17
+ it('removes file for opposite side', async () => {
18
+ const res = await pluginServer.transform(fileSource, 'file.ts') as any
19
+ expect(res.code.trim()).toBe('export default null;')
20
+ })
21
+
22
+ it('keeps function for matching side', async () => {
23
+ const res = await pluginServer.transform(funcSource, 'func.ts') as any
24
+ expect(res.code).toContain('function foo()')
25
+ expect(res.code).not.toContain('use server')
26
+ expect(res.code).not.toContain('bar') // bar should be removed on server
27
+ })
28
+
29
+ it('removes function for opposite side', async () => {
30
+ const res = await pluginClient.transform(funcSource, 'func.ts') as any
31
+ expect(res.code).toContain('bar')
32
+ expect(res.code).not.toContain('foo')
33
+ })
34
+ })
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "ESNext",
5
+ "moduleResolution": "node",
6
+ "declaration": true,
7
+ "outDir": "dist",
8
+ "rootDir": "src",
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "types": ["node"]
14
+ },
15
+ "include": ["src/**/*"],
16
+ "exclude": ["node_modules", "dist"]
17
+ }
package/vite.config.ts ADDED
@@ -0,0 +1,43 @@
1
+ import { defineConfig } from 'vite'
2
+ import dts from 'vite-plugin-dts'
3
+
4
+ // List of Node.js built-in modules to mark as external
5
+ const nodeBuiltins = [
6
+ 'fs', 'path', 'os', 'crypto', 'util', 'events', 'stream', 'buffer',
7
+ 'url', 'querystring', 'http', 'https', 'net', 'tls', 'child_process',
8
+ 'cluster', 'dgram', 'dns', 'domain', 'readline', 'repl', 'tty', 'vm',
9
+ 'zlib', 'assert', 'constants', 'module', 'perf_hooks', 'process',
10
+ 'punycode', 'string_decoder', 'timers', 'trace_events', 'v8', 'worker_threads'
11
+ ]
12
+
13
+ export default defineConfig({
14
+ plugins: [
15
+ dts({
16
+ include: ['src/**/*.ts'],
17
+ outDir: 'dist'
18
+ })
19
+ ],
20
+ build: {
21
+ target: 'esnext',
22
+ sourcemap: true,
23
+ minify: false,
24
+ lib: {
25
+ entry: 'src/index.ts',
26
+ formats: ['es'],
27
+ fileName: 'index'
28
+ },
29
+ rollupOptions: {
30
+ external: [
31
+ // Mark all Node.js built-in modules as external
32
+ ...nodeBuiltins,
33
+ // Also mark any module that starts with 'node:' as external
34
+ /^node:/,
35
+ // Mark vite as external since it's a peer dependency
36
+ 'vite',
37
+ 'vite-plugin-dts',
38
+ '@canvasengine/compiler',
39
+ 'chokidar'
40
+ ]
41
+ }
42
+ },
43
+ })