vite-plugin-opal 0.1.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/README.md ADDED
@@ -0,0 +1,253 @@
1
+ # vite-plugin-opal
2
+
3
+ Vite plugin for compiling Ruby files using [Opal](https://opalrb.com/). Write Ruby code and run it in the browser with Vite's fast development experience.
4
+
5
+ ## Features
6
+
7
+ - ⚑️ **Fast Compilation**: Compile `.rb` files on-demand during development
8
+ - πŸ”₯ **Hot Module Replacement**: Instant updates when Ruby code changes
9
+ - πŸ—ΊοΈ **Source Maps**: Debug Ruby code in browser DevTools
10
+ - πŸ“¦ **Automatic Runtime**: Opal runtime is injected automatically
11
+ - πŸ” **Dependency Tracking**: Handles `require` statements and tracks dependencies
12
+ - πŸ’Ύ **Smart Caching**: Caches compilation results based on file modification time
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install vite-plugin-opal
18
+ # or
19
+ pnpm add vite-plugin-opal
20
+ # or
21
+ yarn add vite-plugin-opal
22
+ ```
23
+
24
+ You also need to have Ruby and the `opal-vite` gem installed:
25
+
26
+ ```bash
27
+ gem install opal-vite
28
+ # or add to Gemfile
29
+ gem 'opal-vite'
30
+ ```
31
+
32
+ ## Usage
33
+
34
+ ### vite.config.ts
35
+
36
+ ```typescript
37
+ import { defineConfig } from 'vite'
38
+ import opal from 'vite-plugin-opal'
39
+
40
+ export default defineConfig({
41
+ plugins: [
42
+ opal({
43
+ loadPaths: ['./src'],
44
+ sourceMap: true,
45
+ debug: false
46
+ })
47
+ ]
48
+ })
49
+ ```
50
+
51
+ ### Ruby Code (src/main.rb)
52
+
53
+ ```ruby
54
+ puts "Hello from Ruby!"
55
+
56
+ class Counter
57
+ def initialize
58
+ @count = 0
59
+ end
60
+
61
+ def increment
62
+ @count += 1
63
+ puts "Count: #{@count}"
64
+ end
65
+ end
66
+
67
+ counter = Counter.new
68
+ counter.increment
69
+ counter.increment
70
+ ```
71
+
72
+ ### JavaScript Loader (src/main_loader.js)
73
+
74
+ **Important**: Due to Vite's module resolution, you cannot directly import `.rb` files in HTML. Use a JavaScript loader:
75
+
76
+ ```javascript
77
+ // src/main_loader.js
78
+ import './main.rb'
79
+ ```
80
+
81
+ ### HTML (index.html)
82
+
83
+ ```html
84
+ <!DOCTYPE html>
85
+ <html>
86
+ <head>
87
+ <title>Opal + Vite</title>
88
+ </head>
89
+ <body>
90
+ <h1>Opal + Vite</h1>
91
+ <script type="module" src="/src/main_loader.js"></script>
92
+ </body>
93
+ </html>
94
+ ```
95
+
96
+ ## Options
97
+
98
+ ### `gemPath`
99
+
100
+ - Type: `string`
101
+ - Default: `'opal-vite'`
102
+
103
+ Path to the `opal-vite` gem. Use a relative or absolute path for local development.
104
+
105
+ ### `sourceMap`
106
+
107
+ - Type: `boolean`
108
+ - Default: `true`
109
+
110
+ Enable source map generation for debugging.
111
+
112
+ ### `loadPaths`
113
+
114
+ - Type: `string[]`
115
+ - Default: `['./src']`
116
+
117
+ Directories to search for Ruby files when using `require`.
118
+
119
+ ### `arityCheck`
120
+
121
+ - Type: `boolean`
122
+ - Default: `false`
123
+
124
+ Enable arity checking in compiled code.
125
+
126
+ ### `freezing`
127
+
128
+ - Type: `boolean`
129
+ - Default: `true`
130
+
131
+ Enable object freezing for immutability.
132
+
133
+ ### `debug`
134
+
135
+ - Type: `boolean`
136
+ - Default: `false`
137
+
138
+ Enable debug logging.
139
+
140
+ ## How It Works
141
+
142
+ ### Development Mode
143
+
144
+ 1. Vite dev server starts and loads the plugin
145
+ 2. When a `.rb` file is imported, the plugin's `load` hook is triggered
146
+ 3. Plugin spawns a Ruby child process to compile the file using Opal
147
+ 4. Compiled JavaScript is returned with source maps
148
+ 5. HMR watches for file changes and invalidates the module
149
+ 6. Browser receives update and hot-reloads the module
150
+
151
+ ### Production Mode
152
+
153
+ 1. `vite build` processes all entry points
154
+ 2. Ruby files are compiled to optimized JavaScript
155
+ 3. Source maps can optionally be generated
156
+ 4. Assets are fingerprinted and added to manifest
157
+
158
+ ## Advanced Usage
159
+
160
+ ### Requiring Other Files
161
+
162
+ ```ruby
163
+ # src/lib/helper.rb
164
+ module Helper
165
+ def self.greet(name)
166
+ "Hello, #{name}!"
167
+ end
168
+ end
169
+
170
+ # src/app.rb
171
+ require 'lib/helper'
172
+
173
+ puts Helper.greet("World")
174
+ ```
175
+
176
+ ### Using Native JavaScript
177
+
178
+ ```ruby
179
+ require 'native'
180
+
181
+ # Access DOM
182
+ element = Native(`document.getElementById('app')`)
183
+ element[:textContent] = 'Updated by Ruby!'
184
+
185
+ # Use JavaScript libraries
186
+ `console.log('From Ruby!')`
187
+ ```
188
+
189
+ ### Multi-file Dependencies
190
+
191
+ The plugin automatically tracks dependencies and ensures proper load order:
192
+
193
+ ```ruby
194
+ # lib/calculator.rb
195
+ class Calculator
196
+ def add(a, b); a + b; end
197
+ end
198
+
199
+ # lib/formatter.rb
200
+ require 'lib/calculator'
201
+
202
+ class Formatter
203
+ def format_sum(a, b)
204
+ calc = Calculator.new
205
+ "#{a} + #{b} = #{calc.add(a, b)}"
206
+ end
207
+ end
208
+
209
+ # app.rb
210
+ require 'lib/formatter'
211
+ puts Formatter.new.format_sum(5, 3)
212
+ ```
213
+
214
+ ## Troubleshooting
215
+
216
+ ### Ruby not found
217
+
218
+ Ensure Ruby is in your PATH:
219
+
220
+ ```bash
221
+ which ruby
222
+ ruby --version
223
+ ```
224
+
225
+ ### Opal gem not found
226
+
227
+ Install the opal-vite gem:
228
+
229
+ ```bash
230
+ gem install opal-vite
231
+ ```
232
+
233
+ ### HMR not working
234
+
235
+ - Ensure Vite dev server is running
236
+ - Use JavaScript loaders for `.rb` files
237
+ - Check browser console for errors
238
+
239
+ ### Compilation errors
240
+
241
+ - Check Ruby syntax with `ruby -c yourfile.rb`
242
+ - Verify all required files are in load paths
243
+ - Some Ruby features are not supported by Opal
244
+
245
+ ## Related Projects
246
+
247
+ - [opal-vite](../../gems/opal-vite) - Core Ruby gem
248
+ - [opal-vite-rails](../../gems/opal-vite-rails) - Rails integration
249
+ - [Opal](https://opalrb.com/) - Ruby to JavaScript compiler
250
+
251
+ ## License
252
+
253
+ MIT
@@ -0,0 +1,38 @@
1
+ import { Plugin } from 'vite';
2
+
3
+ interface OpalPluginOptions {
4
+ /**
5
+ * Path to the opal-vite gem
6
+ * @default 'opal-vite'
7
+ */
8
+ gemPath?: string;
9
+ /**
10
+ * Enable source map generation
11
+ * @default true
12
+ */
13
+ sourceMap?: boolean;
14
+ /**
15
+ * Load paths for Ruby requires
16
+ * @default ['./src']
17
+ */
18
+ loadPaths?: string[];
19
+ /**
20
+ * Enable arity checking
21
+ * @default false
22
+ */
23
+ arityCheck?: boolean;
24
+ /**
25
+ * Enable object freezing
26
+ * @default true
27
+ */
28
+ freezing?: boolean;
29
+ /**
30
+ * Enable debug logging
31
+ * @default false
32
+ */
33
+ debug?: boolean;
34
+ }
35
+
36
+ declare function opalPlugin(options?: OpalPluginOptions): Plugin;
37
+
38
+ export { type OpalPluginOptions, opalPlugin as default };
package/dist/index.js ADDED
@@ -0,0 +1,548 @@
1
+ import { spawn } from 'child_process';
2
+ import * as fs from 'fs/promises';
3
+ import * as path2 from 'path';
4
+ import * as chokidar from 'chokidar';
5
+
6
+ // src/compiler.ts
7
+ var OpalCompiler = class {
8
+ constructor(options = {}) {
9
+ this.cache = /* @__PURE__ */ new Map();
10
+ this.runtimeCache = null;
11
+ this.options = {
12
+ gemPath: options.gemPath || "opal-vite",
13
+ sourceMap: options.sourceMap !== false,
14
+ loadPaths: options.loadPaths || ["./src"],
15
+ arityCheck: options.arityCheck || false,
16
+ freezing: options.freezing !== false,
17
+ debug: options.debug || false
18
+ };
19
+ }
20
+ async compile(filePath) {
21
+ const cached = this.cache.get(filePath);
22
+ if (cached) {
23
+ try {
24
+ const stat3 = await fs.stat(filePath);
25
+ if (stat3.mtimeMs <= cached.mtime) {
26
+ this.log(`Cache hit: ${filePath}`);
27
+ return {
28
+ code: cached.code,
29
+ map: cached.map,
30
+ dependencies: cached.dependencies
31
+ };
32
+ }
33
+ } catch (e) {
34
+ this.cache.delete(filePath);
35
+ }
36
+ }
37
+ this.log(`Compiling: ${filePath}`);
38
+ const result = await this.compileViaRuby(filePath);
39
+ try {
40
+ const stat3 = await fs.stat(filePath);
41
+ this.cache.set(filePath, {
42
+ ...result,
43
+ mtime: stat3.mtimeMs
44
+ });
45
+ } catch (e) {
46
+ }
47
+ return result;
48
+ }
49
+ async getOpalRuntime() {
50
+ if (this.runtimeCache) {
51
+ return this.runtimeCache;
52
+ }
53
+ this.log("Loading Opal runtime");
54
+ const runtime = await this.getRuntimeViaRuby();
55
+ this.runtimeCache = runtime;
56
+ return runtime;
57
+ }
58
+ clearCache(filePath) {
59
+ if (filePath) {
60
+ this.cache.delete(filePath);
61
+ this.log(`Cache cleared: ${filePath}`);
62
+ } else {
63
+ this.cache.clear();
64
+ this.log("Cache cleared (all)");
65
+ }
66
+ }
67
+ async compileViaRuby(filePath) {
68
+ return new Promise((resolve4, reject) => {
69
+ const args = [
70
+ "-I",
71
+ this.resolveGemLibPath(),
72
+ "-r",
73
+ "opal-vite",
74
+ "-e",
75
+ this.getCompilerScript(),
76
+ filePath
77
+ ];
78
+ this.log(`Spawning Ruby: ruby ${args.join(" ")}`);
79
+ const ruby = spawn("ruby", args);
80
+ let stdout = "";
81
+ let stderr = "";
82
+ ruby.stdout.on("data", (data) => {
83
+ stdout += data.toString();
84
+ });
85
+ ruby.stderr.on("data", (data) => {
86
+ stderr += data.toString();
87
+ });
88
+ ruby.on("close", (code) => {
89
+ if (code !== 0) {
90
+ reject(new Error(`Opal compilation failed:
91
+ ${stderr}`));
92
+ return;
93
+ }
94
+ try {
95
+ const result = JSON.parse(stdout);
96
+ resolve4(result);
97
+ } catch (e) {
98
+ reject(new Error(`Failed to parse compiler output:
99
+ ${stdout}
100
+
101
+ Error: ${e}`));
102
+ }
103
+ });
104
+ ruby.on("error", (err) => {
105
+ reject(new Error(`Failed to spawn Ruby process: ${err.message}`));
106
+ });
107
+ });
108
+ }
109
+ async getRuntimeViaRuby() {
110
+ return new Promise((resolve4, reject) => {
111
+ const args = [
112
+ "-I",
113
+ this.resolveGemLibPath(),
114
+ "-r",
115
+ "opal-vite",
116
+ "-e",
117
+ "puts Opal::Vite::Compiler.runtime_code"
118
+ ];
119
+ const ruby = spawn("ruby", args);
120
+ let stdout = "";
121
+ let stderr = "";
122
+ ruby.stdout.on("data", (data) => {
123
+ stdout += data.toString();
124
+ });
125
+ ruby.stderr.on("data", (data) => {
126
+ stderr += data.toString();
127
+ });
128
+ ruby.on("close", (code) => {
129
+ if (code === 0) {
130
+ resolve4(stdout);
131
+ } else {
132
+ reject(new Error(`Failed to get Opal runtime:
133
+ ${stderr}`));
134
+ }
135
+ });
136
+ ruby.on("error", (err) => {
137
+ reject(new Error(`Failed to spawn Ruby process: ${err.message}`));
138
+ });
139
+ });
140
+ }
141
+ getCompilerScript() {
142
+ return `
143
+ require 'opal-vite'
144
+ file_path = ARGV[0]
145
+ Opal::Vite.compile_for_vite(file_path)
146
+ `.trim();
147
+ }
148
+ resolveGemLibPath() {
149
+ if (this.options.gemPath.startsWith(".") || this.options.gemPath.startsWith("/")) {
150
+ return path2.resolve(this.options.gemPath, "lib");
151
+ }
152
+ return this.options.gemPath;
153
+ }
154
+ log(message) {
155
+ if (this.options.debug) {
156
+ console.log(`[vite-plugin-opal] ${message}`);
157
+ }
158
+ }
159
+ };
160
+ var OpalResolver = class {
161
+ constructor(options = {}) {
162
+ this.resolveCache = /* @__PURE__ */ new Map();
163
+ this.options = {
164
+ gemPath: options.gemPath || "opal-vite",
165
+ sourceMap: options.sourceMap !== false,
166
+ loadPaths: options.loadPaths || ["./src"],
167
+ arityCheck: options.arityCheck || false,
168
+ freezing: options.freezing !== false,
169
+ debug: options.debug || false
170
+ };
171
+ this.loadPaths = this.options.loadPaths;
172
+ }
173
+ /**
174
+ * Resolve a Ruby file import
175
+ * Supports:
176
+ * - Absolute paths: /path/to/file.rb
177
+ * - Relative paths: ./file.rb, ../file.rb
178
+ * - Load path resolution: file (searches in loadPaths)
179
+ */
180
+ async resolve(id, importer) {
181
+ const cacheKey = `${id}|${importer || ""}`;
182
+ if (this.resolveCache.has(cacheKey)) {
183
+ return this.resolveCache.get(cacheKey);
184
+ }
185
+ let resolved = null;
186
+ if (id.endsWith(".rb") || !this.hasExtension(id)) {
187
+ resolved = await this.resolveAbsolute(id) || await this.resolveRelative(id, importer) || await this.resolveFromLoadPaths(id);
188
+ }
189
+ this.resolveCache.set(cacheKey, resolved);
190
+ if (resolved && this.options.debug) {
191
+ console.log(`[vite-plugin-opal] Resolved: ${id} -> ${resolved}`);
192
+ }
193
+ return resolved;
194
+ }
195
+ /**
196
+ * Clear the resolution cache
197
+ */
198
+ clearCache(filePath) {
199
+ if (filePath) {
200
+ for (const [key, value] of this.resolveCache.entries()) {
201
+ if (value === filePath || key.startsWith(filePath)) {
202
+ this.resolveCache.delete(key);
203
+ }
204
+ }
205
+ } else {
206
+ this.resolveCache.clear();
207
+ }
208
+ }
209
+ /**
210
+ * Get all load paths
211
+ */
212
+ getLoadPaths() {
213
+ return [...this.loadPaths];
214
+ }
215
+ /**
216
+ * Add a load path
217
+ */
218
+ addLoadPath(loadPath) {
219
+ if (!this.loadPaths.includes(loadPath)) {
220
+ this.loadPaths.push(loadPath);
221
+ this.clearCache();
222
+ }
223
+ }
224
+ async resolveAbsolute(id) {
225
+ if (!path2.isAbsolute(id)) {
226
+ return null;
227
+ }
228
+ if (await this.fileExists(id)) {
229
+ return id;
230
+ }
231
+ if (!id.endsWith(".rb")) {
232
+ const withExt = `${id}.rb`;
233
+ if (await this.fileExists(withExt)) {
234
+ return withExt;
235
+ }
236
+ }
237
+ return null;
238
+ }
239
+ async resolveRelative(id, importer) {
240
+ if (!id.startsWith(".") || !importer) {
241
+ return null;
242
+ }
243
+ const importerDir = path2.dirname(importer);
244
+ const resolved = path2.resolve(importerDir, id);
245
+ if (await this.fileExists(resolved)) {
246
+ return resolved;
247
+ }
248
+ if (!id.endsWith(".rb")) {
249
+ const withExt = `${resolved}.rb`;
250
+ if (await this.fileExists(withExt)) {
251
+ return withExt;
252
+ }
253
+ }
254
+ return null;
255
+ }
256
+ async resolveFromLoadPaths(id) {
257
+ const baseId = id.endsWith(".rb") ? id.slice(0, -3) : id;
258
+ for (const loadPath of this.loadPaths) {
259
+ const fullPath = path2.resolve(loadPath, id);
260
+ if (await this.fileExists(fullPath)) {
261
+ return fullPath;
262
+ }
263
+ const withExt = path2.resolve(loadPath, `${baseId}.rb`);
264
+ if (await this.fileExists(withExt)) {
265
+ return withExt;
266
+ }
267
+ const indexPath = path2.resolve(loadPath, baseId, "index.rb");
268
+ if (await this.fileExists(indexPath)) {
269
+ return indexPath;
270
+ }
271
+ }
272
+ return null;
273
+ }
274
+ async fileExists(filePath) {
275
+ try {
276
+ const stat3 = await fs.stat(filePath);
277
+ return stat3.isFile();
278
+ } catch {
279
+ return false;
280
+ }
281
+ }
282
+ hasExtension(filePath) {
283
+ const ext = path2.extname(filePath);
284
+ return ext !== "";
285
+ }
286
+ };
287
+ var OpalHMRManager = class {
288
+ constructor(server, compiler, resolver, options) {
289
+ this.dependencyGraph = /* @__PURE__ */ new Map();
290
+ this.server = server;
291
+ this.compiler = compiler;
292
+ this.resolver = resolver;
293
+ this.options = options;
294
+ }
295
+ /**
296
+ * Setup HMR file watching
297
+ */
298
+ setup() {
299
+ this.log("Setting up HMR for .rb files");
300
+ this.watcher = chokidar.watch("**/*.rb", {
301
+ ignored: /node_modules/,
302
+ persistent: true,
303
+ cwd: this.server.config.root,
304
+ ignoreInitial: true
305
+ // Don't trigger on initial scan
306
+ });
307
+ this.watcher.on("change", async (filePath) => {
308
+ await this.handleFileChange(filePath);
309
+ });
310
+ this.watcher.on("add", async (filePath) => {
311
+ this.log(`New file detected: ${filePath}`);
312
+ await this.handleFileChange(filePath);
313
+ });
314
+ this.watcher.on("unlink", (filePath) => {
315
+ this.log(`File removed: ${filePath}`);
316
+ const absolutePath = path2.resolve(this.server.config.root, filePath);
317
+ this.compiler.clearCache(absolutePath);
318
+ this.resolver.clearCache(absolutePath);
319
+ this.dependencyGraph.delete(absolutePath);
320
+ });
321
+ this.server.httpServer?.on("close", () => {
322
+ this.cleanup();
323
+ });
324
+ this.log("HMR setup complete");
325
+ }
326
+ /**
327
+ * Cleanup resources
328
+ */
329
+ cleanup() {
330
+ if (this.watcher) {
331
+ this.log("Cleaning up HMR watcher");
332
+ this.watcher.close();
333
+ this.watcher = void 0;
334
+ }
335
+ }
336
+ /**
337
+ * Handle file change and trigger HMR update
338
+ */
339
+ async handleFileChange(filePath) {
340
+ const absolutePath = path2.resolve(this.server.config.root, filePath);
341
+ this.log(`File changed: ${filePath}`);
342
+ try {
343
+ this.compiler.clearCache(absolutePath);
344
+ this.resolver.clearCache(absolutePath);
345
+ const module = this.server.moduleGraph.getModuleById(absolutePath);
346
+ if (!module) {
347
+ this.log(`Module not found in graph: ${filePath}`, "warn");
348
+ return;
349
+ }
350
+ const modulesToUpdate = /* @__PURE__ */ new Set([module]);
351
+ await this.collectDependentModules(module, modulesToUpdate);
352
+ for (const mod of modulesToUpdate) {
353
+ this.server.moduleGraph.invalidateModule(mod);
354
+ this.log(`Invalidated: ${mod.url}`);
355
+ }
356
+ const updates = Array.from(modulesToUpdate).map((mod) => ({
357
+ type: "js-update",
358
+ path: mod.url,
359
+ acceptedPath: mod.url,
360
+ timestamp: Date.now()
361
+ }));
362
+ this.server.ws.send({
363
+ type: "update",
364
+ updates
365
+ });
366
+ this.log(`\u2713 HMR update sent for ${updates.length} module(s)`, "success");
367
+ } catch (error) {
368
+ this.handleError(filePath, error);
369
+ }
370
+ }
371
+ /**
372
+ * Collect all modules that depend on the given module
373
+ */
374
+ async collectDependentModules(module, result) {
375
+ for (const importer of module.importers) {
376
+ if (!result.has(importer)) {
377
+ result.add(importer);
378
+ await this.collectDependentModules(importer, result);
379
+ }
380
+ }
381
+ }
382
+ /**
383
+ * Handle compilation or HMR errors
384
+ */
385
+ handleError(filePath, error) {
386
+ const errorMessage = error instanceof Error ? error.message : String(error);
387
+ this.log(`Error processing ${filePath}: ${errorMessage}`, "error");
388
+ this.server.ws.send({
389
+ type: "error",
390
+ err: {
391
+ message: `Opal compilation failed for ${filePath}`,
392
+ stack: error instanceof Error ? error.stack : void 0,
393
+ id: filePath,
394
+ frame: this.extractErrorFrame(errorMessage),
395
+ plugin: "vite-plugin-opal",
396
+ loc: this.extractErrorLocation(errorMessage)
397
+ }
398
+ });
399
+ }
400
+ /**
401
+ * Extract error frame from error message
402
+ */
403
+ extractErrorFrame(errorMessage) {
404
+ const lines = errorMessage.split("\n");
405
+ const relevantLines = lines.slice(0, 10).join("\n");
406
+ return relevantLines || void 0;
407
+ }
408
+ /**
409
+ * Extract error location from error message
410
+ */
411
+ extractErrorLocation(errorMessage) {
412
+ const match = errorMessage.match(/(.+):(\d+):(\d+)/);
413
+ if (match) {
414
+ return {
415
+ file: match[1],
416
+ line: parseInt(match[2], 10),
417
+ column: parseInt(match[3], 10)
418
+ };
419
+ }
420
+ return void 0;
421
+ }
422
+ /**
423
+ * Log HMR messages
424
+ */
425
+ log(message, level = "info") {
426
+ if (!this.options.debug && level === "info") {
427
+ return;
428
+ }
429
+ const prefix = "[vite-plugin-opal:hmr]";
430
+ const timestamp = (/* @__PURE__ */ new Date()).toLocaleTimeString();
431
+ const colors = {
432
+ info: "\x1B[36m",
433
+ // Cyan
434
+ warn: "\x1B[33m",
435
+ // Yellow
436
+ error: "\x1B[31m",
437
+ // Red
438
+ success: "\x1B[32m"
439
+ // Green
440
+ };
441
+ const reset = "\x1B[0m";
442
+ const color = colors[level];
443
+ const levelText = level.toUpperCase().padEnd(7);
444
+ console.log(`${color}${prefix} ${timestamp} [${levelText}]${reset} ${message}`);
445
+ }
446
+ /**
447
+ * Get dependency graph information (for debugging)
448
+ */
449
+ getDependencyGraph() {
450
+ return new Map(this.dependencyGraph);
451
+ }
452
+ };
453
+
454
+ // src/index.ts
455
+ var VIRTUAL_RUNTIME_ID = "/@opal-runtime";
456
+ var VIRTUAL_RUNTIME_PREFIX = "\0" + VIRTUAL_RUNTIME_ID;
457
+ function opalPlugin(options = {}) {
458
+ const compiler = new OpalCompiler(options);
459
+ const resolver = new OpalResolver(options);
460
+ let server;
461
+ let hmrManager;
462
+ return {
463
+ name: "vite-plugin-opal",
464
+ enforce: "pre",
465
+ // Run before other plugins
466
+ // Mark .rb files as valid imports
467
+ async resolveId(id, importer) {
468
+ if (id === VIRTUAL_RUNTIME_ID) {
469
+ if (options.debug) {
470
+ console.log(`[vite-plugin-opal] Resolved virtual runtime: ${VIRTUAL_RUNTIME_PREFIX}`);
471
+ }
472
+ return VIRTUAL_RUNTIME_PREFIX;
473
+ }
474
+ if (id.endsWith(".rb") || !id.includes(".") && !id.startsWith("/")) {
475
+ const resolved = await resolver.resolve(id, importer);
476
+ if (options.debug) {
477
+ console.log(`[vite-plugin-opal] resolveId: ${id} -> ${resolved}`);
478
+ }
479
+ return resolved;
480
+ }
481
+ return null;
482
+ },
483
+ // Load and compile .rb files
484
+ async load(id) {
485
+ if (id === VIRTUAL_RUNTIME_PREFIX) {
486
+ if (options.debug) {
487
+ console.log(`[vite-plugin-opal] Loading Opal runtime...`);
488
+ }
489
+ const runtime = await compiler.getOpalRuntime();
490
+ if (options.debug) {
491
+ console.log(`[vite-plugin-opal] Opal runtime loaded: ${runtime.length} bytes`);
492
+ }
493
+ return {
494
+ code: runtime,
495
+ map: null
496
+ };
497
+ }
498
+ if (id.endsWith(".rb")) {
499
+ if (options.debug) {
500
+ console.log(`[vite-plugin-opal] load: Compiling ${id}`);
501
+ }
502
+ try {
503
+ const result = await compiler.compile(id);
504
+ if (options.debug) {
505
+ console.log(`[vite-plugin-opal] load: Compiled ${id} -> ${result.code.length} bytes`);
506
+ }
507
+ return {
508
+ code: result.code,
509
+ map: result.map ? JSON.parse(result.map) : null
510
+ };
511
+ } catch (error) {
512
+ console.error(`[vite-plugin-opal] Compilation error for ${id}:`, error);
513
+ this.error(error instanceof Error ? error.message : String(error));
514
+ }
515
+ }
516
+ return null;
517
+ },
518
+ // Auto-inject Opal runtime into HTML
519
+ transformIndexHtml(html) {
520
+ const runtimeScript = `<script type="module" src="${VIRTUAL_RUNTIME_ID}"></script>`;
521
+ if (html.includes("</head>")) {
522
+ return html.replace("</head>", ` ${runtimeScript}
523
+ </head>`);
524
+ } else if (html.includes("<head>")) {
525
+ return html.replace("<head>", `<head>
526
+ ${runtimeScript}`);
527
+ } else {
528
+ return `${runtimeScript}
529
+ ${html}`;
530
+ }
531
+ },
532
+ // Setup HMR for .rb files
533
+ configureServer(_server) {
534
+ server = _server;
535
+ hmrManager = new OpalHMRManager(server, compiler, resolver, options);
536
+ hmrManager.setup();
537
+ return () => {
538
+ if (hmrManager) {
539
+ hmrManager.cleanup();
540
+ }
541
+ };
542
+ }
543
+ };
544
+ }
545
+
546
+ export { opalPlugin as default };
547
+ //# sourceMappingURL=index.js.map
548
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/compiler.ts","../src/resolver.ts","../src/hmr.ts","../src/index.ts"],"names":["stat","resolve","path","fs2","path3"],"mappings":"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","file":"index.js","sourcesContent":["import { spawn } from 'child_process'\nimport * as fs from 'fs/promises'\nimport * as path from 'path'\nimport type { OpalPluginOptions, CompileResult, CacheEntry } from './types'\n\nexport class OpalCompiler {\n private options: Required<OpalPluginOptions>\n private cache: Map<string, CacheEntry> = new Map()\n private runtimeCache: string | null = null\n\n constructor(options: OpalPluginOptions = {}) {\n this.options = {\n gemPath: options.gemPath || 'opal-vite',\n sourceMap: options.sourceMap !== false,\n loadPaths: options.loadPaths || ['./src'],\n arityCheck: options.arityCheck || false,\n freezing: options.freezing !== false,\n debug: options.debug || false\n }\n }\n\n async compile(filePath: string): Promise<CompileResult> {\n // Check cache\n const cached = this.cache.get(filePath)\n if (cached) {\n try {\n const stat = await fs.stat(filePath)\n if (stat.mtimeMs <= cached.mtime) {\n this.log(`Cache hit: ${filePath}`)\n return {\n code: cached.code,\n map: cached.map,\n dependencies: cached.dependencies\n }\n }\n } catch (e) {\n // File might have been deleted, remove from cache\n this.cache.delete(filePath)\n }\n }\n\n // Compile\n this.log(`Compiling: ${filePath}`)\n const result = await this.compileViaRuby(filePath)\n\n // Cache the result\n try {\n const stat = await fs.stat(filePath)\n this.cache.set(filePath, {\n ...result,\n mtime: stat.mtimeMs\n })\n } catch (e) {\n // Ignore if we can't stat the file\n }\n\n return result\n }\n\n async getOpalRuntime(): Promise<string> {\n if (this.runtimeCache) {\n return this.runtimeCache\n }\n\n this.log('Loading Opal runtime')\n const runtime = await this.getRuntimeViaRuby()\n this.runtimeCache = runtime\n return runtime\n }\n\n clearCache(filePath?: string): void {\n if (filePath) {\n this.cache.delete(filePath)\n this.log(`Cache cleared: ${filePath}`)\n } else {\n this.cache.clear()\n this.log('Cache cleared (all)')\n }\n }\n\n private async compileViaRuby(filePath: string): Promise<CompileResult> {\n return new Promise((resolve, reject) => {\n const args = [\n '-I', this.resolveGemLibPath(),\n '-r', 'opal-vite',\n '-e', this.getCompilerScript(),\n filePath\n ]\n\n this.log(`Spawning Ruby: ruby ${args.join(' ')}`)\n\n const ruby = spawn('ruby', args)\n\n let stdout = ''\n let stderr = ''\n\n ruby.stdout.on('data', (data) => {\n stdout += data.toString()\n })\n\n ruby.stderr.on('data', (data) => {\n stderr += data.toString()\n })\n\n ruby.on('close', (code) => {\n if (code !== 0) {\n reject(new Error(`Opal compilation failed:\\n${stderr}`))\n return\n }\n\n try {\n const result = JSON.parse(stdout)\n resolve(result)\n } catch (e) {\n reject(new Error(`Failed to parse compiler output:\\n${stdout}\\n\\nError: ${e}`))\n }\n })\n\n ruby.on('error', (err) => {\n reject(new Error(`Failed to spawn Ruby process: ${err.message}`))\n })\n })\n }\n\n private async getRuntimeViaRuby(): Promise<string> {\n return new Promise((resolve, reject) => {\n const args = [\n '-I', this.resolveGemLibPath(),\n '-r', 'opal-vite',\n '-e', 'puts Opal::Vite::Compiler.runtime_code'\n ]\n\n const ruby = spawn('ruby', args)\n\n let stdout = ''\n let stderr = ''\n\n ruby.stdout.on('data', (data) => {\n stdout += data.toString()\n })\n\n ruby.stderr.on('data', (data) => {\n stderr += data.toString()\n })\n\n ruby.on('close', (code) => {\n if (code === 0) {\n resolve(stdout)\n } else {\n reject(new Error(`Failed to get Opal runtime:\\n${stderr}`))\n }\n })\n\n ruby.on('error', (err) => {\n reject(new Error(`Failed to spawn Ruby process: ${err.message}`))\n })\n })\n }\n\n private getCompilerScript(): string {\n return `\n require 'opal-vite'\n file_path = ARGV[0]\n Opal::Vite.compile_for_vite(file_path)\n `.trim()\n }\n\n private resolveGemLibPath(): string {\n // Try to resolve the gem path\n // In development, this points to our local gem\n // In production, bundler will handle it\n if (this.options.gemPath.startsWith('.') || this.options.gemPath.startsWith('/')) {\n return path.resolve(this.options.gemPath, 'lib')\n }\n return this.options.gemPath\n }\n\n private log(message: string): void {\n if (this.options.debug) {\n console.log(`[vite-plugin-opal] ${message}`)\n }\n }\n}\n","import * as path from 'path'\nimport * as fs from 'fs/promises'\nimport type { OpalPluginOptions } from './types'\n\nexport class OpalResolver {\n private options: Required<OpalPluginOptions>\n private loadPaths: string[]\n private resolveCache: Map<string, string | null> = new Map()\n\n constructor(options: OpalPluginOptions = {}) {\n this.options = {\n gemPath: options.gemPath || 'opal-vite',\n sourceMap: options.sourceMap !== false,\n loadPaths: options.loadPaths || ['./src'],\n arityCheck: options.arityCheck || false,\n freezing: options.freezing !== false,\n debug: options.debug || false\n }\n this.loadPaths = this.options.loadPaths\n }\n\n /**\n * Resolve a Ruby file import\n * Supports:\n * - Absolute paths: /path/to/file.rb\n * - Relative paths: ./file.rb, ../file.rb\n * - Load path resolution: file (searches in loadPaths)\n */\n async resolve(id: string, importer?: string): Promise<string | null> {\n // Check cache first\n const cacheKey = `${id}|${importer || ''}`\n if (this.resolveCache.has(cacheKey)) {\n return this.resolveCache.get(cacheKey)!\n }\n\n let resolved: string | null = null\n\n // Only resolve .rb files or files without extension\n if (id.endsWith('.rb') || !this.hasExtension(id)) {\n // Try different resolution strategies\n resolved = await this.resolveAbsolute(id) ||\n await this.resolveRelative(id, importer) ||\n await this.resolveFromLoadPaths(id)\n }\n\n // Cache the result\n this.resolveCache.set(cacheKey, resolved)\n\n if (resolved && this.options.debug) {\n console.log(`[vite-plugin-opal] Resolved: ${id} -> ${resolved}`)\n }\n\n return resolved\n }\n\n /**\n * Clear the resolution cache\n */\n clearCache(filePath?: string): void {\n if (filePath) {\n // Clear cache entries related to this file\n for (const [key, value] of this.resolveCache.entries()) {\n if (value === filePath || key.startsWith(filePath)) {\n this.resolveCache.delete(key)\n }\n }\n } else {\n this.resolveCache.clear()\n }\n }\n\n /**\n * Get all load paths\n */\n getLoadPaths(): string[] {\n return [...this.loadPaths]\n }\n\n /**\n * Add a load path\n */\n addLoadPath(loadPath: string): void {\n if (!this.loadPaths.includes(loadPath)) {\n this.loadPaths.push(loadPath)\n this.clearCache() // Clear cache when load paths change\n }\n }\n\n private async resolveAbsolute(id: string): Promise<string | null> {\n if (!path.isAbsolute(id)) {\n return null\n }\n\n // Try as-is\n if (await this.fileExists(id)) {\n return id\n }\n\n // Try with .rb extension\n if (!id.endsWith('.rb')) {\n const withExt = `${id}.rb`\n if (await this.fileExists(withExt)) {\n return withExt\n }\n }\n\n return null\n }\n\n private async resolveRelative(id: string, importer?: string): Promise<string | null> {\n if (!id.startsWith('.') || !importer) {\n return null\n }\n\n const importerDir = path.dirname(importer)\n const resolved = path.resolve(importerDir, id)\n\n // Try as-is\n if (await this.fileExists(resolved)) {\n return resolved\n }\n\n // Try with .rb extension\n if (!id.endsWith('.rb')) {\n const withExt = `${resolved}.rb`\n if (await this.fileExists(withExt)) {\n return withExt\n }\n }\n\n return null\n }\n\n private async resolveFromLoadPaths(id: string): Promise<string | null> {\n // Remove .rb extension for load path search\n const baseId = id.endsWith('.rb') ? id.slice(0, -3) : id\n\n for (const loadPath of this.loadPaths) {\n // Try with original id\n const fullPath = path.resolve(loadPath, id)\n if (await this.fileExists(fullPath)) {\n return fullPath\n }\n\n // Try with .rb extension\n const withExt = path.resolve(loadPath, `${baseId}.rb`)\n if (await this.fileExists(withExt)) {\n return withExt\n }\n\n // Try as a directory with index.rb\n const indexPath = path.resolve(loadPath, baseId, 'index.rb')\n if (await this.fileExists(indexPath)) {\n return indexPath\n }\n }\n\n return null\n }\n\n private async fileExists(filePath: string): Promise<boolean> {\n try {\n const stat = await fs.stat(filePath)\n return stat.isFile()\n } catch {\n return false\n }\n }\n\n private hasExtension(filePath: string): boolean {\n const ext = path.extname(filePath)\n return ext !== ''\n }\n}\n","import type { ViteDevServer, ModuleNode, Update } from 'vite'\nimport type { OpalCompiler } from './compiler'\nimport type { OpalResolver } from './resolver'\nimport type { OpalPluginOptions } from './types'\nimport * as path from 'path'\nimport * as chokidar from 'chokidar'\n\nexport interface HMRManager {\n setup(): void\n cleanup(): void\n handleFileChange(filePath: string): Promise<void>\n}\n\nexport class OpalHMRManager implements HMRManager {\n private server: ViteDevServer\n private compiler: OpalCompiler\n private resolver: OpalResolver\n private options: OpalPluginOptions\n private watcher?: chokidar.FSWatcher\n private dependencyGraph: Map<string, Set<string>> = new Map()\n\n constructor(\n server: ViteDevServer,\n compiler: OpalCompiler,\n resolver: OpalResolver,\n options: OpalPluginOptions\n ) {\n this.server = server\n this.compiler = compiler\n this.resolver = resolver\n this.options = options\n }\n\n /**\n * Setup HMR file watching\n */\n setup(): void {\n this.log('Setting up HMR for .rb files')\n\n // Watch .rb files for changes\n this.watcher = chokidar.watch('**/*.rb', {\n ignored: /node_modules/,\n persistent: true,\n cwd: this.server.config.root,\n ignoreInitial: true // Don't trigger on initial scan\n })\n\n this.watcher.on('change', async (filePath: string) => {\n await this.handleFileChange(filePath)\n })\n\n this.watcher.on('add', async (filePath: string) => {\n this.log(`New file detected: ${filePath}`)\n await this.handleFileChange(filePath)\n })\n\n this.watcher.on('unlink', (filePath: string) => {\n this.log(`File removed: ${filePath}`)\n const absolutePath = path.resolve(this.server.config.root, filePath)\n this.compiler.clearCache(absolutePath)\n this.resolver.clearCache(absolutePath)\n this.dependencyGraph.delete(absolutePath)\n })\n\n // Cleanup on server close\n this.server.httpServer?.on('close', () => {\n this.cleanup()\n })\n\n this.log('HMR setup complete')\n }\n\n /**\n * Cleanup resources\n */\n cleanup(): void {\n if (this.watcher) {\n this.log('Cleaning up HMR watcher')\n this.watcher.close()\n this.watcher = undefined\n }\n }\n\n /**\n * Handle file change and trigger HMR update\n */\n async handleFileChange(filePath: string): Promise<void> {\n const absolutePath = path.resolve(this.server.config.root, filePath)\n\n this.log(`File changed: ${filePath}`)\n\n try {\n // Clear caches for the changed file\n this.compiler.clearCache(absolutePath)\n this.resolver.clearCache(absolutePath)\n\n // Get the module from the module graph\n const module = this.server.moduleGraph.getModuleById(absolutePath)\n\n if (!module) {\n this.log(`Module not found in graph: ${filePath}`, 'warn')\n return\n }\n\n // Collect all modules that need to be updated\n const modulesToUpdate = new Set<ModuleNode>([module])\n\n // Find dependent modules (modules that import this one)\n await this.collectDependentModules(module, modulesToUpdate)\n\n // Invalidate all affected modules\n for (const mod of modulesToUpdate) {\n this.server.moduleGraph.invalidateModule(mod)\n this.log(`Invalidated: ${mod.url}`)\n }\n\n // Build HMR updates\n const updates: Update[] = Array.from(modulesToUpdate).map(mod => ({\n type: 'js-update' as const,\n path: mod.url,\n acceptedPath: mod.url,\n timestamp: Date.now()\n }))\n\n // Send HMR update to browser\n this.server.ws.send({\n type: 'update',\n updates\n })\n\n this.log(`βœ“ HMR update sent for ${updates.length} module(s)`, 'success')\n } catch (error) {\n this.handleError(filePath, error)\n }\n }\n\n /**\n * Collect all modules that depend on the given module\n */\n private async collectDependentModules(\n module: ModuleNode,\n result: Set<ModuleNode>\n ): Promise<void> {\n // Get modules that import this module\n for (const importer of module.importers) {\n if (!result.has(importer)) {\n result.add(importer)\n // Recursively collect their dependents\n await this.collectDependentModules(importer, result)\n }\n }\n }\n\n /**\n * Handle compilation or HMR errors\n */\n private handleError(filePath: string, error: unknown): void {\n const errorMessage = error instanceof Error ? error.message : String(error)\n\n this.log(`Error processing ${filePath}: ${errorMessage}`, 'error')\n\n // Send error overlay to browser\n this.server.ws.send({\n type: 'error',\n err: {\n message: `Opal compilation failed for ${filePath}`,\n stack: error instanceof Error ? error.stack : undefined,\n id: filePath,\n frame: this.extractErrorFrame(errorMessage),\n plugin: 'vite-plugin-opal',\n loc: this.extractErrorLocation(errorMessage)\n }\n })\n }\n\n /**\n * Extract error frame from error message\n */\n private extractErrorFrame(errorMessage: string): string | undefined {\n // Try to extract relevant code frame from error\n const lines = errorMessage.split('\\n')\n const relevantLines = lines.slice(0, 10).join('\\n')\n return relevantLines || undefined\n }\n\n /**\n * Extract error location from error message\n */\n private extractErrorLocation(errorMessage: string): { file?: string; line?: number; column?: number } | undefined {\n // Try to parse location from Opal error messages\n // Example: \"file.rb:10:5: error message\"\n const match = errorMessage.match(/(.+):(\\d+):(\\d+)/)\n if (match) {\n return {\n file: match[1],\n line: parseInt(match[2], 10),\n column: parseInt(match[3], 10)\n }\n }\n return undefined\n }\n\n /**\n * Log HMR messages\n */\n private log(message: string, level: 'info' | 'warn' | 'error' | 'success' = 'info'): void {\n if (!this.options.debug && level === 'info') {\n return\n }\n\n const prefix = '[vite-plugin-opal:hmr]'\n const timestamp = new Date().toLocaleTimeString()\n\n const colors = {\n info: '\\x1b[36m', // Cyan\n warn: '\\x1b[33m', // Yellow\n error: '\\x1b[31m', // Red\n success: '\\x1b[32m' // Green\n }\n const reset = '\\x1b[0m'\n\n const color = colors[level]\n const levelText = level.toUpperCase().padEnd(7)\n\n console.log(`${color}${prefix} ${timestamp} [${levelText}]${reset} ${message}`)\n }\n\n /**\n * Get dependency graph information (for debugging)\n */\n getDependencyGraph(): Map<string, Set<string>> {\n return new Map(this.dependencyGraph)\n }\n}\n","import type { Plugin, ViteDevServer } from 'vite'\nimport { OpalCompiler } from './compiler'\nimport { OpalResolver } from './resolver'\nimport { OpalHMRManager } from './hmr'\nimport type { OpalPluginOptions } from './types'\nimport * as path from 'path'\n\nconst VIRTUAL_RUNTIME_ID = '/@opal-runtime'\nconst VIRTUAL_RUNTIME_PREFIX = '\\0' + VIRTUAL_RUNTIME_ID\n\nexport default function opalPlugin(options: OpalPluginOptions = {}): Plugin {\n const compiler = new OpalCompiler(options)\n const resolver = new OpalResolver(options)\n let server: ViteDevServer | undefined\n let hmrManager: OpalHMRManager | undefined\n\n return {\n name: 'vite-plugin-opal',\n enforce: 'pre', // Run before other plugins\n\n // Mark .rb files as valid imports\n async resolveId(id: string, importer?: string) {\n // Handle virtual runtime module\n if (id === VIRTUAL_RUNTIME_ID) {\n if (options.debug) {\n console.log(`[vite-plugin-opal] Resolved virtual runtime: ${VIRTUAL_RUNTIME_PREFIX}`)\n }\n return VIRTUAL_RUNTIME_PREFIX\n }\n\n // Handle .rb files and files without extension (Opal style requires)\n if (id.endsWith('.rb') || (!id.includes('.') && !id.startsWith('/'))) {\n const resolved = await resolver.resolve(id, importer)\n if (options.debug) {\n console.log(`[vite-plugin-opal] resolveId: ${id} -> ${resolved}`)\n }\n return resolved\n }\n\n return null\n },\n\n // Load and compile .rb files\n async load(id: string) {\n // Handle virtual runtime module\n if (id === VIRTUAL_RUNTIME_PREFIX) {\n if (options.debug) {\n console.log(`[vite-plugin-opal] Loading Opal runtime...`)\n }\n const runtime = await compiler.getOpalRuntime()\n if (options.debug) {\n console.log(`[vite-plugin-opal] Opal runtime loaded: ${runtime.length} bytes`)\n }\n return {\n code: runtime,\n map: null\n }\n }\n\n // Handle .rb files\n if (id.endsWith('.rb')) {\n if (options.debug) {\n console.log(`[vite-plugin-opal] load: Compiling ${id}`)\n }\n try {\n const result = await compiler.compile(id)\n if (options.debug) {\n console.log(`[vite-plugin-opal] load: Compiled ${id} -> ${result.code.length} bytes`)\n }\n return {\n code: result.code,\n map: result.map ? JSON.parse(result.map) : null\n }\n } catch (error) {\n console.error(`[vite-plugin-opal] Compilation error for ${id}:`, error)\n this.error(error instanceof Error ? error.message : String(error))\n }\n }\n\n return null\n },\n\n // Auto-inject Opal runtime into HTML\n transformIndexHtml(html: string) {\n // Inject runtime before closing </head> tag\n const runtimeScript = `<script type=\"module\" src=\"${VIRTUAL_RUNTIME_ID}\"></script>`\n\n if (html.includes('</head>')) {\n return html.replace('</head>', ` ${runtimeScript}\\n</head>`)\n } else if (html.includes('<head>')) {\n return html.replace('<head>', `<head>\\n ${runtimeScript}`)\n } else {\n // No head tag, inject at the beginning\n return `${runtimeScript}\\n${html}`\n }\n },\n\n // Setup HMR for .rb files\n configureServer(_server: ViteDevServer) {\n server = _server\n\n // Initialize HMR manager\n hmrManager = new OpalHMRManager(server, compiler, resolver, options)\n hmrManager.setup()\n\n return () => {\n // Cleanup function called when server closes\n if (hmrManager) {\n hmrManager.cleanup()\n }\n }\n }\n }\n}\n\nexport type { OpalPluginOptions } from './types'\n"]}
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "vite-plugin-opal",
3
+ "version": "0.1.0",
4
+ "description": "Vite plugin for Opal - Compile Ruby to JavaScript",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsup",
20
+ "dev": "tsup --watch",
21
+ "test": "vitest",
22
+ "clean": "rm -rf dist"
23
+ },
24
+ "keywords": [
25
+ "vite",
26
+ "vite-plugin",
27
+ "opal",
28
+ "ruby",
29
+ "javascript"
30
+ ],
31
+ "author": "stofu1234",
32
+ "license": "MIT",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/stofu1234/opal-vite.git",
36
+ "directory": "packages/vite-plugin-opal"
37
+ },
38
+ "homepage": "https://github.com/stofu1234/opal-vite#readme",
39
+ "bugs": {
40
+ "url": "https://github.com/stofu1234/opal-vite/issues"
41
+ },
42
+ "peerDependencies": {
43
+ "vite": "^5.0.0"
44
+ },
45
+ "devDependencies": {
46
+ "@types/node": "^20.10.6",
47
+ "tsup": "^8.0.1",
48
+ "typescript": "^5.3.3",
49
+ "vite": "^5.0.10",
50
+ "vitest": "^1.1.0"
51
+ },
52
+ "dependencies": {
53
+ "chokidar": "^3.5.3"
54
+ }
55
+ }