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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: af22a090b1db8846c8025638f07a4bd25dfc4bfb375706b2036181d0213709ab
4
+ data.tar.gz: eee3a42a3cfef2b7e51b2dfccec24cfd3cc800ff13f1010bfa567d4e1e487a5d
5
+ SHA512:
6
+ metadata.gz: a1f3644694f0ac6b95c13e2feef8a13209fff23b512ab7a1076488040d0c520fa60bccbc2d1b0719f296c0f0b9952fb1cc263a8abb5e9544b626a039ed856383
7
+ data.tar.gz: 65c40944f288ad631a07fb913c62dde3af4d1c10494641a1b9a42ab98ac9bb07a6adb691812c74089818de10c4b5e0247a98bb599df1481783be9703c77d06f6
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ def initialize(context)
7
+ super
8
+
9
+ require "bake/node/controller"
10
+ end
11
+
12
+ # Print the import map for the materialized static packages.
13
+ # @parameter root [String] The directory containing package.json.
14
+ # @parameter output [String | Nil] Override the configured output directory.
15
+ def show(root: context.root, output: nil)
16
+ puts Bake::Node::Controller.new(root).import_map(output: output)
17
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ def initialize(context)
7
+ super
8
+
9
+ require "bake/node/controller"
10
+ end
11
+
12
+ # Materialize configured Node.js packages as static files.
13
+ # @parameter root [String] The directory containing package.json.
14
+ # @parameter output [String | Nil] Override the configured output directory.
15
+ def static(root: context.root, output: nil)
16
+ Bake::Node::Controller.new(root).static(output: output)
17
+ end
18
+
19
+ # Check that the materialized static packages are current.
20
+ # @parameter root [String] The directory containing package.json.
21
+ # @parameter output [String | Nil] Override the configured output directory.
22
+ def check(root: context.root, output: nil)
23
+ Bake::Node::Controller.new(root).check(output: output)
24
+ end
data/bake/node.rb ADDED
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ def initialize(context)
7
+ super
8
+
9
+ require "bake/node/controller"
10
+ end
11
+
12
+ # Install packages using the configured package manager.
13
+ # @parameter root [String] The directory containing package.json.
14
+ # @parameter frozen [Boolean] Require the lock file to remain unchanged.
15
+ def install(root: context.root, frozen: false)
16
+ Bake::Node::Controller.new(root).install(frozen: frozen)
17
+ end
18
+
19
+ # Run a package script using the configured package manager.
20
+ # @parameter script [String] The package script to run.
21
+ # @parameter root [String] The directory containing package.json.
22
+ def test(script: "test", root: context.root)
23
+ Bake::Node::Controller.new(root).run(script)
24
+ end
@@ -0,0 +1,125 @@
1
+ # Getting Started
2
+
3
+ This guide explains how to use `bake-node` to install an external JavaScript dependency and expose it as static assets from a Ruby project.
4
+
5
+ ## Installation
6
+
7
+ Add the gem to your project:
8
+
9
+ ~~~ bash
10
+ $ bundle add bake-node
11
+ ~~~
12
+
13
+ Your project also needs Node.js and one supported package manager: npm, pnpm, Yarn or Bun. Bake Node delegates dependency resolution and script execution to that package manager.
14
+
15
+ ## Add a JavaScript Dependency
16
+
17
+ Ruby applications commonly need browser libraries without needing a JavaScript bundler. Declare those libraries as regular production dependencies in `package.json`:
18
+
19
+ ~~~ json
20
+ {
21
+ "private": true,
22
+ "packageManager": "pnpm@10",
23
+ "dependencies": {
24
+ "morphdom": "^2.7"
25
+ }
26
+ }
27
+ ~~~
28
+
29
+ Install the dependencies using the detected package manager:
30
+
31
+ ~~~ bash
32
+ $ bundle exec bake node:install
33
+ ~~~
34
+
35
+ The package manager remains responsible for its lock file and `node_modules`. Use immutable installation in CI:
36
+
37
+ ~~~ bash
38
+ $ bundle exec bake node:install frozen=true
39
+ ~~~
40
+
41
+ ## Select Browser Files
42
+
43
+ Packages often contain development sources, tests and metadata that should not be deployed. Add a `bake-node` section which selects the browser-facing files:
44
+
45
+ ~~~ json
46
+ {
47
+ "private": true,
48
+ "packageManager": "pnpm@10",
49
+ "dependencies": {
50
+ "morphdom": "^2.7"
51
+ },
52
+ "bake-node": {
53
+ "packages": {
54
+ "morphdom": {
55
+ "include": ["morphdom-esm.js"],
56
+ "imports": {
57
+ "morphdom": "morphdom-esm.js"
58
+ }
59
+ }
60
+ }
61
+ }
62
+ }
63
+ ~~~
64
+
65
+ Direct production dependencies are selected by default. The package-specific object narrows the copied files and defines an import-map entry.
66
+
67
+ ## Generate Static Packages
68
+
69
+ Materialize the configured packages:
70
+
71
+ ~~~ bash
72
+ $ bundle exec bake node:packages:static
73
+ ~~~
74
+
75
+ The default output is `public/_components`. Bake Node builds the complete output in a temporary directory and replaces the existing projection only after every package has been validated.
76
+
77
+ Print the generated browser import map:
78
+
79
+ ~~~ bash
80
+ $ bundle exec bake node:importmap:show
81
+ ~~~
82
+
83
+ For the example above, the result includes:
84
+
85
+ ~~~ json
86
+ {
87
+ "imports": {
88
+ "morphdom": "/_components/morphdom/morphdom-esm.js"
89
+ }
90
+ }
91
+ ~~~
92
+
93
+ Your application can embed this JSON in a `<script type="importmap">` element and serve `public/_components` with its other static assets.
94
+
95
+ ## Run JavaScript Tests
96
+
97
+ Bake Node runs scripts from the root `package.json` without imposing a test framework:
98
+
99
+ ~~~ json
100
+ {
101
+ "scripts": {
102
+ "test": "node --test"
103
+ }
104
+ }
105
+ ~~~
106
+
107
+ ~~~ bash
108
+ $ bundle exec bake node:test
109
+ ~~~
110
+
111
+ Pass another script name when a project has multiple JavaScript test suites:
112
+
113
+ ~~~ bash
114
+ $ bundle exec bake node:test script=test:browser
115
+ ~~~
116
+
117
+ ## Verify Generated Files
118
+
119
+ Projects which commit or deploy the static projection can verify that it matches the current dependencies and configuration:
120
+
121
+ ~~~ bash
122
+ $ bundle exec bake node:packages:check
123
+ ~~~
124
+
125
+ See the [Static Packages](../static-packages/index) guide for detailed selection, manifest and import-map configuration. See [Internal Packages](../internal-packages/index) when JavaScript is developed alongside the Ruby code.
@@ -0,0 +1,110 @@
1
+ # Internal Packages
2
+
3
+ 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.
4
+
5
+ ## Why Use Workspace Packages?
6
+
7
+ Internal JavaScript often has its own module boundaries, tests and release concerns. Mixing it into the Ruby `lib/` hierarchy makes both languages harder to navigate, while placing authored code directly in `node_modules` makes it disposable.
8
+
9
+ Bake Node recommends three distinct layers:
10
+
11
+ ~~~ text
12
+ components/ # Authored internal JavaScript packages.
13
+ node_modules/ # Disposable package-manager projection.
14
+ public/_components/ # Generated static deployment projection.
15
+ package.json # Workspace and Bake Node configuration.
16
+ ~~~
17
+
18
+ `components/` describes the role of the code without requiring a second language-level hierarchy. A single directory can contain one or many packages.
19
+
20
+ ## Create an Internal Package
21
+
22
+ Give each internal library its own `package.json` and tests:
23
+
24
+ ~~~ text
25
+ components/
26
+ live/
27
+ package.json
28
+ Live.js
29
+ test/
30
+ Live.js
31
+ ~~~
32
+
33
+ For example:
34
+
35
+ ~~~ json
36
+ {
37
+ "name": "@example/live",
38
+ "private": true,
39
+ "type": "module",
40
+ "exports": "./Live.js",
41
+ "scripts": {
42
+ "test": "node --test"
43
+ }
44
+ }
45
+ ~~~
46
+
47
+ The directory name and package name do not need to match. Package identity comes from the internal `package.json`.
48
+
49
+ ## Add the Workspace
50
+
51
+ Expose internal packages through the root workspace configuration:
52
+
53
+ ~~~ json
54
+ {
55
+ "private": true,
56
+ "workspaces": ["components/*"],
57
+ "bake-node": {
58
+ "packages": {
59
+ "@example/live": {
60
+ "include": ["Live.js"],
61
+ "imports": {
62
+ "live": "Live.js"
63
+ }
64
+ }
65
+ }
66
+ }
67
+ }
68
+ ~~~
69
+
70
+ The package manager projects the workspace package into `node_modules/@example/live`, usually using a link. Bake Node resolves that link, ensures selected files remain inside the package, and copies the resulting files into `public/_components/@example/live`.
71
+
72
+ The `components/` path is a convention rather than a requirement. Any workspace or local-package layout supported by the selected package manager can be used.
73
+
74
+ ## Test Internal Packages
75
+
76
+ Each package can keep its own test command. Define a root script which invokes the workspace tests according to the selected package manager, then let Bake Node run that script:
77
+
78
+ ~~~ json
79
+ {
80
+ "scripts": {
81
+ "test": "npm test --workspaces --if-present"
82
+ }
83
+ }
84
+ ~~~
85
+
86
+ ~~~ bash
87
+ $ bundle exec bake node:test
88
+ ~~~
89
+
90
+ pnpm, Yarn and Bun have their own workspace script syntax. Bake Node deliberately does not abstract those differences; the root script remains the project's explicit test entry point.
91
+
92
+ ## Multiple Internal Libraries
93
+
94
+ No additional multiplexing layer is needed. Add more package directories beneath `components/`, include them in the workspace pattern, and select the packages which should be deployed:
95
+
96
+ ~~~ text
97
+ components/
98
+ editor/
99
+ package.json
100
+ live/
101
+ package.json
102
+ syntax/
103
+ package.json
104
+ ~~~
105
+
106
+ Packages used only for development do not need to appear in `bake-node.packages`. Packages listed there can be deployed even when they are not direct root dependencies.
107
+
108
+ ## Avoid Authoring in `node_modules`
109
+
110
+ `node_modules` is owned by the package manager and can be replaced by any installation command. Keep authored packages in `components/` and treat both `node_modules/` and `public/_components/` as projections which can be rebuilt from source and lock files.
data/guides/links.yaml ADDED
@@ -0,0 +1,6 @@
1
+ getting-started:
2
+ order: 1
3
+ internal-packages:
4
+ order: 2
5
+ static-packages:
6
+ order: 3
@@ -0,0 +1,134 @@
1
+ # Static Packages
2
+
3
+ This guide explains how to control which installed package files are deployed, generate browser import maps and verify the resulting static projection.
4
+
5
+ ## Package Selection
6
+
7
+ All direct `dependencies` from the root `package.json` are selected by default. Use `bake-node.packages` to refine that selection:
8
+
9
+ ~~~ json
10
+ {
11
+ "dependencies": {
12
+ "morphdom": "^2.7",
13
+ "server-only-package": "^1.0"
14
+ },
15
+ "bake-node": {
16
+ "packages": {
17
+ "morphdom": {
18
+ "include": ["morphdom-esm.js"]
19
+ },
20
+ "server-only-package": false,
21
+ "@example/internal": true
22
+ }
23
+ }
24
+ }
25
+ ~~~
26
+
27
+ Set a dependency to `false` to exclude it. Use `true` or an empty object to accept the default behavior. Additional workspace packages can be listed even when they are not direct dependencies.
28
+
29
+ ## Source Directories
30
+
31
+ Many packages publish browser-ready files in `dist/`. Bake Node uses `dist` automatically when that directory exists and otherwise uses the package root.
32
+
33
+ Override the source when a package has another layout:
34
+
35
+ ~~~ json
36
+ {
37
+ "bake-node": {
38
+ "packages": {
39
+ "example": {
40
+ "source": "browser"
41
+ }
42
+ }
43
+ }
44
+ }
45
+ ~~~
46
+
47
+ Set `source` to `.` when the package root should be used even though a `dist/` directory exists.
48
+
49
+ ## Selecting a Subset of Files
50
+
51
+ npm packages frequently contain far more than an application needs at runtime. `include` accepts package-source-relative glob patterns:
52
+
53
+ ~~~ json
54
+ {
55
+ "bake-node": {
56
+ "packages": {
57
+ "mermaid": {
58
+ "source": "dist",
59
+ "include": [
60
+ "mermaid.esm.min.mjs",
61
+ "chunks/mermaid.esm.min/**/*.mjs"
62
+ ]
63
+ }
64
+ }
65
+ }
66
+ }
67
+ ~~~
68
+
69
+ When `include` is omitted, the complete source directory is copied except for nested `.git` and `node_modules` directories. Absolute paths and parent traversal are rejected, and symbolic links cannot escape the installed package.
70
+
71
+ ## Import Maps
72
+
73
+ Map browser import specifiers to selected package files:
74
+
75
+ ~~~ json
76
+ {
77
+ "bake-node": {
78
+ "base": "/_components/",
79
+ "packages": {
80
+ "mermaid": {
81
+ "source": "dist",
82
+ "include": ["mermaid.esm.min.mjs"],
83
+ "imports": {
84
+ "mermaid": "mermaid.esm.min.mjs"
85
+ }
86
+ }
87
+ }
88
+ }
89
+ }
90
+ ~~~
91
+
92
+ Relative import targets must refer to files included in the static projection. Absolute paths and URLs are preserved, which allows a project to combine local files and CDN imports explicitly. Duplicate specifiers are rejected.
93
+
94
+ Generate and inspect the import map with:
95
+
96
+ ~~~ bash
97
+ $ bundle exec bake node:packages:static
98
+ $ bundle exec bake node:importmap:show
99
+ ~~~
100
+
101
+ ## Output and Manifest
102
+
103
+ The default output directory and public URL prefix are configurable:
104
+
105
+ ~~~ json
106
+ {
107
+ "bake-node": {
108
+ "output": "public/_components",
109
+ "base": "/_components/",
110
+ "packageRoot": "node_modules"
111
+ }
112
+ }
113
+ ~~~
114
+
115
+ Static installation writes `.bake-node.json` inside the output directory. The deterministic manifest records:
116
+
117
+ - Package versions and selected source directories.
118
+ - Every installed file and its SHA-256 digest.
119
+ - Import-map entries.
120
+ - A digest of the complete manifest.
121
+
122
+ The manifest can be loaded once by an application or used as a cache key without repeatedly scanning the package tree.
123
+
124
+ ## Keeping Output Current
125
+
126
+ The entire output tree is staged and swapped atomically. A missing file, invalid import, unsafe symbolic link or malformed package prevents replacement of the last working output.
127
+
128
+ Use the check task in deployment or CI when generated files are expected to be current:
129
+
130
+ ~~~ bash
131
+ $ bundle exec bake node:packages:check
132
+ ~~~
133
+
134
+ The check rebuilds the desired manifest in a temporary directory and verifies both the manifest and current file contents.
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ require "json"
7
+ require "pathname"
8
+
9
+ require_relative "errors"
10
+ require_relative "package"
11
+
12
+ module Bake
13
+ module Node
14
+ # Loads and validates Bake Node settings from a project's `package.json` file.
15
+ class Configuration
16
+ DEFAULT_OUTPUT = "public/_components"
17
+ DEFAULT_BASE = "/_components/"
18
+
19
+ # Load the configuration for a project.
20
+ # @parameter root [String | Pathname] The project directory containing `package.json`.
21
+ # @returns [Configuration] The validated project configuration.
22
+ # @raises [ConfigurationError] If `package.json` is missing, malformed or invalid.
23
+ def self.load(root)
24
+ root = Pathname.new(root).expand_path
25
+ package_path = root + "package.json"
26
+
27
+ unless package_path.file?
28
+ raise ConfigurationError, "Could not find package.json in #{root}!"
29
+ end
30
+
31
+ package_json = JSON.parse(package_path.read)
32
+ new(root, package_json)
33
+ rescue JSON::ParserError => error
34
+ raise ConfigurationError, "Could not parse #{package_path}: #{error.message}"
35
+ end
36
+
37
+ # Initialize a configuration from parsed package metadata.
38
+ # @parameter root [String | Pathname] The project root directory.
39
+ # @parameter package_json [Hash] The parsed contents of `package.json`.
40
+ # @raises [ConfigurationError] If the configuration is invalid.
41
+ def initialize(root, package_json)
42
+ @root = Pathname.new(root).expand_path
43
+ @package_json = package_json
44
+
45
+ unless @package_json.is_a?(Hash)
46
+ raise ConfigurationError, "package.json must contain an object!"
47
+ end
48
+
49
+ configuration = @package_json.fetch("bake-node", {})
50
+
51
+ unless configuration.is_a?(Hash)
52
+ raise ConfigurationError, "bake-node configuration must be an object!"
53
+ end
54
+
55
+ @package_root = expand_within_root(configuration.fetch("packageRoot", "node_modules"), "packageRoot")
56
+ @output = configuration.fetch("output", DEFAULT_OUTPUT)
57
+ @base = configuration.fetch("base", DEFAULT_BASE)
58
+ @packages = load_packages(configuration["packages"])
59
+
60
+ unless @base.is_a?(String) && @base.end_with?("/")
61
+ raise ConfigurationError, "bake-node base must be a string ending in '/'!"
62
+ end
63
+
64
+ output_path
65
+ end
66
+
67
+ # @attribute [Pathname] The expanded project root directory.
68
+ attr :root
69
+
70
+ # @attribute [Hash] The parsed contents of `package.json`.
71
+ attr :package_json
72
+
73
+ # @attribute [Pathname] The directory containing installed Node.js packages.
74
+ attr :package_root
75
+
76
+ # @attribute [String] The public URL prefix for static packages.
77
+ attr :base
78
+
79
+ # @attribute [Hash(String, Package)] The packages selected for static deployment.
80
+ attr :packages
81
+
82
+ # Resolve the configured output directory.
83
+ # @parameter override [String | Nil] An optional project-relative output directory.
84
+ # @returns [Pathname] The expanded output directory.
85
+ # @raises [ConfigurationError] If the output directory escapes the project root.
86
+ def output_path(override = nil)
87
+ expand_within_root(override || @output, "output", allow_root: false)
88
+ end
89
+
90
+ private
91
+
92
+ def load_packages(overrides)
93
+ configured = {}
94
+
95
+ dependencies = @package_json.fetch("dependencies", {})
96
+ unless dependencies.is_a?(Hash)
97
+ raise ConfigurationError, "package.json dependencies must be an object!"
98
+ end
99
+
100
+ dependencies.each_key do |name|
101
+ configured[name] = {}
102
+ end
103
+
104
+ case overrides
105
+ when nil
106
+ # All direct dependencies use their defaults.
107
+ when Array
108
+ configured = overrides.to_h{|name| [name, {}]}
109
+ when Hash
110
+ overrides.each do |name, options|
111
+ if options == false
112
+ configured.delete(name)
113
+ else
114
+ configured[name] = options == true ? {} : options
115
+ end
116
+ end
117
+ else
118
+ raise ConfigurationError, "bake-node packages must be an array or object!"
119
+ end
120
+
121
+ configured.sort.to_h do |name, options|
122
+ [name, Package.new(name, options)]
123
+ end
124
+ end
125
+
126
+ def expand_within_root(path, description, allow_root: true)
127
+ unless path.is_a?(String) && !path.empty?
128
+ raise ConfigurationError, "bake-node #{description} must be a non-empty string!"
129
+ end
130
+
131
+ expanded = (@root + path).expand_path
132
+ prefix = @root.to_s + File::SEPARATOR
133
+
134
+ unless expanded.to_s.start_with?(prefix) || (allow_root && expanded == @root)
135
+ raise ConfigurationError, "bake-node #{description} must remain within #{@root}!"
136
+ end
137
+
138
+ expanded
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ require "json"
7
+
8
+ require_relative "configuration"
9
+ require_relative "manifest"
10
+ require_relative "package_manager"
11
+ require_relative "static"
12
+
13
+ module Bake
14
+ module Node
15
+ # Coordinates package-manager commands and static package deployment for a project.
16
+ class Controller
17
+ # Initialize a controller for a project.
18
+ # @parameter root [String | Pathname] The project directory containing `package.json`.
19
+ # @raises [ConfigurationError] If the project configuration is invalid.
20
+ def initialize(root)
21
+ @configuration = Configuration.load(root)
22
+ end
23
+
24
+ # @attribute [Configuration] The validated project configuration.
25
+ attr :configuration
26
+
27
+ # Install Node.js packages using the detected package manager.
28
+ # @parameter frozen [Boolean] Whether the lock file must remain unchanged.
29
+ # @raises [Error] If the package-manager command fails.
30
+ def install(frozen: false)
31
+ package_manager.install(frozen: frozen)
32
+ end
33
+
34
+ # Run a script from the root `package.json` file.
35
+ # @parameter script [String] The script name to run.
36
+ # @raises [Error] If the package-manager command fails.
37
+ def run(script)
38
+ package_manager.run(script)
39
+ end
40
+
41
+ # Materialize the configured packages as static files.
42
+ # @parameter output [String | Nil] An optional project-relative output directory.
43
+ # @returns [Manifest] The generated static package manifest.
44
+ def static(output: nil)
45
+ Static.new(@configuration, output: output).update
46
+ end
47
+
48
+ # Verify that the static package projection is current.
49
+ # @parameter output [String | Nil] An optional project-relative output directory.
50
+ # @returns [Boolean] `true` when the static package projection is current.
51
+ # @raises [CheckError] If the static package projection is missing or out of date.
52
+ def check(output: nil)
53
+ Static.new(@configuration, output: output).check!
54
+ end
55
+
56
+ # Generate an import map from the static package manifest.
57
+ # @parameter output [String | Nil] An optional project-relative output directory.
58
+ # @returns [String] The formatted import map JSON.
59
+ # @raises [CheckError] If the static package manifest is missing or invalid.
60
+ def import_map(output: nil)
61
+ root = @configuration.output_path(output)
62
+ JSON.pretty_generate(Manifest.load(root).import_map)
63
+ end
64
+
65
+ private
66
+
67
+ def package_manager
68
+ @package_manager ||= PackageManager.detect(@configuration)
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ module Bake
7
+ module Node
8
+ # The base class for Bake Node failures.
9
+ class Error < StandardError
10
+ end
11
+
12
+ # Raised when project or package configuration is invalid.
13
+ class ConfigurationError < Error
14
+ end
15
+
16
+ # Raised when an installed package cannot be materialized safely.
17
+ class PackageError < Error
18
+ end
19
+
20
+ # Raised when generated static packages are missing or out of date.
21
+ class CheckError < Error
22
+ end
23
+ end
24
+ end