bake-node 0.0.1
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 +7 -0
- data/bake/node/importmap.rb +17 -0
- data/bake/node/packages.rb +24 -0
- data/bake/node.rb +24 -0
- data/guides/getting-started/readme.md +125 -0
- data/guides/internal-packages/readme.md +110 -0
- data/guides/links.yaml +6 -0
- data/guides/static-packages/readme.md +134 -0
- data/lib/bake/node/configuration.rb +142 -0
- data/lib/bake/node/controller.rb +72 -0
- data/lib/bake/node/errors.rb +24 -0
- data/lib/bake/node/manifest.rb +97 -0
- data/lib/bake/node/package.rb +82 -0
- data/lib/bake/node/package_manager.rb +121 -0
- data/lib/bake/node/static.rb +222 -0
- data/lib/bake/node/version.rb +12 -0
- data/lib/bake/node.rb +13 -0
- data/license.md +21 -0
- data/readme.md +70 -0
- data/releases.md +8 -0
- metadata +75 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require "digest"
|
|
7
|
+
require "json"
|
|
8
|
+
require "pathname"
|
|
9
|
+
|
|
10
|
+
require_relative "errors"
|
|
11
|
+
|
|
12
|
+
module Bake
|
|
13
|
+
module Node
|
|
14
|
+
# Records the content and import mappings of a static package projection.
|
|
15
|
+
class Manifest
|
|
16
|
+
FILENAME = ".bake-node.json"
|
|
17
|
+
|
|
18
|
+
# Build a deterministic manifest.
|
|
19
|
+
# @parameter base [String] The public URL prefix for static packages.
|
|
20
|
+
# @parameter imports [Hash(String, String)] The import-map entries.
|
|
21
|
+
# @parameter packages [Hash(String, Hash)] The installed package metadata.
|
|
22
|
+
# @returns [Manifest] The generated manifest.
|
|
23
|
+
def self.build(base:, imports:, packages:)
|
|
24
|
+
data = {
|
|
25
|
+
"format" => 1,
|
|
26
|
+
"base" => base,
|
|
27
|
+
"imports" => imports.sort.to_h,
|
|
28
|
+
"packages" => packages.sort.to_h,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
data["digest"] = Digest::SHA256.hexdigest(JSON.generate(data))
|
|
32
|
+
new(data)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Load a manifest from a static output directory.
|
|
36
|
+
# @parameter root [String | Pathname] The static output directory.
|
|
37
|
+
# @returns [Manifest] The loaded manifest.
|
|
38
|
+
# @raises [CheckError] If the manifest is missing or malformed.
|
|
39
|
+
def self.load(root)
|
|
40
|
+
path = Pathname.new(root) + FILENAME
|
|
41
|
+
new(JSON.parse(path.read))
|
|
42
|
+
rescue Errno::ENOENT
|
|
43
|
+
raise CheckError, "Static package manifest does not exist at #{path}!"
|
|
44
|
+
rescue JSON::ParserError => error
|
|
45
|
+
raise CheckError, "Could not parse #{path}: #{error.message}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Initialize a manifest with its serialized data.
|
|
49
|
+
# @parameter data [Hash] The manifest data.
|
|
50
|
+
def initialize(data)
|
|
51
|
+
@data = data
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @attribute [Hash] The serialized manifest data.
|
|
55
|
+
attr :data
|
|
56
|
+
|
|
57
|
+
# Write the manifest into a static output directory.
|
|
58
|
+
# @parameter root [String | Pathname] The static output directory.
|
|
59
|
+
# @returns [Integer] The number of bytes written.
|
|
60
|
+
def write(root)
|
|
61
|
+
path = Pathname.new(root) + FILENAME
|
|
62
|
+
path.write(JSON.pretty_generate(@data) + "\n")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Extract the browser import map.
|
|
66
|
+
# @returns [Hash] An import-map object containing the configured imports.
|
|
67
|
+
def import_map
|
|
68
|
+
{"imports" => @data.fetch("imports", {})}
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Check whether every manifested file exists with the expected content.
|
|
72
|
+
# @parameter root [String | Pathname] The static output directory.
|
|
73
|
+
# @returns [Boolean] Whether the directory exactly matches the manifest.
|
|
74
|
+
def valid_tree?(root)
|
|
75
|
+
root = Pathname.new(root)
|
|
76
|
+
expected = []
|
|
77
|
+
|
|
78
|
+
@data.fetch("packages").each do |name, package|
|
|
79
|
+
package.fetch("files").each do |relative_path, digest|
|
|
80
|
+
path = root + name + relative_path
|
|
81
|
+
expected << path.relative_path_from(root).to_s
|
|
82
|
+
|
|
83
|
+
return false unless path.file?
|
|
84
|
+
return false unless Digest::SHA256.file(path).hexdigest == digest
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
actual = root.glob("**/*", File::FNM_DOTMATCH).select(&:file?).map do |path|
|
|
89
|
+
path.relative_path_from(root).to_s
|
|
90
|
+
end
|
|
91
|
+
actual.delete(FILENAME)
|
|
92
|
+
|
|
93
|
+
actual.sort == expected.sort
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require "pathname"
|
|
7
|
+
|
|
8
|
+
require_relative "errors"
|
|
9
|
+
|
|
10
|
+
module Bake
|
|
11
|
+
module Node
|
|
12
|
+
# Describes how one installed Node.js package is exposed as static files.
|
|
13
|
+
class Package
|
|
14
|
+
NAME_PATTERN = /\A(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+\z/i
|
|
15
|
+
|
|
16
|
+
# Initialize a package selection.
|
|
17
|
+
# @parameter name [String] The npm package name, including an optional scope.
|
|
18
|
+
# @parameter options [Hash] The package's source, include patterns and import mappings.
|
|
19
|
+
# @raises [ConfigurationError] If the package name or options are invalid.
|
|
20
|
+
def initialize(name, options = {})
|
|
21
|
+
unless name.is_a?(String) && NAME_PATTERN.match?(name)
|
|
22
|
+
raise ConfigurationError, "Invalid package name: #{name.inspect}!"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
unless options.is_a?(Hash)
|
|
26
|
+
raise ConfigurationError, "Configuration for #{name} must be an object!"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
@name = name
|
|
30
|
+
@source = options["source"]
|
|
31
|
+
@include_patterns = options["include"]
|
|
32
|
+
@imports = options.fetch("imports", {})
|
|
33
|
+
|
|
34
|
+
validate
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @attribute [String] The npm package name.
|
|
38
|
+
attr :name
|
|
39
|
+
|
|
40
|
+
# @attribute [String | Nil] The configured package-relative source directory.
|
|
41
|
+
attr :source
|
|
42
|
+
|
|
43
|
+
# @attribute [Array(String) | Nil] The package-relative file patterns to include.
|
|
44
|
+
attr :include_patterns
|
|
45
|
+
|
|
46
|
+
# @attribute [Hash(String, String)] The import specifiers exposed by the package.
|
|
47
|
+
attr :imports
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def validate
|
|
52
|
+
validate_relative_path(@source, "source") if @source
|
|
53
|
+
|
|
54
|
+
if @include_patterns
|
|
55
|
+
unless @include_patterns.is_a?(Array) && @include_patterns.any?
|
|
56
|
+
raise ConfigurationError, "The include patterns for #{@name} must be a non-empty array!"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
@include_patterns.each do |pattern|
|
|
60
|
+
validate_relative_path(pattern, "include pattern")
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
unless @imports.is_a?(Hash) && @imports.all?{|key, value| key.is_a?(String) && value.is_a?(String)}
|
|
65
|
+
raise ConfigurationError, "The imports for #{@name} must map strings to strings!"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def validate_relative_path(path, description)
|
|
70
|
+
unless path.is_a?(String) && !path.empty?
|
|
71
|
+
raise ConfigurationError, "The #{description} for #{@name} must be a non-empty string!"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
pathname = Pathname.new(path)
|
|
75
|
+
|
|
76
|
+
if pathname.absolute? || pathname.each_filename.any?{|component| component == ".."}
|
|
77
|
+
raise ConfigurationError, "The #{description} for #{@name} must remain within the package: #{path.inspect}!"
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require_relative "errors"
|
|
7
|
+
|
|
8
|
+
module Bake
|
|
9
|
+
module Node
|
|
10
|
+
# Detects and invokes npm-compatible package managers without replacing them.
|
|
11
|
+
class PackageManager
|
|
12
|
+
LOCK_FILES = {
|
|
13
|
+
"package-lock.json" => "npm",
|
|
14
|
+
"npm-shrinkwrap.json" => "npm",
|
|
15
|
+
"pnpm-lock.yaml" => "pnpm",
|
|
16
|
+
"yarn.lock" => "yarn",
|
|
17
|
+
"bun.lock" => "bun",
|
|
18
|
+
"bun.lockb" => "bun",
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
SUPPORTED = ["npm", "pnpm", "yarn", "bun"].freeze
|
|
22
|
+
|
|
23
|
+
# Detect the package manager selected by a project.
|
|
24
|
+
# @parameter configuration [Configuration] The project configuration.
|
|
25
|
+
# @parameter environment [Hash] Environment variables used for explicit selection.
|
|
26
|
+
# @returns [PackageManager] The selected package-manager wrapper.
|
|
27
|
+
# @raises [ConfigurationError] If selection is ambiguous or unsupported.
|
|
28
|
+
def self.detect(configuration, environment: ENV)
|
|
29
|
+
if name = environment["NODE_PACKAGE_MANAGER"]
|
|
30
|
+
return new(configuration.root, name)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
if declaration = configuration.package_json["packageManager"]
|
|
34
|
+
unless declaration.is_a?(String)
|
|
35
|
+
raise ConfigurationError, "packageManager must be a string!"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
return new(configuration.root, declaration.split("@", 2).first)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
managers = LOCK_FILES.filter_map do |path, manager|
|
|
42
|
+
manager if (configuration.root + path).file?
|
|
43
|
+
end.uniq
|
|
44
|
+
|
|
45
|
+
if managers.size > 1
|
|
46
|
+
raise ConfigurationError, "Multiple package manager lock files were found: #{managers.join(', ')}!"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
new(configuration.root, managers.first || "npm")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Initialize a package-manager wrapper.
|
|
53
|
+
# @parameter root [String | Pathname] The directory in which commands will run.
|
|
54
|
+
# @parameter name [String] The package-manager executable name.
|
|
55
|
+
# @raises [ConfigurationError] If the package manager is unsupported.
|
|
56
|
+
def initialize(root, name)
|
|
57
|
+
unless SUPPORTED.include?(name)
|
|
58
|
+
raise ConfigurationError, "Unsupported package manager: #{name.inspect}!"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
@root = root
|
|
62
|
+
@name = name
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# @attribute [String | Pathname] The directory in which commands run.
|
|
66
|
+
attr :root
|
|
67
|
+
|
|
68
|
+
# @attribute [String] The selected package-manager name.
|
|
69
|
+
attr :name
|
|
70
|
+
|
|
71
|
+
# Install the project's dependencies.
|
|
72
|
+
# @parameter frozen [Boolean] Whether the lock file must remain unchanged.
|
|
73
|
+
# @raises [Error] If the package-manager command fails.
|
|
74
|
+
def install(frozen: false)
|
|
75
|
+
execute(install_command(frozen: frozen))
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Run a package script.
|
|
79
|
+
# @parameter script [String] The script name from `package.json`.
|
|
80
|
+
# @raises [ArgumentError] If the script name is empty.
|
|
81
|
+
# @raises [Error] If the package-manager command fails.
|
|
82
|
+
def run(script)
|
|
83
|
+
unless script.is_a?(String) && !script.empty?
|
|
84
|
+
raise ArgumentError, "Script must be a non-empty string!"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
execute([@name, "run", script])
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Construct the installation command for the selected package manager.
|
|
91
|
+
# @parameter frozen [Boolean] Whether the lock file must remain unchanged.
|
|
92
|
+
# @returns [Array(String)] The command and arguments to execute.
|
|
93
|
+
def install_command(frozen: false)
|
|
94
|
+
command = case @name
|
|
95
|
+
when "npm"
|
|
96
|
+
frozen ? ["npm", "ci"] : ["npm", "install"]
|
|
97
|
+
when "pnpm"
|
|
98
|
+
["pnpm", "install"]
|
|
99
|
+
when "yarn"
|
|
100
|
+
["yarn", "install"]
|
|
101
|
+
when "bun"
|
|
102
|
+
["bun", "install"]
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
if frozen && @name != "npm"
|
|
106
|
+
command << (@name == "yarn" ? "--immutable" : "--frozen-lockfile")
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
command
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
private
|
|
113
|
+
|
|
114
|
+
def execute(command)
|
|
115
|
+
unless system(*command, chdir: @root.to_s)
|
|
116
|
+
raise Error, "Command failed: #{command.join(' ')}"
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require "digest"
|
|
7
|
+
require "fileutils"
|
|
8
|
+
require "json"
|
|
9
|
+
require "pathname"
|
|
10
|
+
require "tmpdir"
|
|
11
|
+
|
|
12
|
+
require_relative "errors"
|
|
13
|
+
require_relative "manifest"
|
|
14
|
+
|
|
15
|
+
module Bake
|
|
16
|
+
module Node
|
|
17
|
+
# Builds and validates deterministic static projections of installed packages.
|
|
18
|
+
class Static
|
|
19
|
+
EXCLUDED_COMPONENTS = [".git", "node_modules"].freeze
|
|
20
|
+
|
|
21
|
+
# Initialize a static package builder.
|
|
22
|
+
# @parameter configuration [Configuration] The project configuration.
|
|
23
|
+
# @parameter output [String | Nil] An optional project-relative output directory.
|
|
24
|
+
def initialize(configuration, output: nil)
|
|
25
|
+
@configuration = configuration
|
|
26
|
+
@output = configuration.output_path(output)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @attribute [Configuration] The project configuration.
|
|
30
|
+
attr :configuration
|
|
31
|
+
|
|
32
|
+
# @attribute [Pathname] The static output directory.
|
|
33
|
+
attr :output
|
|
34
|
+
|
|
35
|
+
# Build and atomically replace the static package projection.
|
|
36
|
+
# @returns [Manifest] The generated manifest.
|
|
37
|
+
# @raises [PackageError] If an installed package cannot be materialized safely.
|
|
38
|
+
def update
|
|
39
|
+
FileUtils.mkdir_p(@output.dirname)
|
|
40
|
+
|
|
41
|
+
Dir.mktmpdir(".bake-node-", @output.dirname.to_s) do |temporary_root|
|
|
42
|
+
temporary_root = Pathname.new(temporary_root)
|
|
43
|
+
staging = temporary_root + "static"
|
|
44
|
+
backup = temporary_root + "backup"
|
|
45
|
+
|
|
46
|
+
build(staging)
|
|
47
|
+
replace(staging, backup)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
Manifest.load(@output)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Check whether the static package projection is current.
|
|
54
|
+
# @returns [Boolean] Whether the installed projection matches the desired output.
|
|
55
|
+
# @raises [CheckError] If the current manifest is missing or malformed.
|
|
56
|
+
def check
|
|
57
|
+
installed = Manifest.load(@output)
|
|
58
|
+
|
|
59
|
+
Dir.mktmpdir("bake-node-check-") do |temporary_root|
|
|
60
|
+
desired = build(Pathname.new(temporary_root) + "static")
|
|
61
|
+
|
|
62
|
+
return installed.data == desired.data && installed.valid_tree?(@output)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Require the static package projection to be current.
|
|
67
|
+
# @returns [Boolean] `true` when the projection is current.
|
|
68
|
+
# @raises [CheckError] If the projection is missing or out of date.
|
|
69
|
+
def check!
|
|
70
|
+
unless check
|
|
71
|
+
raise CheckError, "Static Node.js packages are out of date. Run `bake node:packages:static`."
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
true
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def build(destination)
|
|
80
|
+
FileUtils.mkdir_p(destination)
|
|
81
|
+
|
|
82
|
+
packages = {}
|
|
83
|
+
imports = {}
|
|
84
|
+
|
|
85
|
+
@configuration.packages.each_value do |package|
|
|
86
|
+
package_data, package_imports = install(package, destination)
|
|
87
|
+
packages[package.name] = package_data
|
|
88
|
+
|
|
89
|
+
package_imports.each do |specifier, path|
|
|
90
|
+
if imports.key?(specifier)
|
|
91
|
+
raise ConfigurationError, "Import specifier #{specifier.inspect} is configured more than once!"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
imports[specifier] = path
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
manifest = Manifest.build(base: @configuration.base, imports: imports, packages: packages)
|
|
99
|
+
manifest.write(destination)
|
|
100
|
+
manifest
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def install(package, destination)
|
|
104
|
+
package_path = @configuration.package_root + package.name
|
|
105
|
+
|
|
106
|
+
unless package_path.directory?
|
|
107
|
+
raise PackageError, "Package #{package.name} was not found at #{package_path}!"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
package_root = package_path.realpath
|
|
111
|
+
source = package.source || default_source(package_root)
|
|
112
|
+
source_root = resolve_within(package_root, source, "source for #{package.name}")
|
|
113
|
+
|
|
114
|
+
unless source_root.directory?
|
|
115
|
+
raise PackageError, "Package source does not exist for #{package.name}: #{source_root}!"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
paths = included_paths(package, source_root)
|
|
119
|
+
install_root = destination + package.name
|
|
120
|
+
files = {}
|
|
121
|
+
|
|
122
|
+
paths.each do |relative_path|
|
|
123
|
+
source_path = source_root + relative_path
|
|
124
|
+
install_path = install_root + relative_path
|
|
125
|
+
|
|
126
|
+
FileUtils.mkdir_p(install_path.dirname)
|
|
127
|
+
FileUtils.cp(source_path, install_path)
|
|
128
|
+
files[relative_path] = Digest::SHA256.file(install_path).hexdigest
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
package_json = read_package_json(package_root)
|
|
132
|
+
imports = resolve_imports(package, install_root)
|
|
133
|
+
|
|
134
|
+
[
|
|
135
|
+
{
|
|
136
|
+
"version" => package_json["version"],
|
|
137
|
+
"source" => source,
|
|
138
|
+
"files" => files.sort.to_h,
|
|
139
|
+
},
|
|
140
|
+
imports,
|
|
141
|
+
]
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def default_source(package_root)
|
|
145
|
+
(package_root + "dist").directory? ? "dist" : "."
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def included_paths(package, source_root)
|
|
149
|
+
patterns = package.include_patterns || ["**/*"]
|
|
150
|
+
|
|
151
|
+
paths = patterns.flat_map do |pattern|
|
|
152
|
+
Dir.glob(pattern, File::FNM_DOTMATCH, base: source_root.to_s)
|
|
153
|
+
end.uniq.sort.select do |relative_path|
|
|
154
|
+
components = Pathname.new(relative_path).each_filename.to_a
|
|
155
|
+
next false if components.any?{|component| EXCLUDED_COMPONENTS.include?(component)}
|
|
156
|
+
|
|
157
|
+
path = source_root + relative_path
|
|
158
|
+
next false unless path.file?
|
|
159
|
+
|
|
160
|
+
resolve_within(source_root, relative_path, "included file for #{package.name}")
|
|
161
|
+
true
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
if paths.empty?
|
|
165
|
+
raise PackageError, "No files matched for #{package.name}!"
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
paths
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def resolve_imports(package, install_root)
|
|
172
|
+
package.imports.sort.to_h do |specifier, value|
|
|
173
|
+
if value.match?(/\A(?:[a-z][a-z0-9+.-]*:|\/\/|\/)/i)
|
|
174
|
+
[specifier, value]
|
|
175
|
+
else
|
|
176
|
+
path = install_root + value
|
|
177
|
+
|
|
178
|
+
unless path.file?
|
|
179
|
+
raise PackageError, "Import #{specifier.inspect} refers to a file which was not installed: #{value.inspect}!"
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
base = @configuration.base
|
|
183
|
+
[specifier, "#{base}#{package.name}/#{value}"]
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def read_package_json(package_root)
|
|
189
|
+
path = package_root + "package.json"
|
|
190
|
+
return {} unless path.file?
|
|
191
|
+
|
|
192
|
+
JSON.parse(path.read)
|
|
193
|
+
rescue JSON::ParserError => error
|
|
194
|
+
raise PackageError, "Could not parse #{path}: #{error.message}"
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def resolve_within(root, relative_path, description)
|
|
198
|
+
path = (root + relative_path).realpath
|
|
199
|
+
prefix = root.to_s + File::SEPARATOR
|
|
200
|
+
|
|
201
|
+
unless path == root || path.to_s.start_with?(prefix)
|
|
202
|
+
raise PackageError, "The #{description} escapes #{root}: #{relative_path.inspect}!"
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
path
|
|
206
|
+
rescue Errno::ENOENT
|
|
207
|
+
raise PackageError, "The #{description} does not exist: #{relative_path.inspect}!"
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def replace(staging, backup)
|
|
211
|
+
File.rename(@output, backup) if @output.exist?
|
|
212
|
+
|
|
213
|
+
begin
|
|
214
|
+
File.rename(staging, @output)
|
|
215
|
+
rescue Exception
|
|
216
|
+
File.rename(backup, @output) if backup.exist? && !@output.exist?
|
|
217
|
+
raise
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
data/lib/bake/node.rb
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require_relative "node/version"
|
|
7
|
+
require_relative "node/errors"
|
|
8
|
+
require_relative "node/package"
|
|
9
|
+
require_relative "node/configuration"
|
|
10
|
+
require_relative "node/package_manager"
|
|
11
|
+
require_relative "node/manifest"
|
|
12
|
+
require_relative "node/static"
|
|
13
|
+
require_relative "node/controller"
|
data/license.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
|
|
3
|
+
Copyright, 2026, by Samuel Williams.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/readme.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Bake Node
|
|
2
|
+
|
|
3
|
+
Bake Node integrates Node.js packages into Ruby projects using [Bake](https://github.com/ioquatix/bake). It delegates installation and scripts to npm-compatible package managers, then materializes selected packages as deterministic static assets.
|
|
4
|
+
|
|
5
|
+
[](https://github.com/socketry/bake-node/actions?workflow=Test)
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- Supports npm, pnpm, Yarn and Bun without replacing their package-management behavior.
|
|
10
|
+
- Treats registry dependencies and local workspace packages consistently through `node_modules`.
|
|
11
|
+
- Copies complete distributions or carefully selected files into `public/_components`.
|
|
12
|
+
- Generates a deterministic manifest and browser import map.
|
|
13
|
+
- Provides Bake tasks for installation, scripts, static deployment and verification.
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
Please see the [project documentation](https://socketry.github.io/bake-node/) for more details.
|
|
18
|
+
|
|
19
|
+
- [Getting Started](https://socketry.github.io/bake-node/guides/getting-started/index) - This guide explains how to use `bake-node` to install an external JavaScript dependency and expose it as static assets from a Ruby project.
|
|
20
|
+
|
|
21
|
+
- [Internal Packages](https://socketry.github.io/bake-node/guides/internal-packages/index) - This guide explains how to organize JavaScript developed inside a Ruby project as independent workspace packages while using Bake Node for testing and static deployment.
|
|
22
|
+
|
|
23
|
+
- [Static Packages](https://socketry.github.io/bake-node/guides/static-packages/index) - This guide explains how to control which installed package files are deployed, generate browser import maps and verify the resulting static projection.
|
|
24
|
+
|
|
25
|
+
## Releases
|
|
26
|
+
|
|
27
|
+
Please see the [project releases](https://socketry.github.io/bake-node/releases/index) for all releases.
|
|
28
|
+
|
|
29
|
+
### v0.0.1
|
|
30
|
+
|
|
31
|
+
- [Added](https://socketry.github.io/bake-node/releases/index#added)
|
|
32
|
+
|
|
33
|
+
## Contributing
|
|
34
|
+
|
|
35
|
+
We welcome contributions to this project.
|
|
36
|
+
|
|
37
|
+
1. Fork the repository.
|
|
38
|
+
2. Create your feature branch (`git checkout -b my-new-feature`).
|
|
39
|
+
3. Commit your changes (`git commit -am 'Add some feature.'`).
|
|
40
|
+
4. Push to the branch (`git push origin my-new-feature`).
|
|
41
|
+
5. Create a new pull request.
|
|
42
|
+
|
|
43
|
+
### Running Tests
|
|
44
|
+
|
|
45
|
+
To run the test suite:
|
|
46
|
+
|
|
47
|
+
``` bash
|
|
48
|
+
$ bundle exec bake test
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Making Releases
|
|
52
|
+
|
|
53
|
+
To make a new release:
|
|
54
|
+
|
|
55
|
+
``` bash
|
|
56
|
+
$ bundle exec bake gem:release:patch # or minor or major
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Developer Certificate of Origin
|
|
60
|
+
|
|
61
|
+
In order to protect users of this project, we require all contributors to comply with the [Developer Certificate of Origin](https://developercertificate.org/). This ensures that all contributions are properly licensed and attributed.
|
|
62
|
+
|
|
63
|
+
### Community Guidelines
|
|
64
|
+
|
|
65
|
+
This project is best served by a collaborative and respectful environment. Treat each other professionally, respect differing viewpoints, and engage constructively. Harassment, discrimination, or harmful behavior is not tolerated. Communicate clearly, listen actively, and support one another. If any issues arise, please inform the project maintainers.
|
|
66
|
+
|
|
67
|
+
## See Also
|
|
68
|
+
|
|
69
|
+
- [Bake](https://github.com/ioquatix/bake) — Ruby task execution.
|
|
70
|
+
- [Utopia](https://github.com/socketry/utopia) — The original static component installer.
|