klenod-plugin-javascript 0.0.12-x86_64-linux-gnu → 0.0.13-x86_64-linux-gnu
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 +4 -4
- data/README.md +77 -3
- data/lib/klenod/build/plugins/javascript_plugin.rb +18 -3
- data/lib/klenod/build/plugins/node_modules_plugin/package_exports.rb +315 -0
- data/lib/klenod/build/plugins/node_modules_plugin.rb +376 -0
- data/lib/klenod/plugin/javascript/version.rb +1 -1
- data/lib/klenod/plugin/javascript.rb +1 -0
- metadata +6 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 93e12c1ce189a9594b75b4dd1089c0b89836820779e2e7eeb24c0d90d0ccc917
|
|
4
|
+
data.tar.gz: 7f37d8ce72b820bdf3f4d49811339277632a3ea50106425785134ed4823a4635
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9b20181d84ce6a159b32e0c771d352dfb52286b2e6f795418ccb599589f621710119d3d9bafd42602f228d976a74aa20f26623f9fcd6e7d80058513954462191
|
|
7
|
+
data.tar.gz: 26159b0c1ec522c4b4a48cd242442818aa6c4df009f391f515cfbc0cbb1040d4af5bdaf02ba3da4dab1d4d1831f4ed6f23ccd2dcc1595208dbe50bc034c75f0e
|
data/README.md
CHANGED
|
@@ -7,12 +7,10 @@ It:
|
|
|
7
7
|
|
|
8
8
|
- collects static imports, re-exports, and string-literal dynamic imports
|
|
9
9
|
- transforms JavaScript, TypeScript, JSX, and TSX
|
|
10
|
-
- rewrites
|
|
10
|
+
- rewrites graph imports to content-hashed asset paths
|
|
11
11
|
- emits image and SVG wrappers backed by shared `ImageBase`, `ImageMetadata`, `ImageVariant`, and `SvgMetadata` classes, with frozen concrete metadata values, only for assets imported from JavaScript
|
|
12
12
|
- preserves external URL imports
|
|
13
13
|
|
|
14
|
-
Bare package imports and npm resolution are not currently supported.
|
|
15
|
-
|
|
16
14
|
## Options
|
|
17
15
|
|
|
18
16
|
```ruby
|
|
@@ -27,3 +25,79 @@ Klenod::Build::Plugins::JavaScriptPlugin.new(
|
|
|
27
25
|
- `source_maps:` accepts `false`, `true`, or `:development` (the default).
|
|
28
26
|
- `minify:` also minifies in development when `true`; build output is always
|
|
29
27
|
minified.
|
|
28
|
+
|
|
29
|
+
## npm Packages
|
|
30
|
+
|
|
31
|
+
`NodeModulesPlugin` adds npm packages to the Klenod graph. It is opt-in; register
|
|
32
|
+
it before `JavaScriptPlugin` so the JavaScript plugin can transform the files it
|
|
33
|
+
resolves:
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
plugins [
|
|
37
|
+
Klenod::Build::Plugins::NodeModulesPlugin.new,
|
|
38
|
+
Klenod::Build::Plugins::JavaScriptPlugin.new
|
|
39
|
+
]
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Use an explicit `npm://` URL from application code. This keeps package lookup
|
|
43
|
+
separate from Klenod's usual rule that a bare specifier is relative to its
|
|
44
|
+
importer.
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
import * as THREE from "npm://three";
|
|
48
|
+
import { vec3 } from "npm://gl-matrix";
|
|
49
|
+
import { OrbitControls } from "npm://three/addons/controls/OrbitControls.js";
|
|
50
|
+
import { ReactiveElement } from "npm://@lit/reactive-element";
|
|
51
|
+
import { css } from "npm://@lit/reactive-element/css-tag.js";
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The part after `npm://` is the package name, followed by an optional exported
|
|
55
|
+
subpath. Scoped names retain the usual `@scope/package` form. The scheme works
|
|
56
|
+
with static imports, side-effect imports, re-exports, and string-literal dynamic
|
|
57
|
+
imports:
|
|
58
|
+
|
|
59
|
+
```js
|
|
60
|
+
import "npm://@scope/widget";
|
|
61
|
+
export { html } from "npm://lit";
|
|
62
|
+
button.addEventListener("click", () => import("npm://three"));
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Exported subpaths must match the package's `exports` map. Include an extension
|
|
66
|
+
when the export includes one: use
|
|
67
|
+
`npm://three/addons/controls/OrbitControls.js` for Three.js, but
|
|
68
|
+
`npm://zod/mini` for Zod.
|
|
69
|
+
|
|
70
|
+
`NodeModulesPlugin` selects package exports using the `browser`, `import`,
|
|
71
|
+
`module`, and `default` conditions. It respects the order of conditions in
|
|
72
|
+
`package.json`. Packages without `exports` fall back to `module`, then `main`,
|
|
73
|
+
then `index.js`.
|
|
74
|
+
|
|
75
|
+
Once inside an npm package, relative imports continue within that package and
|
|
76
|
+
bare specifiers resolve its dependencies from the nearest `node_modules`
|
|
77
|
+
directory. Package self-references and `#` imports are supported through the
|
|
78
|
+
package's `exports` and `imports` maps. Extensionless package imports probe
|
|
79
|
+
`.js`, `.mjs`, `index.js`, and `index.mjs`; targets from an `exports` map are
|
|
80
|
+
exact and are not probed.
|
|
81
|
+
|
|
82
|
+
Resolved package files are ordinary graph modules. Klenod transforms them,
|
|
83
|
+
rewrites their imports, emits content-hashed assets, and minifies them in build
|
|
84
|
+
mode.
|
|
85
|
+
|
|
86
|
+
### Configuration
|
|
87
|
+
|
|
88
|
+
- `root:` overrides the `node_modules` directory. By default the nearest one at
|
|
89
|
+
or above the source directory is used.
|
|
90
|
+
- `conditions:` overrides the matched export conditions. The default is
|
|
91
|
+
`browser`, `import`, `module`, and `default`, matched in the order the package
|
|
92
|
+
lists them.
|
|
93
|
+
|
|
94
|
+
### Compatibility
|
|
95
|
+
|
|
96
|
+
Only ES modules are supported. CommonJS packages, Node builtins, the legacy
|
|
97
|
+
`browser` field, and JSON package imports are not supported. A `browser`
|
|
98
|
+
condition inside `exports` is supported.
|
|
99
|
+
|
|
100
|
+
Each package name can refer to only one physical directory in a graph because
|
|
101
|
+
module IDs use the form `npm://<package>/<path>`. If resolution finds two copies
|
|
102
|
+
of a package, Klenod reports the conflict; deduplicate that package in the
|
|
103
|
+
lockfile.
|
|
@@ -30,6 +30,9 @@ module Klenod
|
|
|
30
30
|
JSX_RUNTIME_MODULE_ID = ModuleId.new("virtual:klenod/jsx-runtime.js", nil)
|
|
31
31
|
LOCAL_SPECIFIER_PATTERN = %r{\A(?:\.{1,2}/|/|app:/)}
|
|
32
32
|
EXTERNAL_SPECIFIER_PATTERN = %r{\A(?:[A-Za-z][A-Za-z0-9+.-]*:)?//}
|
|
33
|
+
# Also matches EXTERNAL_SPECIFIER_PATTERN, so npm imports have to be
|
|
34
|
+
# recognised before a specifier is treated as an external URL.
|
|
35
|
+
NPM_SPECIFIER_PATTERN = %r{\Anpm://}
|
|
33
36
|
VALID_SOURCE_MAP_MODES = [false, true, :development].freeze
|
|
34
37
|
IDENTIFIER_PATTERN = '[$_\p{Alpha}][$\u200c\u200d\p{Alnum}_]*'
|
|
35
38
|
DEFAULT_EXPORT_CLASS_PATTERN = /\bexport\s+default\s+class\s+(#{IDENTIFIER_PATTERN})\b/
|
|
@@ -217,9 +220,13 @@ module Klenod
|
|
|
217
220
|
end
|
|
218
221
|
|
|
219
222
|
def build_dependencies(module_id, imports)
|
|
223
|
+
# Inside an npm package a bare specifier names another package, so
|
|
224
|
+
# let it reach the resolver instead of rejecting it here.
|
|
225
|
+
npm_importer = module_id.scheme == :npm
|
|
226
|
+
|
|
220
227
|
imports.filter_map.with_index do |import, index|
|
|
221
|
-
next if external_specifier?(import.specifier)
|
|
222
|
-
raise DynamicImportError, unsupported_specifier_message(import) unless
|
|
228
|
+
next if external_specifier?(import.specifier) && !npm_specifier?(import.specifier)
|
|
229
|
+
raise DynamicImportError, unsupported_specifier_message(import) unless npm_importer || bundled_specifier?(import.specifier)
|
|
223
230
|
|
|
224
231
|
Dependency
|
|
225
232
|
.create(
|
|
@@ -415,13 +422,21 @@ module Klenod
|
|
|
415
422
|
end
|
|
416
423
|
|
|
417
424
|
def unsupported_specifier_message(import)
|
|
418
|
-
"Unsupported JavaScript import #{import.specifier.inspect} at #{import.loc}. Only relative, app-root, and external URL imports are supported."
|
|
425
|
+
"Unsupported JavaScript import #{import.specifier.inspect} at #{import.loc}. Only relative, app-root, npm, and external URL imports are supported."
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
def bundled_specifier?(specifier)
|
|
429
|
+
local_specifier?(specifier) || npm_specifier?(specifier) || specifier == JSX_RUNTIME_SPECIFIER
|
|
419
430
|
end
|
|
420
431
|
|
|
421
432
|
def local_specifier?(specifier)
|
|
422
433
|
specifier.match?(LOCAL_SPECIFIER_PATTERN)
|
|
423
434
|
end
|
|
424
435
|
|
|
436
|
+
def npm_specifier?(specifier)
|
|
437
|
+
specifier.match?(NPM_SPECIFIER_PATTERN)
|
|
438
|
+
end
|
|
439
|
+
|
|
425
440
|
def external_specifier?(specifier)
|
|
426
441
|
specifier.match?(EXTERNAL_SPECIFIER_PATTERN)
|
|
427
442
|
end
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Klenod
|
|
4
|
+
module Build
|
|
5
|
+
module Plugins
|
|
6
|
+
module NodeModulesPlugin
|
|
7
|
+
# Resolves a package subpath to a package-relative file path using the
|
|
8
|
+
# metadata in a `package.json`.
|
|
9
|
+
#
|
|
10
|
+
# This module never touches the filesystem. It reports what a package
|
|
11
|
+
# claims about itself; checking that the returned path exists, and
|
|
12
|
+
# probing for extensions when it does not, belongs to the caller.
|
|
13
|
+
module PackageExports
|
|
14
|
+
# Conditions are matched in `package.json` key order, so this is a
|
|
15
|
+
# membership set rather than a priority list.
|
|
16
|
+
#
|
|
17
|
+
# "require" is absent on purpose. It names the CommonJS entry, which
|
|
18
|
+
# the JavaScript plugin cannot transform, and leaving it out turns a
|
|
19
|
+
# CommonJS-only package into a resolve-time error instead of a module
|
|
20
|
+
# that fails in the browser.
|
|
21
|
+
DEFAULT_CONDITIONS = ["browser", "import", "module", "default"].freeze
|
|
22
|
+
|
|
23
|
+
# Consulted in order when a package has no "exports" field. "module"
|
|
24
|
+
# precedes "main" because it is the conventional ES module entry,
|
|
25
|
+
# while "main" is usually CommonJS.
|
|
26
|
+
LEGACY_ENTRY_FIELDS = ["module", "main"].freeze
|
|
27
|
+
LEGACY_INDEX = "index.js"
|
|
28
|
+
|
|
29
|
+
# Only "require" names a CommonJS entry. "node" says nothing about the
|
|
30
|
+
# module system, so a node-only target is reported as an unmatched
|
|
31
|
+
# condition instead.
|
|
32
|
+
COMMONJS_CONDITIONS = ["require"].freeze
|
|
33
|
+
MAX_LISTED_SUBPATHS = 8
|
|
34
|
+
|
|
35
|
+
BLOCKED = :blocked
|
|
36
|
+
|
|
37
|
+
Result = Data.define(:path, :reason, :message) do
|
|
38
|
+
def self.resolved(path)
|
|
39
|
+
new(path, nil, nil)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.unresolved(reason, message)
|
|
43
|
+
new(nil, reason, message)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def resolved?
|
|
47
|
+
!path.nil?
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
module_function
|
|
52
|
+
|
|
53
|
+
# Resolve +subpath+ against +package_json+.
|
|
54
|
+
#
|
|
55
|
+
# Returns a Result whose +path+ is relative to the package root, or an
|
|
56
|
+
# unresolved Result carrying a +reason+ and a human-readable +message+.
|
|
57
|
+
def resolve(package_json, subpath: ".", conditions: DEFAULT_CONDITIONS)
|
|
58
|
+
return Result.unresolved(:invalid, "package.json must contain an object.") unless package_json.is_a?(Hash)
|
|
59
|
+
|
|
60
|
+
subpath = normalize_subpath(subpath)
|
|
61
|
+
return legacy_resolve(package_json, subpath) unless package_json.key?("exports")
|
|
62
|
+
|
|
63
|
+
resolve_exports(package_json["exports"], subpath, conditions)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Resolve a "#"-prefixed private import against the package's "imports"
|
|
67
|
+
# field.
|
|
68
|
+
#
|
|
69
|
+
# Unlike an export target, an import target may name another package,
|
|
70
|
+
# so a resolved path keeps its "./" prefix when it refers to a file
|
|
71
|
+
# inside this package and is a bare specifier otherwise.
|
|
72
|
+
def resolve_private(package_json, specifier:, conditions: DEFAULT_CONDITIONS)
|
|
73
|
+
return Result.unresolved(:invalid, "package.json must contain an object.") unless package_json.is_a?(Hash)
|
|
74
|
+
|
|
75
|
+
unless specifier.is_a?(String) && specifier.start_with?("#") && specifier != "#" && !specifier.start_with?("#/")
|
|
76
|
+
return Result.unresolved(:invalid, "#{specifier.inspect} is not a valid private import.")
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
imports = package_json["imports"]
|
|
80
|
+
|
|
81
|
+
unless imports.is_a?(Hash)
|
|
82
|
+
return Result.unresolved(:no_match, "The package has no imports field to resolve #{specifier.inspect}.")
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
if imports.key?(specifier)
|
|
86
|
+
return resolve_matched_target(imports.fetch(specifier), specifier, conditions, nil, validate: false)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
key, pattern_match = best_pattern_match(imports, specifier)
|
|
90
|
+
|
|
91
|
+
unless key
|
|
92
|
+
return Result.unresolved(
|
|
93
|
+
:no_match,
|
|
94
|
+
"The package does not define #{specifier.inspect}. Available private imports: #{imports.keys.sort.first(MAX_LISTED_SUBPATHS).join(", ")}."
|
|
95
|
+
)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
resolve_matched_target(imports.fetch(key), specifier, conditions, pattern_match, validate: false)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def normalize_subpath(subpath)
|
|
102
|
+
value = subpath.to_s
|
|
103
|
+
return "." if value.empty? || value == "." || value == "./"
|
|
104
|
+
return value if value.start_with?("./")
|
|
105
|
+
|
|
106
|
+
"./#{value.delete_prefix("/")}"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def legacy_resolve(package_json, subpath)
|
|
110
|
+
return relative_path(subpath, "subpath") unless subpath == "."
|
|
111
|
+
|
|
112
|
+
field = LEGACY_ENTRY_FIELDS.find { package_json[it].is_a?(String) && !package_json[it].empty? }
|
|
113
|
+
return relative_path(LEGACY_INDEX, "default entry") unless field
|
|
114
|
+
|
|
115
|
+
relative_path(package_json.fetch(field), "#{field} field")
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def resolve_exports(exports, subpath, conditions)
|
|
119
|
+
if exports.is_a?(Hash) && subpath_map?(exports)
|
|
120
|
+
return mixed_keys_error(exports) unless exports.keys.all? { it.start_with?(".") }
|
|
121
|
+
|
|
122
|
+
return resolve_subpath_map(exports, subpath, conditions)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# A string, an array, or a bare condition object is sugar for ".".
|
|
126
|
+
unless subpath == "."
|
|
127
|
+
return Result.unresolved(:no_match, "The package does not export #{subpath.inspect}. It only exports \".\".")
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
resolve_matched_target(exports, subpath, conditions, nil)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def subpath_map?(exports)
|
|
134
|
+
exports.keys.any? { it.start_with?(".") }
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def resolve_subpath_map(exports, subpath, conditions)
|
|
138
|
+
return resolve_matched_target(exports.fetch(subpath), subpath, conditions, nil) if exports.key?(subpath)
|
|
139
|
+
|
|
140
|
+
key, pattern_match = best_pattern_match(exports, subpath)
|
|
141
|
+
return no_match_error(exports, subpath) unless key
|
|
142
|
+
|
|
143
|
+
resolve_matched_target(exports.fetch(key), subpath, conditions, pattern_match)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Node picks the pattern key with the longest prefix before "*",
|
|
147
|
+
# breaking ties on the longest suffix after it.
|
|
148
|
+
def best_pattern_match(exports, subpath)
|
|
149
|
+
best_key = nil
|
|
150
|
+
best_prefix = nil
|
|
151
|
+
best_suffix = nil
|
|
152
|
+
|
|
153
|
+
exports.each_key do |key|
|
|
154
|
+
prefix, star, suffix = key.partition("*")
|
|
155
|
+
next if star.empty? || suffix.include?("*")
|
|
156
|
+
next unless subpath.start_with?(prefix) && subpath.end_with?(suffix)
|
|
157
|
+
next if subpath.length < prefix.length + suffix.length
|
|
158
|
+
next if best_key && !better_pattern?(prefix, suffix, best_prefix, best_suffix)
|
|
159
|
+
|
|
160
|
+
best_key = key
|
|
161
|
+
best_prefix = prefix
|
|
162
|
+
best_suffix = suffix
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
return [nil, nil] unless best_key
|
|
166
|
+
|
|
167
|
+
[best_key, subpath[best_prefix.length...(subpath.length - best_suffix.length)]]
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def better_pattern?(prefix, suffix, best_prefix, best_suffix)
|
|
171
|
+
return true if prefix.length > best_prefix.length
|
|
172
|
+
|
|
173
|
+
prefix.length == best_prefix.length && suffix.length > best_suffix.length
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def resolve_matched_target(target, subpath, conditions, pattern_match, validate: true)
|
|
177
|
+
resolved = resolve_target(target, conditions, pattern_match)
|
|
178
|
+
|
|
179
|
+
case resolved
|
|
180
|
+
when String
|
|
181
|
+
if validate
|
|
182
|
+
relative_path(resolved, "export target for #{subpath.inspect}")
|
|
183
|
+
else
|
|
184
|
+
private_target(resolved, subpath)
|
|
185
|
+
end
|
|
186
|
+
when BLOCKED
|
|
187
|
+
Result.unresolved(:blocked, "The package explicitly blocks #{subpath.inspect}.")
|
|
188
|
+
else
|
|
189
|
+
conditions_unmet_error(target, subpath, conditions)
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Returns the target path, BLOCKED when the package maps the subpath to
|
|
194
|
+
# null, or nil when no condition matched.
|
|
195
|
+
def resolve_target(target, conditions, pattern_match)
|
|
196
|
+
case target
|
|
197
|
+
when String
|
|
198
|
+
substitute(target, pattern_match)
|
|
199
|
+
when Hash
|
|
200
|
+
target.each do |condition, value|
|
|
201
|
+
next unless condition == "default" || conditions.include?(condition)
|
|
202
|
+
|
|
203
|
+
resolved = resolve_target(value, conditions, pattern_match)
|
|
204
|
+
return resolved if resolved
|
|
205
|
+
end
|
|
206
|
+
nil
|
|
207
|
+
when Array
|
|
208
|
+
# A blocked entry inside an array is a failed alternative rather
|
|
209
|
+
# than a block, so keep looking.
|
|
210
|
+
target.each do |value|
|
|
211
|
+
resolved = resolve_target(value, conditions, pattern_match)
|
|
212
|
+
return resolved if resolved.is_a?(String)
|
|
213
|
+
end
|
|
214
|
+
nil
|
|
215
|
+
when nil
|
|
216
|
+
BLOCKED
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def substitute(target, pattern_match)
|
|
221
|
+
return target unless pattern_match && target.include?("*")
|
|
222
|
+
|
|
223
|
+
target.gsub("*", pattern_match)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# An "imports" target either points inside the package, keeping its
|
|
227
|
+
# "./" prefix so the caller can tell the two apart, or names another
|
|
228
|
+
# package.
|
|
229
|
+
def private_target(value, specifier)
|
|
230
|
+
description = "import target for #{specifier.inspect}"
|
|
231
|
+
|
|
232
|
+
if value.empty? || value.include?("*")
|
|
233
|
+
return Result.unresolved(:invalid, "The #{description} #{value.inspect} is not a usable target.")
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
unless value.start_with?("./")
|
|
237
|
+
if value.start_with?("../", "/", "#")
|
|
238
|
+
return Result.unresolved(:invalid, "The #{description} #{value.inspect} must stay within the package or name another package.")
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
return Result.resolved(value)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
inner = value.delete_prefix("./")
|
|
245
|
+
|
|
246
|
+
if inner.empty? || inner.split("/").include?("..")
|
|
247
|
+
return Result.unresolved(:invalid, "The #{description} #{value.inspect} must stay within the package.")
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
Result.resolved(value)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def relative_path(value, description)
|
|
254
|
+
path = value.to_s.delete_prefix("./")
|
|
255
|
+
|
|
256
|
+
if path.include?("*")
|
|
257
|
+
return Result.unresolved(:invalid, "The #{description} #{value.inspect} has an unmatched wildcard.")
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
if path.empty? || path.start_with?("/") || path.split("/").include?("..")
|
|
261
|
+
return Result.unresolved(:invalid, "The #{description} #{value.inspect} must stay within the package.")
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
Result.resolved(path)
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def conditions_unmet_error(target, subpath, conditions)
|
|
268
|
+
available = available_conditions(target)
|
|
269
|
+
|
|
270
|
+
if (available & COMMONJS_CONDITIONS).any?
|
|
271
|
+
return Result.unresolved(
|
|
272
|
+
:commonjs_only,
|
|
273
|
+
"#{subpath.inspect} resolves to a CommonJS entry only. Klenod can only import ES modules."
|
|
274
|
+
)
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
Result.unresolved(
|
|
278
|
+
:conditions_unmet,
|
|
279
|
+
"No condition matched for #{subpath.inspect}. " \
|
|
280
|
+
"Klenod uses #{conditions.to_a.join(", ")}; the package offers #{available.join(", ")}."
|
|
281
|
+
)
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def available_conditions(target)
|
|
285
|
+
case target
|
|
286
|
+
when Hash then target.flat_map { |condition, value| [condition, *available_conditions(value)] }.uniq
|
|
287
|
+
when Array then target.flat_map { available_conditions(it) }.uniq
|
|
288
|
+
else []
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def no_match_error(exports, subpath)
|
|
293
|
+
available = exports.keys.sort
|
|
294
|
+
listed = available.first(MAX_LISTED_SUBPATHS)
|
|
295
|
+
ellipsis = (available.length > listed.length) ? ", ..." : ""
|
|
296
|
+
|
|
297
|
+
Result.unresolved(
|
|
298
|
+
:no_match,
|
|
299
|
+
"The package does not export #{subpath.inspect}. Available subpaths: #{listed.join(", ")}#{ellipsis}."
|
|
300
|
+
)
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def mixed_keys_error(exports)
|
|
304
|
+
conditions = exports.keys.reject { it.start_with?(".") }
|
|
305
|
+
|
|
306
|
+
Result.unresolved(
|
|
307
|
+
:invalid,
|
|
308
|
+
"package.json exports mixes subpaths with conditions: #{conditions.join(", ")}."
|
|
309
|
+
)
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
end
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
require "klenod/build/dependency"
|
|
6
|
+
require "klenod/build/errors"
|
|
7
|
+
require "klenod/build/hashing"
|
|
8
|
+
require "klenod/build/load_result"
|
|
9
|
+
require "klenod/build/module_id"
|
|
10
|
+
require "klenod/build/plugin"
|
|
11
|
+
|
|
12
|
+
require_relative "node_modules_plugin/package_exports"
|
|
13
|
+
|
|
14
|
+
module Klenod
|
|
15
|
+
module Build
|
|
16
|
+
module Plugins
|
|
17
|
+
# Resolves `npm://` imports against an installed `node_modules` directory.
|
|
18
|
+
#
|
|
19
|
+
# Bare specifiers stay relative in Klenod, so npm packages are addressed
|
|
20
|
+
# through an explicit scheme: `import { vec3 } from "npm://gl-matrix"`.
|
|
21
|
+
# Once a module resolves to the npm scheme, every import inside it comes
|
|
22
|
+
# back to this plugin, which keeps package resolution from leaking into
|
|
23
|
+
# application resolution.
|
|
24
|
+
#
|
|
25
|
+
# Module IDs are `npm://<package>/<path within the package>`, so a package
|
|
26
|
+
# name identifies exactly one directory for the life of a graph. Finding
|
|
27
|
+
# the same name in two places is reported rather than resolved, because
|
|
28
|
+
# the ID has nowhere to record which copy a module came from.
|
|
29
|
+
module NodeModulesPlugin
|
|
30
|
+
def self.new(...)
|
|
31
|
+
Plugin.new(...)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
class Plugin < Klenod::Build::Plugin
|
|
35
|
+
SCHEME = :npm
|
|
36
|
+
DIRECTORY_NAME = "node_modules"
|
|
37
|
+
NPM_SPECIFIER_PATTERN = %r{\Anpm://}
|
|
38
|
+
# A leading "/" addresses the package root, matching how Klenod treats
|
|
39
|
+
# "/foo" as relative to the current scheme root.
|
|
40
|
+
RELATIVE_SPECIFIER_PATTERN = %r{\A(?:\.{1,2}/|/)}
|
|
41
|
+
# Tried for imports that omit an extension. CommonJS builds are not
|
|
42
|
+
# probed for, since they cannot be transformed anyway.
|
|
43
|
+
CANDIDATE_EXTENSIONS = [".js", ".mjs"].freeze
|
|
44
|
+
INDEX_BASENAMES = ["index.js", "index.mjs"].freeze
|
|
45
|
+
NODE_SCHEME = "node:"
|
|
46
|
+
PRIVATE_PREFIX = "#"
|
|
47
|
+
# Only consulted when a bare specifier resolves to nothing, so a
|
|
48
|
+
# package that really did install a shim named "path" still wins.
|
|
49
|
+
NODE_BUILTINS = %w[
|
|
50
|
+
assert async_hooks buffer child_process cluster console constants crypto
|
|
51
|
+
dgram diagnostics_channel dns domain events fs http http2 https inspector
|
|
52
|
+
module net os path perf_hooks process punycode querystring readline repl
|
|
53
|
+
stream string_decoder sys timers tls trace_events tty url util v8 vm wasi
|
|
54
|
+
worker_threads zlib
|
|
55
|
+
].freeze
|
|
56
|
+
|
|
57
|
+
def initialize(root: nil, conditions: PackageExports::DEFAULT_CONDITIONS)
|
|
58
|
+
@root = root && Pathname.new(root).expand_path
|
|
59
|
+
@conditions = conditions.freeze
|
|
60
|
+
@package_roots = {}
|
|
61
|
+
@package_metadata = {}
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def resolve(dependency, context)
|
|
65
|
+
specifier = dependency.specifier.to_s
|
|
66
|
+
|
|
67
|
+
if specifier.match?(NPM_SPECIFIER_PATTERN)
|
|
68
|
+
return resolve_scheme(ModuleId.parse(specifier), dependency, context)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
importer_id = dependency.importer_id
|
|
72
|
+
return nil unless importer_id&.scheme == SCHEME
|
|
73
|
+
|
|
74
|
+
# "node:fs" parses as a scheme, so it has to be caught before the
|
|
75
|
+
# check that hands other schemes back to the graph.
|
|
76
|
+
raise node_builtin_error(specifier.delete_prefix(NODE_SCHEME), importer_id) if specifier.start_with?(NODE_SCHEME)
|
|
77
|
+
return resolve_private(specifier, dependency, importer_id, context) if specifier.start_with?(PRIVATE_PREFIX)
|
|
78
|
+
return nil if specifier.match?(ModuleId::SCHEME_PATTERN)
|
|
79
|
+
|
|
80
|
+
if specifier.match?(RELATIVE_SPECIFIER_PATTERN)
|
|
81
|
+
resolve_relative(importer_id.merge(specifier), dependency, context)
|
|
82
|
+
else
|
|
83
|
+
resolve_bare(specifier, dependency, importer_id, context)
|
|
84
|
+
end
|
|
85
|
+
rescue ResolveError => error
|
|
86
|
+
raise error.with_resolution_context(dependency: dependency, importer_id: dependency.importer_id)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def load(module_id, context)
|
|
90
|
+
return nil unless module_id.scheme == SCHEME
|
|
91
|
+
|
|
92
|
+
path = absolute_path(module_id, context)
|
|
93
|
+
LoadResult.new(path.read(encoding: "UTF-8"), Hashing.file_hexdigest(path), nil)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
# An "npm://name/subpath" import from application code.
|
|
99
|
+
def resolve_scheme(module_id, dependency, context)
|
|
100
|
+
package_name, subpath = split_package(module_id)
|
|
101
|
+
package_root = root_package_root(package_name, context)
|
|
102
|
+
|
|
103
|
+
resolve_entry(package_name, package_root, subpath, dependency, module_id)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# A bare specifier inside a package names another package, or the
|
|
107
|
+
# package itself.
|
|
108
|
+
def resolve_bare(specifier, dependency, importer_id, context)
|
|
109
|
+
package_name, subpath = split_specifier(specifier)
|
|
110
|
+
importer_package, importer_subpath = split_package(importer_id)
|
|
111
|
+
|
|
112
|
+
package_root =
|
|
113
|
+
if package_name == importer_package
|
|
114
|
+
self_reference_root(package_name, subpath, importer_id, context)
|
|
115
|
+
else
|
|
116
|
+
transitive_package_root(package_name, importer_package, importer_subpath, importer_id, context)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
resolve_entry(package_name, package_root, subpath, dependency, "#{specifier.inspect} in #{importer_id}")
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# A "#"-prefixed specifier resolves against the importing package's own
|
|
123
|
+
# "imports" field, and may land on a file in that package or on another
|
|
124
|
+
# package entirely.
|
|
125
|
+
def resolve_private(specifier, dependency, importer_id, context)
|
|
126
|
+
importer_package, = split_package(importer_id)
|
|
127
|
+
package_root = package_root_for(importer_package, context)
|
|
128
|
+
metadata = package_metadata(importer_package, package_root)
|
|
129
|
+
result = PackageExports.resolve_private(metadata, specifier: specifier, conditions: @conditions)
|
|
130
|
+
requested = "#{specifier.inspect} in #{importer_id}"
|
|
131
|
+
|
|
132
|
+
unless result.resolved?
|
|
133
|
+
raise ResolveError, "Could not resolve #{requested} (#{result.reason}): #{result.message}"
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
target = result.path
|
|
137
|
+
return resolve_bare(target, dependency, importer_id, context) unless target.start_with?("./")
|
|
138
|
+
|
|
139
|
+
resolved(dependency, importer_package, package_root, target.delete_prefix("./"), requested, probe: true)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def resolve_relative(module_id, dependency, context)
|
|
143
|
+
package_name, subpath = split_package(module_id)
|
|
144
|
+
package_root = package_root_for(package_name, context)
|
|
145
|
+
|
|
146
|
+
resolved(dependency, package_name, package_root, subpath.delete_prefix("./"), module_id, probe: true)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def resolve_entry(package_name, package_root, subpath, dependency, requested)
|
|
150
|
+
metadata = package_metadata(package_name, package_root)
|
|
151
|
+
result = PackageExports.resolve(metadata, subpath: subpath, conditions: @conditions)
|
|
152
|
+
|
|
153
|
+
unless result.resolved?
|
|
154
|
+
raise ResolveError, "Could not resolve #{requested} (#{result.reason}): #{result.message}"
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# An "exports" target names an exact file. Only the legacy fields and
|
|
158
|
+
# bare subpaths of packages without "exports" are probed.
|
|
159
|
+
resolved(dependency, package_name, package_root, result.path, requested, probe: !metadata.key?("exports"))
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def resolved(dependency, package_name, package_root, relative_path, requested, probe: false)
|
|
163
|
+
path = package_root.join(relative_path).expand_path
|
|
164
|
+
assert_inside_package!(package_root, path, requested)
|
|
165
|
+
|
|
166
|
+
found = probe ? probe_path(path) : (path if path.file?)
|
|
167
|
+
|
|
168
|
+
unless found
|
|
169
|
+
raise ResolveError, "Could not resolve #{requested}. #{package_name} has no file at #{relative_path}."
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
assert_inside_package!(package_root, found, requested)
|
|
173
|
+
|
|
174
|
+
ResolvedDependency.new(
|
|
175
|
+
dependency,
|
|
176
|
+
module_id_for(package_name, found.relative_path_from(package_root).to_s),
|
|
177
|
+
{scheme: SCHEME, path: found.to_s}
|
|
178
|
+
)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def probe_path(path)
|
|
182
|
+
return path if path.file?
|
|
183
|
+
|
|
184
|
+
CANDIDATE_EXTENSIONS.each do |extension|
|
|
185
|
+
candidate = Pathname.new("#{path}#{extension}")
|
|
186
|
+
return candidate if candidate.file?
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
return nil unless path.directory?
|
|
190
|
+
|
|
191
|
+
INDEX_BASENAMES.each do |basename|
|
|
192
|
+
candidate = path.join(basename)
|
|
193
|
+
return candidate if candidate.file?
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
nil
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def absolute_path(module_id, context)
|
|
200
|
+
package_name, subpath = split_package(module_id)
|
|
201
|
+
package_root = package_root_for(package_name, context)
|
|
202
|
+
path = package_root.join(subpath.delete_prefix("./")).expand_path
|
|
203
|
+
assert_inside_package!(package_root, path, module_id)
|
|
204
|
+
|
|
205
|
+
path
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# "npm://gl-matrix/esm/vec3.js" is package "gl-matrix" and subpath
|
|
209
|
+
# "./esm/vec3.js". Scoped packages put the scope in the authority, so
|
|
210
|
+
# "npm://@scope/pkg/sub.js" spends its first path segment on the name.
|
|
211
|
+
def split_package(module_id)
|
|
212
|
+
host = module_id.host.to_s
|
|
213
|
+
raise ResolveError, "npm imports need a package name: #{module_id}" if host.empty?
|
|
214
|
+
|
|
215
|
+
segments = module_id.relative_path.split("/")
|
|
216
|
+
|
|
217
|
+
if host.start_with?("@")
|
|
218
|
+
scoped_name = segments.shift
|
|
219
|
+
raise ResolveError, "Scoped npm imports need a package name: #{module_id}" if scoped_name.to_s.empty?
|
|
220
|
+
|
|
221
|
+
package_name = "#{host}/#{scoped_name}"
|
|
222
|
+
else
|
|
223
|
+
package_name = host
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
[package_name, subpath_for(segments)]
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# "@scope/pkg/sub.js" is package "@scope/pkg" and subpath "./sub.js".
|
|
230
|
+
def split_specifier(specifier)
|
|
231
|
+
segments = specifier.split("/")
|
|
232
|
+
package_name = segments.shift
|
|
233
|
+
|
|
234
|
+
if package_name.start_with?("@")
|
|
235
|
+
scoped_name = segments.shift
|
|
236
|
+
raise ResolveError, "Scoped npm imports need a package name: #{specifier}" if scoped_name.to_s.empty?
|
|
237
|
+
|
|
238
|
+
package_name = "#{package_name}/#{scoped_name}"
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
[package_name, subpath_for(segments)]
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def subpath_for(segments)
|
|
245
|
+
segments.empty? ? "." : "./#{segments.join("/")}"
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def module_id_for(package_name, relative_path)
|
|
249
|
+
ModuleId.new("npm://#{package_name}/#{relative_path}")
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Node only allows a package to import itself by name when it declares
|
|
253
|
+
# "exports", because that field is what defines its own entry points.
|
|
254
|
+
def self_reference_root(package_name, subpath, importer_id, context)
|
|
255
|
+
package_root = package_root_for(package_name, context)
|
|
256
|
+
|
|
257
|
+
unless package_metadata(package_name, package_root).key?("exports")
|
|
258
|
+
raise ResolveError,
|
|
259
|
+
"#{importer_id} imports its own package as #{package_name.inspect}, " \
|
|
260
|
+
"but #{package_name} has no exports field to resolve #{subpath.inspect} against."
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
package_root
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def transitive_package_root(package_name, importer_package, importer_subpath, importer_id, context)
|
|
267
|
+
importer_root = package_root_for(importer_package, context)
|
|
268
|
+
start = importer_root.join(importer_subpath.delete_prefix("./")).dirname
|
|
269
|
+
|
|
270
|
+
each_node_modules_directory(start, context) do |directory|
|
|
271
|
+
candidate = directory.join(package_name)
|
|
272
|
+
return register_package_root(package_name, candidate) if candidate.directory?
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
raise node_builtin_error(package_name, importer_id) if NODE_BUILTINS.include?(package_name)
|
|
276
|
+
|
|
277
|
+
raise ResolveError,
|
|
278
|
+
"Could not find package #{package_name.inspect} imported by #{importer_id}. " \
|
|
279
|
+
"Is it a dependency of #{importer_package}?"
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def node_builtin_error(name, importer_id)
|
|
283
|
+
ResolveError.new(
|
|
284
|
+
"#{importer_id} imports the Node builtin #{name.inspect}, which has no browser equivalent. " \
|
|
285
|
+
"Klenod bundles for the browser, so packages that need Node builtins cannot be used."
|
|
286
|
+
)
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# Node's lookup: every parent directory contributes its "node_modules",
|
|
290
|
+
# nearest first. The configured root is tried last so an explicit root:
|
|
291
|
+
# still applies when it sits outside the importer's tree.
|
|
292
|
+
def each_node_modules_directory(start, context)
|
|
293
|
+
start.ascend do |directory|
|
|
294
|
+
next if directory.basename.to_s == DIRECTORY_NAME
|
|
295
|
+
|
|
296
|
+
candidate = directory.join(DIRECTORY_NAME)
|
|
297
|
+
yield candidate if candidate.directory?
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
root = node_modules_root(context)
|
|
301
|
+
yield root if root.directory?
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def package_root_for(package_name, context)
|
|
305
|
+
@package_roots[package_name] || root_package_root(package_name, context)
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def root_package_root(package_name, context)
|
|
309
|
+
return @package_roots.fetch(package_name) if @package_roots.key?(package_name)
|
|
310
|
+
|
|
311
|
+
root = node_modules_root(context)
|
|
312
|
+
path = root.join(package_name)
|
|
313
|
+
|
|
314
|
+
unless path.directory?
|
|
315
|
+
raise ResolveError, "Could not find package #{package_name.inspect} in #{root}. Is it installed?"
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
register_package_root(package_name, path)
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# Module IDs carry only a package name, so a name has to mean one
|
|
322
|
+
# directory. Resolve symlinks first, so pnpm's store and workspace
|
|
323
|
+
# links are recognised as the same package rather than two copies.
|
|
324
|
+
def register_package_root(package_name, path)
|
|
325
|
+
path = path.realpath
|
|
326
|
+
existing = @package_roots[package_name]
|
|
327
|
+
|
|
328
|
+
if existing && existing != path
|
|
329
|
+
raise ResolveError,
|
|
330
|
+
"Found two copies of #{package_name.inspect}:\n #{existing}\n #{path}\n" \
|
|
331
|
+
"Klenod can only bundle one copy of a package. Deduplicate it in your lockfile."
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
@package_roots[package_name] = path
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def package_metadata(package_name, package_root)
|
|
338
|
+
@package_metadata.fetch(package_name) do
|
|
339
|
+
path = package_root.join("package.json")
|
|
340
|
+
|
|
341
|
+
raise ResolveError, "Package #{package_name.inspect} has no package.json at #{path}." unless path.file?
|
|
342
|
+
|
|
343
|
+
@package_metadata[package_name] = JSON.parse(path.read(encoding: "UTF-8"))
|
|
344
|
+
rescue JSON::ParserError => error
|
|
345
|
+
raise ResolveError, "Could not parse #{path}: #{error.message}"
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def node_modules_root(context)
|
|
350
|
+
return @root if @root
|
|
351
|
+
|
|
352
|
+
@node_modules_root = discover_root(context.source_dir) unless defined?(@node_modules_root)
|
|
353
|
+
|
|
354
|
+
@node_modules_root || raise(ResolveError, "Could not find a #{DIRECTORY_NAME} directory above #{context.source_dir}.")
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def discover_root(source_dir)
|
|
358
|
+
Pathname.new(source_dir).expand_path.ascend do |directory|
|
|
359
|
+
candidate = directory.join(DIRECTORY_NAME)
|
|
360
|
+
return candidate if candidate.directory?
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
nil
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def assert_inside_package!(package_root, path, requested)
|
|
367
|
+
prefix = "#{package_root}#{File::SEPARATOR}"
|
|
368
|
+
return if path.to_s.start_with?(prefix)
|
|
369
|
+
|
|
370
|
+
raise ResolveError, "npm import escapes its package: #{requested}"
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: klenod-plugin-javascript
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.13
|
|
5
5
|
platform: x86_64-linux-gnu
|
|
6
6
|
authors:
|
|
7
7
|
- Andrés Alin
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-10 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: klenod-build
|
|
@@ -16,14 +16,14 @@ dependencies:
|
|
|
16
16
|
requirements:
|
|
17
17
|
- - '='
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: 0.0.
|
|
19
|
+
version: 0.0.13
|
|
20
20
|
type: :runtime
|
|
21
21
|
prerelease: false
|
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
23
|
requirements:
|
|
24
24
|
- - '='
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: 0.0.
|
|
26
|
+
version: 0.0.13
|
|
27
27
|
- !ruby/object:Gem::Dependency
|
|
28
28
|
name: rake-compiler
|
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -47,6 +47,8 @@ extra_rdoc_files: []
|
|
|
47
47
|
files:
|
|
48
48
|
- README.md
|
|
49
49
|
- lib/klenod/build/plugins/javascript_plugin.rb
|
|
50
|
+
- lib/klenod/build/plugins/node_modules_plugin.rb
|
|
51
|
+
- lib/klenod/build/plugins/node_modules_plugin/package_exports.rb
|
|
50
52
|
- lib/klenod/plugin/javascript.rb
|
|
51
53
|
- lib/klenod/plugin/javascript/native.so
|
|
52
54
|
- lib/klenod/plugin/javascript/parser.rb
|