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
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,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
|