islandjs-rails 0.7.0 → 1.0.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,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IslandjsRails
4
+ # Handles Vite integration for Islands architecture
5
+ class ViteIntegration
6
+ attr_reader :root_path
7
+
8
+ def initialize(root_path = Rails.root)
9
+ @root_path = Pathname.new(root_path)
10
+ end
11
+
12
+ # Check if Vite is already installed in the project
13
+ def vite_installed?
14
+ vite_config_exists? || vite_json_exists?
15
+ end
16
+
17
+ # Check if Inertia is installed
18
+ def inertia_installed?
19
+ inertia_gem_installed? || inertia_layout_exists?
20
+ end
21
+
22
+ # Check if Islands is already configured
23
+ def islands_configured?
24
+ islands_vite_config_exists? && islands_structure_exists?
25
+ end
26
+
27
+ # Get the path to the Islands Vite config
28
+ def islands_vite_config_path
29
+ root_path.join('vite.config.islands.ts')
30
+ end
31
+
32
+ # Get the path to the Islands manifest
33
+ def islands_manifest_path
34
+ root_path.join('public/islands/.vite/manifest.json')
35
+ end
36
+
37
+ # Get the path to the main Vite config
38
+ def vite_config_path
39
+ root_path.join('vite.config.ts')
40
+ end
41
+
42
+ # Get the path to vite.json
43
+ def vite_json_path
44
+ root_path.join('vite.json')
45
+ end
46
+
47
+ # Check if Islands Vite config exists
48
+ def islands_vite_config_exists?
49
+ islands_vite_config_path.exist?
50
+ end
51
+
52
+ # Check if Islands structure exists
53
+ def islands_structure_exists?
54
+ root_path.join('app/javascript/islands').directory?
55
+ end
56
+
57
+ # Check if vite.config.ts exists
58
+ def vite_config_exists?
59
+ vite_config_path.exist?
60
+ end
61
+
62
+ # Check if vite.json exists
63
+ def vite_json_exists?
64
+ vite_json_path.exist?
65
+ end
66
+
67
+ # Check if inertia_rails gem is installed
68
+ def inertia_gem_installed?
69
+ Gem.loaded_specs.key?('inertia_rails')
70
+ end
71
+
72
+ # Check if Inertia layout exists
73
+ def inertia_layout_exists?
74
+ root_path.join('app/views/layouts/inertia.html.erb').exist?
75
+ end
76
+
77
+ # Read Islands manifest
78
+ def read_islands_manifest
79
+ return {} unless islands_manifest_path.exist?
80
+
81
+ JSON.parse(islands_manifest_path.read)
82
+ rescue JSON::ParserError
83
+ {}
84
+ end
85
+
86
+ # Get Islands bundle path from manifest
87
+ def islands_bundle_path
88
+ manifest = read_islands_manifest
89
+ entry = manifest['app/javascript/entrypoints/islands.js']
90
+
91
+ return nil unless entry
92
+
93
+ "/islands/#{entry['file']}"
94
+ end
95
+
96
+ # Check if package.json exists
97
+ def package_json_exists?
98
+ root_path.join('package.json').exist?
99
+ end
100
+
101
+ # Read package.json
102
+ def read_package_json
103
+ return {} unless package_json_exists?
104
+
105
+ JSON.parse(root_path.join('package.json').read)
106
+ rescue JSON::ParserError
107
+ {}
108
+ end
109
+
110
+ # Write package.json
111
+ def write_package_json(data)
112
+ root_path.join('package.json').write(JSON.pretty_generate(data) + "\n")
113
+ end
114
+
115
+ # Update package.json scripts for Islands
116
+ def update_package_json_scripts!
117
+ package_json = read_package_json
118
+ scripts = package_json['scripts'] || {}
119
+
120
+ # Always use namespaced scripts for consistency
121
+ scripts['build:islands'] = 'vite build --config vite.config.islands.ts'
122
+ scripts['watch:islands'] = 'vite build --config vite.config.islands.ts --watch'
123
+
124
+ # If there's an existing Vite setup (like Inertia), update main build script
125
+ if scripts['build'] && scripts['build'].include?('vite') && !scripts['build'].include?('build:islands')
126
+ scripts['build'] = "#{scripts['build']} && yarn build:islands"
127
+ end
128
+
129
+ package_json['scripts'] = scripts
130
+ write_package_json(package_json)
131
+ end
132
+
133
+ # Detect which layout to use for Islands
134
+ def islands_layout_path
135
+ # Always use application.html.erb for Islands (ERB pages)
136
+ # Never touch inertia.html.erb (that's for SPA)
137
+ root_path.join('app/views/layouts/application.html.erb')
138
+ end
139
+
140
+ # Check if Islands helper is already in layout
141
+ def layout_has_islands_helper?
142
+ return false unless islands_layout_path.exist?
143
+
144
+ content = islands_layout_path.read
145
+ content.include?('<%= islands %>') || content.include?('islands %>')
146
+ end
147
+ end
148
+ end
@@ -1,5 +1,6 @@
1
1
  require_relative "islandjs_rails/version"
2
2
  require_relative "islandjs_rails/configuration"
3
+ require_relative "islandjs_rails/vite_integration"
3
4
  require_relative "islandjs_rails/core"
4
5
  require_relative "islandjs_rails/vendor_manager"
5
6
  require_relative "islandjs_rails/cli"
@@ -0,0 +1,20 @@
1
+ // IslandJS Rails - Islands Entrypoint
2
+ // This file exports all your Island components to window.islandjsRails
3
+ // Components exported here can be used in ERB templates via <%= react_component('ComponentName') %>
4
+
5
+ // Import your island components
6
+ import HelloWorld from '../islands/components/HelloWorld.jsx'
7
+
8
+ // Export to global namespace for ERB template access
9
+ window.islandjsRails = {
10
+ HelloWorld,
11
+ // Add more components here as you create them:
12
+ // ChatWidget,
13
+ // UserProfile,
14
+ // etc.
15
+ }
16
+
17
+ // Optional: Log available components in development
18
+ if (import.meta.env.DEV) {
19
+ console.log('🏝️ IslandJS components loaded:', Object.keys(window.islandjsRails))
20
+ }
@@ -19,7 +19,7 @@
19
19
  </li>
20
20
  <li class="flex items-start">
21
21
  <span class="text-green-500 mr-2">✓</span>
22
- Zero webpack complexity - UMD libraries from vendor files
22
+ Simplified build process - UMD libraries from vendor files
23
23
  </li>
24
24
  <li class="flex items-start">
25
25
  <span class="text-green-500 mr-2">✓</span>
@@ -75,7 +75,7 @@
75
75
  <ul class="space-y-1">
76
76
  <li>• React & ReactDOM served from <code class="bg-gray-200 px-1 rounded">/islands/vendor/</code></li>
77
77
  <li>• Browser caching for optimal performance</li>
78
- <li>• No webpack bundling of external libraries</li>
78
+ <li>• No bundling of external libraries (loaded as UMD)</li>
79
79
  </ul>
80
80
  </div>
81
81
  <div>
@@ -3,19 +3,12 @@
3
3
  "version": "1.0.0",
4
4
  "private": true,
5
5
  "scripts": {
6
- "build": "NODE_ENV=production webpack",
7
- "build:dev": "NODE_ENV=production webpack",
8
- "watch": "NODE_ENV=development webpack --watch"
6
+ "build:islands": "vite build --config vite.config.islands.ts",
7
+ "watch:islands": "vite build --config vite.config.islands.ts --watch"
9
8
  },
10
9
  "dependencies": {},
11
10
  "devDependencies": {
12
- "@babel/core": "^7.23.0",
13
- "@babel/preset-env": "^7.23.0",
14
- "@babel/preset-react": "^7.23.0",
15
- "babel-loader": "^9.1.3",
16
- "terser-webpack-plugin": "^5.3.14",
17
- "webpack": "^5.88.2",
18
- "webpack-cli": "^5.1.4",
19
- "webpack-manifest-plugin": "^5.0.1"
11
+ "vite": "^5.4.19",
12
+ "@vitejs/plugin-react": "^5.0.0"
20
13
  }
21
14
  }
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * IslandJS Rails - Atomic Vite Build Script
5
+ *
6
+ * Builds both Inertia (if present) and Islands bundles atomically.
7
+ * If either build fails, the entire operation fails.
8
+ *
9
+ * Usage: node script/build-vite-atomic.js
10
+ * Or via package.json: yarn build
11
+ */
12
+
13
+ import { execSync } from 'child_process'
14
+ import fs from 'fs'
15
+ import path from 'path'
16
+ import { fileURLToPath } from 'url'
17
+
18
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
19
+ const rootDir = path.join(__dirname, '..')
20
+
21
+ // Check if a file exists
22
+ function fileExists(filepath) {
23
+ try {
24
+ return fs.existsSync(path.join(rootDir, filepath))
25
+ } catch {
26
+ return false
27
+ }
28
+ }
29
+
30
+ // Execute command and handle errors
31
+ function exec(command, description) {
32
+ console.log(`\n📦 ${description}...`)
33
+ try {
34
+ execSync(command, {
35
+ cwd: rootDir,
36
+ stdio: 'inherit',
37
+ env: process.env
38
+ })
39
+ console.log(`✅ ${description} succeeded`)
40
+ return true
41
+ } catch (error) {
42
+ console.error(`❌ ${description} failed`)
43
+ throw error
44
+ }
45
+ }
46
+
47
+ async function buildAtomic() {
48
+ console.log('🏗️ Building Vite assets atomically...\n')
49
+
50
+ const hasInertia = fileExists('vite.config.ts')
51
+ const hasIslands = fileExists('vite.config.islands.ts')
52
+
53
+ if (!hasInertia && !hasIslands) {
54
+ console.error('❌ No Vite configs found!')
55
+ console.error(' Expected: vite.config.ts or vite.config.islands.ts')
56
+ process.exit(1)
57
+ }
58
+
59
+ try {
60
+ // Build Inertia if config exists
61
+ if (hasInertia) {
62
+ exec('vite build --emptyOutDir', 'Building Inertia assets')
63
+ }
64
+
65
+ // Build Islands if config exists
66
+ if (hasIslands) {
67
+ exec('vite build --config vite.config.islands.ts', 'Building Islands bundle')
68
+ }
69
+
70
+ console.log('\n🎉 All builds succeeded!')
71
+ console.log('\n📦 Built assets:')
72
+
73
+ if (hasInertia) {
74
+ console.log(' ✓ Inertia: public/vite-dev/')
75
+ }
76
+ if (hasIslands) {
77
+ console.log(' ✓ Islands: public/islands/')
78
+ }
79
+
80
+ process.exit(0)
81
+
82
+ } catch (error) {
83
+ console.error('\n❌ Build failed!')
84
+ console.error(' No changes were deployed (atomic guarantee)')
85
+ process.exit(1)
86
+ }
87
+ }
88
+
89
+ buildAtomic()
@@ -0,0 +1,67 @@
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+ import path from 'path'
4
+ import fs from 'fs'
5
+
6
+ // IslandJS Rails - Islands Architecture Build Configuration
7
+ // This config builds React components as IIFE bundles for use in ERB templates
8
+ // Separate from your main Vite build (Inertia, etc.)
9
+
10
+ export default defineConfig({
11
+ plugins: [react()],
12
+
13
+ // CRITICAL: Disable copying public/ directory into build output
14
+ // Prevents Vite from copying public/vendor/islands → public/islands/vendor/islands
15
+ // We want vendor files to stay ONLY in public/vendor/islands (single source of truth)
16
+ publicDir: false,
17
+
18
+ // Define global constants for browser (replace Node.js process.env)
19
+ // CRITICAL: These must be string replacements, not JSON objects
20
+ define: {
21
+ 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production'),
22
+ },
23
+
24
+ build: {
25
+ // Library mode for IIFE output
26
+ lib: {
27
+ entry: path.resolve(__dirname, 'app/javascript/entrypoints/islands.js'),
28
+ name: 'islandjsRails',
29
+ formats: ['iife'],
30
+ // Don't specify fileName here - let rollupOptions handle it
31
+ fileName: 'islands_bundle'
32
+ },
33
+
34
+ // Externalize React - loaded via UMD from CDN
35
+ rollupOptions: {
36
+ external: ['react', 'react-dom'],
37
+ output: {
38
+ globals: {
39
+ react: 'React',
40
+ 'react-dom': 'ReactDOM'
41
+ },
42
+ // Use [hash] for content-based fingerprinting (auto cache-busting)
43
+ // This ensures every build change gets a new filename
44
+ entryFileNames: 'islands_bundle.[hash].js',
45
+ chunkFileNames: 'chunks/[name].[hash].js',
46
+ assetFileNames: 'assets/[name].[hash][extname]'
47
+ }
48
+ },
49
+
50
+ // Output to public/islands directory
51
+ outDir: 'public/islands',
52
+ // Clean output directory (vendor files are in public/vendor/islands, separate)
53
+ emptyOutDir: true,
54
+
55
+ // Generate manifest for Rails helpers (critical for fingerprinting!)
56
+ manifest: true,
57
+
58
+ // Source maps for development
59
+ sourcemap: process.env.NODE_ENV !== 'production'
60
+ },
61
+
62
+ resolve: {
63
+ alias: {
64
+ '@': path.resolve(__dirname, 'app/javascript')
65
+ }
66
+ }
67
+ })
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: islandjs-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eric Arnold
@@ -114,7 +114,7 @@ dependencies:
114
114
  - !ruby/object:Gem::Version
115
115
  version: '0.22'
116
116
  description: IslandJS Rails enables React and other JavaScript islands in Rails apps
117
- with zero webpack configuration. Load UMD libraries from CDNs, integrate with ERB
117
+ with zero build configuration. Load UMD libraries from CDNs, integrate with ERB
118
118
  partials, and render components with Turbo-compatible lifecycle management.
119
119
  email:
120
120
  - ericarnold00+praxisemergent@gmail.com
@@ -127,7 +127,6 @@ files:
127
127
  - LICENSE.md
128
128
  - README.md
129
129
  - exe/islandjs-rails
130
- - islandjs-rails.gemspec
131
130
  - lib/islandjs-rails.rb
132
131
  - lib/islandjs_rails.rb
133
132
  - lib/islandjs_rails/cli.rb
@@ -139,18 +138,19 @@ files:
139
138
  - lib/islandjs_rails/tasks.rb
140
139
  - lib/islandjs_rails/vendor_manager.rb
141
140
  - lib/islandjs_rails/version.rb
141
+ - lib/islandjs_rails/vite_installer.rb
142
+ - lib/islandjs_rails/vite_integration.rb
142
143
  - lib/templates/app/controllers/islandjs_demo_controller.rb
144
+ - lib/templates/app/javascript/entrypoints/islands.js
143
145
  - lib/templates/app/javascript/islands/components/.gitkeep
144
146
  - lib/templates/app/javascript/islands/components/HelloWorld.jsx
145
- - lib/templates/app/javascript/islands/index.js
146
147
  - lib/templates/app/javascript/islands/utils/turbo.js
147
148
  - lib/templates/app/views/islandjs_demo/index.html.erb
148
149
  - lib/templates/app/views/islandjs_demo/react.html.erb
149
150
  - lib/templates/config/demo_routes.rb
150
151
  - lib/templates/package.json
151
- - lib/templates/webpack.config.js
152
- - package.json
153
- - yarn.lock
152
+ - lib/templates/script/build-vite-atomic.js
153
+ - lib/templates/vite.config.islands.ts
154
154
  homepage: https://github.com/praxis-emergent/islandjs-rails
155
155
  licenses:
156
156
  - MIT
@@ -160,7 +160,8 @@ metadata:
160
160
  changelog_uri: https://github.com/praxis-emergent/islandjs-rails/blob/main/CHANGELOG.md
161
161
  documentation_uri: https://github.com/praxis-emergent/islandjs-rails/blob/main/README.md
162
162
  post_install_message: "\n\U0001F3DD️ IslandJS Rails installed successfully!\n\n\U0001F4CB
163
- Next step: Initialize IslandJS in your Rails app\n\n rails islandjs:init\n\n"
163
+ Next step: Initialize IslandJS in your Rails app\n\n rails islandjs:init\n\nThis
164
+ will set up Vite for Islands architecture alongside your existing setup.\n\n"
164
165
  rdoc_options: []
165
166
  require_paths:
166
167
  - lib
@@ -1,55 +0,0 @@
1
- require_relative "lib/islandjs_rails/version"
2
-
3
- Gem::Specification.new do |spec|
4
- spec.name = "islandjs-rails"
5
- spec.version = IslandjsRails::VERSION
6
- spec.authors = ["Eric Arnold"]
7
- spec.email = ["ericarnold00+praxisemergent@gmail.com"]
8
-
9
- spec.summary = "Simple, modern JavaScript islands for Rails"
10
- spec.description = "IslandJS Rails enables React and other JavaScript islands in Rails apps with zero webpack configuration. Load UMD libraries from CDNs, integrate with ERB partials, and render components with Turbo-compatible lifecycle management."
11
- spec.homepage = "https://github.com/praxis-emergent/islandjs-rails"
12
- spec.license = "MIT"
13
- spec.required_ruby_version = ">= 3.0.0"
14
-
15
- spec.metadata["homepage_uri"] = spec.homepage
16
- spec.metadata["source_code_uri"] = "https://github.com/praxis-emergent/islandjs-rails"
17
- spec.metadata["changelog_uri"] = "https://github.com/praxis-emergent/islandjs-rails/blob/main/CHANGELOG.md"
18
- spec.metadata["documentation_uri"] = "https://github.com/praxis-emergent/islandjs-rails/blob/main/README.md"
19
-
20
- # Specify which files should be added to the gem when it is released.
21
- spec.files = Dir.chdir(__dir__) do
22
- `git ls-files -z`.split("\x0").reject do |f|
23
- (File.expand_path(f) == __FILE__) ||
24
- f.start_with?(*%w[bin/ test/ spec/ features/ .git .github .claude appveyor Gemfile]) ||
25
- f.match?(%r{\A(\.rspec|Rakefile)\z}) ||
26
- f.end_with?(".gem")
27
- end
28
- end
29
-
30
- spec.bindir = "exe"
31
- spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
32
- spec.require_paths = ["lib"]
33
-
34
- # Post-install message
35
- spec.post_install_message = <<~MSG
36
-
37
- 🏝️ IslandJS Rails installed successfully!
38
-
39
- 📋 Next step: Initialize IslandJS in your Rails app
40
-
41
- rails islandjs:init
42
-
43
- MSG
44
-
45
- # Rails integration
46
- spec.add_dependency "rails", ">= 7.0", "< 9.0"
47
- spec.add_dependency "thor", "~> 1.0"
48
-
49
- # Development dependencies
50
- spec.add_development_dependency "rake", "~> 13.0"
51
- spec.add_development_dependency "rspec", "~> 3.0"
52
- spec.add_development_dependency "webmock", "~> 3.0"
53
- spec.add_development_dependency "vcr", "~> 6.0"
54
- spec.add_development_dependency "simplecov", "~> 0.22"
55
- end
@@ -1,10 +0,0 @@
1
- // IslandJS Rails - Main entry point
2
- // This file is the webpack entry point for your JavaScript islands
3
-
4
- // React component imports
5
- import HelloWorld from './components/HelloWorld.jsx';
6
-
7
- // Mount components to the global islandjsRails namespace
8
- window.islandjsRails = {
9
- HelloWorld
10
- };
@@ -1,85 +0,0 @@
1
- const path = require('path');
2
- const TerserPlugin = require('terser-webpack-plugin');
3
- const { WebpackManifestPlugin } = require('webpack-manifest-plugin');
4
- const fs = require('fs');
5
-
6
- const isProduction = process.env.NODE_ENV === 'production';
7
-
8
- // Custom plugin to clean old island bundle files
9
- class CleanIslandsPlugin {
10
- apply(compiler) {
11
- compiler.hooks.afterEmit.tap('CleanIslandsPlugin', (compilation) => {
12
- const publicDir = path.resolve(__dirname, 'public');
13
- if (!fs.existsSync(publicDir)) return;
14
-
15
- // Get the newly emitted files
16
- const emittedFiles = Object.keys(compilation.assets).map(
17
- filename => filename.split('/').pop() // Get just the filename
18
- );
19
-
20
- const files = fs.readdirSync(publicDir);
21
- files.forEach(file => {
22
- // Clean old islands files, but keep the newly emitted ones
23
- // Also include .map and .LICENSE.txt files
24
- const isEmitted = emittedFiles.includes(file) ||
25
- emittedFiles.some(ef => file.startsWith(ef) && (
26
- file.endsWith('.map') || file.endsWith('.LICENSE.txt')
27
- ));
28
-
29
- if (file.startsWith('islands_') && !isEmitted) {
30
- const filePath = path.join(publicDir, file);
31
- try {
32
- fs.unlinkSync(filePath);
33
- } catch (err) {
34
- // Ignore errors
35
- }
36
- }
37
- });
38
- });
39
- }
40
- }
41
-
42
- module.exports = {
43
- mode: isProduction ? 'production' : 'development',
44
- entry: {
45
- islands_bundle: ['./app/javascript/islands/index.js']
46
- },
47
- externals: {
48
- // IslandJS managed externals - do not edit manually
49
- },
50
- output: {
51
- filename: '[name].[contenthash].js',
52
- path: path.resolve(__dirname, 'public'),
53
- publicPath: '/',
54
- clean: false
55
- },
56
- module: {
57
- rules: [
58
- {
59
- test: /\.(js|jsx)$/,
60
- exclude: /node_modules/,
61
- use: {
62
- loader: 'babel-loader',
63
- options: {
64
- presets: ['@babel/preset-env', '@babel/preset-react']
65
- }
66
- }
67
- }
68
- ]
69
- },
70
- resolve: {
71
- extensions: ['.js', '.jsx']
72
- },
73
- optimization: {
74
- minimize: isProduction,
75
- minimizer: [new TerserPlugin()]
76
- },
77
- plugins: [
78
- new CleanIslandsPlugin(),
79
- new WebpackManifestPlugin({
80
- fileName: 'islands_manifest.json',
81
- publicPath: '/'
82
- })
83
- ],
84
- devtool: isProduction ? false : 'source-map'
85
- };
data/package.json DELETED
@@ -1,12 +0,0 @@
1
- {
2
- "devDependencies": {
3
- "@babel/core": "^7.23.0",
4
- "@babel/preset-env": "^7.23.0",
5
- "@babel/preset-react": "^7.23.0",
6
- "babel-loader": "^9.1.3",
7
- "terser-webpack-plugin": "^5.3.14",
8
- "webpack": "^5.88.2",
9
- "webpack-cli": "^5.1.4",
10
- "webpack-manifest-plugin": "^5.0.1"
11
- }
12
- }