Skip to main content

Flask-Node

Flask-Node manages an isolated Node/npm project for Flask applications and extensions. It contains no JavaScript library integrations, bundler, asset server, or frontend framework.

Requires Python 3.10+ and Flask 2.2–3.x. Install Node.js (including npm/npx) separately when executing commands; importing and initializing the Flask extension does not require it.

pip install Flask-Node

Application setup

from flask import Flask
from flask_node import Node

node = Node()


def create_app():
    app = Flask(__name__)
    node.init_app(app)
    return app

Node(app) is also supported. init_app() registers defaults and the CLI, without creating directories or launching commands. Each application owns a separate NodeManager in app.extensions["node"]; a shared Node facade uses the current application context. node.get_manager(app) gives explicit access outside a context. Duplicate registration raises ConfigurationError.

Configuration

Setting Default Meaning
NODE_DIR .node Relative to app.root_path, or an absolute path
NODE_PYPROJECT pyproject.toml Application configuration, relative to app.root_path or absolute
NODE_BIN node Node executable name or path
NODE_NPM_BIN npm npm executable name or path
NODE_NPX_BIN npx npx executable name or path

Configure before calling init_app(). Configuration is captured per app. For a directory beside an application package, configure an absolute project path. No behavior depends on the shell's current directory.

Declarative application requirements

Commit your application's pyproject.toml with:

[tool.flask-node]
version = 1

[tool.flask-node.packages]
leaflet = "^1.9"
"chart.js" = "^4.5"

[tool.flask-node.dev-packages]
tailwindcss = "^4.1"
"@tailwindcss/cli" = "^4.1"

Configuration is read and validated during init_app(), without creating files or running npm. A missing file or table means no application declarations. Unknown settings, unsupported schema versions, and invalid declarations fail clearly. For a packaged Flask application, set NODE_PYPROJECT to the project file (for example ../pyproject.toml); paths never depend on the shell directory. Restart the application after changing declarations.

Extensions continue to call require(). Their requirements remain in memory and are never written to the application's TOML. manager.requirements (also node.requirements in an app context) returns a sorted snapshot of the combined requirements. Identical version strings and dependency sections merge; all other duplicates raise DependencyConflictError, including potentially overlapping semver ranges. Flask-Node does not attempt npm semver resolution.

flask --app your_app node sync

sync() generates a deterministic, private .node/package.json containing exactly the effective declarations, then runs npm install. It removes stale manifest dependencies and custom metadata/scripts. Declare every needed direct package in TOML or in its owning extension before using sync. The .node/ directory is disposable: deleting it and running sync reconstructs the project. Commit the TOML and Python dependencies; the generated manifest need not be committed. The existing install/uninstall and raw npm APIs are imperative tools: they do not persist application declarations, and their changes are lost on sync.

Version ranges reconstruct the requirements, but can resolve to newer releases. For an identical dependency tree, preserve a matching npm lockfile separately, restore it to .node/package-lock.json, generate the manifest with sync() (which runs npm install), and use ci() for subsequent locked installs. npm owns lockfile consistency and transitive resolution. No npm operation runs at startup.

Lifecycle and dependencies

with app.app_context():
    node.require("example", version="^1")
    node.require("@scope/build-tool", version="^2", dev=True)
    node.initialize()
    node.install()
    node.install("another-package", version="^3")
    node.uninstall("another-package")

require() only records an in-memory declaration. Consumer extensions may call it during application setup. It creates no files and does not check or install packages. Identical declarations are idempotent; differing versions or dependency sections raise DependencyConflictError. Version declarations are compared as strings; Flask-Node does not solve semver ranges.

initialize() creates .node/package.json with private: true and empty dependencies / devDependencies. It preserves an existing valid manifest without rewriting it. Invalid manifests fail clearly. npm creates the lockfile and installed modules later:

.node/
    package.json
    package-lock.json
    node_modules/

install() initializes if necessary, merges active declarations into the manifest, preserves unrelated fields and dependencies, and runs npm install. Declarations take precedence over persisted entries for the same package. install(name, version=None, dev=False) additionally installs a registry package; without a version, npm chooses it unless an active declaration supplies one. Conflicting explicit options fail before installation. Uninstalling an actively required package is rejected. npm failures may leave manifest changes or partial installation artifacts; operations are not transactional.

ci() requires package-lock.json and declarations matching the manifest, then runs npm ci without rewriting the manifest. npm validates lockfile consistency. For imperative workflows, preserve the manifest and lockfile for locked builds. For declarative workflows, the manifest is generated and the lockfile must be preserved separately if exact dependency-tree reproducibility is needed.

Commands and diagnostics

with app.app_context():
    result = node.npm("run", "custom-script")
    result = node.npx("some-tool", "--help", capture_output=False)
    print(node.status())

Raw commands require an initialized environment and run with its directory as cwd. Arguments are passed individually with shell=False. Python calls capture output by default; use capture_output=False to inherit terminal streams. CommandResult exposes args, returncode, stdout, and stderr.

status() reports the directory, manifest presence, and executable locations (or None). It launches no processes and does not validate executable versions. Missing executables raise ExecutableNotFoundError; nonzero exits and other launch failures raise CommandExecutionError, which retains command, cwd, returncode, stdout, and stderr. All public errors derive from NodeError.

npm/npx may access the network and run package scripts. The managed working directory is not a process sandbox; invoked tools can write elsewhere.

Flask CLI

flask --app your_app node init
flask --app your_app node sync
flask --app your_app node install
flask --app your_app node install example --version '^1' --dev
flask --app your_app node uninstall example
flask --app your_app node npm -- run custom-script --flag
flask --app your_app node npx -- some-tool --help
flask --app your_app node ci
flask --app your_app node status

CLI commands use the same Python operations, stream subprocess output, and report extension errors with a nonzero exit status. -- separates forwarding arguments from Click's own options.

Installed assets and extension consumers

manager = app.extensions["node"]
manager.require("example", "^1")  # Safe during consumer init_app().

# After an explicit installation step:
asset = manager.resolve("example", "dist/example.js")
package = manager.package("example")
assert package.resolve("dist/example.js") == asset
print(package.name, package.version, package.root)

The consumer must initialize Flask-Node first. It owns its library-specific configuration, asset selection, rendering, and build/watch commands. Flask serves published assets through its configured static route. Flask-Node declares/installs dependencies, executes commands, locates package files, and publishes explicitly selected assets.

Package lookup supports ordinary and scoped registry names. Asset lookup returns an existing Path; absolute paths, parent traversal, Windows-style paths, and symlinks escaping the package or managed environment are rejected. Externally linked packages are intentionally unsupported. Missing or malformed installed packages raise PackageNotFoundError; unsafe or missing assets raise AssetResolutionError. This is filesystem path validation, not protection against concurrent malicious filesystem changes.

Publishing browser assets

Consuming extensions choose which npm dependencies and browser assets they need. During their init_app(), after Flask-Node is initialized, they can register:

manager = node.get_manager(app)
manager.require("some-package", version="^1")
asset = manager.register_asset(
    package="some-package",
    source="dist/library.js",
    destination="vendor/library/library.js",
)
manager.register_asset(
    package="some-package",
    source="dist/images",
    destination="vendor/library/images",
)

Registration validates declarations and records them on the application's manager. It does not copy files, require installed packages, change the manifest, or run npm. Requirements and assets are registered separately. manager.assets and node.assets expose a sorted tuple of immutable NodeAsset declarations. Identical registrations deduplicate; different sources targeting the same normalized destination or overlapping parent/child destinations raise AssetConflictError.

Reconstruct the environment and publish explicitly:

pip install -e .
flask --app your_app node sync
flask --app your_app node publish

This copies selected files from .node/node_modules/ into Flask's actual configured static_folder. The CLI reports each package, source, destination, and the total count. sync does not publish. Publishing does not run npm. The default result above is static/vendor/library/library.js and a recursive copy of the images directory. Custom Flask static folders are supported; static serving disabled with static_folder=None produces a clear publication error. A missing static directory is created during publication.

Python callers can use manager.publish_assets() / node.publish_assets() to publish registrations and receive a tuple of destination Path objects. publish_asset(package, source, destination) immediately publishes one asset without registering it and returns its destination Path.

Files are replaced; registered directories are replaced completely, removing stale files. Unrelated static files are preserved. File/directory type mismatches fail clearly. Sources and destinations are preflighted before any batch copy; copies are staged beside each destination and the previous destination is restored if the final rename fails. A batch is not transactional: earlier assets may already be updated if a later filesystem operation fails.

Sources resolve through the existing installed-package API. Absolute paths, parent traversal, Windows drive paths, empty/root paths, and backslashes are rejected. Resolved destinations must remain within the configured static root. Directory source trees reject symlinks and special files; destination paths and existing destination trees reject symlinks below the static root. The configured static folder's resolved path defines that root. Source/destination overlap is rejected. These checks do not protect against concurrent malicious filesystem changes. Missing packages/assets and publication failures raise Flask-Node errors, including AssetPublicationError for filesystem copy failures.

Consumers can use the registered static filename with Flask:

from flask import url_for

url = url_for("static", filename=asset.destination)

HTML/Jinja rendering stays with the consuming extension. Flask-Node has no library-specific asset selection, bundling, or rendering behavior.

Both .node/ and registered static/vendor/ destinations are generated, reconstructable state. Applications can generally ignore them in Git, while committing application source, Python dependencies, and pyproject.toml. Flask-Node does not edit .gitignore. Exact dependency-tree reconstruction still requires preserving a matching npm lockfile as described above.

Development

python -m venv .venv
.venv/bin/python -m pip install -e '.[test]' build
.venv/bin/python -m pytest
.venv/bin/python -m build

Tests inject/mock the runner and subprocess boundary. They never download npm packages or require Node. A custom runner can be supplied to Node(runner=...) or NodeManager(..., runner=...) for testing. This dependency injection is not a consumer plugin protocol.

Inspired by Flask-Tailwind-Manager.

Metadata

Release files for Flask-Node 0.1.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for Flask-Node 0.1.2
File Size Uploaded
flask_node-0.1.2.tar.gz 20.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for Flask-Node 0.1.2
File Interpreter ABI Platform
flask_node-0.1.2-py3-none-any.whl Python 3 none any Details

Total release size: 37.8 kB

Release files / flask_node-0.1.2.tar.gz

Download URL flask_node-0.1.2.tar.gz
Size 20.3 kB
Tags Source
SHA-256 checksum
How to use checksums
e7bc0c57914bb5004f144855b9c1a53b76f3ddef7b39fb515191fe2c8c9e0856
BLAKE2b-256 checksum
How to use checksums
cf4ab56e3e1af87b4894df217ff673eba96f27c81a2caa472ec43c6db4855abc
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.9 {"installer":{"name":"uv","version":"0.12.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / flask_node-0.1.2-py3-none-any.whl

Download URL flask_node-0.1.2-py3-none-any.whl
Size 17.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
5d7f87035ce0b8316652c6851ac033c46926f03eab780abc858d861a8a49aa81
BLAKE2b-256 checksum
How to use checksums
dac964684eab1032f4b9b7042cd391efb140796016521ba4e6a21a008330820b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.9 {"installer":{"name":"uv","version":"0.12.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release history Release notifications | RSS feed

0.2.0

2 release files

This release

0.1.2 This release

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page