Flask-Node 0.1.0__tar.gz
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.
- flask_node-0.1.0/LICENSE +21 -0
- flask_node-0.1.0/MANIFEST.in +1 -0
- flask_node-0.1.0/PKG-INFO +185 -0
- flask_node-0.1.0/README.md +172 -0
- flask_node-0.1.0/pyproject.toml +26 -0
- flask_node-0.1.0/setup.cfg +4 -0
- flask_node-0.1.0/src/Flask_Node.egg-info/PKG-INFO +185 -0
- flask_node-0.1.0/src/Flask_Node.egg-info/SOURCES.txt +23 -0
- flask_node-0.1.0/src/Flask_Node.egg-info/dependency_links.txt +1 -0
- flask_node-0.1.0/src/Flask_Node.egg-info/requires.txt +4 -0
- flask_node-0.1.0/src/Flask_Node.egg-info/top_level.txt +1 -0
- flask_node-0.1.0/src/flask_node/__init__.py +15 -0
- flask_node-0.1.0/src/flask_node/cli.py +78 -0
- flask_node-0.1.0/src/flask_node/exceptions.py +41 -0
- flask_node-0.1.0/src/flask_node/extension.py +76 -0
- flask_node-0.1.0/src/flask_node/manager.py +162 -0
- flask_node-0.1.0/src/flask_node/package.py +36 -0
- flask_node-0.1.0/src/flask_node/py.typed +0 -0
- flask_node-0.1.0/src/flask_node/runner.py +44 -0
- flask_node-0.1.0/tests/conftest.py +25 -0
- flask_node-0.1.0/tests/test_cli.py +47 -0
- flask_node-0.1.0/tests/test_extension.py +59 -0
- flask_node-0.1.0/tests/test_manager.py +118 -0
- flask_node-0.1.0/tests/test_package.py +70 -0
- flask_node-0.1.0/tests/test_runner.py +39 -0
flask_node-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sebastian Salinas
|
|
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.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
recursive-include tests *.py
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: Flask-Node
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Isolated Node/npm infrastructure for Flask extensions
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: Flask<4,>=2.2
|
|
10
|
+
Provides-Extra: test
|
|
11
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
12
|
+
Dynamic: license-file
|
|
13
|
+
|
|
14
|
+
# Flask-Node
|
|
15
|
+
|
|
16
|
+
Flask-Node manages an isolated Node/npm project for Flask applications and
|
|
17
|
+
extensions. It contains no JavaScript library integrations, bundler, asset
|
|
18
|
+
server, or frontend framework.
|
|
19
|
+
|
|
20
|
+
Requires Python 3.10+ and Flask 2.2–3.x. Install Node.js (including npm/npx)
|
|
21
|
+
separately when executing commands; importing and initializing the Flask
|
|
22
|
+
extension does not require it.
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install Flask-Node
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Application setup
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from flask import Flask
|
|
32
|
+
from flask_node import Node
|
|
33
|
+
|
|
34
|
+
node = Node()
|
|
35
|
+
|
|
36
|
+
def create_app():
|
|
37
|
+
app = Flask(__name__)
|
|
38
|
+
node.init_app(app)
|
|
39
|
+
return app
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`Node(app)` is also supported. `init_app()` registers defaults and the CLI,
|
|
43
|
+
without creating directories or launching commands. Each application owns a
|
|
44
|
+
separate `NodeManager` in `app.extensions["node"]`; a shared `Node` facade uses
|
|
45
|
+
the current application context. `node.get_manager(app)` gives explicit access
|
|
46
|
+
outside a context. Duplicate registration raises `ConfigurationError`.
|
|
47
|
+
|
|
48
|
+
## Configuration
|
|
49
|
+
|
|
50
|
+
| Setting | Default | Meaning |
|
|
51
|
+
| --- | --- | --- |
|
|
52
|
+
| `NODE_DIR` | `.node` | Relative to `app.root_path`, or an absolute path |
|
|
53
|
+
| `NODE_BIN` | `node` | Node executable name or path |
|
|
54
|
+
| `NODE_NPM_BIN` | `npm` | npm executable name or path |
|
|
55
|
+
| `NODE_NPX_BIN` | `npx` | npx executable name or path |
|
|
56
|
+
|
|
57
|
+
Configure before calling `init_app()`. Configuration is captured per app.
|
|
58
|
+
For a directory beside an application package, configure an absolute project
|
|
59
|
+
path. No behavior depends on the shell's current directory.
|
|
60
|
+
|
|
61
|
+
## Lifecycle and dependencies
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
with app.app_context():
|
|
65
|
+
node.require("example", version="^1")
|
|
66
|
+
node.require("@scope/build-tool", version="^2", dev=True)
|
|
67
|
+
node.initialize()
|
|
68
|
+
node.install()
|
|
69
|
+
node.install("another-package", version="^3")
|
|
70
|
+
node.uninstall("another-package")
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`require()` only records an in-memory declaration. Consumer extensions may call
|
|
74
|
+
it during application setup. It creates no files and does not check or install
|
|
75
|
+
packages. Identical declarations are idempotent; differing versions or dependency
|
|
76
|
+
sections raise `DependencyConflictError`. Version declarations are compared as
|
|
77
|
+
strings; Flask-Node does not solve semver ranges.
|
|
78
|
+
|
|
79
|
+
`initialize()` creates `.node/package.json` with `private: true` and empty
|
|
80
|
+
`dependencies` / `devDependencies`. It preserves an existing valid manifest
|
|
81
|
+
without rewriting it. Invalid manifests fail clearly. npm creates the lockfile
|
|
82
|
+
and installed modules later:
|
|
83
|
+
|
|
84
|
+
```text
|
|
85
|
+
.node/
|
|
86
|
+
package.json
|
|
87
|
+
package-lock.json
|
|
88
|
+
node_modules/
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`install()` initializes if necessary, merges active declarations into the
|
|
92
|
+
manifest, preserves unrelated fields and dependencies, and runs `npm install`.
|
|
93
|
+
Declarations take precedence over persisted entries for the same package.
|
|
94
|
+
`install(name, version=None, dev=False)` additionally installs a registry package;
|
|
95
|
+
without a version, npm chooses it unless an active declaration supplies one.
|
|
96
|
+
Conflicting explicit options fail before installation. Uninstalling an actively
|
|
97
|
+
required package is rejected. npm failures may leave manifest changes or partial
|
|
98
|
+
installation artifacts; operations are not transactional.
|
|
99
|
+
|
|
100
|
+
`ci()` requires `package-lock.json` and declarations matching the manifest, then
|
|
101
|
+
runs `npm ci` without rewriting the manifest. npm validates lockfile consistency.
|
|
102
|
+
Commit `.node/package.json` and `.node/package-lock.json` for reproducible builds;
|
|
103
|
+
ignore `.node/node_modules/`.
|
|
104
|
+
|
|
105
|
+
## Commands and diagnostics
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
with app.app_context():
|
|
109
|
+
result = node.npm("run", "custom-script")
|
|
110
|
+
result = node.npx("some-tool", "--help", capture_output=False)
|
|
111
|
+
print(node.status())
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Raw commands require an initialized environment and run with its directory as
|
|
115
|
+
`cwd`. Arguments are passed individually with `shell=False`. Python calls capture
|
|
116
|
+
output by default; use `capture_output=False` to inherit terminal streams.
|
|
117
|
+
`CommandResult` exposes `args`, `returncode`, `stdout`, and `stderr`.
|
|
118
|
+
|
|
119
|
+
`status()` reports the directory, manifest presence, and executable locations
|
|
120
|
+
(or `None`). It launches no processes and does not validate executable versions.
|
|
121
|
+
Missing executables raise `ExecutableNotFoundError`; nonzero exits and other
|
|
122
|
+
launch failures raise `CommandExecutionError`, which retains `command`, `cwd`,
|
|
123
|
+
`returncode`, `stdout`, and `stderr`. All public errors derive from `NodeError`.
|
|
124
|
+
|
|
125
|
+
npm/npx may access the network and run package scripts. The managed working
|
|
126
|
+
directory is not a process sandbox; invoked tools can write elsewhere.
|
|
127
|
+
|
|
128
|
+
## Flask CLI
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
flask --app your_app node init
|
|
132
|
+
flask --app your_app node install
|
|
133
|
+
flask --app your_app node install example --version '^1' --dev
|
|
134
|
+
flask --app your_app node uninstall example
|
|
135
|
+
flask --app your_app node npm -- run custom-script --flag
|
|
136
|
+
flask --app your_app node npx -- some-tool --help
|
|
137
|
+
flask --app your_app node ci
|
|
138
|
+
flask --app your_app node status
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
CLI commands use the same Python operations, stream subprocess output, and
|
|
142
|
+
report extension errors with a nonzero exit status. `--` separates forwarding
|
|
143
|
+
arguments from Click's own options.
|
|
144
|
+
|
|
145
|
+
## Installed assets and extension consumers
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
manager = app.extensions["node"]
|
|
149
|
+
manager.require("example", "^1") # Safe during consumer init_app().
|
|
150
|
+
|
|
151
|
+
# After an explicit installation step:
|
|
152
|
+
asset = manager.resolve("example", "dist/example.js")
|
|
153
|
+
package = manager.package("example")
|
|
154
|
+
assert package.resolve("dist/example.js") == asset
|
|
155
|
+
print(package.name, package.version, package.root)
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
The consumer must initialize Flask-Node first. It owns its library-specific
|
|
159
|
+
configuration, rendering, asset copying/serving, and build/watch commands.
|
|
160
|
+
Flask-Node only declares/installs dependencies, executes commands, and locates
|
|
161
|
+
files. No registration protocol is needed.
|
|
162
|
+
|
|
163
|
+
Package lookup supports ordinary and scoped registry names. Asset lookup returns
|
|
164
|
+
an existing `Path`; absolute paths, parent traversal, Windows-style paths, and
|
|
165
|
+
symlinks escaping the package or managed environment are rejected. Externally
|
|
166
|
+
linked packages are intentionally unsupported. Missing or malformed installed
|
|
167
|
+
packages raise `PackageNotFoundError`; unsafe or missing assets raise
|
|
168
|
+
`AssetResolutionError`. This is filesystem path validation, not protection
|
|
169
|
+
against concurrent malicious filesystem changes.
|
|
170
|
+
|
|
171
|
+
## Development
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
python -m venv .venv
|
|
175
|
+
.venv/bin/python -m pip install -e '.[test]' build
|
|
176
|
+
.venv/bin/python -m pytest
|
|
177
|
+
.venv/bin/python -m build
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Tests inject/mock the runner and subprocess boundary. They never download npm
|
|
181
|
+
packages or require Node. A custom runner can be supplied to `Node(runner=...)`
|
|
182
|
+
or `NodeManager(..., runner=...)` for testing. This dependency injection is not a
|
|
183
|
+
consumer plugin protocol.
|
|
184
|
+
|
|
185
|
+
Inspired by [Flask-Tailwind-Manager](https://github.com/SebaSalinass/flask-tailwind-manager).
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# Flask-Node
|
|
2
|
+
|
|
3
|
+
Flask-Node manages an isolated Node/npm project for Flask applications and
|
|
4
|
+
extensions. It contains no JavaScript library integrations, bundler, asset
|
|
5
|
+
server, or frontend framework.
|
|
6
|
+
|
|
7
|
+
Requires Python 3.10+ and Flask 2.2–3.x. Install Node.js (including npm/npx)
|
|
8
|
+
separately when executing commands; importing and initializing the Flask
|
|
9
|
+
extension does not require it.
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install Flask-Node
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Application setup
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from flask import Flask
|
|
19
|
+
from flask_node import Node
|
|
20
|
+
|
|
21
|
+
node = Node()
|
|
22
|
+
|
|
23
|
+
def create_app():
|
|
24
|
+
app = Flask(__name__)
|
|
25
|
+
node.init_app(app)
|
|
26
|
+
return app
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`Node(app)` is also supported. `init_app()` registers defaults and the CLI,
|
|
30
|
+
without creating directories or launching commands. Each application owns a
|
|
31
|
+
separate `NodeManager` in `app.extensions["node"]`; a shared `Node` facade uses
|
|
32
|
+
the current application context. `node.get_manager(app)` gives explicit access
|
|
33
|
+
outside a context. Duplicate registration raises `ConfigurationError`.
|
|
34
|
+
|
|
35
|
+
## Configuration
|
|
36
|
+
|
|
37
|
+
| Setting | Default | Meaning |
|
|
38
|
+
| --- | --- | --- |
|
|
39
|
+
| `NODE_DIR` | `.node` | Relative to `app.root_path`, or an absolute path |
|
|
40
|
+
| `NODE_BIN` | `node` | Node executable name or path |
|
|
41
|
+
| `NODE_NPM_BIN` | `npm` | npm executable name or path |
|
|
42
|
+
| `NODE_NPX_BIN` | `npx` | npx executable name or path |
|
|
43
|
+
|
|
44
|
+
Configure before calling `init_app()`. Configuration is captured per app.
|
|
45
|
+
For a directory beside an application package, configure an absolute project
|
|
46
|
+
path. No behavior depends on the shell's current directory.
|
|
47
|
+
|
|
48
|
+
## Lifecycle and dependencies
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
with app.app_context():
|
|
52
|
+
node.require("example", version="^1")
|
|
53
|
+
node.require("@scope/build-tool", version="^2", dev=True)
|
|
54
|
+
node.initialize()
|
|
55
|
+
node.install()
|
|
56
|
+
node.install("another-package", version="^3")
|
|
57
|
+
node.uninstall("another-package")
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`require()` only records an in-memory declaration. Consumer extensions may call
|
|
61
|
+
it during application setup. It creates no files and does not check or install
|
|
62
|
+
packages. Identical declarations are idempotent; differing versions or dependency
|
|
63
|
+
sections raise `DependencyConflictError`. Version declarations are compared as
|
|
64
|
+
strings; Flask-Node does not solve semver ranges.
|
|
65
|
+
|
|
66
|
+
`initialize()` creates `.node/package.json` with `private: true` and empty
|
|
67
|
+
`dependencies` / `devDependencies`. It preserves an existing valid manifest
|
|
68
|
+
without rewriting it. Invalid manifests fail clearly. npm creates the lockfile
|
|
69
|
+
and installed modules later:
|
|
70
|
+
|
|
71
|
+
```text
|
|
72
|
+
.node/
|
|
73
|
+
package.json
|
|
74
|
+
package-lock.json
|
|
75
|
+
node_modules/
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`install()` initializes if necessary, merges active declarations into the
|
|
79
|
+
manifest, preserves unrelated fields and dependencies, and runs `npm install`.
|
|
80
|
+
Declarations take precedence over persisted entries for the same package.
|
|
81
|
+
`install(name, version=None, dev=False)` additionally installs a registry package;
|
|
82
|
+
without a version, npm chooses it unless an active declaration supplies one.
|
|
83
|
+
Conflicting explicit options fail before installation. Uninstalling an actively
|
|
84
|
+
required package is rejected. npm failures may leave manifest changes or partial
|
|
85
|
+
installation artifacts; operations are not transactional.
|
|
86
|
+
|
|
87
|
+
`ci()` requires `package-lock.json` and declarations matching the manifest, then
|
|
88
|
+
runs `npm ci` without rewriting the manifest. npm validates lockfile consistency.
|
|
89
|
+
Commit `.node/package.json` and `.node/package-lock.json` for reproducible builds;
|
|
90
|
+
ignore `.node/node_modules/`.
|
|
91
|
+
|
|
92
|
+
## Commands and diagnostics
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
with app.app_context():
|
|
96
|
+
result = node.npm("run", "custom-script")
|
|
97
|
+
result = node.npx("some-tool", "--help", capture_output=False)
|
|
98
|
+
print(node.status())
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Raw commands require an initialized environment and run with its directory as
|
|
102
|
+
`cwd`. Arguments are passed individually with `shell=False`. Python calls capture
|
|
103
|
+
output by default; use `capture_output=False` to inherit terminal streams.
|
|
104
|
+
`CommandResult` exposes `args`, `returncode`, `stdout`, and `stderr`.
|
|
105
|
+
|
|
106
|
+
`status()` reports the directory, manifest presence, and executable locations
|
|
107
|
+
(or `None`). It launches no processes and does not validate executable versions.
|
|
108
|
+
Missing executables raise `ExecutableNotFoundError`; nonzero exits and other
|
|
109
|
+
launch failures raise `CommandExecutionError`, which retains `command`, `cwd`,
|
|
110
|
+
`returncode`, `stdout`, and `stderr`. All public errors derive from `NodeError`.
|
|
111
|
+
|
|
112
|
+
npm/npx may access the network and run package scripts. The managed working
|
|
113
|
+
directory is not a process sandbox; invoked tools can write elsewhere.
|
|
114
|
+
|
|
115
|
+
## Flask CLI
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
flask --app your_app node init
|
|
119
|
+
flask --app your_app node install
|
|
120
|
+
flask --app your_app node install example --version '^1' --dev
|
|
121
|
+
flask --app your_app node uninstall example
|
|
122
|
+
flask --app your_app node npm -- run custom-script --flag
|
|
123
|
+
flask --app your_app node npx -- some-tool --help
|
|
124
|
+
flask --app your_app node ci
|
|
125
|
+
flask --app your_app node status
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
CLI commands use the same Python operations, stream subprocess output, and
|
|
129
|
+
report extension errors with a nonzero exit status. `--` separates forwarding
|
|
130
|
+
arguments from Click's own options.
|
|
131
|
+
|
|
132
|
+
## Installed assets and extension consumers
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
manager = app.extensions["node"]
|
|
136
|
+
manager.require("example", "^1") # Safe during consumer init_app().
|
|
137
|
+
|
|
138
|
+
# After an explicit installation step:
|
|
139
|
+
asset = manager.resolve("example", "dist/example.js")
|
|
140
|
+
package = manager.package("example")
|
|
141
|
+
assert package.resolve("dist/example.js") == asset
|
|
142
|
+
print(package.name, package.version, package.root)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The consumer must initialize Flask-Node first. It owns its library-specific
|
|
146
|
+
configuration, rendering, asset copying/serving, and build/watch commands.
|
|
147
|
+
Flask-Node only declares/installs dependencies, executes commands, and locates
|
|
148
|
+
files. No registration protocol is needed.
|
|
149
|
+
|
|
150
|
+
Package lookup supports ordinary and scoped registry names. Asset lookup returns
|
|
151
|
+
an existing `Path`; absolute paths, parent traversal, Windows-style paths, and
|
|
152
|
+
symlinks escaping the package or managed environment are rejected. Externally
|
|
153
|
+
linked packages are intentionally unsupported. Missing or malformed installed
|
|
154
|
+
packages raise `PackageNotFoundError`; unsafe or missing assets raise
|
|
155
|
+
`AssetResolutionError`. This is filesystem path validation, not protection
|
|
156
|
+
against concurrent malicious filesystem changes.
|
|
157
|
+
|
|
158
|
+
## Development
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
python -m venv .venv
|
|
162
|
+
.venv/bin/python -m pip install -e '.[test]' build
|
|
163
|
+
.venv/bin/python -m pytest
|
|
164
|
+
.venv/bin/python -m build
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Tests inject/mock the runner and subprocess boundary. They never download npm
|
|
168
|
+
packages or require Node. A custom runner can be supplied to `Node(runner=...)`
|
|
169
|
+
or `NodeManager(..., runner=...)` for testing. This dependency injection is not a
|
|
170
|
+
consumer plugin protocol.
|
|
171
|
+
|
|
172
|
+
Inspired by [Flask-Tailwind-Manager](https://github.com/SebaSalinass/flask-tailwind-manager).
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "Flask-Node"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Isolated Node/npm infrastructure for Flask extensions"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
dependencies = ["Flask>=2.2,<4"]
|
|
14
|
+
|
|
15
|
+
[project.optional-dependencies]
|
|
16
|
+
test = ["pytest>=7"]
|
|
17
|
+
|
|
18
|
+
[tool.setuptools.packages.find]
|
|
19
|
+
where = ["src"]
|
|
20
|
+
|
|
21
|
+
[tool.setuptools.package-data]
|
|
22
|
+
flask_node = ["py.typed"]
|
|
23
|
+
|
|
24
|
+
[tool.pytest.ini_options]
|
|
25
|
+
testpaths = ["tests"]
|
|
26
|
+
pythonpath = ["src"]
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: Flask-Node
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Isolated Node/npm infrastructure for Flask extensions
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: Flask<4,>=2.2
|
|
10
|
+
Provides-Extra: test
|
|
11
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
12
|
+
Dynamic: license-file
|
|
13
|
+
|
|
14
|
+
# Flask-Node
|
|
15
|
+
|
|
16
|
+
Flask-Node manages an isolated Node/npm project for Flask applications and
|
|
17
|
+
extensions. It contains no JavaScript library integrations, bundler, asset
|
|
18
|
+
server, or frontend framework.
|
|
19
|
+
|
|
20
|
+
Requires Python 3.10+ and Flask 2.2–3.x. Install Node.js (including npm/npx)
|
|
21
|
+
separately when executing commands; importing and initializing the Flask
|
|
22
|
+
extension does not require it.
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install Flask-Node
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Application setup
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from flask import Flask
|
|
32
|
+
from flask_node import Node
|
|
33
|
+
|
|
34
|
+
node = Node()
|
|
35
|
+
|
|
36
|
+
def create_app():
|
|
37
|
+
app = Flask(__name__)
|
|
38
|
+
node.init_app(app)
|
|
39
|
+
return app
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`Node(app)` is also supported. `init_app()` registers defaults and the CLI,
|
|
43
|
+
without creating directories or launching commands. Each application owns a
|
|
44
|
+
separate `NodeManager` in `app.extensions["node"]`; a shared `Node` facade uses
|
|
45
|
+
the current application context. `node.get_manager(app)` gives explicit access
|
|
46
|
+
outside a context. Duplicate registration raises `ConfigurationError`.
|
|
47
|
+
|
|
48
|
+
## Configuration
|
|
49
|
+
|
|
50
|
+
| Setting | Default | Meaning |
|
|
51
|
+
| --- | --- | --- |
|
|
52
|
+
| `NODE_DIR` | `.node` | Relative to `app.root_path`, or an absolute path |
|
|
53
|
+
| `NODE_BIN` | `node` | Node executable name or path |
|
|
54
|
+
| `NODE_NPM_BIN` | `npm` | npm executable name or path |
|
|
55
|
+
| `NODE_NPX_BIN` | `npx` | npx executable name or path |
|
|
56
|
+
|
|
57
|
+
Configure before calling `init_app()`. Configuration is captured per app.
|
|
58
|
+
For a directory beside an application package, configure an absolute project
|
|
59
|
+
path. No behavior depends on the shell's current directory.
|
|
60
|
+
|
|
61
|
+
## Lifecycle and dependencies
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
with app.app_context():
|
|
65
|
+
node.require("example", version="^1")
|
|
66
|
+
node.require("@scope/build-tool", version="^2", dev=True)
|
|
67
|
+
node.initialize()
|
|
68
|
+
node.install()
|
|
69
|
+
node.install("another-package", version="^3")
|
|
70
|
+
node.uninstall("another-package")
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`require()` only records an in-memory declaration. Consumer extensions may call
|
|
74
|
+
it during application setup. It creates no files and does not check or install
|
|
75
|
+
packages. Identical declarations are idempotent; differing versions or dependency
|
|
76
|
+
sections raise `DependencyConflictError`. Version declarations are compared as
|
|
77
|
+
strings; Flask-Node does not solve semver ranges.
|
|
78
|
+
|
|
79
|
+
`initialize()` creates `.node/package.json` with `private: true` and empty
|
|
80
|
+
`dependencies` / `devDependencies`. It preserves an existing valid manifest
|
|
81
|
+
without rewriting it. Invalid manifests fail clearly. npm creates the lockfile
|
|
82
|
+
and installed modules later:
|
|
83
|
+
|
|
84
|
+
```text
|
|
85
|
+
.node/
|
|
86
|
+
package.json
|
|
87
|
+
package-lock.json
|
|
88
|
+
node_modules/
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`install()` initializes if necessary, merges active declarations into the
|
|
92
|
+
manifest, preserves unrelated fields and dependencies, and runs `npm install`.
|
|
93
|
+
Declarations take precedence over persisted entries for the same package.
|
|
94
|
+
`install(name, version=None, dev=False)` additionally installs a registry package;
|
|
95
|
+
without a version, npm chooses it unless an active declaration supplies one.
|
|
96
|
+
Conflicting explicit options fail before installation. Uninstalling an actively
|
|
97
|
+
required package is rejected. npm failures may leave manifest changes or partial
|
|
98
|
+
installation artifacts; operations are not transactional.
|
|
99
|
+
|
|
100
|
+
`ci()` requires `package-lock.json` and declarations matching the manifest, then
|
|
101
|
+
runs `npm ci` without rewriting the manifest. npm validates lockfile consistency.
|
|
102
|
+
Commit `.node/package.json` and `.node/package-lock.json` for reproducible builds;
|
|
103
|
+
ignore `.node/node_modules/`.
|
|
104
|
+
|
|
105
|
+
## Commands and diagnostics
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
with app.app_context():
|
|
109
|
+
result = node.npm("run", "custom-script")
|
|
110
|
+
result = node.npx("some-tool", "--help", capture_output=False)
|
|
111
|
+
print(node.status())
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Raw commands require an initialized environment and run with its directory as
|
|
115
|
+
`cwd`. Arguments are passed individually with `shell=False`. Python calls capture
|
|
116
|
+
output by default; use `capture_output=False` to inherit terminal streams.
|
|
117
|
+
`CommandResult` exposes `args`, `returncode`, `stdout`, and `stderr`.
|
|
118
|
+
|
|
119
|
+
`status()` reports the directory, manifest presence, and executable locations
|
|
120
|
+
(or `None`). It launches no processes and does not validate executable versions.
|
|
121
|
+
Missing executables raise `ExecutableNotFoundError`; nonzero exits and other
|
|
122
|
+
launch failures raise `CommandExecutionError`, which retains `command`, `cwd`,
|
|
123
|
+
`returncode`, `stdout`, and `stderr`. All public errors derive from `NodeError`.
|
|
124
|
+
|
|
125
|
+
npm/npx may access the network and run package scripts. The managed working
|
|
126
|
+
directory is not a process sandbox; invoked tools can write elsewhere.
|
|
127
|
+
|
|
128
|
+
## Flask CLI
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
flask --app your_app node init
|
|
132
|
+
flask --app your_app node install
|
|
133
|
+
flask --app your_app node install example --version '^1' --dev
|
|
134
|
+
flask --app your_app node uninstall example
|
|
135
|
+
flask --app your_app node npm -- run custom-script --flag
|
|
136
|
+
flask --app your_app node npx -- some-tool --help
|
|
137
|
+
flask --app your_app node ci
|
|
138
|
+
flask --app your_app node status
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
CLI commands use the same Python operations, stream subprocess output, and
|
|
142
|
+
report extension errors with a nonzero exit status. `--` separates forwarding
|
|
143
|
+
arguments from Click's own options.
|
|
144
|
+
|
|
145
|
+
## Installed assets and extension consumers
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
manager = app.extensions["node"]
|
|
149
|
+
manager.require("example", "^1") # Safe during consumer init_app().
|
|
150
|
+
|
|
151
|
+
# After an explicit installation step:
|
|
152
|
+
asset = manager.resolve("example", "dist/example.js")
|
|
153
|
+
package = manager.package("example")
|
|
154
|
+
assert package.resolve("dist/example.js") == asset
|
|
155
|
+
print(package.name, package.version, package.root)
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
The consumer must initialize Flask-Node first. It owns its library-specific
|
|
159
|
+
configuration, rendering, asset copying/serving, and build/watch commands.
|
|
160
|
+
Flask-Node only declares/installs dependencies, executes commands, and locates
|
|
161
|
+
files. No registration protocol is needed.
|
|
162
|
+
|
|
163
|
+
Package lookup supports ordinary and scoped registry names. Asset lookup returns
|
|
164
|
+
an existing `Path`; absolute paths, parent traversal, Windows-style paths, and
|
|
165
|
+
symlinks escaping the package or managed environment are rejected. Externally
|
|
166
|
+
linked packages are intentionally unsupported. Missing or malformed installed
|
|
167
|
+
packages raise `PackageNotFoundError`; unsafe or missing assets raise
|
|
168
|
+
`AssetResolutionError`. This is filesystem path validation, not protection
|
|
169
|
+
against concurrent malicious filesystem changes.
|
|
170
|
+
|
|
171
|
+
## Development
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
python -m venv .venv
|
|
175
|
+
.venv/bin/python -m pip install -e '.[test]' build
|
|
176
|
+
.venv/bin/python -m pytest
|
|
177
|
+
.venv/bin/python -m build
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Tests inject/mock the runner and subprocess boundary. They never download npm
|
|
181
|
+
packages or require Node. A custom runner can be supplied to `Node(runner=...)`
|
|
182
|
+
or `NodeManager(..., runner=...)` for testing. This dependency injection is not a
|
|
183
|
+
consumer plugin protocol.
|
|
184
|
+
|
|
185
|
+
Inspired by [Flask-Tailwind-Manager](https://github.com/SebaSalinass/flask-tailwind-manager).
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
MANIFEST.in
|
|
3
|
+
README.md
|
|
4
|
+
pyproject.toml
|
|
5
|
+
src/Flask_Node.egg-info/PKG-INFO
|
|
6
|
+
src/Flask_Node.egg-info/SOURCES.txt
|
|
7
|
+
src/Flask_Node.egg-info/dependency_links.txt
|
|
8
|
+
src/Flask_Node.egg-info/requires.txt
|
|
9
|
+
src/Flask_Node.egg-info/top_level.txt
|
|
10
|
+
src/flask_node/__init__.py
|
|
11
|
+
src/flask_node/cli.py
|
|
12
|
+
src/flask_node/exceptions.py
|
|
13
|
+
src/flask_node/extension.py
|
|
14
|
+
src/flask_node/manager.py
|
|
15
|
+
src/flask_node/package.py
|
|
16
|
+
src/flask_node/py.typed
|
|
17
|
+
src/flask_node/runner.py
|
|
18
|
+
tests/conftest.py
|
|
19
|
+
tests/test_cli.py
|
|
20
|
+
tests/test_extension.py
|
|
21
|
+
tests/test_manager.py
|
|
22
|
+
tests/test_package.py
|
|
23
|
+
tests/test_runner.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
flask_node
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Generic Node/npm infrastructure for Flask applications."""
|
|
2
|
+
from .extension import Node
|
|
3
|
+
from .manager import NodeManager
|
|
4
|
+
from .package import Package
|
|
5
|
+
from .runner import CommandResult, CommandRunner
|
|
6
|
+
from .exceptions import (
|
|
7
|
+
NodeError, ConfigurationError, EnvironmentError, ExecutableNotFoundError,
|
|
8
|
+
CommandExecutionError, DependencyConflictError, PackageNotFoundError,
|
|
9
|
+
AssetResolutionError,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = ["Node", "NodeManager", "Package", "CommandResult", "CommandRunner", "NodeError",
|
|
13
|
+
"ConfigurationError", "EnvironmentError", "ExecutableNotFoundError",
|
|
14
|
+
"CommandExecutionError", "DependencyConflictError", "PackageNotFoundError",
|
|
15
|
+
"AssetResolutionError"]
|