utopia 3.0.3 → 3.0.4

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: c16b554d772d132bd1820ec17f402bbdf2d3720a5c6965f8673c0f06054f1c29
4
- data.tar.gz: d6d839b6d55e3aff38507b9c4324d2631eb5a6b7f64eaeddb7e6b30185409b0a
3
+ metadata.gz: 3e9f5a6eb41a075cebcde2daf0e8811e1af414776f7807a14e07e9ed228c7657
4
+ data.tar.gz: 1d566c0e5b68123699f6f33185857ce00e6eb3b1659eae5c0f0b896443a36527
5
5
  SHA512:
6
- metadata.gz: ab0202e8f8259c62856341332dcc7f37532535e968e90b75dceb4e8370eee7092ac5e7cdc4e3a2072f0394ba87b39994c18fa2ca1f17864d9f04f5974847a9b2
7
- data.tar.gz: 14260960ab36d1cea1cabc02b6c43c29661efefb6e3e1566ef7e61ce87ba9f6525e111f9182e3beeac0a973bf303c147a71d3edc65d1cb75153c3c7377e7f2a8
6
+ metadata.gz: dc90dd50e914c27b28463992bbe33b0ef52eb077bd666519b876575202cee39babfed39720e2c22b66f3c456460bdcd16446876bb2827714b9665ed72c655ce0
7
+ data.tar.gz: 18b31ba92269e3340cc385e2a8b953bbe7b75b0db1f7323dd4e00d55faa03ebf71a097a940de69c84183a5115d2e982ba0a8aa2f480ca495a8d2119a93d2aa4f
checksums.yaml.gz.sig CHANGED
Binary file
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ NPM = ENV["NPM"] || "npm"
7
+
8
+ # Update public components from production JavaScript packages.
9
+ #
10
+ # Packages are copied from their `dist` directory when present, or otherwise
11
+ # from the package root. The `utopia.components` section of `package.json` can
12
+ # specify per-package `include` patterns to select only required files.
13
+ #
14
+ # @parameter root [String] The project root directory.
15
+ def update(root: context.root)
16
+ require "json"
17
+ require "open3"
18
+ require "utopia/components"
19
+
20
+ components = Utopia::Components.new(root)
21
+ production_packages = fetch_production_packages(components.package_root)
22
+
23
+ components.update(production_packages)
24
+ end
25
+
26
+ private
27
+
28
+ def fetch_production_packages(package_root)
29
+ stdout, _status = Open3.capture2(NPM, "ls", "--production", "--json", chdir: package_root.to_s)
30
+ json = JSON.parse(stdout)
31
+
32
+ flatten_package_dependencies(json).sort.uniq
33
+ end
34
+
35
+ def flatten_package_dependencies(json, into = [])
36
+ if json["dependencies"]
37
+ json["dependencies"].each do |name, details|
38
+ into << name
39
+ flatten_package_dependencies(details, into)
40
+ end
41
+ end
42
+
43
+ return into
44
+ end
@@ -17,7 +17,7 @@ $ npm install jquery
17
17
  Copy the distribution files to `public/_components`:
18
18
 
19
19
  ```bash
20
- $ bundle exec bake utopia:node:update
20
+ $ bundle exec bake utopia:components:update
21
21
  ```
22
22
 
23
23
  This will copy the library's distribution files (typically from `node_modules/*/dist/`) to your `public/_components/` directory, making them available for local serving.
@@ -0,0 +1,179 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ require "fileutils"
7
+ require "json"
8
+ require "pathname"
9
+
10
+ module Utopia
11
+ # Installs JavaScript packages into the public components directory. Package contents are copied from `dist` when it exists, otherwise from the package root.
12
+ #
13
+ # By default, the complete source directory is installed. Projects can limit an individual package to a set of files using `utopia.components` in their `package.json` file.
14
+ class Components
15
+ # Initialize a component installer for the given project root.
16
+ #
17
+ # @parameter root [String | Pathname] The project root directory.
18
+ def initialize(root)
19
+ @root = Pathname.new(root)
20
+ @package_root = @root + "node_modules"
21
+
22
+ # This is a legacy path:
23
+ unless @package_root.directory?
24
+ @package_root = @root + "lib/components"
25
+ end
26
+
27
+ @install_root = @root + "public/_components"
28
+ @configuration = load_configuration
29
+ end
30
+
31
+ # @attribute [Pathname] The directory containing the installed JavaScript packages.
32
+ attr :package_root
33
+
34
+ # Update the specified packages in the public components directory.
35
+ #
36
+ # @parameter package_names [Array(String)] The production package names to install.
37
+ def update(package_names)
38
+ expand_package_paths(@package_root).each do |package_path|
39
+ package_name = package_path.relative_path_from(@package_root).to_s
40
+
41
+ if package_names.include?(package_name)
42
+ install(package_name, package_path)
43
+ end
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ # Load the optional per-package installation rules. A missing `package.json`, or a file without `utopia.components`, preserves the default behaviour of copying complete packages.
50
+ # @returns [Hash] The per-package installation rules.
51
+ def load_configuration
52
+ package_path = @root + "package.json"
53
+
54
+ unless package_path.file?
55
+ return {}
56
+ end
57
+
58
+ configuration = JSON.parse(package_path.read).dig("utopia", "components") || {}
59
+
60
+ unless configuration.is_a?(Hash)
61
+ raise ArgumentError, "utopia.components must be an object!"
62
+ end
63
+
64
+ return configuration
65
+ end
66
+
67
+ # Install one package. Distribution directories are preferred because they generally contain the browser-ready form of a package.
68
+ # @parameter package_name [String] The package name relative to `node_modules`.
69
+ # @parameter package_path [Pathname] The package source directory.
70
+ def install(package_name, package_path)
71
+ install_path = @install_root + package_name
72
+ dist_path = package_path + "dist"
73
+
74
+ if dist_path.directory?
75
+ source_path = dist_path
76
+ else
77
+ source_path = package_path
78
+ end
79
+
80
+ configuration = @configuration[package_name]
81
+
82
+ if configuration
83
+ install_selected(package_name, source_path, install_path, configuration)
84
+ else
85
+ FileUtils::Verbose.rm_rf(install_path)
86
+ FileUtils::Verbose.mkpath(install_path.dirname)
87
+ FileUtils::Verbose.cp_r(source_path, install_path)
88
+ end
89
+ end
90
+
91
+ # Install only the files matched by the configured include patterns. Every pattern is resolved before removing the existing installation, so an invalid configuration cannot leave a package partially installed or remove a previously working copy.
92
+ # @parameter package_name [String] The package name relative to `node_modules`.
93
+ # @parameter source_path [Pathname] The package source directory.
94
+ # @parameter install_path [Pathname] The destination directory.
95
+ # @parameter configuration [Hash] The package installation rules.
96
+ def install_selected(package_name, source_path, install_path, configuration)
97
+ unless configuration.is_a?(Hash)
98
+ raise ArgumentError, "utopia.components.#{package_name}.include must be a non-empty array!"
99
+ end
100
+
101
+ include_patterns = configuration["include"]
102
+
103
+ unless include_patterns.is_a?(Array) && include_patterns.any?
104
+ raise ArgumentError, "utopia.components.#{package_name}.include must be a non-empty array!"
105
+ end
106
+
107
+ paths = include_patterns.flat_map do |pattern|
108
+ included_paths(package_name, source_path, pattern)
109
+ end.uniq.sort
110
+
111
+ FileUtils::Verbose.rm_rf(install_path)
112
+
113
+ paths.each do |relative_path|
114
+ source_file = source_path + relative_path
115
+ install_file = install_path + relative_path
116
+
117
+ FileUtils::Verbose.mkpath(install_file.dirname)
118
+ FileUtils::Verbose.cp(source_file, install_file)
119
+ end
120
+ end
121
+
122
+ # Expand one include pattern into files relative to the package source. Directories are excluded so each result can be copied independently.
123
+ # @parameter package_name [String] The package name used in validation errors.
124
+ # @parameter source_path [Pathname] The package source directory.
125
+ # @parameter pattern [String] The include pattern to expand.
126
+ # @returns [Array(String)] The matching file paths relative to the package source.
127
+ def included_paths(package_name, source_path, pattern)
128
+ unless pattern.is_a?(String) && relative_pattern?(pattern)
129
+ raise ArgumentError, "Invalid include pattern for #{package_name}: #{pattern.inspect}"
130
+ end
131
+
132
+ paths = Dir.glob(pattern, base: source_path.to_s).select do |relative_path|
133
+ (source_path + relative_path).file?
134
+ end
135
+
136
+ if paths.empty?
137
+ raise ArgumentError, "Include pattern for #{package_name} matched no files: #{pattern.inspect}"
138
+ end
139
+
140
+ return paths
141
+ end
142
+
143
+ # Determine whether the pattern is contained within the package source. Absolute paths and parent traversal are rejected because they could otherwise copy arbitrary files from outside the package.
144
+ # @parameter pattern [String] The include pattern to validate.
145
+ # @returns [Boolean] Whether the pattern is relative and does not contain parent traversal.
146
+ def relative_pattern?(pattern)
147
+ path = Pathname.new(pattern)
148
+
149
+ if path.absolute?
150
+ return false
151
+ end
152
+
153
+ if path.each_filename.any?{|component| component == ".."}
154
+ return false
155
+ end
156
+
157
+ return true
158
+ end
159
+
160
+ # Enumerate packages in `node_modules`, descending through scoped package directories such as `@socketry` while preserving their scoped names.
161
+ # @parameter root [Pathname] The directory to enumerate.
162
+ # @parameter into [Array(Pathname)] The array into which package paths are appended.
163
+ # @returns [Array(Pathname)] The discovered package directories.
164
+ def expand_package_paths(root, into = [])
165
+ root.children.select(&:directory?).each do |path|
166
+ basename = path.basename.to_s
167
+
168
+ # Handle organisation sub-directories which start with an '@' symbol:
169
+ if basename.start_with?("@")
170
+ expand_package_paths(path, into)
171
+ else
172
+ into << path
173
+ end
174
+ end
175
+
176
+ return into
177
+ end
178
+ end
179
+ end
@@ -5,5 +5,5 @@
5
5
 
6
6
  # @namespace
7
7
  module Utopia
8
- VERSION = "3.0.3"
8
+ VERSION = "3.0.4"
9
9
  end
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: utopia
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.3
4
+ version: 3.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -272,8 +272,8 @@ extensions: []
272
272
  extra_rdoc_files: []
273
273
  files:
274
274
  - bake/utopia.rb
275
+ - bake/utopia/components.rb
275
276
  - bake/utopia/environment.rb
276
- - bake/utopia/node.rb
277
277
  - bake/utopia/server.rb
278
278
  - bake/utopia/shell.rb
279
279
  - bake/utopia/site.rb
@@ -287,6 +287,7 @@ files:
287
287
  - context/what-is-xnode.md
288
288
  - lib/utopia.rb
289
289
  - lib/utopia/application.rb
290
+ - lib/utopia/components.rb
290
291
  - lib/utopia/content.rb
291
292
  - lib/utopia/content/builder.rb
292
293
  - lib/utopia/content/document.rb
metadata.gz.sig CHANGED
Binary file
data/bake/utopia/node.rb DELETED
@@ -1,87 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Released under the MIT License.
4
- # Copyright, 2016-2025, by Samuel Williams.
5
-
6
- NPM = ENV["NPM"] || "npm"
7
-
8
- def update
9
- require "fileutils"
10
- require "utopia/path"
11
-
12
- root = Pathname.new(context.root)
13
- package_root = root + "node_modules"
14
-
15
- # This is a legacy path:
16
- unless package_root.directory?
17
- package_root = root + "lib/components"
18
- end
19
-
20
- install_root = root + "public/_components"
21
-
22
- # Fetch only production dependencies using `npm ls --production`
23
- production_packages = fetch_production_packages(package_root)
24
- package_paths = expand_package_paths(package_root).select do |path|
25
- package_name = path.relative_path_from(package_root).to_s
26
- production_packages.include?(package_name)
27
- end
28
-
29
- package_paths.each do |package_path|
30
- package_directory = package_path.relative_path_from(package_root)
31
- install_path = install_root + package_directory
32
-
33
- dist_path = package_path + "dist"
34
-
35
- FileUtils::Verbose.rm_rf(install_path)
36
- FileUtils::Verbose.mkpath(install_path.dirname)
37
-
38
- # If a package has a dist directory, we only symlink that... otherwise we have to do the entire package, and hope that bower's ignore was setup correctly:
39
- if dist_path.exist?
40
- link_path = Utopia::Path.shortest_path(dist_path, install_path)
41
- else
42
- link_path = Utopia::Path.shortest_path(package_path, install_path)
43
- end
44
-
45
- FileUtils::Verbose.cp_r File.expand_path(link_path, install_path), install_path
46
- end
47
- end
48
-
49
- private
50
-
51
- def fetch_production_packages(package_root)
52
- require "json"
53
- require "open3"
54
-
55
- stdout, _status = Open3.capture2(NPM, "ls", "--production", "--json", chdir: package_root.to_s)
56
-
57
- json = JSON.parse(stdout)
58
-
59
- flatten_package_dependencies(json).sort.uniq
60
- end
61
-
62
- def flatten_package_dependencies(json, into = [])
63
- if json["dependencies"]
64
- json["dependencies"].each do |name, details|
65
- into << name
66
- flatten_package_dependencies(details, into)
67
- end
68
- end
69
-
70
- return into
71
- end
72
-
73
- def expand_package_paths(root, into = [])
74
- paths = root.children.select(&:directory?)
75
-
76
- paths.each do |path|
77
- basename = path.basename.to_s
78
- # Handle organisation sub-directories which start with an '@' symbol:
79
- if basename.start_with?("@")
80
- expand_package_paths(path, into)
81
- else
82
- into << path
83
- end
84
- end
85
-
86
- return into
87
- end