From 76bcce65c64acd7f4f93566104722cd06213d1da Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Wed, 3 Jul 2024 01:13:44 +0000 Subject: [PATCH 1/8] fix: craft-cli branch ref to main --- requirements-dev.txt | 2 +- requirements-doc.txt | 2 +- requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 5f2912ec5..8edd40eb5 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -20,7 +20,7 @@ colorama==0.4.6 coverage==7.5.1 craft-application @ git+https://github.com/canonical/craft-application@rockcraft craft-archives==1.1.3 -craft-cli @ git+https://github.com/canonical/craft-cli@rockcraft +craft-cli @ git+https://github.com/canonical/craft-cli@main craft-grammar==1.2.0 craft-parts @ git+https://github.com/canonical/craft-parts@rockcraft craft-providers @ git+https://github.com/canonical/craft-providers@rockcraft diff --git a/requirements-doc.txt b/requirements-doc.txt index d55cb2d7a..29a446e38 100644 --- a/requirements-doc.txt +++ b/requirements-doc.txt @@ -12,7 +12,7 @@ click==8.1.7 colorama==0.4.6 craft-application @ git+https://github.com/canonical/craft-application@rockcraft craft-archives==1.1.3 -craft-cli @ git+https://github.com/canonical/craft-cli@rockcraft +craft-cli @ git+https://github.com/canonical/craft-cli@main craft-grammar==1.2.0 craft-parts @ git+https://github.com/canonical/craft-parts@rockcraft craft-providers @ git+https://github.com/canonical/craft-providers@rockcraft diff --git a/requirements.txt b/requirements.txt index 5cdc627bf..9925894b0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ cffi==1.16.0 charset-normalizer==3.3.2 craft-application @ git+https://github.com/canonical/craft-application@rockcraft craft-archives==1.1.3 -craft-cli @ git+https://github.com/canonical/craft-cli@rockcraft +craft-cli @ git+https://github.com/canonical/craft-cli@main craft-grammar==1.2.0 craft-parts @ git+https://github.com/canonical/craft-parts@rockcraft craft-providers @ git+https://github.com/canonical/craft-providers@rockcraft From ce9efe5b148764f78fa273dc7352c1eb420a67e6 Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Wed, 18 Dec 2024 02:49:15 +0000 Subject: [PATCH 2/8] high-level functions --- rockcraft/extensions/express.py | 92 +++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 rockcraft/extensions/express.py diff --git a/rockcraft/extensions/express.py b/rockcraft/extensions/express.py new file mode 100644 index 000000000..e462bebdc --- /dev/null +++ b/rockcraft/extensions/express.py @@ -0,0 +1,92 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2024 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""An extension for the NodeJS based Javascript application extension.""" +from typing import Any, Dict, Tuple + +from overrides import override + +from .extension import Extension + + +class ExpressJSFramework(Extension): + """An extension for constructing Javascript applications based on the ExpressJS framework.""" + + @staticmethod + @override + def get_supported_bases() -> Tuple[str, ...]: + """Return supported bases.""" + return "bare", "ubuntu@22.04", "ubuntu:22.04", "ubuntu@24.04", "ubuntu:24.04" + + @staticmethod + @override + def is_experimental(base: str | None) -> bool: + """Check if the extension is in an experimental state.""" + return False + + @property + @override + def framework(self) -> str: + """Return the framework name, i.e. expressjs.""" + return "expressjs" + + @override + def get_root_snippet(self) -> Dict[str, Any]: + """Fill in some default root components. + + Default values: + - run_user: _daemon_ + - build-base: ubuntu:22.04 (only if user specify bare without a build-base) + - platform: amd64 + - services: a service to run the ExpressJS server + - parts: see ExpressJSFramework._gen_parts + """ + snippet: Dict[str, Any] = { + "run-user": "_daemon_", + "services": { + "app": { + "override": "replace", + "command": "npm start", + "startup": "enabled", + "on-success": "shutdown", + "on-failure": "shutdown", + "working-dir": "/lib/node_modules/expressjs-project", + } + }, + } + return snippet + + @override + def get_part_snippet(self) -> dict[str, Any]: + """Return the part snippet to apply to existing parts.""" + return {} + + @override + def get_parts_snippet(self) -> dict[str, Any]: + """Return the parts to add to parts.""" + return {} + + def _gen_parts(self) -> dict: + """Generate the parts associated with this extension.""" + ... + + def _check_project(self): + """Ensure this extension can apply to the current rockcraft project. + + The ExpressJS framework assumes that: + - The npm start script exists. + """ + ... From a9c6c5a736f6837b66ab3a9e3a1185d82aaa73cd Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Thu, 19 Dec 2024 06:38:39 +0000 Subject: [PATCH 3/8] public interfaces --- rockcraft/extensions/express.py | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/rockcraft/extensions/express.py b/rockcraft/extensions/express.py index e462bebdc..39cad8776 100644 --- a/rockcraft/extensions/express.py +++ b/rockcraft/extensions/express.py @@ -63,7 +63,7 @@ def get_root_snippet(self) -> Dict[str, Any]: "startup": "enabled", "on-success": "shutdown", "on-failure": "shutdown", - "working-dir": "/lib/node_modules/expressjs-project", + "working-dir": f"{self.framework}/app", } }, } @@ -71,16 +71,41 @@ def get_root_snippet(self) -> Dict[str, Any]: @override def get_part_snippet(self) -> dict[str, Any]: - """Return the part snippet to apply to existing parts.""" + """Return the part snippet to apply to existing parts. + + This is unused but is required by the ABC. + """ return {} @override def get_parts_snippet(self) -> dict[str, Any]: - """Return the parts to add to parts.""" + """Return the parts to add to parts. + + This is unused but is required by the ABC. + """ return {} def _gen_parts(self) -> dict: - """Generate the parts associated with this extension.""" + """Generate the parts associated with this extension. + + The parts generated are the following: + 1. install-app: Install the application with npm installs. + 2. install-dependencies: Install application host dependencies (e.g. pg lib) + """ + ... + + def _gen_npm_plugin_part(self) -> dict: + """Generate the part using NPM plugin.""" + ... + + @property + def _app_prime(self): + """Return the prime list for the ExpressJS proejct. + + Use the paths generated by the + express-generator (https://expressjs.com/en/starter/generator.html) tool by default if no + user prime paths are provided. Use only user prime paths otherwise. + """ ... def _check_project(self): From 8e234d9ce0eb962d898911381d0c91308cdc6dab Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Thu, 19 Dec 2024 06:40:35 +0000 Subject: [PATCH 4/8] Revert "fix: craft-cli branch ref to main" This reverts commit 76bcce65c64acd7f4f93566104722cd06213d1da. --- requirements-dev.txt | 2 +- requirements-doc.txt | 2 +- requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 8edd40eb5..5f2912ec5 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -20,7 +20,7 @@ colorama==0.4.6 coverage==7.5.1 craft-application @ git+https://github.com/canonical/craft-application@rockcraft craft-archives==1.1.3 -craft-cli @ git+https://github.com/canonical/craft-cli@main +craft-cli @ git+https://github.com/canonical/craft-cli@rockcraft craft-grammar==1.2.0 craft-parts @ git+https://github.com/canonical/craft-parts@rockcraft craft-providers @ git+https://github.com/canonical/craft-providers@rockcraft diff --git a/requirements-doc.txt b/requirements-doc.txt index 29a446e38..d55cb2d7a 100644 --- a/requirements-doc.txt +++ b/requirements-doc.txt @@ -12,7 +12,7 @@ click==8.1.7 colorama==0.4.6 craft-application @ git+https://github.com/canonical/craft-application@rockcraft craft-archives==1.1.3 -craft-cli @ git+https://github.com/canonical/craft-cli@main +craft-cli @ git+https://github.com/canonical/craft-cli@rockcraft craft-grammar==1.2.0 craft-parts @ git+https://github.com/canonical/craft-parts@rockcraft craft-providers @ git+https://github.com/canonical/craft-providers@rockcraft diff --git a/requirements.txt b/requirements.txt index 9925894b0..5cdc627bf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ cffi==1.16.0 charset-normalizer==3.3.2 craft-application @ git+https://github.com/canonical/craft-application@rockcraft craft-archives==1.1.3 -craft-cli @ git+https://github.com/canonical/craft-cli@main +craft-cli @ git+https://github.com/canonical/craft-cli@rockcraft craft-grammar==1.2.0 craft-parts @ git+https://github.com/canonical/craft-parts@rockcraft craft-providers @ git+https://github.com/canonical/craft-providers@rockcraft From 70a1aafadf8afa11bc9560ba7346cb7f7f17c16e Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Thu, 19 Dec 2024 06:41:51 +0000 Subject: [PATCH 5/8] rename --- rockcraft/extensions/express.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/rockcraft/extensions/express.py b/rockcraft/extensions/express.py index 39cad8776..aeef053f7 100644 --- a/rockcraft/extensions/express.py +++ b/rockcraft/extensions/express.py @@ -94,8 +94,12 @@ def _gen_parts(self) -> dict: """ ... - def _gen_npm_plugin_part(self) -> dict: - """Generate the part using NPM plugin.""" + def _gen_install_app_part(self) -> dict: + """Generate the install app part using NPM plugin.""" + ... + + def _gen_install_dependencies_part(self) -> dict: + """Generate the install dependencies part using dump plugin.""" ... @property From 3dd3ae7d3421caf0d71a8a32663ce3bb446147c5 Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Mon, 23 Dec 2024 04:59:41 +0000 Subject: [PATCH 6/8] parts --- rockcraft/extensions/express.py | 116 +++++++++++++++++++++++++------- 1 file changed, 91 insertions(+), 25 deletions(-) diff --git a/rockcraft/extensions/express.py b/rockcraft/extensions/express.py index aeef053f7..db493e7c5 100644 --- a/rockcraft/extensions/express.py +++ b/rockcraft/extensions/express.py @@ -15,33 +15,43 @@ # along with this program. If not, see . """An extension for the NodeJS based Javascript application extension.""" +import json +import re from typing import Any, Dict, Tuple from overrides import override +from ..errors import ExtensionError from .extension import Extension class ExpressJSFramework(Extension): """An extension for constructing Javascript applications based on the ExpressJS framework.""" + IMAGE_BASE_DIR = "app" + EXPRESS_GENERATOR_DIRS = ( + "bin", + "public", + "routes", + "views", + "app.js", + "package.json", + "package-lock.json", + "node_modules", + ) + RUNTIME_DEPENDENCIES = ["ca-certificates_data", "libpq5", "node"] + @staticmethod @override def get_supported_bases() -> Tuple[str, ...]: """Return supported bases.""" - return "bare", "ubuntu@22.04", "ubuntu:22.04", "ubuntu@24.04", "ubuntu:24.04" + return "bare", "ubuntu@22.04", "ubuntu@24.04" @staticmethod @override def is_experimental(base: str | None) -> bool: """Check if the extension is in an experimental state.""" - return False - - @property - @override - def framework(self) -> str: - """Return the framework name, i.e. expressjs.""" - return "expressjs" + return True @override def get_root_snippet(self) -> Dict[str, Any]: @@ -54,6 +64,8 @@ def get_root_snippet(self) -> Dict[str, Any]: - services: a service to run the ExpressJS server - parts: see ExpressJSFramework._gen_parts """ + self._check_project() + snippet: Dict[str, Any] = { "run-user": "_daemon_", "services": { @@ -63,10 +75,15 @@ def get_root_snippet(self) -> Dict[str, Any]: "startup": "enabled", "on-success": "shutdown", "on-failure": "shutdown", - "working-dir": f"{self.framework}/app", + "working-dir": "/app", } }, } + + snippet["parts"] = { + "expressjs-framework/install-app": self._gen_install_app_part(), + "expressjs-framework/runtime-dependencies": self._gen_runtime_dependencies_part(), + } return snippet @override @@ -85,37 +102,86 @@ def get_parts_snippet(self) -> dict[str, Any]: """ return {} - def _gen_parts(self) -> dict: - """Generate the parts associated with this extension. - - The parts generated are the following: - 1. install-app: Install the application with npm installs. - 2. install-dependencies: Install application host dependencies (e.g. pg lib) - """ - ... - def _gen_install_app_part(self) -> dict: """Generate the install app part using NPM plugin.""" - ... + return { + "plugin": "npm", + "npm-include-node": False, + "source": "app/", + "organise": self._app_organise, + "override-prime": f"rm -rf lib/node_modules/{self._app_name}", + } - def _gen_install_dependencies_part(self) -> dict: + def _gen_runtime_dependencies_part(self) -> dict: """Generate the install dependencies part using dump plugin.""" - ... + return { + "plugin": "nil", + "stage-packages": self.RUNTIME_DEPENDENCIES, + } + + @property + def _app_package_json(self): + """Return the app package.json contents.""" + package_json_file = self.project_root / "package.json" + if not package_json_file.exists(): + raise ExtensionError( + "missing package.json file", + doc_slug="/reference/extensions/expressjs-framework", + logpath_report=False, + ) + package_json_contents = package_json_file.read_text(encoding="utf-8") + return json.loads(package_json_contents) + + @property + def _app_name(self) -> str: + """Return the application name as defined on package.json.""" + return self._app_package_json["name"] @property - def _app_prime(self): - """Return the prime list for the ExpressJS proejct. + def _app_organise(self): + """Return the organised mapping for the ExpressJS project. Use the paths generated by the express-generator (https://expressjs.com/en/starter/generator.html) tool by default if no user prime paths are provided. Use only user prime paths otherwise. """ - ... + user_prime: list[str] = ( + self.yaml_data.get("parts", {}) + .get("expressjs-framework/install-app", {}) + .get("prime", []) + ) + if not all(re.match(f"-? *{self.IMAGE_BASE_DIR}/", p) for p in user_prime): + raise ExtensionError( + "expressjs-framework extension requires the 'prime' entry in the " + f"expressjs-framework/install-app part to start with {self.IMAGE_BASE_DIR}/", + doc_slug="/reference/extensions/expressjs-framework", + logpath_report=False, + ) + if not user_prime: + user_prime = [ + f"{self.project_root}/{f}" for f in self.EXPRESS_GENERATOR_DIRS + ] + lib_dir = f"lib/node_modules/{self._app_name}" + return { + f"{lib_dir}/{f}": f"app/{f}" + for f in user_prime + if (self.project_root / f).exists() + } def _check_project(self): """Ensure this extension can apply to the current rockcraft project. The ExpressJS framework assumes that: - The npm start script exists. + - The application name is defined. """ - ... + if ( + "scripts" not in self._app_package_json + or "start" not in self._app_package_json["scripts"] + or "name" not in self._app_package_json + ): + raise ExtensionError( + "missing start script", + doc_slug="/reference/extensions/expressjs-framework", + logpath_report=False, + ) From a03d789b2ca5b44d30b0255c4c8766d0f0b1bb30 Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Thu, 2 Jan 2025 03:03:18 +0000 Subject: [PATCH 7/8] chore: rename express js module --- rockcraft/extensions/__init__.py | 2 ++ .../extensions/{express.py => expressjs.py} | 25 ++++++++++++------- 2 files changed, 18 insertions(+), 9 deletions(-) rename rockcraft/extensions/{express.py => expressjs.py} (90%) diff --git a/rockcraft/extensions/__init__.py b/rockcraft/extensions/__init__.py index 621619b56..1815ebc35 100644 --- a/rockcraft/extensions/__init__.py +++ b/rockcraft/extensions/__init__.py @@ -17,6 +17,7 @@ """Extension processor and related utilities.""" from ._utils import apply_extensions +from .expressjs import ExpressJSFramework from .fastapi import FastAPIFramework from .go import GoFramework from .gunicorn import DjangoFramework, FlaskFramework @@ -31,6 +32,7 @@ ] register("django-framework", DjangoFramework) +register("expressjs-framework", ExpressJSFramework) register("fastapi-framework", FastAPIFramework) register("flask-framework", FlaskFramework) register("go-framework", GoFramework) diff --git a/rockcraft/extensions/express.py b/rockcraft/extensions/expressjs.py similarity index 90% rename from rockcraft/extensions/express.py rename to rockcraft/extensions/expressjs.py index db493e7c5..e6db3292d 100644 --- a/rockcraft/extensions/express.py +++ b/rockcraft/extensions/expressjs.py @@ -28,8 +28,8 @@ class ExpressJSFramework(Extension): """An extension for constructing Javascript applications based on the ExpressJS framework.""" - IMAGE_BASE_DIR = "app" - EXPRESS_GENERATOR_DIRS = ( + IMAGE_BASE_DIR = "app/" + EXPRESS_GENERATOR_DIRS = [ "bin", "public", "routes", @@ -38,7 +38,7 @@ class ExpressJSFramework(Extension): "package.json", "package-lock.json", "node_modules", - ) + ] RUNTIME_DEPENDENCIES = ["ca-certificates_data", "libpq5", "node"] @staticmethod @@ -150,6 +150,7 @@ def _app_organise(self): .get("expressjs-framework/install-app", {}) .get("prime", []) ) + print(user_prime) if not all(re.match(f"-? *{self.IMAGE_BASE_DIR}/", p) for p in user_prime): raise ExtensionError( "expressjs-framework extension requires the 'prime' entry in the " @@ -158,14 +159,15 @@ def _app_organise(self): logpath_report=False, ) if not user_prime: - user_prime = [ - f"{self.project_root}/{f}" for f in self.EXPRESS_GENERATOR_DIRS - ] + user_prime = self.EXPRESS_GENERATOR_DIRS + project_relative_file_paths = [ + prime_path.removeprefix(self.IMAGE_BASE_DIR) for prime_path in user_prime + ] lib_dir = f"lib/node_modules/{self._app_name}" return { f"{lib_dir}/{f}": f"app/{f}" - for f in user_prime - if (self.project_root / f).exists() + for f in project_relative_file_paths + if (self.project_root / "app" / f).exists() } def _check_project(self): @@ -178,10 +180,15 @@ def _check_project(self): if ( "scripts" not in self._app_package_json or "start" not in self._app_package_json["scripts"] - or "name" not in self._app_package_json ): raise ExtensionError( "missing start script", doc_slug="/reference/extensions/expressjs-framework", logpath_report=False, ) + if "name" not in self._app_package_json: + raise ExtensionError( + "missing application name", + doc_slug="/reference/extensions/expressjs-framework", + logpath_report=False, + ) From fd98937c37c5f0e4a70525b313a83749a5da81ac Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Thu, 2 Jan 2025 03:03:28 +0000 Subject: [PATCH 8/8] test: unit test expressjs module --- tests/unit/extensions/test_expressjs.py | 153 ++++++++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 tests/unit/extensions/test_expressjs.py diff --git a/tests/unit/extensions/test_expressjs.py b/tests/unit/extensions/test_expressjs.py new file mode 100644 index 000000000..ba05dc0b7 --- /dev/null +++ b/tests/unit/extensions/test_expressjs.py @@ -0,0 +1,153 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2024 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +import pytest + +from rockcraft import extensions +from rockcraft.errors import ExtensionError + + +@pytest.fixture(name="expressjs_input_yaml") +def expressjs_input_yaml_fixture(): + return { + "name": "foo-bar", + "base": "ubuntu@24.04", + "platforms": {"amd64": {}}, + "extensions": ["expressjs-framework"], + } + + +@pytest.fixture +def expressjs_extension(mock_extensions, monkeypatch): + monkeypatch.setenv("ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS", "1") + extensions.register("expressjs-framework", extensions.ExpressJSFramework) + + +@pytest.fixture +def expressjs_project_name(): + return "test-expressjs-project" + + +@pytest.fixture +def package_json_file(tmp_path, expressjs_project_name): + (tmp_path / "package.json").write_text( + f"""{{ + "name": "{expressjs_project_name}", + "scripts": {{ + "start": "node ./bin/www" + }} +}}""" + ) + + +@pytest.mark.usefixtures("expressjs_extension", "package_json_file") +def test_expressjs_extension_default( + tmp_path, expressjs_project_name, expressjs_input_yaml +): + applied = extensions.apply_extensions(tmp_path, expressjs_input_yaml) + + assert applied == { + "base": "ubuntu@24.04", + "name": "foo-bar", + "platforms": { + "amd64": {}, + }, + "run-user": "_daemon_", + "parts": { + "expressjs-framework/install-app": { + "plugin": "npm", + "npm-include-node": False, + "source": "app/", + "organise": { + f"lib/node_modules/{expressjs_project_name}/package.json": "app/package.json", + }, + "override-prime": f"rm -rf lib/node_modules/{expressjs_project_name}", + }, + "expressjs-framework/runtime-dependencies": { + "plugin": "nil", + "stage-packages": [ + "ca-certificates_data", + "libpq5", + "node", + ], + }, + }, + "services": { + "app": { + "command": "npm start", + "on-failure": "shutdown", + "on-success": "shutdown", + "override": "replace", + "startup": "enabled", + "working-dir": "/app", + }, + }, + } + + +@pytest.mark.usefixtures("expressjs_extension") +def test_expressjs_no_package_json_error(tmp_path, expressjs_input_yaml): + with pytest.raises(ExtensionError) as exc: + extensions.apply_extensions(tmp_path, expressjs_input_yaml) + assert str(exc.value) == "missing package.json file" + assert str(exc.value.doc_slug) == "/reference/extensions/expressjs-framework" + + +@pytest.mark.parametrize( + "package_json_contents, error_message", + [ + ("{}", "missing start script"), + ('{"scripts":{}}', "missing start script"), + ('{"scripts":{"start":"node ./bin/www"}}', "missing application name"), + ], +) +@pytest.mark.usefixtures("expressjs_extension") +def test_expressjs_invalid_package_json_scripts_error( + tmp_path, expressjs_input_yaml, package_json_contents, error_message +): + (tmp_path / "package.json").write_text(package_json_contents) + with pytest.raises(ExtensionError) as exc: + extensions.apply_extensions(tmp_path, expressjs_input_yaml) + assert str(exc.value) == error_message + assert str(exc.value.doc_slug) == "/reference/extensions/expressjs-framework" + + +@pytest.mark.parametrize( + "existing_files, missing_files, expected_organise", + [ + pytest.param( + ["lib/node_modules/test-expressjs-project/app.js"], + [], + {"lib/node_modules/test-expressjs-project/app.js": "app/app.js"}, + id="single file defined", + ), + ], +) +@pytest.mark.usefixtures("expressjs_extension", "package_json_file") +def test_expressjs_install_app_prime_to_organise_map( + tmp_path, expressjs_input_yaml, existing_files, missing_files, expected_organise +): + for file in existing_files: + (tmp_path / file).parent.mkdir(parents=True) + (tmp_path / file).touch() + prime_files = [*existing_files, *missing_files] + expressjs_input_yaml["parts"] = { + "expressjs-framework/install-app": {"prime": prime_files} + } + applied = extensions.apply_extensions(tmp_path, expressjs_input_yaml) + assert ( + applied["parts"]["expressjs-framework/install-app"]["organise"] + == expected_organise + )