jspm-rails 0.1.0 → 0.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d28f515c7c82e31da220cc7dca25540b499ad2f3ca9f98f0b9e9092dc31940a6
4
- data.tar.gz: 2fa31fa61006a069f3d8876ce5c68e60e0e766852749ff062656a1436e67f7e6
3
+ metadata.gz: 647327a342d54b7e908d058985494a887120aebb49c07e6f220af7b843252ea7
4
+ data.tar.gz: 8595959e6a8e642ef2fb5c419274401afdad6b0caad89d32ab846ccda49da646
5
5
  SHA512:
6
- metadata.gz: 1dc7843f5631aa615fb7797120c6d047c74808ff08d6ec429db85b3ef1efff1a61419a35f5dd10c644df650cfd9484ea20a455ecd8ef25151714eb959678e2fa
7
- data.tar.gz: 2074d972cf1e2a2fb11a8753df72228a5cb1984b43455c906e23c176d3bb8d5a110e846c43ebffe4c267544e5faedc16fab871050e9bc9fe70197bb1eee16a3b
6
+ metadata.gz: 5fdfb17f58799c9cf1755352615eb27a01b48a3bd219b2dcac1ecdd2a1e93090e678b6ef77691095fe58fecb079ffac5bf03e211d1913e59d66181f151793e96
7
+ data.tar.gz: d4490661eaaa59ef3ba76ade81c6659e4db588e4ea692a9bd6d74c17c4f3619b272e9710110c2adca8228b17245b38d3879b113544004e0928ea546a89a1d5b1
data/CHANGELOG.md CHANGED
@@ -1,9 +1,16 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.0 (unreleased)
3
+ ## 0.2.0 (unreleased)
4
4
 
5
- - `bin/rails jspm:install` traces `app/javascript/application.js` with jspm and vendors every module it reaches. It copies npm dependencies' stylesheets, fonts and images. `config/importmap.json` locks versions, and `package-lock.json` overrides it for packages installed with npm.
5
+ - Reads Yarn 1's `yarn.lock` as well as `package-lock.json`.
6
+ - No longer copies npm packages' stylesheets, fonts and images. Put the directories you use on Propshaft's load path instead.
7
+ - A package in the lockfile wins over a gem's file with the same name.
8
+
9
+ ## 0.1.0 (2026-11-13)
10
+
11
+ - `bin/rails jspm:install` traces `app/javascript/application.js` with jspm and vendors every module it reaches. `config/importmap.json` locks versions, and `package-lock.json` overrides it for packages installed with npm.
6
12
  - `javascript_jspm_tags` renders the import map with Propshaft's digested paths, preloads, and the entry module.
7
13
  - Modules on Propshaft's load path, including engines', are importable by asset name, the way importmap-rails pins work.
14
+ - Modules on Propshaft's load path, including engines', are importable by asset name, the way importmap-rails pins work.
8
15
  - `bin/rails jspm:check` fails when `config/importmap.json` or `vendor/jspm` is out of date.
9
16
  - Warns when the import map can't match `package-lock.json`.
data/README.md CHANGED
@@ -19,7 +19,6 @@ bin/rails jspm:install
19
19
  ```
20
20
  traced ./app/javascript/application.js
21
21
  vendored 31 modules from ga.jspm.io
22
- copied 73 assets from node_modules
23
22
  wrote config/importmap.json
24
23
  ```
25
24
 
@@ -112,7 +111,7 @@ const { default: katex } = await import('katex')
112
111
 
113
112
  **Plain jspm works on its own.** The install resolves each package you import from jspm.io. That's the latest version, or the newest within a range you give in `package.json`'s `dependencies`, as jspm does. `config/importmap.json` is jspm's lockfile: every install keeps the versions it already holds.
114
113
 
115
- **Install a package with npm when you want more.** Its version in `package-lock.json` then overrides the import map, and you get:
114
+ **Install a package with npm or Yarn 1 when you want more.** Its version in `package-lock.json` or `yarn.lock` then overrides the import map, and you get:
116
115
 
117
116
  - Dependabot pull requests for it.
118
117
  - Its stylesheets, fonts and images in `node_modules`, at the same version as its JavaScript.
@@ -121,17 +120,19 @@ Mix the two freely. A package's JavaScript always comes from jspm.io either way.
121
120
 
122
121
  ## Stylesheets, fonts and images
123
122
 
124
- jspm.io serves JavaScript. For a package's other files, install it with npm as a dependency. The install copies the stylesheets, fonts and images of every package in `dependencies`, and their dependencies, into `vendor/jspm/<package>/`:
123
+ jspm.io serves JavaScript. For a package's other files, install it with npm and put the directory that holds them on Propshaft's load path, as Rails's generated `config/initializers/assets.rb` suggests:
125
124
 
126
- ```erb
127
- <%= stylesheet_link_tag 'katex/dist/katex' %>
125
+ ```ruby
126
+ Rails.application.config.assets.paths << Rails.root.join('node_modules/katex/dist')
128
127
  ```
129
128
 
130
- Propshaft rewrites the `url()`s in the stylesheet to digested paths, so the fonts load too. The CSS comes from npm and the JavaScript from jspm.io, both at the version in `package-lock.json`.
129
+ ```erb
130
+ <%= stylesheet_link_tag 'katex' %>
131
+ ```
131
132
 
132
- Each name starts with its package, so two packages' `style.css` never collide. `devDependencies` are build tools, so the install leaves them out.
133
+ Propshaft rewrites the `url()`s in the stylesheet to digested paths, so the fonts load too. The CSS comes from npm and the JavaScript from jspm.io, both at the version in your lockfile.
133
134
 
134
- Why copy, instead of putting `node_modules` on the load path? Propshaft checks every file on its load path for changes before each request in development. With one real app's `node_modules` (8,900 files), that took a page request from 1.4 ms to 98 ms. Production would also precompile all of `node_modules` into `public/assets`.
135
+ Add the directory you use, not the whole package or all of `node_modules`. Propshaft checks every file on its load path before each development request and precompiles every one, so `bootstrap-icons/font` (7 files) costs far less than the package (2,089).
135
136
 
136
137
  ## Keeping up to date
137
138
 
@@ -145,7 +146,7 @@ Why copy, instead of putting `node_modules` on the load path? Propshaft checks e
145
146
  bin/rails jspm:check
146
147
  ```
147
148
 
148
- It traces again without writing anything. It fails when an install would change `config/importmap.json`, for example after Dependabot bumps `package-lock.json`. It also fails when a module is missing from `vendor/jspm`, or a copied stylesheet, font or image no longer matches `node_modules`:
149
+ It traces again without writing anything. It fails when an install would change `config/importmap.json`, for example after Dependabot bumps `package-lock.json`. It also fails when a module is missing from `vendor/jspm`:
149
150
 
150
151
  ```
151
152
  stale config/importmap.json
@@ -165,7 +166,7 @@ npm installs two versions of a package when its dependents' ranges don't overlap
165
166
  ## Deploying
166
167
 
167
168
  - **Commit `config/importmap.json` and `vendor/jspm`.** `assets:precompile` digests the vendored modules like any other asset.
168
- - **Production needs no Node, no npm and no jspm.io.**
169
+ - **Production needs no jspm.io,** and no Node unless you serve stylesheets from `node_modules`, which needs your packages installed before precompiling.
169
170
  - **An `asset_host` on another origin must send `Access-Control-Allow-Origin`.** Browsers load modules and fonts across origins only with CORS.
170
171
  - **Content Security Policy works.** Every tag carries the request's nonce.
171
172
  - **Turbo reloads the page when the import map changes,** because the map's tag has `data-turbo-track="reload"`.
@@ -176,8 +177,7 @@ npm installs two versions of a package when its dependents' ranges don't overlap
176
177
 
177
178
  1. Boots the app, then traces `app/javascript/application.js` with [`@jspm/generator`](https://github.com/jspm/generator). Every module on Propshaft's load path is importable by asset name. The previous `config/importmap.json` locks versions, and `package-lock.json` pins every package npm installed. Only packages your code reaches end up in the map, so KaTeX's command-line dependency stays out.
178
179
  2. Downloads each traced module from jspm.io, with its source map, into `vendor/jspm/ga.jspm.io`.
179
- 3. Copies each npm dependency's stylesheets, fonts and images into `vendor/jspm/<package>/`.
180
- 4. Writes jspm's import map to `config/importmap.json`, with the list of modules to preload. Local modules appear as `asset:` plus their logical path, such as `asset:astrology/math_controller.js`, so the map is the same on every machine.
180
+ 3. Writes jspm's import map to `config/importmap.json`, with the list of modules to preload. Local modules appear as `asset:` plus their logical path, such as `asset:astrology/math_controller.js`, so the map is the same on every machine.
181
181
 
182
182
  `javascript_jspm_tags` renders that map for Propshaft. Propshaft serves each file under a digested name, so the helper rewrites the map's URLs:
183
183
 
@@ -239,15 +239,15 @@ Node printed the reason just above. Usually Node isn't on your `PATH`, or `@jspm
239
239
  **Modules or fonts fail to load with an asset host**
240
240
  The asset host must send `Access-Control-Allow-Origin`. See [Deploying](#deploying).
241
241
 
242
- **A package's stylesheet 404s**
243
- Install the package with npm as a dependency, not a devDependency, and run the install. See [Stylesheets, fonts and images](#stylesheets-fonts-and-images).
242
+ **A package's stylesheet raises `Propshaft::MissingAssetError`**
243
+ Add its directory to `config.assets.paths`. See [Stylesheets, fonts and images](#stylesheets-fonts-and-images).
244
244
 
245
245
  ## Development
246
246
 
247
247
  ```sh
248
248
  bundle install
249
249
  npm ci && npx playwright install chromium
250
- (cd spec/dummy && npm ci) && (cd spec/fixtures/conflicted && npm ci)
250
+ (cd spec/dummy && npm ci) && (cd spec/fixtures/conflicted && npm ci) && (cd spec/fixtures/yarn && yarn install --frozen-lockfile)
251
251
  bundle exec rake
252
252
  ```
253
253
 
data/lib/jspm/check.rb CHANGED
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'fileutils'
4
3
  require 'json'
5
4
 
6
5
  module Jspm
@@ -10,11 +9,10 @@ module Jspm
10
9
  def self.call(asset_paths:, output:, root:)
11
10
  lockfile = Lockfile.read(root)
12
11
  trace = Trace.call(asset_paths:, lockfile:, root:)
13
- new(lockfile:, output:, root:, trace:).call
12
+ new(output:, root:, trace:).call
14
13
  end
15
14
 
16
- def initialize(lockfile:, output:, root:, trace:)
17
- @lockfile = lockfile
15
+ def initialize(output:, root:, trace:)
18
16
  @output = output
19
17
  @root = root
20
18
  @trace = trace
@@ -23,12 +21,10 @@ module Jspm
23
21
  def call
24
22
  written = written?
25
23
  missing = missing_modules
26
- stale_assets = stale_asset_paths
27
24
  report('stale', Paths::IMPORT_MAP) unless written
28
25
  missing.each { |path| report('missing', path) }
29
- stale_assets.each { |path| report('stale', path) }
30
26
 
31
- up_to_date = written && missing.empty? && stale_assets.empty?
27
+ up_to_date = written && missing.empty?
32
28
  if up_to_date
33
29
  report('ok', 'nothing to install')
34
30
  else
@@ -39,22 +35,13 @@ module Jspm
39
35
 
40
36
  private
41
37
 
42
- attr_reader :lockfile, :output, :root, :trace
38
+ attr_reader :output, :root, :trace
43
39
 
44
40
  def written?
45
41
  import_map_file = root.join(Paths::IMPORT_MAP)
46
42
  import_map_file.exist? && JSON.parse(import_map_file.read) == trace.import_map
47
43
  end
48
44
 
49
- def stale_asset_paths
50
- copies = Assets.copies(lockfile:, root:)
51
- stale =
52
- copies.reject do |source, target|
53
- target.file? && FileUtils.compare_file(source, target)
54
- end
55
- stale.map { |_source, target| target.relative_path_from(root).to_s }
56
- end
57
-
58
45
  def missing_modules
59
46
  vendored = trace.cdn_urls.map { |url| File.join(Paths::VENDOR, Paths.logical(url)) }
60
47
  vendored.reject { |path| root.join(path).file? }
data/lib/jspm/install.rb CHANGED
@@ -5,8 +5,7 @@ require 'json'
5
5
  require 'net/http'
6
6
 
7
7
  module Jspm
8
- # Vendors every module the trace reaches from jspm.io, copies npm
9
- # dependencies' stylesheets, fonts and images, and writes
8
+ # Vendors every module the trace reaches from jspm.io, and writes
10
9
  # config/importmap.json.
11
10
  class Install
12
11
  SOURCE_MAP = %r{//# sourceMappingURL=(?<path>\S+\.map)\s*\z}
@@ -32,16 +31,10 @@ module Jspm
32
31
  cdn = URI(Paths::CDN)
33
32
  cdn_urls = trace.cdn_urls
34
33
  Net::HTTP.start(cdn.host, cdn.port, use_ssl: true) { |http| download_all(http, cdn_urls, vendor_dir) }
35
- copies = Assets.copies(lockfile:, root:)
36
- copies.each do |source, target|
37
- target.dirname.mkpath
38
- FileUtils.cp(source, target)
39
- end
40
34
  root.join(Paths::IMPORT_MAP).write("#{JSON.pretty_generate(trace.import_map)}\n")
41
35
 
42
36
  report('traced', Paths::ENTRY_PATH)
43
37
  report('vendored', "#{cdn_urls.size} modules from ga.jspm.io")
44
- report('copied', "#{copies.size} assets from node_modules")
45
38
  report('wrote', Paths::IMPORT_MAP)
46
39
  warn_about_versions
47
40
  end
data/lib/jspm/lockfile.rb CHANGED
@@ -3,14 +3,11 @@
3
3
  require 'json'
4
4
 
5
5
  module Jspm
6
- # The packages npm installed, as package-lock.json records them.
6
+ # The packages npm or Yarn installed, as package-lock.json or yarn.lock
7
+ # records them.
7
8
  class Lockfile
8
9
  Package =
9
- Data.define(:dev, :name, :path, :top_level, :version) do
10
- def browser?
11
- top_level && !dev
12
- end
13
-
10
+ Data.define(:name, :top_level, :version) do
14
11
  def resolution
15
12
  [name, version]
16
13
  end
@@ -21,18 +18,24 @@ module Jspm
21
18
  end
22
19
  private_constant :Package
23
20
 
24
- def self.read(root)
25
- json = JSON.parse(root.join('package-lock.json').read)
26
- installed = json.fetch('packages').reject { |path, _package| path.empty? }
27
- packages =
21
+ class << self
22
+ def read(root)
23
+ packages = root.join('yarn.lock').exist? ? YarnLock.packages(root) : npm_packages(root)
24
+ new(packages: packages.map { |package| Package.new(**package) })
25
+ end
26
+
27
+ private
28
+
29
+ def npm_packages(root)
30
+ json = JSON.parse(root.join('package-lock.json').read)
31
+ installed = json.fetch('packages').reject { |path, _package| path.empty? }
28
32
  installed.map do |path, package|
29
33
  name = path.split('node_modules/').last
30
34
  top_level = path.scan('node_modules/').one?
31
35
  version = package.fetch('version')
32
- dev = package.fetch('dev', false)
33
- Package.new(dev:, name:, path:, top_level:, version:)
36
+ { name:, top_level:, version: }
34
37
  end
35
- new(packages:)
38
+ end
36
39
  end
37
40
 
38
41
  def initialize(packages:)
@@ -43,10 +46,6 @@ module Jspm
43
46
  packages.select(&:top_level).to_h(&:resolution)
44
47
  end
45
48
 
46
- def browser_packages
47
- packages.select(&:browser?)
48
- end
49
-
50
49
  def nested_versions(name)
51
50
  nested = packages.select { |package| package.nested?(name) }
52
51
  nested.map(&:version)
data/lib/jspm/trace.rb CHANGED
@@ -17,10 +17,14 @@ module Jspm
17
17
  pathnames = asset_paths.map { |path| Pathname(path) }
18
18
  module_paths = pathnames.reject { |path| path == vendor_dir }
19
19
  local_modules = LocalModules.find(asset_paths: module_paths)
20
+ resolutions = lockfile.resolutions
21
+ # A package the app installed beats a gem's file of the same name, such
22
+ # as actiontext's trix.js, so the app and its packages share one copy.
23
+ pins = local_modules.pins.reject { |name, _path| resolutions.key?(name) }
20
24
  input = JSON.generate(
21
25
  entry: Paths::ENTRY_PATH,
22
- inputMap: { imports: locked_versions(root).merge(local_modules.pins), scopes: {} },
23
- resolutions: lockfile.resolutions,
26
+ inputMap: { imports: locked_versions(root).merge(pins), scopes: {} },
27
+ resolutions:,
24
28
  root: root.to_s
25
29
  )
26
30
  script = File.expand_path('trace.mjs', __dir__)
data/lib/jspm/version.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Jspm
4
- VERSION = '0.1.0'
4
+ VERSION = '0.2.0'
5
5
  public_constant :VERSION
6
6
  end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Jspm
6
+ # Reads Yarn 1's yarn.lock. Unlike package-lock.json, it doesn't record
7
+ # where each package sits in node_modules. A package locked at one version
8
+ # is the top-level one. When Yarn locked several versions, the one
9
+ # node_modules holds at its top level is.
10
+ module YarnLock
11
+ Entry = Data.define(:name, :version)
12
+ private_constant :Entry
13
+
14
+ class << self
15
+ def packages(root)
16
+ entries = parse(root.join('yarn.lock').read)
17
+ counts = entries.map(&:name).tally
18
+ entries.map do |entry|
19
+ name = entry.name
20
+ version = entry.version
21
+ locked_once = counts.fetch(name) == 1
22
+ installed = installed_version(root.join('node_modules', name)) == version
23
+ { name:, top_level: locked_once || installed, version: }
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def installed_version(package_dir)
30
+ manifest = package_dir.join('package.json')
31
+ JSON.parse(manifest.read).fetch('version') if manifest.exist?
32
+ end
33
+
34
+ def parse(text)
35
+ blocks =
36
+ text.split(/\n{2,}/).reject do |block|
37
+ block.start_with?('#') || block.strip.empty?
38
+ end
39
+ blocks.map { |block| parse_entry(block) }
40
+ end
41
+
42
+ def parse_entry(block)
43
+ header, *body = block.lines(chomp: true)
44
+ spec = header.delete_suffix(':').split(', ').first.delete('"')
45
+ version = body.find { |line| line.start_with?(' version ') }[/"(.*)"/, 1]
46
+ Entry.new(name: spec.rpartition('@').first, version:)
47
+ end
48
+ end
49
+ end
50
+ end
data/lib/jspm-rails.rb CHANGED
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'jspm/assets'
4
3
  require 'jspm/check'
5
4
  require 'jspm/helper'
6
5
  require 'jspm/install'
@@ -11,3 +10,4 @@ require 'jspm/railtie'
11
10
  require 'jspm/tags'
12
11
  require 'jspm/trace'
13
12
  require 'jspm/version'
13
+ require 'jspm/yarn_lock'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jspm-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Clay Shentrup
@@ -49,7 +49,6 @@ files:
49
49
  - LICENSE
50
50
  - README.md
51
51
  - lib/jspm-rails.rb
52
- - lib/jspm/assets.rb
53
52
  - lib/jspm/check.rb
54
53
  - lib/jspm/helper.rb
55
54
  - lib/jspm/install.rb
@@ -61,6 +60,7 @@ files:
61
60
  - lib/jspm/trace.mjs
62
61
  - lib/jspm/trace.rb
63
62
  - lib/jspm/version.rb
63
+ - lib/jspm/yarn_lock.rb
64
64
  - lib/tasks/jspm.rake
65
65
  homepage: https://github.com/ClayShentrup/jspm-rails
66
66
  licenses:
data/lib/jspm/assets.rb DELETED
@@ -1,46 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Jspm
4
- # The stylesheets, fonts and images of each npm dependency, and where each
5
- # one is vendored. devDependencies are build tools, so they stay out.
6
- module Assets
7
- EXTENSIONS = %w[
8
- .avif
9
- .css
10
- .eot
11
- .gif
12
- .jpeg
13
- .jpg
14
- .otf
15
- .png
16
- .svg
17
- .ttf
18
- .webp
19
- .woff
20
- .woff2
21
- ].freeze
22
- private_constant :EXTENSIONS
23
-
24
- class << self
25
- # Each copy is a pair: the file in node_modules, and its vendored path.
26
- def copies(lockfile:, root:)
27
- lockfile.browser_packages.flat_map { |package| package_copies(package, root) }
28
- end
29
-
30
- private
31
-
32
- def package_copies(package, root)
33
- package_dir = root.join(package.path)
34
- vendor_dir = root.join(Paths::VENDOR, package.name)
35
- files =
36
- package_dir.glob('**/*').select do |file|
37
- EXTENSIONS.include?(file.extname.downcase)
38
- end
39
- files.map do |file|
40
- relative_path = file.relative_path_from(package_dir)
41
- [file, vendor_dir.join(relative_path)]
42
- end
43
- end
44
- end
45
- end
46
- end