Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion invenio_cli/cli/assets.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2020 CERN.
# Copyright (C) 2022 Graz University of Technology.
# Copyright (C) 2022-2025 Graz University of Technology.
#
# Invenio-Cli is free software; you can redistribute it and/or modify it
# under the terms of the MIT License; see LICENSE file for more details.
Expand Down Expand Up @@ -53,6 +53,14 @@ def build(cli_config, no_wipe, production, node_log_file):
)


@assets.command()
@pass_cli_config
def lock(cli_config):
"""Create a lockfile for your assets."""
commands = AssetsCommands(cli_config)
commands.lock()


@assets.command()
@click.argument("path", type=click.Path(exists=True))
@pass_cli_config
Expand Down
52 changes: 52 additions & 0 deletions invenio_cli/commands/assets.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2026 California Institute of Technology.
# Copyright (C) 2020 CERN.
#
# Invenio-Cli is free software; you can redistribute it and/or modify it
Expand All @@ -8,6 +9,7 @@
"""Invenio module to ease the creation and management of applications."""

from pathlib import Path
from shutil import copyfile

import click

Expand Down Expand Up @@ -74,6 +76,24 @@ def _build_script(module_pkg):
status_code=status_code,
)

def _cache_js_lock_files(self):
"""Cache js lock files."""
instance_path = self.cli_config.get_instance_path()
project_dir = self.cli_config.get_project_dir()

lock_file = self.cli_config.javascript_package_manager.lock_file_name
target_path = project_dir / lock_file
source_path = instance_path / "assets" / lock_file
if not source_path.is_symlink():
copyfile(source_path, target_path)

target_path = project_dir / "package.json"
source_path = instance_path / "assets" / "package.json"
if not source_path.is_symlink():
copyfile(source_path, target_path)

return ProcessResponse(output="Cached js lock files.", status_code=0)

def watch_assets(self):
"""High-level command to watch assets for changes."""
watch_cmd = self.cli_config.python_package_manager.run_command(
Expand Down Expand Up @@ -137,3 +157,35 @@ def watch_js_module(self, path, link=True):
)

return steps

def lock(self, debug=True, log_file=None):
"""Lock assets."""
py_pkg_man = self.cli_config.python_package_manager
js_pkg_man = self.cli_config.javascript_package_manager

lock_arg = self.cli_config.javascript_package_manager.lock_dependencies()

ops = [py_pkg_man.run_command("invenio", "collect", "--verbose")]
ops.append(py_pkg_man.run_command("invenio", "webpack", "clean", "create"))
lock_args = ["invenio", "webpack", "install"] + lock_arg
ops.append(py_pkg_man.run_command(*lock_args))
ops.append(self._cache_js_lock_files)
messages = {
"build": "Locking assets...",
}

with env(FLASK_DEBUG="1" if debug else "0"):
for op in ops:
if callable(op):
response = op()
else:
if op[-1] in messages:
click.secho(messages[op[-1]], fg="green")
response = run_interactive(
op,
env={"PIPENV_VERBOSITY": "-1", **js_pkg_man.env_overrides()},
log_file=log_file,
)
if response.status_code != 0:
break
return response
29 changes: 29 additions & 0 deletions invenio_cli/commands/local.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2026 California Institute of Technology.
# Copyright (C) 2020 CERN.
# Copyright (C) 2022 Graz University of Technology.
#
Expand All @@ -13,6 +14,7 @@
from distutils.dir_util import copy_tree
from os import environ
from pathlib import Path
from shutil import copyfile
from subprocess import Popen as popen

import click
Expand Down Expand Up @@ -79,6 +81,26 @@ def _statics(self):
status_code=0,
)

def _copy_js_lock_files(self):
"""Copy js lock files."""
instance_path = self.cli_config.get_instance_path()
project_dir = self.cli_config.get_project_dir()

lock_file = self.cli_config.javascript_package_manager.lock_file_name
source_path = project_dir / lock_file
target_path = instance_path / "assets" / lock_file
if Path(source_path).exists():
copyfile(source_path, target_path)

source_path = project_dir / "package.json"
Comment thread
tmorrell marked this conversation as resolved.
target_path = instance_path / "assets" / "package.json"
if Path(source_path).exists():
copyfile(source_path, target_path)

return ProcessResponse(
output="Copied js lock files to instance.", status_code=0
)

def update_statics_and_assets(self, force, debug=False, log_file=None):
"""High-level command to update less/js/images/... files.

Expand All @@ -91,11 +113,18 @@ def update_statics_and_assets(self, force, debug=False, log_file=None):

if force:
ops.append(py_pkg_man.run_command("invenio", "webpack", "clean", "create"))
# We need to copy the js lock files here, since webpack regenerates
# package.json and we want the locked version instead.
ops.append(self._copy_js_lock_files)
ops.append(py_pkg_man.run_command("invenio", "webpack", "install"))
else:
ops.append(py_pkg_man.run_command("invenio", "webpack", "create"))
# We need to copy the js lock files here, since webpack regenerates
# package.json and we want the locked version instead.
ops.append(self._copy_js_lock_files)
ops.append(self._statics)
ops.append(py_pkg_man.run_command("invenio", "webpack", "build"))

# Keep the same messages for some of the operations for backward compatibility
messages = {
"build": "Building assets...",
Expand Down
15 changes: 15 additions & 0 deletions invenio_cli/helpers/package_managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ class JavascriptPackageManager(ABC):
"""Interface for creating tool-specific JS package management commands."""

name = None
lock_file_name = None

def create_pynpm_package(self, package_json_path: Union[Path, str]) -> NPMPackage:
"""Create a variant of ``NPMPackage`` with the path to ``package.json``."""
Expand All @@ -209,11 +210,16 @@ def package_linking_steps(self) -> List[AssetsFunction]:
"""Generate steps to link the target package to the project."""
raise NotImplementedError()

def lock_dependencies(self) -> List[str]:
"""Update the lock file."""
raise NotImplementedError()


class NPM(JavascriptPackageManager):
"""Generate ``npm`` commands for managing JS packages."""

name = "npm"
lock_file_name = "package-lock.json"

def create_pynpm_package(self, package_json_path):
"""Create an ``NPMPackage`` with the path to ``package.json``."""
Expand All @@ -223,6 +229,10 @@ def install_local_package(self, path):
"""Install the local JS package."""
return ["--prefix", str(path)]

def lock_dependencies(self):
"""Lock the JS package."""
return ["--package-lock-only", "--ignore-scripts"]

def env_overrides(self):
"""Provide environment overrides for building Invenio assets."""
return {"INVENIO_WEBPACKEXT_NPM_PKG_CLS": "pynpm:NPMPackage"}
Expand Down Expand Up @@ -278,6 +288,7 @@ class PNPM(JavascriptPackageManager):
"""Generate ``pnpm`` commands for managing JS packages."""

name = "pnpm"
lock_file_name = "pnpm-lock.yaml"

def create_pynpm_package(self, package_json_path):
"""Create a ``PNPMPackage`` with the path to ``package.json``."""
Expand All @@ -287,6 +298,10 @@ def install_local_package(self, path):
"""Install the local JS package."""
return ["-C", str(path)]

def lock_dependencies(self):
"""Lock the JS package."""
return ["--lockfile-only"]

def env_overrides(self):
"""Provide environment overrides for building Invenio assets."""
return {"INVENIO_WEBPACKEXT_NPM_PKG_CLS": "pynpm:PNPMPackage"}
Expand Down