From 1bf14da881d0b4ed54b47443b79505208e44dba0 Mon Sep 17 00:00:00 2001 From: charlie4284 Date: Thu, 2 Jan 2025 11:14:50 +0800 Subject: [PATCH 01/64] feat: expressjs framework extension --- rockcraft/extensions/expressjs.py | 194 ++++++++++++++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 rockcraft/extensions/expressjs.py diff --git a/rockcraft/extensions/expressjs.py b/rockcraft/extensions/expressjs.py new file mode 100644 index 000000000..e6db3292d --- /dev/null +++ b/rockcraft/extensions/expressjs.py @@ -0,0 +1,194 @@ +# -*- 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.""" +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@24.04" + + @staticmethod + @override + def is_experimental(base: str | None) -> bool: + """Check if the extension is in an experimental state.""" + return True + + @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 + """ + self._check_project() + + snippet: Dict[str, Any] = { + "run-user": "_daemon_", + "services": { + "app": { + "override": "replace", + "command": "npm start", + "startup": "enabled", + "on-success": "shutdown", + "on-failure": "shutdown", + "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 + def get_part_snippet(self) -> dict[str, Any]: + """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. + + This is unused but is required by the ABC. + """ + return {} + + 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_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_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", []) + ) + 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 " + 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 = 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 project_relative_file_paths + if (self.project_root / "app" / 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"] + ): + 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 5d5b8ba1764394fc0558f5bea341585f794ebe14 Mon Sep 17 00:00:00 2001 From: charlie4284 Date: Thu, 2 Jan 2025 11:15:00 +0800 Subject: [PATCH 02/64] test: expressjs framework extension unit tests --- 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 + ) From 8afa87e62c6de54e0ec764d1b819c9098719b0fb Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Thu, 2 Jan 2025 03:55:40 +0000 Subject: [PATCH 03/64] fix: lint --- rockcraft/extensions/__init__.py | 2 ++ rockcraft/extensions/expressjs.py | 16 ++++++++-------- tests/unit/extensions/test_expressjs.py | 1 - 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/rockcraft/extensions/__init__.py b/rockcraft/extensions/__init__.py index 621619b56..015de6b0e 100644 --- a/rockcraft/extensions/__init__.py +++ b/rockcraft/extensions/__init__.py @@ -19,6 +19,7 @@ from ._utils import apply_extensions from .fastapi import FastAPIFramework from .go import GoFramework +from .expressjs import ExpressJSFramework from .gunicorn import DjangoFramework, FlaskFramework from .registry import get_extension_class, get_extension_names, register, unregister @@ -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/expressjs.py b/rockcraft/extensions/expressjs.py index e6db3292d..c0ac4f81f 100644 --- a/rockcraft/extensions/expressjs.py +++ b/rockcraft/extensions/expressjs.py @@ -15,9 +15,10 @@ # 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 typing import Any from overrides import override @@ -43,7 +44,7 @@ class ExpressJSFramework(Extension): @staticmethod @override - def get_supported_bases() -> Tuple[str, ...]: + def get_supported_bases() -> tuple[str, ...]: """Return supported bases.""" return "bare", "ubuntu@22.04", "ubuntu@24.04" @@ -54,7 +55,7 @@ def is_experimental(base: str | None) -> bool: return True @override - def get_root_snippet(self) -> Dict[str, Any]: + def get_root_snippet(self) -> dict[str, Any]: """Fill in some default root components. Default values: @@ -66,7 +67,7 @@ def get_root_snippet(self) -> Dict[str, Any]: """ self._check_project() - snippet: Dict[str, Any] = { + snippet: dict[str, Any] = { "run-user": "_daemon_", "services": { "app": { @@ -120,7 +121,7 @@ def _gen_runtime_dependencies_part(self) -> dict: } @property - def _app_package_json(self): + def _app_package_json(self) -> dict: """Return the app package.json contents.""" package_json_file = self.project_root / "package.json" if not package_json_file.exists(): @@ -138,7 +139,7 @@ def _app_name(self) -> str: return self._app_package_json["name"] @property - def _app_organise(self): + def _app_organise(self) -> dict: """Return the organised mapping for the ExpressJS project. Use the paths generated by the @@ -150,7 +151,6 @@ 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 " @@ -170,7 +170,7 @@ def _app_organise(self): if (self.project_root / "app" / f).exists() } - def _check_project(self): + def _check_project(self) -> None: """Ensure this extension can apply to the current rockcraft project. The ExpressJS framework assumes that: diff --git a/tests/unit/extensions/test_expressjs.py b/tests/unit/extensions/test_expressjs.py index ba05dc0b7..363b45540 100644 --- a/tests/unit/extensions/test_expressjs.py +++ b/tests/unit/extensions/test_expressjs.py @@ -14,7 +14,6 @@ # 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 From a887f0016f594bddc680a59d3967abf5e02df80b Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Thu, 2 Jan 2025 06:26:03 +0000 Subject: [PATCH 04/64] fix: user prime app prefixing --- rockcraft/extensions/expressjs.py | 8 +++++--- tests/unit/extensions/test_expressjs.py | 6 ++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/rockcraft/extensions/expressjs.py b/rockcraft/extensions/expressjs.py index c0ac4f81f..4b9e2d661 100644 --- a/rockcraft/extensions/expressjs.py +++ b/rockcraft/extensions/expressjs.py @@ -36,6 +36,8 @@ class ExpressJSFramework(Extension): "routes", "views", "app.js", + ] + EXPRESS_PACKAGE_DIRS = [ "package.json", "package-lock.json", "node_modules", @@ -151,17 +153,17 @@ def _app_organise(self) -> dict: .get("expressjs-framework/install-app", {}) .get("prime", []) ) - if not all(re.match(f"-? *{self.IMAGE_BASE_DIR}/", p) for p in 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 " - f"expressjs-framework/install-app part to start with {self.IMAGE_BASE_DIR}/", + 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 = self.EXPRESS_GENERATOR_DIRS project_relative_file_paths = [ - prime_path.removeprefix(self.IMAGE_BASE_DIR) for prime_path in user_prime + prime_path.removeprefix(self.IMAGE_BASE_DIR) for prime_path in user_prime + self.EXPRESS_PACKAGE_DIRS ] lib_dir = f"lib/node_modules/{self._app_name}" return { diff --git a/tests/unit/extensions/test_expressjs.py b/tests/unit/extensions/test_expressjs.py index 363b45540..5cc6ee37f 100644 --- a/tests/unit/extensions/test_expressjs.py +++ b/tests/unit/extensions/test_expressjs.py @@ -69,9 +69,7 @@ def test_expressjs_extension_default( "plugin": "npm", "npm-include-node": False, "source": "app/", - "organise": { - f"lib/node_modules/{expressjs_project_name}/package.json": "app/package.json", - }, + "organise": {}, "override-prime": f"rm -rf lib/node_modules/{expressjs_project_name}", }, "expressjs-framework/runtime-dependencies": { @@ -127,7 +125,7 @@ def test_expressjs_invalid_package_json_scripts_error( "existing_files, missing_files, expected_organise", [ pytest.param( - ["lib/node_modules/test-expressjs-project/app.js"], + ["app/app.js"], [], {"lib/node_modules/test-expressjs-project/app.js": "app/app.js"}, id="single file defined", From be56040b8b7226fa3e1b5bdb777e2c4ab6a6cc41 Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Thu, 2 Jan 2025 06:59:53 +0000 Subject: [PATCH 05/64] test: spread test for express js --- .../rockcraft/extension-expressjs/app/app.js | 41 +++++++++ .../rockcraft/extension-expressjs/app/bin/www | 90 +++++++++++++++++++ .../extension-expressjs/app/package.json | 16 ++++ .../app/public/stylesheets/style.css | 8 ++ .../extension-expressjs/app/routes/index.js | 9 ++ .../extension-expressjs/app/routes/users.js | 9 ++ .../extension-expressjs/app/views/error.jade | 6 ++ .../extension-expressjs/app/views/index.jade | 5 ++ .../extension-expressjs/app/views/layout.jade | 7 ++ .../rockcraft/extension-expressjs/task.yaml | 45 ++++++++++ 10 files changed, 236 insertions(+) create mode 100644 tests/spread/rockcraft/extension-expressjs/app/app.js create mode 100755 tests/spread/rockcraft/extension-expressjs/app/bin/www create mode 100644 tests/spread/rockcraft/extension-expressjs/app/package.json create mode 100644 tests/spread/rockcraft/extension-expressjs/app/public/stylesheets/style.css create mode 100644 tests/spread/rockcraft/extension-expressjs/app/routes/index.js create mode 100644 tests/spread/rockcraft/extension-expressjs/app/routes/users.js create mode 100644 tests/spread/rockcraft/extension-expressjs/app/views/error.jade create mode 100644 tests/spread/rockcraft/extension-expressjs/app/views/index.jade create mode 100644 tests/spread/rockcraft/extension-expressjs/app/views/layout.jade create mode 100644 tests/spread/rockcraft/extension-expressjs/task.yaml diff --git a/tests/spread/rockcraft/extension-expressjs/app/app.js b/tests/spread/rockcraft/extension-expressjs/app/app.js new file mode 100644 index 000000000..662bcc927 --- /dev/null +++ b/tests/spread/rockcraft/extension-expressjs/app/app.js @@ -0,0 +1,41 @@ +var createError = require('http-errors'); +var express = require('express'); +var path = require('path'); +var cookieParser = require('cookie-parser'); +var logger = require('morgan'); + +var indexRouter = require('./routes/index'); +var usersRouter = require('./routes/users'); + +var app = express(); + +// view engine setup +app.set('views', path.join(__dirname, 'views')); +app.set('view engine', 'jade'); + +app.use(logger('dev')); +app.use(express.json()); +app.use(express.urlencoded({ extended: false })); +app.use(cookieParser()); +app.use(express.static(path.join(__dirname, 'public'))); + +app.use('/', indexRouter); +app.use('/users', usersRouter); + +// catch 404 and forward to error handler +app.use(function(req, res, next) { + next(createError(404)); +}); + +// error handler +app.use(function(err, req, res, next) { + // set locals, only providing error in development + res.locals.message = err.message; + res.locals.error = req.app.get('env') === 'development' ? err : {}; + + // render the error page + res.status(err.status || 500); + res.render('error'); +}); + +module.exports = app; diff --git a/tests/spread/rockcraft/extension-expressjs/app/bin/www b/tests/spread/rockcraft/extension-expressjs/app/bin/www new file mode 100755 index 000000000..a8c2d36e0 --- /dev/null +++ b/tests/spread/rockcraft/extension-expressjs/app/bin/www @@ -0,0 +1,90 @@ +#!/usr/bin/env node + +/** + * Module dependencies. + */ + +var app = require('../app'); +var debug = require('debug')('app:server'); +var http = require('http'); + +/** + * Get port from environment and store in Express. + */ + +var port = normalizePort(process.env.PORT || '3000'); +app.set('port', port); + +/** + * Create HTTP server. + */ + +var server = http.createServer(app); + +/** + * Listen on provided port, on all network interfaces. + */ + +server.listen(port); +server.on('error', onError); +server.on('listening', onListening); + +/** + * Normalize a port into a number, string, or false. + */ + +function normalizePort(val) { + var port = parseInt(val, 10); + + if (isNaN(port)) { + // named pipe + return val; + } + + if (port >= 0) { + // port number + return port; + } + + return false; +} + +/** + * Event listener for HTTP server "error" event. + */ + +function onError(error) { + if (error.syscall !== 'listen') { + throw error; + } + + var bind = typeof port === 'string' + ? 'Pipe ' + port + : 'Port ' + port; + + // handle specific listen errors with friendly messages + switch (error.code) { + case 'EACCES': + console.error(bind + ' requires elevated privileges'); + process.exit(1); + break; + case 'EADDRINUSE': + console.error(bind + ' is already in use'); + process.exit(1); + break; + default: + throw error; + } +} + +/** + * Event listener for HTTP server "listening" event. + */ + +function onListening() { + var addr = server.address(); + var bind = typeof addr === 'string' + ? 'pipe ' + addr + : 'port ' + addr.port; + debug('Listening on ' + bind); +} diff --git a/tests/spread/rockcraft/extension-expressjs/app/package.json b/tests/spread/rockcraft/extension-expressjs/app/package.json new file mode 100644 index 000000000..5d49060cc --- /dev/null +++ b/tests/spread/rockcraft/extension-expressjs/app/package.json @@ -0,0 +1,16 @@ +{ + "name": "app", + "version": "0.0.0", + "private": true, + "scripts": { + "start": "node ./bin/www" + }, + "dependencies": { + "cookie-parser": "~1.4.4", + "debug": "~2.6.9", + "express": "~4.16.1", + "http-errors": "~1.6.3", + "jade": "~1.11.0", + "morgan": "~1.9.1" + } +} diff --git a/tests/spread/rockcraft/extension-expressjs/app/public/stylesheets/style.css b/tests/spread/rockcraft/extension-expressjs/app/public/stylesheets/style.css new file mode 100644 index 000000000..9453385b9 --- /dev/null +++ b/tests/spread/rockcraft/extension-expressjs/app/public/stylesheets/style.css @@ -0,0 +1,8 @@ +body { + padding: 50px; + font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; +} + +a { + color: #00B7FF; +} diff --git a/tests/spread/rockcraft/extension-expressjs/app/routes/index.js b/tests/spread/rockcraft/extension-expressjs/app/routes/index.js new file mode 100644 index 000000000..ecca96a56 --- /dev/null +++ b/tests/spread/rockcraft/extension-expressjs/app/routes/index.js @@ -0,0 +1,9 @@ +var express = require('express'); +var router = express.Router(); + +/* GET home page. */ +router.get('/', function(req, res, next) { + res.render('index', { title: 'Express' }); +}); + +module.exports = router; diff --git a/tests/spread/rockcraft/extension-expressjs/app/routes/users.js b/tests/spread/rockcraft/extension-expressjs/app/routes/users.js new file mode 100644 index 000000000..623e4302b --- /dev/null +++ b/tests/spread/rockcraft/extension-expressjs/app/routes/users.js @@ -0,0 +1,9 @@ +var express = require('express'); +var router = express.Router(); + +/* GET users listing. */ +router.get('/', function(req, res, next) { + res.send('respond with a resource'); +}); + +module.exports = router; diff --git a/tests/spread/rockcraft/extension-expressjs/app/views/error.jade b/tests/spread/rockcraft/extension-expressjs/app/views/error.jade new file mode 100644 index 000000000..51ec12c6a --- /dev/null +++ b/tests/spread/rockcraft/extension-expressjs/app/views/error.jade @@ -0,0 +1,6 @@ +extends layout + +block content + h1= message + h2= error.status + pre #{error.stack} diff --git a/tests/spread/rockcraft/extension-expressjs/app/views/index.jade b/tests/spread/rockcraft/extension-expressjs/app/views/index.jade new file mode 100644 index 000000000..3d63b9a04 --- /dev/null +++ b/tests/spread/rockcraft/extension-expressjs/app/views/index.jade @@ -0,0 +1,5 @@ +extends layout + +block content + h1= title + p Welcome to #{title} diff --git a/tests/spread/rockcraft/extension-expressjs/app/views/layout.jade b/tests/spread/rockcraft/extension-expressjs/app/views/layout.jade new file mode 100644 index 000000000..15af079bf --- /dev/null +++ b/tests/spread/rockcraft/extension-expressjs/app/views/layout.jade @@ -0,0 +1,7 @@ +doctype html +html + head + title= title + link(rel='stylesheet', href='/stylesheets/style.css') + body + block content diff --git a/tests/spread/rockcraft/extension-expressjs/task.yaml b/tests/spread/rockcraft/extension-expressjs/task.yaml new file mode 100644 index 000000000..e0e8c06f7 --- /dev/null +++ b/tests/spread/rockcraft/extension-expressjs/task.yaml @@ -0,0 +1,45 @@ +summary: expressjs extension test +environment: + SCENARIO/bare: bare + SCENARIO/base_2404: ubuntu-24.04 + ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS: "true" + +execute: | + NAME="expressjs-${SCENARIO//./-}" + ROCK_FILE="${NAME}_0.1_amd64.rock" + IMAGE="${NAME}:0.1" + + run_rockcraft init --name "${NAME}" --profile expressjs-framework + sed -i "s/^name: .*/name: ${NAME}/g" rockcraft.yaml + sed -i "s/^base: .*/base: ${SCENARIO//-/@}/g" rockcraft.yaml + if [ "${SCENARIO}" != "bare" ]; then + sed -i "s/^build-base: .*/build-base: ${SCENARIO//-/@}/g" rockcraft.yaml + fi + + run_rockcraft pack + + test -f "${ROCK_FILE}" + test ! -d work + + # Ensure docker does not have this container image + docker rmi --force "${IMAGE}" + # Install container + sudo rockcraft.skopeo --insecure-policy copy "oci-archive:${ROCK_FILE}" "docker-daemon:${IMAGE}" + # Ensure container exists + docker images "${IMAGE}" | MATCH "${NAME}" + + # ensure container doesn't exist + docker rm -f "${NAME}-container" + + # test the default expressjs service + docker run --name "${NAME}-container" -d -p 3000:8000 "${IMAGE}" + retry -n 5 --wait 2 curl localhost:3000 + http_status=$(curl -s -o /dev/null -w "%{http_code}" localhost:3000) + [ "${http_status}" -eq 200 ] + +restore: | + NAME="expressjs-${SCENARIO//./-}" + docker stop "${NAME}-container" + docker rm "${NAME}-container" + rm -f "*.rock" rockcraft.yaml + docker system prune -a -f From 91e87e919cdb7668a40bfc3ed6a568ead713fb91 Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Thu, 2 Jan 2025 07:09:34 +0000 Subject: [PATCH 06/64] fix: linting --- rockcraft/extensions/expressjs.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rockcraft/extensions/expressjs.py b/rockcraft/extensions/expressjs.py index 4b9e2d661..59e60937f 100644 --- a/rockcraft/extensions/expressjs.py +++ b/rockcraft/extensions/expressjs.py @@ -163,7 +163,8 @@ def _app_organise(self) -> dict: if not user_prime: user_prime = self.EXPRESS_GENERATOR_DIRS project_relative_file_paths = [ - prime_path.removeprefix(self.IMAGE_BASE_DIR) for prime_path in user_prime + self.EXPRESS_PACKAGE_DIRS + prime_path.removeprefix(self.IMAGE_BASE_DIR) + for prime_path in user_prime + self.EXPRESS_PACKAGE_DIRS ] lib_dir = f"lib/node_modules/{self._app_name}" return { From 9d9be673630c1a752b846d82760d2f3d725ad31b Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Thu, 2 Jan 2025 11:08:18 +0000 Subject: [PATCH 07/64] feat: add template --- .../expressjs-framework/rockcraft.yaml.j2 | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 rockcraft/templates/expressjs-framework/rockcraft.yaml.j2 diff --git a/rockcraft/templates/expressjs-framework/rockcraft.yaml.j2 b/rockcraft/templates/expressjs-framework/rockcraft.yaml.j2 new file mode 100644 index 000000000..fe99c7f8f --- /dev/null +++ b/rockcraft/templates/expressjs-framework/rockcraft.yaml.j2 @@ -0,0 +1,71 @@ +name: {{name}} +# see {{versioned_url}}/explanation/bases/ +# for more information about bases and using 'bare' bases for chiselled rocks +base: bare # as an alternative, a ubuntu base can be used +build-base: ubuntu@24.04 # build-base is required when the base is bare +version: '0.1' # just for humans. Semantic versioning is recommended +summary: A summary of your ExpresssJS application # 79 char long summary +description: | + This is {{name}}'s description. You have a paragraph or two to tell the + most important story about it. Keep it under 100 words though, + we live in tweetspace and your description wants to look good in the + container registries out there. +# the platforms this rock should be built on and run on. +# you can check your architecture with `dpkg --print-architecture` +platforms: + amd64: + # arm64: + # ppc64el: + # s390x: + +# to ensure the expressjs-framework extension works properly, your ExpressJS +# application should be inside the app, src or a directory with the project +# name, and have inside the app directory a `index.js` or `server.js` file with +# the starting script defined in the package.json's scripts section under the +# "run" key. +extensions: + - expressjs-framework + +# uncomment the sections you need and adjust according to your requirements. + +# parts: # you need to uncomment this line to add or update any part. +# expressjs-framework/install-app: +# prime: +# # by default, only the files in bin/, public/, routes/, views/, app.js, +# # package.json package-lock.json are copied into the image. +# # You can modify the list below to override the default list and +# # include or exclude specific files/directories in your project. +# # note: prefix each entry with "app/" followed by the local path. +# - app/app +# - app/templates +# - app/static + +# you may need packages to build a npm package. Add them here if necessary. +# expressjs-framework/dependencies: +# build-packages: +# # for example, if you need pkg-config and libxmlsec1-dev to build one +# # of your packages: +# - pkg-config +# - libxmlsec1-dev + + +# you can add package slices or Debian packages to the image. +# package slices are subsets of Debian packages, which result +# in smaller and more secure images. +# see https://documentation.ubuntu.com/rockcraft/en/latest/explanation/chisel/ + +# add this part if you want to add packages slices to your image. +# you can find a list of packages slices at https://github.com/canonical/chisel-releases +# runtime-slices: +# plugin: nil +# stage-packages: +# # list the required package slices for your expressjs application below. +# # for example, for the slice libs of libpq5: +# - libpq5_libs + +# if you want to add a Debian package to your image, add the next part +runtime-debs: + plugin: nil + stage-packages: + # list required Debian packages for your expressjs application below. + - libpq5 # for Postgresql connection. From cb84f171f7f100318925622338b4653d506b6c7d Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Thu, 2 Jan 2025 14:53:13 +0000 Subject: [PATCH 08/64] fix: app project dir --- rockcraft/extensions/expressjs.py | 2 +- tests/unit/extensions/test_expressjs.py | 26 ++++++++++++++++++------- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/rockcraft/extensions/expressjs.py b/rockcraft/extensions/expressjs.py index 59e60937f..01b7e558e 100644 --- a/rockcraft/extensions/expressjs.py +++ b/rockcraft/extensions/expressjs.py @@ -125,7 +125,7 @@ def _gen_runtime_dependencies_part(self) -> dict: @property def _app_package_json(self) -> dict: """Return the app package.json contents.""" - package_json_file = self.project_root / "package.json" + package_json_file = self.project_root / self.IMAGE_BASE_DIR / "package.json" if not package_json_file.exists(): raise ExtensionError( "missing package.json file", diff --git a/tests/unit/extensions/test_expressjs.py b/tests/unit/extensions/test_expressjs.py index 5cc6ee37f..4022d9439 100644 --- a/tests/unit/extensions/test_expressjs.py +++ b/tests/unit/extensions/test_expressjs.py @@ -40,8 +40,15 @@ def expressjs_project_name(): @pytest.fixture -def package_json_file(tmp_path, expressjs_project_name): - (tmp_path / "package.json").write_text( +def app_path(tmp_path): + app_path = tmp_path / "app" + app_path.mkdir(parents=True, exist_ok=True) + return app_path + + +@pytest.fixture +def package_json_file(app_path, expressjs_project_name): + (app_path / "package.json").write_text( f"""{{ "name": "{expressjs_project_name}", "scripts": {{ @@ -69,7 +76,9 @@ def test_expressjs_extension_default( "plugin": "npm", "npm-include-node": False, "source": "app/", - "organise": {}, + "organise": { + "lib/node_modules/test-expressjs-project/package.json": "app/package.json", + }, "override-prime": f"rm -rf lib/node_modules/{expressjs_project_name}", }, "expressjs-framework/runtime-dependencies": { @@ -112,9 +121,9 @@ def test_expressjs_no_package_json_error(tmp_path, expressjs_input_yaml): ) @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, app_path, expressjs_input_yaml, package_json_contents, error_message ): - (tmp_path / "package.json").write_text(package_json_contents) + (app_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 @@ -127,7 +136,10 @@ def test_expressjs_invalid_package_json_scripts_error( pytest.param( ["app/app.js"], [], - {"lib/node_modules/test-expressjs-project/app.js": "app/app.js"}, + { + "lib/node_modules/test-expressjs-project/app.js": "app/app.js", + "lib/node_modules/test-expressjs-project/package.json": "app/package.json", + }, id="single file defined", ), ], @@ -137,7 +149,7 @@ 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).parent.mkdir(parents=True, exist_ok=True) (tmp_path / file).touch() prime_files = [*existing_files, *missing_files] expressjs_input_yaml["parts"] = { From 0af6e6afef5b4add6c381b84d006ae660e0fc8ac Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Fri, 3 Jan 2025 02:55:33 +0000 Subject: [PATCH 09/64] chore: comment runtime debs --- .../templates/expressjs-framework/rockcraft.yaml.j2 | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/rockcraft/templates/expressjs-framework/rockcraft.yaml.j2 b/rockcraft/templates/expressjs-framework/rockcraft.yaml.j2 index fe99c7f8f..9c80df447 100644 --- a/rockcraft/templates/expressjs-framework/rockcraft.yaml.j2 +++ b/rockcraft/templates/expressjs-framework/rockcraft.yaml.j2 @@ -27,7 +27,6 @@ extensions: - expressjs-framework # uncomment the sections you need and adjust according to your requirements. - # parts: # you need to uncomment this line to add or update any part. # expressjs-framework/install-app: # prime: @@ -48,7 +47,6 @@ extensions: # - pkg-config # - libxmlsec1-dev - # you can add package slices or Debian packages to the image. # package slices are subsets of Debian packages, which result # in smaller and more secure images. @@ -64,8 +62,8 @@ extensions: # - libpq5_libs # if you want to add a Debian package to your image, add the next part -runtime-debs: - plugin: nil - stage-packages: - # list required Debian packages for your expressjs application below. - - libpq5 # for Postgresql connection. +# runtime-debs: +# plugin: nil +# stage-packages: +# # list required Debian packages for your expressjs application below. +# - libpq5 # for Postgresql connection. From eacf0a1988c39bb54ea852ed130b5b64ff6b1fed Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Fri, 3 Jan 2025 03:50:41 +0000 Subject: [PATCH 10/64] chore: rename organise to organize --- rockcraft/extensions/expressjs.py | 24 ++++++++++++------- .../expressjs-framework/rockcraft.yaml.j2 | 10 ++++---- tests/unit/extensions/test_expressjs.py | 12 +++++----- 3 files changed, 27 insertions(+), 19 deletions(-) diff --git a/rockcraft/extensions/expressjs.py b/rockcraft/extensions/expressjs.py index 01b7e558e..5a6a929cc 100644 --- a/rockcraft/extensions/expressjs.py +++ b/rockcraft/extensions/expressjs.py @@ -42,7 +42,8 @@ class ExpressJSFramework(Extension): "package-lock.json", "node_modules", ] - RUNTIME_DEPENDENCIES = ["ca-certificates_data", "libpq5", "node"] + RUNTIME_DEBS = ["ca-certificates_data", "node"] + RUNTIME_SLICES = ["libpq5"] @staticmethod @override @@ -85,7 +86,8 @@ def get_root_snippet(self) -> dict[str, Any]: snippet["parts"] = { "expressjs-framework/install-app": self._gen_install_app_part(), - "expressjs-framework/runtime-dependencies": self._gen_runtime_dependencies_part(), + "expressjs-framework/runtime-debs": self._gen_runtime_debs_part(), + "expressjs-framework/runtime-slices": self._gen_runtime_slices_part(), } return snippet @@ -111,17 +113,23 @@ def _gen_install_app_part(self) -> dict: "plugin": "npm", "npm-include-node": False, "source": "app/", - "organise": self._app_organise, + "organize": self._app_organize, "override-prime": f"rm -rf lib/node_modules/{self._app_name}", } - def _gen_runtime_dependencies_part(self) -> dict: - """Generate the install dependencies part using dump plugin.""" + def _gen_runtime_debs_part(self) -> dict: + """Generate the runtime debs part.""" return { "plugin": "nil", - "stage-packages": self.RUNTIME_DEPENDENCIES, + "stage-packages": self.RUNTIME_DEBS, } + def _gen_runtime_slices_part(self) -> dict: + """Generate the runtime slices part.""" + # Runtime slices have been separated from runtime debs since rockcraft does not support + # mixing the two. See https://github.com/canonical/rockcraft/issues/183. + return {"plugin": "nil", "stage-packages": self.RUNTIME_SLICES} + @property def _app_package_json(self) -> dict: """Return the app package.json contents.""" @@ -141,8 +149,8 @@ def _app_name(self) -> str: return self._app_package_json["name"] @property - def _app_organise(self) -> dict: - """Return the organised mapping for the ExpressJS project. + def _app_organize(self) -> dict: + """Return the organized 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 diff --git a/rockcraft/templates/expressjs-framework/rockcraft.yaml.j2 b/rockcraft/templates/expressjs-framework/rockcraft.yaml.j2 index 9c80df447..77bac8aad 100644 --- a/rockcraft/templates/expressjs-framework/rockcraft.yaml.j2 +++ b/rockcraft/templates/expressjs-framework/rockcraft.yaml.j2 @@ -62,8 +62,8 @@ extensions: # - libpq5_libs # if you want to add a Debian package to your image, add the next part -# runtime-debs: -# plugin: nil -# stage-packages: -# # list required Debian packages for your expressjs application below. -# - libpq5 # for Postgresql connection. +# runtime-debs: +# plugin: nil +# stage-packages: +# # list required Debian packages for your expressjs application below. +# - libpq5 # for Postgresql connection. diff --git a/tests/unit/extensions/test_expressjs.py b/tests/unit/extensions/test_expressjs.py index 4022d9439..2b398b2bf 100644 --- a/tests/unit/extensions/test_expressjs.py +++ b/tests/unit/extensions/test_expressjs.py @@ -76,7 +76,7 @@ def test_expressjs_extension_default( "plugin": "npm", "npm-include-node": False, "source": "app/", - "organise": { + "organize": { "lib/node_modules/test-expressjs-project/package.json": "app/package.json", }, "override-prime": f"rm -rf lib/node_modules/{expressjs_project_name}", @@ -131,7 +131,7 @@ def test_expressjs_invalid_package_json_scripts_error( @pytest.mark.parametrize( - "existing_files, missing_files, expected_organise", + "existing_files, missing_files, expected_organize", [ pytest.param( ["app/app.js"], @@ -145,8 +145,8 @@ def test_expressjs_invalid_package_json_scripts_error( ], ) @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 +def test_expressjs_install_app_prime_to_organize_map( + tmp_path, expressjs_input_yaml, existing_files, missing_files, expected_organize ): for file in existing_files: (tmp_path / file).parent.mkdir(parents=True, exist_ok=True) @@ -157,6 +157,6 @@ def test_expressjs_install_app_prime_to_organise_map( } applied = extensions.apply_extensions(tmp_path, expressjs_input_yaml) assert ( - applied["parts"]["expressjs-framework/install-app"]["organise"] - == expected_organise + applied["parts"]["expressjs-framework/install-app"]["organize"] + == expected_organize ) From dc787d009f2e6af588e5f250d42a37e745d98ed8 Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Fri, 3 Jan 2025 04:03:15 +0000 Subject: [PATCH 11/64] test: fix tests --- tests/unit/extensions/test_expressjs.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/unit/extensions/test_expressjs.py b/tests/unit/extensions/test_expressjs.py index 2b398b2bf..cf2dcf75d 100644 --- a/tests/unit/extensions/test_expressjs.py +++ b/tests/unit/extensions/test_expressjs.py @@ -81,14 +81,17 @@ def test_expressjs_extension_default( }, "override-prime": f"rm -rf lib/node_modules/{expressjs_project_name}", }, - "expressjs-framework/runtime-dependencies": { + "expressjs-framework/runtime-debs": { "plugin": "nil", "stage-packages": [ "ca-certificates_data", - "libpq5", "node", ], }, + "expressjs-framework/runtime-slices": { + "plugin": "nil", + "stage-packages": ["libpq5"], + }, }, "services": { "app": { From 98fbdd96d8710abe374be888f0e173528e08767c Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Fri, 3 Jan 2025 04:57:04 +0000 Subject: [PATCH 12/64] chore: move ca-certificates_data to slice def --- rockcraft/extensions/expressjs.py | 4 ++-- tests/unit/extensions/test_expressjs.py | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/rockcraft/extensions/expressjs.py b/rockcraft/extensions/expressjs.py index 5a6a929cc..b739cfbc7 100644 --- a/rockcraft/extensions/expressjs.py +++ b/rockcraft/extensions/expressjs.py @@ -42,8 +42,8 @@ class ExpressJSFramework(Extension): "package-lock.json", "node_modules", ] - RUNTIME_DEBS = ["ca-certificates_data", "node"] - RUNTIME_SLICES = ["libpq5"] + RUNTIME_DEBS = ["node"] + RUNTIME_SLICES = ["ca-certificates_data", "libpq5"] @staticmethod @override diff --git a/tests/unit/extensions/test_expressjs.py b/tests/unit/extensions/test_expressjs.py index cf2dcf75d..abd4d09e7 100644 --- a/tests/unit/extensions/test_expressjs.py +++ b/tests/unit/extensions/test_expressjs.py @@ -84,13 +84,12 @@ def test_expressjs_extension_default( "expressjs-framework/runtime-debs": { "plugin": "nil", "stage-packages": [ - "ca-certificates_data", "node", ], }, "expressjs-framework/runtime-slices": { "plugin": "nil", - "stage-packages": ["libpq5"], + "stage-packages": ["ca-certificates_data", "libpq5"], }, }, "services": { From d7c51cd5eb3f761d277e2ca04ff2151654ef12ba Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Fri, 3 Jan 2025 05:35:04 +0000 Subject: [PATCH 13/64] chore: move node slice --- rockcraft/extensions/expressjs.py | 4 ++-- tests/unit/extensions/test_expressjs.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/rockcraft/extensions/expressjs.py b/rockcraft/extensions/expressjs.py index b739cfbc7..2e5d75f49 100644 --- a/rockcraft/extensions/expressjs.py +++ b/rockcraft/extensions/expressjs.py @@ -42,8 +42,8 @@ class ExpressJSFramework(Extension): "package-lock.json", "node_modules", ] - RUNTIME_DEBS = ["node"] - RUNTIME_SLICES = ["ca-certificates_data", "libpq5"] + RUNTIME_DEBS = ["ca-certificates_data"] + RUNTIME_SLICES = ["node", "libpq5"] @staticmethod @override diff --git a/tests/unit/extensions/test_expressjs.py b/tests/unit/extensions/test_expressjs.py index abd4d09e7..815f99148 100644 --- a/tests/unit/extensions/test_expressjs.py +++ b/tests/unit/extensions/test_expressjs.py @@ -84,12 +84,12 @@ def test_expressjs_extension_default( "expressjs-framework/runtime-debs": { "plugin": "nil", "stage-packages": [ - "node", + "ca-certificates_data", ], }, "expressjs-framework/runtime-slices": { "plugin": "nil", - "stage-packages": ["ca-certificates_data", "libpq5"], + "stage-packages": ["node", "libpq5"], }, }, "services": { From b7a21a5a212b727a7277f465be6498374bf85b00 Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Fri, 3 Jan 2025 06:10:37 +0000 Subject: [PATCH 14/64] chore: retrigger ci From 92334927551035ece4fc116241894b10af428972 Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Fri, 3 Jan 2025 07:13:30 +0000 Subject: [PATCH 15/64] fix: add node build dependency --- rockcraft/extensions/expressjs.py | 1 + tests/unit/extensions/test_expressjs.py | 1 + 2 files changed, 2 insertions(+) diff --git a/rockcraft/extensions/expressjs.py b/rockcraft/extensions/expressjs.py index 2e5d75f49..3b964ac47 100644 --- a/rockcraft/extensions/expressjs.py +++ b/rockcraft/extensions/expressjs.py @@ -115,6 +115,7 @@ def _gen_install_app_part(self) -> dict: "source": "app/", "organize": self._app_organize, "override-prime": f"rm -rf lib/node_modules/{self._app_name}", + "build-snaps": ["node"], } def _gen_runtime_debs_part(self) -> dict: diff --git a/tests/unit/extensions/test_expressjs.py b/tests/unit/extensions/test_expressjs.py index 815f99148..8e8e0a829 100644 --- a/tests/unit/extensions/test_expressjs.py +++ b/tests/unit/extensions/test_expressjs.py @@ -79,6 +79,7 @@ def test_expressjs_extension_default( "organize": { "lib/node_modules/test-expressjs-project/package.json": "app/package.json", }, + "build-snaps": ["node"], "override-prime": f"rm -rf lib/node_modules/{expressjs_project_name}", }, "expressjs-framework/runtime-debs": { From acc3c05bd919114a52ad2d384fff80e4fcc1cffb Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Fri, 3 Jan 2025 08:22:27 +0000 Subject: [PATCH 16/64] fix: use nodejs package --- rockcraft/extensions/expressjs.py | 2 +- tests/unit/extensions/test_expressjs.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rockcraft/extensions/expressjs.py b/rockcraft/extensions/expressjs.py index 3b964ac47..898bea325 100644 --- a/rockcraft/extensions/expressjs.py +++ b/rockcraft/extensions/expressjs.py @@ -115,7 +115,7 @@ def _gen_install_app_part(self) -> dict: "source": "app/", "organize": self._app_organize, "override-prime": f"rm -rf lib/node_modules/{self._app_name}", - "build-snaps": ["node"], + "build-packages": ["nodejs"], } def _gen_runtime_debs_part(self) -> dict: diff --git a/tests/unit/extensions/test_expressjs.py b/tests/unit/extensions/test_expressjs.py index 8e8e0a829..c02e2af7c 100644 --- a/tests/unit/extensions/test_expressjs.py +++ b/tests/unit/extensions/test_expressjs.py @@ -79,7 +79,7 @@ def test_expressjs_extension_default( "organize": { "lib/node_modules/test-expressjs-project/package.json": "app/package.json", }, - "build-snaps": ["node"], + "build-packages": ["nodejs"], "override-prime": f"rm -rf lib/node_modules/{expressjs_project_name}", }, "expressjs-framework/runtime-debs": { From b6b9ca2f34e1dae15c2e2a3b7d40ffa0572a288a Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Fri, 3 Jan 2025 09:25:06 +0000 Subject: [PATCH 17/64] fix: add npm plugin dependency --- rockcraft/extensions/expressjs.py | 1 + tests/unit/extensions/test_expressjs.py | 1 + 2 files changed, 2 insertions(+) diff --git a/rockcraft/extensions/expressjs.py b/rockcraft/extensions/expressjs.py index 898bea325..ed2fcee7f 100644 --- a/rockcraft/extensions/expressjs.py +++ b/rockcraft/extensions/expressjs.py @@ -116,6 +116,7 @@ def _gen_install_app_part(self) -> dict: "organize": self._app_organize, "override-prime": f"rm -rf lib/node_modules/{self._app_name}", "build-packages": ["nodejs"], + "after": ["npm-deps"], } def _gen_runtime_debs_part(self) -> dict: diff --git a/tests/unit/extensions/test_expressjs.py b/tests/unit/extensions/test_expressjs.py index c02e2af7c..339f5877e 100644 --- a/tests/unit/extensions/test_expressjs.py +++ b/tests/unit/extensions/test_expressjs.py @@ -81,6 +81,7 @@ def test_expressjs_extension_default( }, "build-packages": ["nodejs"], "override-prime": f"rm -rf lib/node_modules/{expressjs_project_name}", + "after": ["npm-deps"], }, "expressjs-framework/runtime-debs": { "plugin": "nil", From a1b52090b17afe87da864e5abfdb86f4fe34a27d Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Fri, 3 Jan 2025 11:31:04 +0000 Subject: [PATCH 18/64] fix: add plugin dependencies --- rockcraft/extensions/expressjs.py | 3 +-- tests/unit/extensions/test_expressjs.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/rockcraft/extensions/expressjs.py b/rockcraft/extensions/expressjs.py index ed2fcee7f..b68585eb5 100644 --- a/rockcraft/extensions/expressjs.py +++ b/rockcraft/extensions/expressjs.py @@ -115,8 +115,7 @@ def _gen_install_app_part(self) -> dict: "source": "app/", "organize": self._app_organize, "override-prime": f"rm -rf lib/node_modules/{self._app_name}", - "build-packages": ["nodejs"], - "after": ["npm-deps"], + "build-packages": ["nodejs", "npm"], } def _gen_runtime_debs_part(self) -> dict: diff --git a/tests/unit/extensions/test_expressjs.py b/tests/unit/extensions/test_expressjs.py index 339f5877e..fc83e366f 100644 --- a/tests/unit/extensions/test_expressjs.py +++ b/tests/unit/extensions/test_expressjs.py @@ -79,9 +79,8 @@ def test_expressjs_extension_default( "organize": { "lib/node_modules/test-expressjs-project/package.json": "app/package.json", }, - "build-packages": ["nodejs"], + "build-packages": ["nodejs", "npm"], "override-prime": f"rm -rf lib/node_modules/{expressjs_project_name}", - "after": ["npm-deps"], }, "expressjs-framework/runtime-debs": { "plugin": "nil", From 9a699708e074a8f030dd8e8873470d94de8f8c93 Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Fri, 3 Jan 2025 18:03:33 +0000 Subject: [PATCH 19/64] fix: nodejs as deb --- rockcraft/extensions/expressjs.py | 4 ++-- tests/unit/extensions/test_expressjs.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/rockcraft/extensions/expressjs.py b/rockcraft/extensions/expressjs.py index b68585eb5..7b341ee1a 100644 --- a/rockcraft/extensions/expressjs.py +++ b/rockcraft/extensions/expressjs.py @@ -42,8 +42,8 @@ class ExpressJSFramework(Extension): "package-lock.json", "node_modules", ] - RUNTIME_DEBS = ["ca-certificates_data"] - RUNTIME_SLICES = ["node", "libpq5"] + RUNTIME_DEBS = ["ca-certificates_data", "nodejs"] + RUNTIME_SLICES = ["npm", "libpq5"] @staticmethod @override diff --git a/tests/unit/extensions/test_expressjs.py b/tests/unit/extensions/test_expressjs.py index fc83e366f..54b6f4a91 100644 --- a/tests/unit/extensions/test_expressjs.py +++ b/tests/unit/extensions/test_expressjs.py @@ -86,11 +86,12 @@ def test_expressjs_extension_default( "plugin": "nil", "stage-packages": [ "ca-certificates_data", + "nodejs", ], }, "expressjs-framework/runtime-slices": { "plugin": "nil", - "stage-packages": ["node", "libpq5"], + "stage-packages": ["npm", "libpq5"], }, }, "services": { From f55e87b352284b2ce0078247a6cce2c44db39992 Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Mon, 6 Jan 2025 01:31:00 +0000 Subject: [PATCH 20/64] fix: nodejs package/slice separation --- rockcraft/extensions/expressjs.py | 4 ++-- tests/unit/extensions/test_expressjs.py | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/rockcraft/extensions/expressjs.py b/rockcraft/extensions/expressjs.py index 7b341ee1a..d0ced2d3f 100644 --- a/rockcraft/extensions/expressjs.py +++ b/rockcraft/extensions/expressjs.py @@ -42,8 +42,8 @@ class ExpressJSFramework(Extension): "package-lock.json", "node_modules", ] - RUNTIME_DEBS = ["ca-certificates_data", "nodejs"] - RUNTIME_SLICES = ["npm", "libpq5"] + RUNTIME_DEBS = ["ca-certificates_data"] + RUNTIME_SLICES = ["nodejs", "npm", "libpq5"] @staticmethod @override diff --git a/tests/unit/extensions/test_expressjs.py b/tests/unit/extensions/test_expressjs.py index 54b6f4a91..58f29ca0d 100644 --- a/tests/unit/extensions/test_expressjs.py +++ b/tests/unit/extensions/test_expressjs.py @@ -86,12 +86,11 @@ def test_expressjs_extension_default( "plugin": "nil", "stage-packages": [ "ca-certificates_data", - "nodejs", ], }, "expressjs-framework/runtime-slices": { "plugin": "nil", - "stage-packages": ["npm", "libpq5"], + "stage-packages": ["nodejs", "npm", "libpq5"], }, }, "services": { From 544434b7823e6c854d9fba9c8dce0333b80e4980 Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Mon, 6 Jan 2025 02:55:41 +0000 Subject: [PATCH 21/64] ci: retrigger (LAUCHPAD DOWN) From 6ba9c5ab926f2a5045e1180664907cb30df1bb94 Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Mon, 6 Jan 2025 05:25:27 +0000 Subject: [PATCH 22/64] fix: prime default after deletion --- rockcraft/extensions/expressjs.py | 2 +- tests/unit/extensions/test_expressjs.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/rockcraft/extensions/expressjs.py b/rockcraft/extensions/expressjs.py index d0ced2d3f..4200f2a4a 100644 --- a/rockcraft/extensions/expressjs.py +++ b/rockcraft/extensions/expressjs.py @@ -114,7 +114,7 @@ def _gen_install_app_part(self) -> dict: "npm-include-node": False, "source": "app/", "organize": self._app_organize, - "override-prime": f"rm -rf lib/node_modules/{self._app_name}", + "override-prime": f"rm -rf lib/node_modules/{self._app_name}\ncraftctl default", "build-packages": ["nodejs", "npm"], } diff --git a/tests/unit/extensions/test_expressjs.py b/tests/unit/extensions/test_expressjs.py index 58f29ca0d..dafd91d3b 100644 --- a/tests/unit/extensions/test_expressjs.py +++ b/tests/unit/extensions/test_expressjs.py @@ -80,7 +80,8 @@ def test_expressjs_extension_default( "lib/node_modules/test-expressjs-project/package.json": "app/package.json", }, "build-packages": ["nodejs", "npm"], - "override-prime": f"rm -rf lib/node_modules/{expressjs_project_name}", + "override-prime": f"rm -rf lib/node_modules/{expressjs_project_name}\ncraftctl " + "default", }, "expressjs-framework/runtime-debs": { "plugin": "nil", From 82bbf94f1c12cef4287d67cf3aeae809c3cf717a Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Thu, 9 Jan 2025 05:37:49 +0000 Subject: [PATCH 23/64] feat: node version option fallthrough --- rockcraft/extensions/expressjs.py | 65 ++++++++++++------- .../rockcraft/extension-expressjs/task.yaml | 9 +-- tests/unit/extensions/test_expressjs.py | 52 +++++++++++---- 3 files changed, 85 insertions(+), 41 deletions(-) diff --git a/rockcraft/extensions/expressjs.py b/rockcraft/extensions/expressjs.py index 4200f2a4a..1afd043df 100644 --- a/rockcraft/extensions/expressjs.py +++ b/rockcraft/extensions/expressjs.py @@ -42,14 +42,19 @@ class ExpressJSFramework(Extension): "package-lock.json", "node_modules", ] - RUNTIME_DEBS = ["ca-certificates_data"] - RUNTIME_SLICES = ["nodejs", "npm", "libpq5"] + RUNTIME_DEBS = [ + "ca-certificates_data", + "bash_bins", + "coreutils_bins", + "libc6_libs", + "libnode109_libs", + ] @staticmethod @override def get_supported_bases() -> tuple[str, ...]: """Return supported bases.""" - return "bare", "ubuntu@22.04", "ubuntu@24.04" + return "bare", "ubuntu@24.04" @staticmethod @override @@ -86,8 +91,6 @@ def get_root_snippet(self) -> dict[str, Any]: snippet["parts"] = { "expressjs-framework/install-app": self._gen_install_app_part(), - "expressjs-framework/runtime-debs": self._gen_runtime_debs_part(), - "expressjs-framework/runtime-slices": self._gen_runtime_slices_part(), } return snippet @@ -111,25 +114,24 @@ def _gen_install_app_part(self) -> dict: """Generate the install app part using NPM plugin.""" return { "plugin": "npm", - "npm-include-node": False, + "npm-include-node": True, + "npm-node-version": self._install_app_node_version, "source": "app/", "organize": self._app_organize, - "override-prime": f"rm -rf lib/node_modules/{self._app_name}\ncraftctl default", - "build-packages": ["nodejs", "npm"], - } - - def _gen_runtime_debs_part(self) -> dict: - """Generate the runtime debs part.""" - return { - "plugin": "nil", + "override-prime": ( + "craftctl default\n" + f"rm -rf ${{CRAFT_PRIME}}/lib/node_modules/{self._app_name}\n" + "echo 'script-shell=bash' >> ${CRAFT_PRIME}/app/.npmrc" + ), "stage-packages": self.RUNTIME_DEBS, } - def _gen_runtime_slices_part(self) -> dict: - """Generate the runtime slices part.""" - # Runtime slices have been separated from runtime debs since rockcraft does not support - # mixing the two. See https://github.com/canonical/rockcraft/issues/183. - return {"plugin": "nil", "stage-packages": self.RUNTIME_SLICES} + # def _gen_runtime_debs_part(self) -> dict: + # """Generate the runtime debs part.""" + # return { + # "plugin": "nil", + # "stage-packages": self.RUNTIME_DEBS, + # } @property def _app_package_json(self) -> dict: @@ -149,6 +151,13 @@ def _app_name(self) -> str: """Return the application name as defined on package.json.""" return self._app_package_json["name"] + @property + def _user_install_app_part(self) -> dict: + """Return the user defined install app part.""" + return self.yaml_data.get("parts", {}).get( + "expressjs-framework/install-app", {} + ) + @property def _app_organize(self) -> dict: """Return the organized mapping for the ExpressJS project. @@ -157,11 +166,8 @@ def _app_organize(self) -> dict: 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", []) - ) + user_prime: list[str] = self._user_install_app_part.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 " @@ -176,11 +182,20 @@ def _app_organize(self) -> dict: for prime_path in user_prime + self.EXPRESS_PACKAGE_DIRS ] lib_dir = f"lib/node_modules/{self._app_name}" - return { + file_mappings = { f"{lib_dir}/{f}": f"app/{f}" for f in project_relative_file_paths if (self.project_root / "app" / f).exists() } + file_mappings[f"{lib_dir}/node_modules"] = "app/node_modules" + return file_mappings + + @property + def _install_app_node_version(self) -> str: + node_version = self._user_install_app_part.get("npm-node-version", "node") + if not node_version: + return "node" + return node_version def _check_project(self) -> None: """Ensure this extension can apply to the current rockcraft project. diff --git a/tests/spread/rockcraft/extension-expressjs/task.yaml b/tests/spread/rockcraft/extension-expressjs/task.yaml index e0e8c06f7..02d86da2b 100644 --- a/tests/spread/rockcraft/extension-expressjs/task.yaml +++ b/tests/spread/rockcraft/extension-expressjs/task.yaml @@ -24,7 +24,8 @@ execute: | # Ensure docker does not have this container image docker rmi --force "${IMAGE}" # Install container - sudo rockcraft.skopeo --insecure-policy copy "oci-archive:${ROCK_FILE}" "docker-daemon:${IMAGE}" + sudo rockcraft.skopeo --insecure-policy copy "oci-archive:${ROCK_FILE}" \ + "docker-daemon:${IMAGE}" # Ensure container exists docker images "${IMAGE}" | MATCH "${NAME}" @@ -32,9 +33,9 @@ execute: | docker rm -f "${NAME}-container" # test the default expressjs service - docker run --name "${NAME}-container" -d -p 3000:8000 "${IMAGE}" - retry -n 5 --wait 2 curl localhost:3000 - http_status=$(curl -s -o /dev/null -w "%{http_code}" localhost:3000) + docker run --name "${NAME}-container" -d -p 8137:3000 "${IMAGE}" + retry -n 5 --wait 2 curl localhost:8137 + http_status=$(curl -s -o /dev/null -w "%{http_code}" localhost:8137) [ "${http_status}" -eq 200 ] restore: | diff --git a/tests/unit/extensions/test_expressjs.py b/tests/unit/extensions/test_expressjs.py index dafd91d3b..de0ce8f85 100644 --- a/tests/unit/extensions/test_expressjs.py +++ b/tests/unit/extensions/test_expressjs.py @@ -74,25 +74,26 @@ def test_expressjs_extension_default( "parts": { "expressjs-framework/install-app": { "plugin": "npm", - "npm-include-node": False, + "npm-node-version": "node", + "npm-include-node": True, "source": "app/", "organize": { - "lib/node_modules/test-expressjs-project/package.json": "app/package.json", + f"lib/node_modules/{expressjs_project_name}/package.json": "app/package.json", + f"lib/node_modules/{expressjs_project_name}/node_modules": "app/node_modules", }, - "build-packages": ["nodejs", "npm"], - "override-prime": f"rm -rf lib/node_modules/{expressjs_project_name}\ncraftctl " - "default", - }, - "expressjs-framework/runtime-debs": { - "plugin": "nil", + "override-prime": ( + "craftctl default\n" + f"rm -rf ${{CRAFT_PRIME}}/lib/node_modules/{expressjs_project_name}\n" + "echo 'script-shell=bash' >> ${CRAFT_PRIME}/app/.npmrc" + ), "stage-packages": [ "ca-certificates_data", + "bash_bins", + "coreutils_bins", + "libc6_libs", + "libnode109_libs", ], }, - "expressjs-framework/runtime-slices": { - "plugin": "nil", - "stage-packages": ["nodejs", "npm", "libpq5"], - }, }, "services": { "app": { @@ -134,6 +135,32 @@ def test_expressjs_invalid_package_json_scripts_error( assert str(exc.value.doc_slug) == "/reference/extensions/expressjs-framework" +@pytest.mark.parametrize( + "input_version, expected_version", + [ + pytest.param("20.12.2", "20.12.2", id="exact version"), + pytest.param("20.12", "20.12", id="major minor version"), + pytest.param("20", "20", id="major version"), + pytest.param("lts/iron", "lts/iron", id="LTS code name"), + pytest.param("node", "node", id="latest mainline"), + pytest.param("", "node", id="default"), + ], +) +@pytest.mark.usefixtures("expressjs_extension", "package_json_file") +def test_expressjs_override_node_version( + tmp_path, expressjs_input_yaml, input_version, expected_version +): + expressjs_input_yaml["parts"] = { + "expressjs-framework/install-app": {"npm-node-version": input_version} + } + print(expressjs_input_yaml) + applied = extensions.apply_extensions(tmp_path, expressjs_input_yaml) + assert ( + applied["parts"]["expressjs-framework/install-app"]["npm-node-version"] + == expected_version + ) + + @pytest.mark.parametrize( "existing_files, missing_files, expected_organize", [ @@ -143,6 +170,7 @@ def test_expressjs_invalid_package_json_scripts_error( { "lib/node_modules/test-expressjs-project/app.js": "app/app.js", "lib/node_modules/test-expressjs-project/package.json": "app/package.json", + "lib/node_modules/test-expressjs-project/node_modules": "app/node_modules", }, id="single file defined", ), From 582811bb16ec9ea80434c2cf4604c6b301a3a42d Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Fri, 10 Jan 2025 04:17:59 +0000 Subject: [PATCH 24/64] chore: add rockcraft to supported extensions --- docs/reference/rockcraft.yaml.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/reference/rockcraft.yaml.rst b/docs/reference/rockcraft.yaml.rst index d256fbdc4..d9bdf8959 100644 --- a/docs/reference/rockcraft.yaml.rst +++ b/docs/reference/rockcraft.yaml.rst @@ -262,6 +262,7 @@ Extensions to enable when building the ROCK. Currently supported extensions: +- ``expressjs-framework`` - ``flask-framework`` - ``django-framework`` - ``go-framework`` From 650cab0ff630db8021f0a042e3d3977e96dbf245 Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Fri, 10 Jan 2025 04:19:51 +0000 Subject: [PATCH 25/64] docs: expressjs tutorial --- docs/tutorial/code/expressjs/app.js | 43 +++ docs/tutorial/code/expressjs/package.json | 16 + docs/tutorial/code/expressjs/task.yaml | 136 +++++++ docs/tutorial/code/expressjs/time.js | 8 + docs/tutorial/expressjs.rst | 418 ++++++++++++++++++++++ docs/tutorial/index.rst | 2 +- 6 files changed, 622 insertions(+), 1 deletion(-) create mode 100644 docs/tutorial/code/expressjs/app.js create mode 100644 docs/tutorial/code/expressjs/package.json create mode 100644 docs/tutorial/code/expressjs/task.yaml create mode 100644 docs/tutorial/code/expressjs/time.js create mode 100644 docs/tutorial/expressjs.rst diff --git a/docs/tutorial/code/expressjs/app.js b/docs/tutorial/code/expressjs/app.js new file mode 100644 index 000000000..803c96b76 --- /dev/null +++ b/docs/tutorial/code/expressjs/app.js @@ -0,0 +1,43 @@ +var createError = require('http-errors'); +var express = require('express'); +var path = require('path'); +var cookieParser = require('cookie-parser'); +var logger = require('morgan'); + +var indexRouter = require('./routes/index'); +var usersRouter = require('./routes/users'); +var timeRouter = require('./routes/time'); + +var app = express(); + +// view engine setup +app.set('views', path.join(__dirname, 'views')); +app.set('view engine', 'jade'); + +app.use(logger('dev')); +app.use(express.json()); +app.use(express.urlencoded({ extended: false })); +app.use(cookieParser()); +app.use(express.static(path.join(__dirname, 'public'))); + +app.use('/', indexRouter); +app.use('/users', usersRouter); +app.use('/time', timeRouter); + +// catch 404 and forward to error handler +app.use(function (req, res, next) { + next(createError(404)); +}); + +// error handler +app.use(function (err, req, res, next) { + // set locals, only providing error in development + res.locals.message = err.message; + res.locals.error = req.app.get('env') === 'development' ? err : {}; + + // render the error page + res.status(err.status || 500); + res.render('error'); +}); + +module.exports = app; diff --git a/docs/tutorial/code/expressjs/package.json b/docs/tutorial/code/expressjs/package.json new file mode 100644 index 000000000..a7226591b --- /dev/null +++ b/docs/tutorial/code/expressjs/package.json @@ -0,0 +1,16 @@ +{ + "name": "app", + "version": "0.0.0", + "private": true, + "scripts": { + "start": "node ./bin/www" + }, + "dependencies": { + "cookie-parser": "~1.4.4", + "debug": "~2.6.9", + "express": "~4.16.1", + "http-errors": "~1.6.3", + "jade": "~1.11.0", + "morgan": "~1.9.1" + } +} diff --git a/docs/tutorial/code/expressjs/task.yaml b/docs/tutorial/code/expressjs/task.yaml new file mode 100644 index 000000000..4ddea7ced --- /dev/null +++ b/docs/tutorial/code/expressjs/task.yaml @@ -0,0 +1,136 @@ +########################################### +# IMPORTANT +# Comments matter! +# The docs use the wrapping comments as +# markers for including said instructions +# as snippets in the docs. +########################################### +summary: Getting started with FastAPI tutorial + +environment: + +execute: | + # [docs:create-venv] + sudo apt update && sudo apt install npm + echo "y" | npx express-generator app + cd app && npm install + # [docs:create-venv-end] + + npm start & + retry -n 5 --wait 2 curl --fail localhost:3000 + + # [docs:curl-expressjs] + curl localhost:3000 + # [docs:curl-expressjs-end] + + kill $! + + # [docs:create-rockcraft-yaml] + rockcraft init --profile expressjs-framework + # [docs:create-rockcraft-yaml-end] + sed -i "s/name: .*/name: expressjs-hello-world/g" rockcraft.yaml + + # [docs:pack] + ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true rockcraft pack + # [docs:pack-end] + + # [docs:ls-rock] + ls *.rock -l --block-size=MB + # [docs:ls-rock-end] + + # [docs:skopeo-copy] + sudo rockcraft.skopeo --insecure-policy \ + copy oci-archive:expressjs-hello-world_0.1_amd64.rock \ + docker-daemon:expressjs-hello-world:0.1 + # [docs:skopeo-copy-end] + + # [docs:docker-images] + sudo docker images expressjs-hello-world:0.1 + # [docs:docker-images-end] + + # [docs:docker-run] + sudo docker run --rm -d -p 3000:3000 \ + --name expressjs-hello-world expressjs-hello-world:0.1 + # [docs:docker-run-end] + retry -n 5 --wait 2 curl --fail localhost:3000 + + # [docs:curl-expressjs-rock] + curl localhost:3000 + # [docs:curl-expressjs-rock-end] + + # [docs:get-logs] + sudo docker exec expressjs-hello-world pebble logs expressjs + # [docs:get-logs-end] + + # [docs:stop-docker] + sudo docker stop expressjs-hello-world + sudo docker rmi expressjs-hello-world:0.1 + # [docs:stop-docker-end] + + # [docs:change-base] + sed -i \ + "s/base: .*/base: bare\nbuild-base: ubuntu@24.04/g" \ + rockcraft.yaml + # [docs:change-base-end] + sed -i "s/version: .*/version: 0.1-chiselled/g" rockcraft.yaml + + # [docs:chisel-pack] + ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true rockcraft pack + # [docs:chisel-pack-end] + + # [docs:ls-bare-rock] + ls *.rock -l --block-size=MB + # [docs:ls-bare-rock-end] + + # [docs:docker-run-chisel] + sudo rockcraft.skopeo --insecure-policy \ + copy oci-archive:expressjs-hello-world_0.1-chiselled_amd64.rock \ + docker-daemon:expressjs-hello-world:0.1-chiselled + sudo docker images expressjs-hello-world:0.1-chiselled + sudo docker run --rm -d -p 3000:3000 \ + --name expressjs-hello-world expressjs-hello-world:0.1-chiselled + # [docs:docker-run-chisel-end] + retry -n 5 --wait 2 curl --fail localhost:3000 + + # [docs:curl-expressjs-bare-rock] + curl localhost:3000 + # [docs:curl-expressjs-bare-rock-end] + + # [docs:stop-docker-chisel] + sudo docker stop expressjs-hello-world + sudo docker rmi expressjs-hello-world:0.1-chiselled + # [docs:stop-docker-chisel-end] + + cat time_app.py > app.py + sed -i "s/version: .*/version: 0.2/g" rockcraft.yaml + + # [docs:docker-run-update] + ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true rockcraft pack + sudo rockcraft.skopeo --insecure-policy \ + copy oci-archive:expressjs-hello-world_0.2_amd64.rock \ + docker-daemon:expressjs-hello-world:0.2 + sudo docker images expressjs-hello-world:0.2 + sudo docker run --rm -d -p 3000:3000 \ + --name expressjs-hello-world expressjs-hello-world:0.2 + # [docs:docker-run-update-end] + retry -n 5 --wait 2 curl --fail localhost:3000/time + + # [docs:curl-time] + curl localhost:3000/time + # [docs:curl-time-end] + + # [docs:stop-docker-updated] + sudo docker stop expressjs-hello-world + sudo docker rmi expressjs-hello-world:0.2 + # [docs:stop-docker-updated-end] + + # [docs:cleanup] + # exit and delete the virtual environment + deactivate + rm -rf .venv __pycache__ + # delete all the files created during the tutorial + rm expressjs-hello-world_0.1_amd64.rock \ + expressjs-hello-world_0.1-chiselled_amd64.rock \ + expressjs-hello-world_0.2_amd64.rock \ + rockcraft.yaml app.py requirements.txt + # [docs:cleanup-end] diff --git a/docs/tutorial/code/expressjs/time.js b/docs/tutorial/code/expressjs/time.js new file mode 100644 index 000000000..eeb7c7ddd --- /dev/null +++ b/docs/tutorial/code/expressjs/time.js @@ -0,0 +1,8 @@ +var express = require('express'); +var router = express.Router(); + +router.get('/', function (req, res, next) { + res.send(Date()); +}); + +module.exports = router; diff --git a/docs/tutorial/expressjs.rst b/docs/tutorial/expressjs.rst new file mode 100644 index 000000000..db17fc096 --- /dev/null +++ b/docs/tutorial/expressjs.rst @@ -0,0 +1,418 @@ +.. _build-a-rock-for-an-expressjs-application: + +Build a rock for an ExpressJS application +----------------------------------------- + +In this tutorial, we'll create a simple ExpressJS application and learn how to +containerise it in a rock with Rockcraft's +:ref:`expressjs-framework ` extension. + +Setup +===== + +.. include:: /reuse/tutorial/setup.rst + +Before we go any further, for this tutorial we'll need the most recent version +of Rockcraft on the edge channel. Run ``sudo snap refresh rockcraft --channel +latest/edge`` to switch to it. + +Finally, create a new directory for this tutorial and go inside it: + +.. code-block:: bash + + mkdir expressjs-hello-world + cd expressjs-hello-world + +Create the ExpressJS application +================================ + +Let's start by creating the "Hello, world" ExpressJS application that we'll use +throughout this tutorial. + +Create the ExpressJS application by running the express-generator. + + +.. literalinclude:: code/expressjs/task.yaml + :language: bash + :start-after: [docs:create-venv] + :end-before: [docs:create-venv-end] + :dedent: 2 + +Run the ExpressJS application using ``npm start`` to verify +that it works. + +Test the ExpressJS application by using ``curl`` to send a request to the root +endpoint. We'll need a new terminal for this -- if we're using Multipass, run +``multipass shell rock-dev`` to get another terminal: + +.. literalinclude:: code/expressjs/task.yaml + :language: bash + :start-after: [docs:curl-expressjs] + :end-before: [docs:curl-expressjs-end] + :dedent: 2 + +The ExpressJS application should respond with a ``Welcome to Express`` HTML web +page. + +The application looks good, so let's stop it for now by pressing :kbd:`Ctrl` + +:kbd:`C`. + +Pack the ExpressJS application into a rock +========================================== + +First, we'll need a ``rockcraft.yaml`` file. Rockcraft will automate its +creation and tailoring for a ExpressJS application by using the +``expressjs-framework`` profile: + +.. literalinclude:: code/expressjs/task.yaml + :language: bash + :start-after: [docs:create-rockcraft-yaml] + :end-before: [docs:create-rockcraft-yaml-end] + :dedent: 2 + +The ``rockcraft.yaml`` file will automatically be created in the project's +working directory. Open it in a text editor and check that the ``name`` is +``expressjs-hello-world``. Ensure that ``platforms`` includes the architecture of +the host. For example, if the host uses the ARM +architecture, include ``arm64`` in ``platforms``. + +.. note:: + For this tutorial, we'll use the ``name`` ``expressjs-hello-world`` and assume + we're running on the ``amd64`` platform. Check the architecture of the + system using ``dpkg --print-architecture``. + + The ``name``, ``version`` and ``platform`` all influence the name of the + generated ``.rock`` file. + +Pack the rock: + +.. literalinclude:: code/expressjs/task.yaml + :language: bash + :start-after: [docs:pack] + :end-before: [docs:pack-end] + :dedent: 2 + +.. note:: + + Depending on the network, this step can take a couple of minutes to finish. + + ``ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS`` is required whilst the + ExpressJS extension is experimental. + +Once Rockcraft has finished packing the ExpressJS rock, we'll find a new file in +the project's working directory (an `OCI `_ archive) with +the ``.rock`` extension: + +.. literalinclude:: code/expressjs/task.yaml + :language: bash + :start-after: [docs:ls-rock] + :end-before: [docs:ls-rock-end] + :dedent: 2 + +The created rock is about 75MB in size. We will reduce its size later in this +tutorial. + +.. note:: + If we changed the ``name`` or ``version`` in ``rockcraft.yaml`` or are not + on an ``amd64`` platform, the name of the ``.rock`` file will be + different. + + The size of the rock may vary depending on factors like the architecture + we are building on and the packages installed at the time of packing. + +Run the ExpressJS rock with Docker +================================== + +We already have the rock as an `OCI `_ archive. Now we +need to load it into Docker: + +.. literalinclude:: code/expressjs/task.yaml + :language: bash + :start-after: [docs:skopeo-copy] + :end-before: [docs:skopeo-copy-end] + :dedent: 2 + +Check that the image was successfully loaded into Docker: + +.. literalinclude:: code/expressjs/task.yaml + :language: bash + :start-after: [docs:docker-images] + :end-before: [docs:docker-images-end] + :dedent: 2 + +The output should list the ExpressJS container image, along with its tag, ID and +size: + +.. code-block:: text + :class: log-snippets + + REPOSITORY TAG IMAGE ID CREATED SIZE + expressjs-hello-world 0.1 30c7e5aed202 2 weeks ago 193MB + +.. note:: + The size of the image reported by Docker is the uncompressed size which is + larger than the size of the compressed ``.rock`` file. + +Now we're finally ready to run the rock and test the containerised ExpressJS +application: + +.. literalinclude:: code/expressjs/task.yaml + :language: text + :start-after: [docs:docker-run] + :end-before: [docs:docker-run-end] + :dedent: 2 + +Use the same ``curl`` command as before to send a request to the ExpressJS +application's root endpoint which is running inside the container: + +.. literalinclude:: code/expressjs/task.yaml + :language: text + :start-after: [docs:curl-expressjs-rock] + :end-before: [docs:curl-expressjs-rock-end] + :dedent: 2 + +The ExpressJS application should again respond with ``Welcome to Express`` HTML. + +View the application logs +~~~~~~~~~~~~~~~~~~~~~~~~~ + +When deploying the ExpressJS rock, we can always get the application logs via +``pebble``: + +.. literalinclude:: code/expressjs/task.yaml + :language: text + :start-after: [docs:get-logs] + :end-before: [docs:get-logs-end] + :dedent: 2 + +As a result, :ref:`pebble_explanation_page` will give us the logs for the +``expressjs`` service running inside the container. +We should expect to see something similar to this: + +.. code-block:: text + :class: log-snippets + + app@0.0.0 start + node ./bin/www + GET / 200 62.934 ms - 170 + +We can also choose to follow the logs by using the ``-f`` option with the +``pebble logs`` command above. To stop following the logs, press :kbd:`Ctrl` + +:kbd:`C`. + +Cleanup +~~~~~~~ + +Now we have a fully functional rock for a ExpressJS application! This concludes +the first part of this tutorial, so we'll stop the container and remove the +respective image for now: + +.. literalinclude:: code/expressjs/task.yaml + :language: bash + :start-after: [docs:stop-docker] + :end-before: [docs:stop-docker-end] + :dedent: 2 + +Chisel the rock +=============== + +This is an optional but recommended step, especially if we're looking to +deploy the rock into a production environment. With :ref:`chisel_explanation` +we can produce lean and production-ready rocks by getting rid of all the +contents that are not needed for the ExpressJS application to run. This results +in a much smaller rock with a reduced attack surface. + +.. note:: + It is recommended to run chiselled images in production. For development, + we may prefer non-chiselled images as they will include additional + development tooling (such as for debugging). + +The first step towards chiselling the rock is to ensure we are using a +``bare`` :ref:`base `. +In ``rockcraft.yaml``, change the ``base`` to ``bare`` and add +``build-base: ubuntu@24.04``: + +.. literalinclude:: code/expressjs/task.yaml + :language: bash + :start-after: [docs:change-base] + :end-before: [docs:change-base-end] + :dedent: 2 + +.. note:: + The ``sed`` command replaces the current ``base`` in ``rockcraft.yaml`` with + the ``bare`` base. The command also adds a ``build-base`` which is required + when using the ``bare`` base. + +So that we can compare the size after chiselling, open the ``rockcraft.yaml`` +file and change the ``version`` (e.g. to ``0.1-chiselled``). Pack the rock with +the new ``bare`` :ref:`base `: + +.. literalinclude:: code/expressjs/task.yaml + :language: bash + :start-after: [docs:chisel-pack] + :end-before: [docs:chisel-pack-end] + :dedent: 2 + +As before, verify that the new rock was created: + +.. literalinclude:: code/expressjs/task.yaml + :language: bash + :start-after: [docs:ls-bare-rock] + :end-before: [docs:ls-bare-rock-end] + :dedent: 2 + +We'll verify that the new ExpressJS rock is now approximately **20% smaller** +in size! And that's just because of the simple change of ``base``. + +And the functionality is still the same. As before, we can confirm this by +running the rock with Docker + +.. literalinclude:: code/expressjs/task.yaml + :language: text + :start-after: [docs:docker-run-chisel] + :end-before: [docs:docker-run-chisel-end] + :dedent: 2 + +and then using the same ``curl`` request: + +.. literalinclude:: code/expressjs/task.yaml + :language: text + :start-after: [docs:curl-expressjs-bare-rock] + :end-before: [docs:curl-expressjs-bare-rock-end] + :dedent: 2 + +Unsurprisingly, the ExpressJS application should still respond with +``Welcome to Express`` HTML. + +Cleanup +~~~~~~~ + +And that's it. We can now stop the container and remove the corresponding +image: + +.. literalinclude:: code/expressjs/task.yaml + :language: bash + :start-after: [docs:stop-docker-chisel] + :end-before: [docs:stop-docker-chisel-end] + :dedent: 2 + +.. _update-expressjs-application: + +Update the ExpressJS application +================================ + +As a final step, let's update our application. For example, +we want to add a new ``/time`` endpoint which returns the current time. + +Start by opening the ``time.js`` file in a text editor and update the code to +look like the following: + +.. literalinclude:: code/expressjs/time.js + :language: python + +Place ``time.js`` file into the appropriate ``routes/`` directory. Import the +time route from the the main ``app.js`` file and update the code to look like +the following: + +.. literalinclude:: code/expressjs/app.js + :language: javascript + +Notice the addition of timerouter import and the registration of the ``/time`` +endpoint. + +Since we are creating a new version of the application, open the +``rockcraft.yaml`` file and change the ``version`` (e.g. to ``0.2``). + +.. note:: + + ``rockcraft pack`` will create a new image with the updated code even if we + don't change the version. It is recommended to change the version whenever + we make changes to the application in the image. + +Pack and run the rock using similar commands as before: + +.. literalinclude:: code/expressjs/task.yaml + :language: text + :start-after: [docs:docker-run-update] + :end-before: [docs:docker-run-update-end] + :dedent: 2 + +.. note:: + + Note that the resulting ``.rock`` file will now be named differently, as + its new version will be part of the filename. + +Finally, use ``curl`` to send a request to the ``/time`` endpoint: + +.. literalinclude:: code/expressjs/task.yaml + :language: text + :start-after: [docs:curl-time] + :end-before: [docs:curl-time-end] + :dedent: 2 + +The updated application should respond with the current date and time (e.g. +``Fri Jan 10 2025 03:11:44 GMT+0000 (Coordinated Universal Time)``). + +.. note:: + + If you are getting a ``404`` for the ``/time`` endpoint, check the + :ref:`troubleshooting-expressjs` steps below. + +Cleanup +~~~~~~~ + +We can now stop the container and remove the corresponding image: + +.. literalinclude:: code/expressjs/task.yaml + :language: bash + :start-after: [docs:stop-docker-updated] + :end-before: [docs:stop-docker-updated-end] + :dedent: 2 + +Reset the environment +===================== + +We've reached the end of this tutorial. + +If we'd like to reset the working environment, we can simply run the +following: + +.. literalinclude:: code/expressjs/task.yaml + :language: bash + :start-after: [docs:cleanup] + :end-before: [docs:cleanup-end] + :dedent: 2 + +.. collapse:: If using Multipass... + + If we created an instance using Multipass, we can also clean it up. + Start by exiting it: + + .. code-block:: bash + + exit + + And then we can proceed with its deletion: + + .. code-block:: bash + + multipass delete rock-dev + multipass purge + +---- + +.. _troubleshooting-expressjs: + +Troubleshooting +=============== + +**Application updates not taking effect?** + +Upon changing your ExpressJS application and re-packing the rock, if you believe +your changes are not taking effect (e.g. the ``/time`` +:ref:`endpoint ` is returning a +404), try running ``rockcraft clean`` and pack the rock again with +``rockcraft pack``. + +.. _`lxd-docker-connectivity-issue`: https://documentation.ubuntu.com/lxd/en/latest/howto/network_bridge_firewalld/#prevent-connectivity-issues-with-lxd-and-docker +.. _`install-multipass`: https://multipass.run/docs/install-multipass diff --git a/docs/tutorial/index.rst b/docs/tutorial/index.rst index 315951f4a..374a11105 100644 --- a/docs/tutorial/index.rst +++ b/docs/tutorial/index.rst @@ -28,4 +28,4 @@ code into container applications: 6. Build a rock for a Django application 7. Build a rock for a FastAPI application 8. Build a rock for a Go application - + 9. Build a rock for an ExpressJS application From a25ea2ad02639bb92fd30601eb011cc0ba695679 Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Fri, 10 Jan 2025 04:31:13 +0000 Subject: [PATCH 26/64] chore: separate out dependencies part --- rockcraft/extensions/expressjs.py | 36 ++++++++++++++----------- tests/unit/extensions/test_expressjs.py | 10 ++++++- 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/rockcraft/extensions/expressjs.py b/rockcraft/extensions/expressjs.py index 1afd043df..9d84cf42b 100644 --- a/rockcraft/extensions/expressjs.py +++ b/rockcraft/extensions/expressjs.py @@ -40,9 +40,9 @@ class ExpressJSFramework(Extension): EXPRESS_PACKAGE_DIRS = [ "package.json", "package-lock.json", - "node_modules", ] - RUNTIME_DEBS = [ + BUILD_GENERATED_DIRS = ["node_modules", ".npmrc"] + RUNTIME_SLICES = [ "ca-certificates_data", "bash_bins", "coreutils_bins", @@ -91,6 +91,7 @@ def get_root_snippet(self) -> dict[str, Any]: snippet["parts"] = { "expressjs-framework/install-app": self._gen_install_app_part(), + "expressjs-framework/runtime": self._gen_runtime_part(), } return snippet @@ -118,20 +119,22 @@ def _gen_install_app_part(self) -> dict: "npm-node-version": self._install_app_node_version, "source": "app/", "organize": self._app_organize, + "override-build": ( + "craftctl default\n" + "npm config set script-shell=bash --location project" + ), "override-prime": ( "craftctl default\n" f"rm -rf ${{CRAFT_PRIME}}/lib/node_modules/{self._app_name}\n" - "echo 'script-shell=bash' >> ${CRAFT_PRIME}/app/.npmrc" ), - "stage-packages": self.RUNTIME_DEBS, } - # def _gen_runtime_debs_part(self) -> dict: - # """Generate the runtime debs part.""" - # return { - # "plugin": "nil", - # "stage-packages": self.RUNTIME_DEBS, - # } + def _gen_runtime_part(self) -> dict: + """Generate the runtime debs part.""" + return { + "plugin": "nil", + "stage-packages": self.RUNTIME_SLICES, + } @property def _app_package_json(self) -> dict: @@ -182,13 +185,16 @@ def _app_organize(self) -> dict: for prime_path in user_prime + self.EXPRESS_PACKAGE_DIRS ] lib_dir = f"lib/node_modules/{self._app_name}" - file_mappings = { - f"{lib_dir}/{f}": f"app/{f}" + app_dir = "app" + organize_mappings = { + f"{lib_dir}/{f}": f"{app_dir}/{f}" for f in project_relative_file_paths - if (self.project_root / "app" / f).exists() + if (self.project_root / app_dir / f).exists() } - file_mappings[f"{lib_dir}/node_modules"] = "app/node_modules" - return file_mappings + for file_or_dir in self.BUILD_GENERATED_DIRS: + organize_mappings[f"{lib_dir}/{file_or_dir}"] = f"{app_dir}/{file_or_dir}" + + return organize_mappings @property def _install_app_node_version(self) -> str: diff --git a/tests/unit/extensions/test_expressjs.py b/tests/unit/extensions/test_expressjs.py index de0ce8f85..2ebb94ded 100644 --- a/tests/unit/extensions/test_expressjs.py +++ b/tests/unit/extensions/test_expressjs.py @@ -80,12 +80,19 @@ def test_expressjs_extension_default( "organize": { f"lib/node_modules/{expressjs_project_name}/package.json": "app/package.json", f"lib/node_modules/{expressjs_project_name}/node_modules": "app/node_modules", + f"lib/node_modules/{expressjs_project_name}/.npmrc": "app/.npmrc", }, + "override-build": ( + "craftctl default\n" + "npm config set script-shell=bash --location project" + ), "override-prime": ( "craftctl default\n" f"rm -rf ${{CRAFT_PRIME}}/lib/node_modules/{expressjs_project_name}\n" - "echo 'script-shell=bash' >> ${CRAFT_PRIME}/app/.npmrc" ), + }, + "expressjs-framework/runtime": { + "plugin": "nil", "stage-packages": [ "ca-certificates_data", "bash_bins", @@ -168,6 +175,7 @@ def test_expressjs_override_node_version( ["app/app.js"], [], { + "lib/node_modules/test-expressjs-project/.npmrc": "app/.npmrc", "lib/node_modules/test-expressjs-project/app.js": "app/app.js", "lib/node_modules/test-expressjs-project/package.json": "app/package.json", "lib/node_modules/test-expressjs-project/node_modules": "app/node_modules", From dc3b61bb8a052827851632a3e1248f9b63a7a254 Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Fri, 10 Jan 2025 04:35:24 +0000 Subject: [PATCH 27/64] feat: add generation template --- .../expressjs-framework/rockcraft.yaml.j2 | 26 ++++--------------- 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/rockcraft/templates/expressjs-framework/rockcraft.yaml.j2 b/rockcraft/templates/expressjs-framework/rockcraft.yaml.j2 index 77bac8aad..b7dabdb73 100644 --- a/rockcraft/templates/expressjs-framework/rockcraft.yaml.j2 +++ b/rockcraft/templates/expressjs-framework/rockcraft.yaml.j2 @@ -1,7 +1,7 @@ name: {{name}} # see {{versioned_url}}/explanation/bases/ # for more information about bases and using 'bare' bases for chiselled rocks -base: bare # as an alternative, a ubuntu base can be used +base: ubuntu@24.04 # as an alternative, a 'bare' base can be used build-base: ubuntu@24.04 # build-base is required when the base is bare version: '0.1' # just for humans. Semantic versioning is recommended summary: A summary of your ExpresssJS application # 79 char long summary @@ -20,9 +20,8 @@ platforms: # to ensure the expressjs-framework extension works properly, your ExpressJS # application should be inside the app, src or a directory with the project -# name, and have inside the app directory a `index.js` or `server.js` file with -# the starting script defined in the package.json's scripts section under the -# "run" key. +# name, and have inside the app directory a `app.js`file with the starting +# script defined in the package.json's scripts section under the "run" key. extensions: - expressjs-framework @@ -31,7 +30,7 @@ extensions: # expressjs-framework/install-app: # prime: # # by default, only the files in bin/, public/, routes/, views/, app.js, -# # package.json package-lock.json are copied into the image. +# # package.json package-lock.json are copied into the image. # # You can modify the list below to override the default list and # # include or exclude specific files/directories in your project. # # note: prefix each entry with "app/" followed by the local path. @@ -39,14 +38,6 @@ extensions: # - app/templates # - app/static -# you may need packages to build a npm package. Add them here if necessary. -# expressjs-framework/dependencies: -# build-packages: -# # for example, if you need pkg-config and libxmlsec1-dev to build one -# # of your packages: -# - pkg-config -# - libxmlsec1-dev - # you can add package slices or Debian packages to the image. # package slices are subsets of Debian packages, which result # in smaller and more secure images. @@ -54,16 +45,9 @@ extensions: # add this part if you want to add packages slices to your image. # you can find a list of packages slices at https://github.com/canonical/chisel-releases -# runtime-slices: +# expressjs-framework/runtime: # plugin: nil # stage-packages: # # list the required package slices for your expressjs application below. # # for example, for the slice libs of libpq5: # - libpq5_libs - -# if you want to add a Debian package to your image, add the next part -# runtime-debs: -# plugin: nil -# stage-packages: -# # list required Debian packages for your expressjs application below. -# - libpq5 # for Postgresql connection. From 6e68078a3cd0d5bb90a8d77bd63908bb5e331b30 Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Fri, 10 Jan 2025 04:35:43 +0000 Subject: [PATCH 28/64] docs: expressjs reference --- .../extensions/expressjs-framework.rst | 65 +++++++++++++++++++ docs/reference/extensions/index.rst | 1 + 2 files changed, 66 insertions(+) create mode 100644 docs/reference/extensions/expressjs-framework.rst diff --git a/docs/reference/extensions/expressjs-framework.rst b/docs/reference/extensions/expressjs-framework.rst new file mode 100644 index 000000000..3c403a69d --- /dev/null +++ b/docs/reference/extensions/expressjs-framework.rst @@ -0,0 +1,65 @@ +.. _expressjs-framework-reference: + +expressjs-framework +------------------- + +The ExpressJS extension streamlines the process of building ExpressJS application +rocks. + +It facilitates the installation of ExpressJS application dependencies, including +Node and NPM, inside the rock. Additionally, it transfers your project files to +``/app`` within the rock. + +.. note:: + The ExpressJS extension is compatible with the ``bare`` and ``ubuntu@24.04`` + bases. + +Project requirements +==================== + +There is 1 requirement to be able to use the ``expressjs-framework`` extension: + +1. There must be a ``package.json`` file in the ``app`` directory of the project + with ``start`` script defined. + +``parts`` > ``expressjs-framework/dependencies:`` > ``stage-packages`` +====================================================================== + +You can use this key to specify any dependencies required for your ExpressJS +application. In the following example we use it to specify ``libpq-dev``: + +.. code-block:: yaml + + parts: + expressjs-framework/dependencies: + stage-packages: + # list required packages or slices for your ExpressJS application below. + - libpq-dev + + +``parts`` > ``expressjs-framework/install-app`` > ``prime`` +=========================================================== + +You can use this field to specify the files to be included or excluded from +your rock upon ``rockcraft pack``. Follow the ``app/`` notation. For +example: + +.. code-block:: yaml + + parts: + expressjs-framework/install-app: + prime: + - app/.env + - app/script.js + - app/templates + - app/static + +Some files/directories, if they exist, are included by default. These include: +````, ``app.js``, ``migrate``, ``migrate.sh``, ``migrate.py``, +``bin``, ``public``, ``routes``, ``views``, ``package.json``, +``package-lock.json``. + +Useful links +============ + +- :ref:`build-a-rock-for-an-expressjs-application` diff --git a/docs/reference/extensions/index.rst b/docs/reference/extensions/index.rst index 3bf8ae6c6..74f405d56 100644 --- a/docs/reference/extensions/index.rst +++ b/docs/reference/extensions/index.rst @@ -11,6 +11,7 @@ initiating a new rock. .. toctree:: :maxdepth: 1 + expressjs-framework flask-framework django-framework fastapi-framework From 024ea8ed17dacb086ae048cbfbe0d7770d797935 Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Fri, 10 Jan 2025 05:02:54 +0000 Subject: [PATCH 29/64] docs: fix linting line too long --- docs/reference/extensions/expressjs-framework.rst | 4 ++-- docs/tutorial/expressjs.rst | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/reference/extensions/expressjs-framework.rst b/docs/reference/extensions/expressjs-framework.rst index 3c403a69d..4b8697ed6 100644 --- a/docs/reference/extensions/expressjs-framework.rst +++ b/docs/reference/extensions/expressjs-framework.rst @@ -3,8 +3,8 @@ expressjs-framework ------------------- -The ExpressJS extension streamlines the process of building ExpressJS application -rocks. +The ExpressJS extension streamlines the process of building ExpressJS +application rocks. It facilitates the installation of ExpressJS application dependencies, including Node and NPM, inside the rock. Additionally, it transfers your project files to diff --git a/docs/tutorial/expressjs.rst b/docs/tutorial/expressjs.rst index db17fc096..394b16d2a 100644 --- a/docs/tutorial/expressjs.rst +++ b/docs/tutorial/expressjs.rst @@ -72,14 +72,14 @@ creation and tailoring for a ExpressJS application by using the The ``rockcraft.yaml`` file will automatically be created in the project's working directory. Open it in a text editor and check that the ``name`` is -``expressjs-hello-world``. Ensure that ``platforms`` includes the architecture of -the host. For example, if the host uses the ARM -architecture, include ``arm64`` in ``platforms``. +``expressjs-hello-world``. Ensure that ``platforms`` includes the architecture +of the host. For example, if the host uses the ARM architecture, include +``arm64`` in ``platforms``. .. note:: - For this tutorial, we'll use the ``name`` ``expressjs-hello-world`` and assume - we're running on the ``amd64`` platform. Check the architecture of the - system using ``dpkg --print-architecture``. + For this tutorial, we'll use the ``name`` ``expressjs-hello-world`` and + assume we're running on the ``amd64`` platform. Check the architecture of + the system using ``dpkg --print-architecture``. The ``name``, ``version`` and ``platform`` all influence the name of the generated ``.rock`` file. From 685d9be99fccc14d4d2e6acbec9cc9aea733a9a0 Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Fri, 10 Jan 2025 08:09:13 +0000 Subject: [PATCH 30/64] fix: 24.04 package clash fix --- rockcraft/extensions/expressjs.py | 16 ++++++++--- tests/unit/extensions/test_expressjs.py | 37 ++++++++++++++++++++----- 2 files changed, 42 insertions(+), 11 deletions(-) diff --git a/rockcraft/extensions/expressjs.py b/rockcraft/extensions/expressjs.py index 9d84cf42b..8bf83ff2b 100644 --- a/rockcraft/extensions/expressjs.py +++ b/rockcraft/extensions/expressjs.py @@ -42,8 +42,8 @@ class ExpressJSFramework(Extension): "package-lock.json", ] BUILD_GENERATED_DIRS = ["node_modules", ".npmrc"] - RUNTIME_SLICES = [ - "ca-certificates_data", + RUNTIME_SLICES = ["ca-certificates_data"] + BARE_RUNTIME_SLICES = [ "bash_bins", "coreutils_bins", "libc6_libs", @@ -68,7 +68,7 @@ def get_root_snippet(self) -> dict[str, Any]: Default values: - run_user: _daemon_ - - build-base: ubuntu:22.04 (only if user specify bare without a build-base) + - build-base: ubuntu:24.04 - platform: amd64 - services: a service to run the ExpressJS server - parts: see ExpressJSFramework._gen_parts @@ -131,11 +131,19 @@ def _gen_install_app_part(self) -> dict: def _gen_runtime_part(self) -> dict: """Generate the runtime debs part.""" + runtime_packages = [*self.RUNTIME_SLICES] + if self._rock_base == "bare": + runtime_packages += [*self.BARE_RUNTIME_SLICES] return { "plugin": "nil", - "stage-packages": self.RUNTIME_SLICES, + "stage-packages": runtime_packages, } + @property + def _rock_base(self) -> str: + """Return the base of the rockcraft project.""" + return self.yaml_data["base"] + @property def _app_package_json(self) -> dict: """Return the app package.json contents.""" diff --git a/tests/unit/extensions/test_expressjs.py b/tests/unit/extensions/test_expressjs.py index 2ebb94ded..071550991 100644 --- a/tests/unit/extensions/test_expressjs.py +++ b/tests/unit/extensions/test_expressjs.py @@ -93,13 +93,7 @@ def test_expressjs_extension_default( }, "expressjs-framework/runtime": { "plugin": "nil", - "stage-packages": [ - "ca-certificates_data", - "bash_bins", - "coreutils_bins", - "libc6_libs", - "libnode109_libs", - ], + "stage-packages": ["ca-certificates_data"], }, }, "services": { @@ -200,3 +194,32 @@ def test_expressjs_install_app_prime_to_organize_map( applied["parts"]["expressjs-framework/install-app"]["organize"] == expected_organize ) + + +@pytest.mark.parametrize( + "rock_base, expected_runtime_packages", + [ + pytest.param( + "bare", + [ + "ca-certificates_data", + "bash_bins", + "coreutils_bins", + "libc6_libs", + "libnode109_libs", + ], + id="bare", + ), + pytest.param("ubuntu@24.04", ["ca-certificates_data"], id="ubuntu@24.04"), + ], +) +@pytest.mark.usefixtures("expressjs_extension", "package_json_file") +def test_expressjs_gen_runtime_part( + tmp_path, expressjs_input_yaml, rock_base, expected_runtime_packages +): + expressjs_input_yaml["base"] = rock_base + applied = extensions.apply_extensions(tmp_path, expressjs_input_yaml) + assert ( + applied["parts"]["expressjs-framework/runtime"]["stage-packages"] + == expected_runtime_packages + ) From b900591acce994ae40908b091e9528705e9b3a44 Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Fri, 10 Jan 2025 09:55:19 +0000 Subject: [PATCH 31/64] fix: move stage packages --- rockcraft/extensions/expressjs.py | 15 ++++----------- tests/unit/extensions/test_expressjs.py | 5 +---- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/rockcraft/extensions/expressjs.py b/rockcraft/extensions/expressjs.py index 8bf83ff2b..1b62b31dd 100644 --- a/rockcraft/extensions/expressjs.py +++ b/rockcraft/extensions/expressjs.py @@ -90,8 +90,7 @@ def get_root_snippet(self) -> dict[str, Any]: } snippet["parts"] = { - "expressjs-framework/install-app": self._gen_install_app_part(), - "expressjs-framework/runtime": self._gen_runtime_part(), + "expressjs-framework/install-app": self._gen_install_app_part() } return snippet @@ -113,6 +112,9 @@ def get_parts_snippet(self) -> dict[str, Any]: def _gen_install_app_part(self) -> dict: """Generate the install app part using NPM plugin.""" + runtime_packages = [*self.RUNTIME_SLICES] + if self._rock_base == "bare": + runtime_packages += [*self.BARE_RUNTIME_SLICES] return { "plugin": "npm", "npm-include-node": True, @@ -127,15 +129,6 @@ def _gen_install_app_part(self) -> dict: "craftctl default\n" f"rm -rf ${{CRAFT_PRIME}}/lib/node_modules/{self._app_name}\n" ), - } - - def _gen_runtime_part(self) -> dict: - """Generate the runtime debs part.""" - runtime_packages = [*self.RUNTIME_SLICES] - if self._rock_base == "bare": - runtime_packages += [*self.BARE_RUNTIME_SLICES] - return { - "plugin": "nil", "stage-packages": runtime_packages, } diff --git a/tests/unit/extensions/test_expressjs.py b/tests/unit/extensions/test_expressjs.py index 071550991..2e016edb9 100644 --- a/tests/unit/extensions/test_expressjs.py +++ b/tests/unit/extensions/test_expressjs.py @@ -90,9 +90,6 @@ def test_expressjs_extension_default( "craftctl default\n" f"rm -rf ${{CRAFT_PRIME}}/lib/node_modules/{expressjs_project_name}\n" ), - }, - "expressjs-framework/runtime": { - "plugin": "nil", "stage-packages": ["ca-certificates_data"], }, }, @@ -220,6 +217,6 @@ def test_expressjs_gen_runtime_part( expressjs_input_yaml["base"] = rock_base applied = extensions.apply_extensions(tmp_path, expressjs_input_yaml) assert ( - applied["parts"]["expressjs-framework/runtime"]["stage-packages"] + applied["parts"]["expressjs-framework/install-app"]["stage-packages"] == expected_runtime_packages ) From cea6fe4e7f521e869c15972798b0ed6029c2002a Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Mon, 13 Jan 2025 08:20:31 +0000 Subject: [PATCH 32/64] fix: npmrc install --- rockcraft/extensions/expressjs.py | 4 +++- tests/unit/extensions/test_expressjs.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/rockcraft/extensions/expressjs.py b/rockcraft/extensions/expressjs.py index 1b62b31dd..d34d35173 100644 --- a/rockcraft/extensions/expressjs.py +++ b/rockcraft/extensions/expressjs.py @@ -123,7 +123,9 @@ def _gen_install_app_part(self) -> dict: "organize": self._app_organize, "override-build": ( "craftctl default\n" - "npm config set script-shell=bash --location project" + "npm config set script-shell=bash --location project\n" + "cp ${CRAFT_PART_BUILD}/.npmrc ${CRAFT_PART_INSTALL}/lib/node_modules/" + f"{self._app_name}/.npmrc" ), "override-prime": ( "craftctl default\n" diff --git a/tests/unit/extensions/test_expressjs.py b/tests/unit/extensions/test_expressjs.py index 2e016edb9..24ddd75a8 100644 --- a/tests/unit/extensions/test_expressjs.py +++ b/tests/unit/extensions/test_expressjs.py @@ -84,7 +84,9 @@ def test_expressjs_extension_default( }, "override-build": ( "craftctl default\n" - "npm config set script-shell=bash --location project" + "npm config set script-shell=bash --location project\n" + "cp ${CRAFT_PART_BUILD}/.npmrc ${CRAFT_PART_INSTALL}/lib/node_modules/" + f"{expressjs_project_name}/.npmrc" ), "override-prime": ( "craftctl default\n" From b20ebeacf0a3c2dd0d9ffb7dc7cd6dfde765e74b Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Mon, 13 Jan 2025 08:20:54 +0000 Subject: [PATCH 33/64] fix: tutorial code fix --- docs/tutorial/code/expressjs/task.yaml | 22 +++++++++---------- docs/tutorial/expressjs.rst | 4 ++-- .../expressjs-framework/rockcraft.yaml.j2 | 1 - 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/docs/tutorial/code/expressjs/task.yaml b/docs/tutorial/code/expressjs/task.yaml index 4ddea7ced..6452d7731 100644 --- a/docs/tutorial/code/expressjs/task.yaml +++ b/docs/tutorial/code/expressjs/task.yaml @@ -10,20 +10,22 @@ summary: Getting started with FastAPI tutorial environment: execute: | - # [docs:create-venv] - sudo apt update && sudo apt install npm + # [docs:init-app] + sudo apt-get update -y && sudo apt-get install npm -y + npm install -g express-generator echo "y" | npx express-generator app cd app && npm install - # [docs:create-venv-end] + # [docs:init-app-end] npm start & + cd .. retry -n 5 --wait 2 curl --fail localhost:3000 # [docs:curl-expressjs] curl localhost:3000 # [docs:curl-expressjs-end] - kill $! + kill $(lsof -t -i:3000) # [docs:create-rockcraft-yaml] rockcraft init --profile expressjs-framework @@ -68,9 +70,7 @@ execute: | # [docs:stop-docker-end] # [docs:change-base] - sed -i \ - "s/base: .*/base: bare\nbuild-base: ubuntu@24.04/g" \ - rockcraft.yaml + sed -i "s/base: bare/base: ubuntu@24.04/g" rockcraft.yaml # [docs:change-base-end] sed -i "s/version: .*/version: 0.1-chiselled/g" rockcraft.yaml @@ -101,7 +101,9 @@ execute: | sudo docker rmi expressjs-hello-world:0.1-chiselled # [docs:stop-docker-chisel-end] - cat time_app.py > app.py + ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=True rockcraft clean + mv time.js app/routes/ + mv app.js app/ sed -i "s/version: .*/version: 0.2/g" rockcraft.yaml # [docs:docker-run-update] @@ -126,11 +128,9 @@ execute: | # [docs:cleanup] # exit and delete the virtual environment - deactivate - rm -rf .venv __pycache__ # delete all the files created during the tutorial rm expressjs-hello-world_0.1_amd64.rock \ expressjs-hello-world_0.1-chiselled_amd64.rock \ expressjs-hello-world_0.2_amd64.rock \ - rockcraft.yaml app.py requirements.txt + rockcraft.yaml # [docs:cleanup-end] diff --git a/docs/tutorial/expressjs.rst b/docs/tutorial/expressjs.rst index 394b16d2a..1fa02d626 100644 --- a/docs/tutorial/expressjs.rst +++ b/docs/tutorial/expressjs.rst @@ -34,8 +34,8 @@ Create the ExpressJS application by running the express-generator. .. literalinclude:: code/expressjs/task.yaml :language: bash - :start-after: [docs:create-venv] - :end-before: [docs:create-venv-end] + :start-after: [docs:init-app] + :end-before: [docs:init-app-end] :dedent: 2 Run the ExpressJS application using ``npm start`` to verify diff --git a/rockcraft/templates/expressjs-framework/rockcraft.yaml.j2 b/rockcraft/templates/expressjs-framework/rockcraft.yaml.j2 index b7dabdb73..30da70342 100644 --- a/rockcraft/templates/expressjs-framework/rockcraft.yaml.j2 +++ b/rockcraft/templates/expressjs-framework/rockcraft.yaml.j2 @@ -2,7 +2,6 @@ name: {{name}} # see {{versioned_url}}/explanation/bases/ # for more information about bases and using 'bare' bases for chiselled rocks base: ubuntu@24.04 # as an alternative, a 'bare' base can be used -build-base: ubuntu@24.04 # build-base is required when the base is bare version: '0.1' # just for humans. Semantic versioning is recommended summary: A summary of your ExpresssJS application # 79 char long summary description: | From a5044034fa4f28c24fbd491033b981f042b50fcb Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Mon, 13 Jan 2025 08:28:32 +0000 Subject: [PATCH 34/64] fix: pebble lint --- rockcraft/pebble.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/rockcraft/pebble.py b/rockcraft/pebble.py index 8508a9a55..795f2588d 100644 --- a/rockcraft/pebble.py +++ b/rockcraft/pebble.py @@ -137,9 +137,7 @@ class Pebble: "stage-snaps": ["pebble/latest/stable"], # We need this because "services" is Optional, but the directory must exist "override-prime": str( - "craftctl default\n" - f"mkdir -p {PEBBLE_LAYERS_PATH}\n" - f"chmod 777 {PEBBLE_PATH}" + f"craftctl default\nmkdir -p {PEBBLE_LAYERS_PATH}\nchmod 777 {PEBBLE_PATH}" ), } PEBBLE_PART_SPEC = { From f6d31a2f7198feab2b3e8bb5a4921a5fd4899385 Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Mon, 13 Jan 2025 09:19:09 +0000 Subject: [PATCH 35/64] test: add back build base --- docs/tutorial/code/expressjs/task.yaml | 2 +- rockcraft/templates/expressjs-framework/rockcraft.yaml.j2 | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/tutorial/code/expressjs/task.yaml b/docs/tutorial/code/expressjs/task.yaml index 6452d7731..bc66132d8 100644 --- a/docs/tutorial/code/expressjs/task.yaml +++ b/docs/tutorial/code/expressjs/task.yaml @@ -70,7 +70,7 @@ execute: | # [docs:stop-docker-end] # [docs:change-base] - sed -i "s/base: bare/base: ubuntu@24.04/g" rockcraft.yaml + sed -i "s/base: ubuntu@24.04/base: bare/g" rockcraft.yaml # [docs:change-base-end] sed -i "s/version: .*/version: 0.1-chiselled/g" rockcraft.yaml diff --git a/rockcraft/templates/expressjs-framework/rockcraft.yaml.j2 b/rockcraft/templates/expressjs-framework/rockcraft.yaml.j2 index 30da70342..b7dabdb73 100644 --- a/rockcraft/templates/expressjs-framework/rockcraft.yaml.j2 +++ b/rockcraft/templates/expressjs-framework/rockcraft.yaml.j2 @@ -2,6 +2,7 @@ name: {{name}} # see {{versioned_url}}/explanation/bases/ # for more information about bases and using 'bare' bases for chiselled rocks base: ubuntu@24.04 # as an alternative, a 'bare' base can be used +build-base: ubuntu@24.04 # build-base is required when the base is bare version: '0.1' # just for humans. Semantic versioning is recommended summary: A summary of your ExpresssJS application # 79 char long summary description: | From 2a46caba6c8d9d6c3f4cb1d7005462074dbdac02 Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Mon, 13 Jan 2025 10:16:53 +0000 Subject: [PATCH 36/64] fix: base change --- docs/tutorial/code/expressjs/task.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/code/expressjs/task.yaml b/docs/tutorial/code/expressjs/task.yaml index bc66132d8..5628be503 100644 --- a/docs/tutorial/code/expressjs/task.yaml +++ b/docs/tutorial/code/expressjs/task.yaml @@ -70,7 +70,7 @@ execute: | # [docs:stop-docker-end] # [docs:change-base] - sed -i "s/base: ubuntu@24.04/base: bare/g" rockcraft.yaml + sed -i "s/^base: ubuntu@24.04/base: bare/g" rockcraft.yaml # [docs:change-base-end] sed -i "s/version: .*/version: 0.1-chiselled/g" rockcraft.yaml From 63b914d2f6b88408fb04a5286fdbefd24c00d2f1 Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Tue, 14 Jan 2025 02:37:41 +0000 Subject: [PATCH 37/64] docs: documentation improvements --- docs/reference/extensions/expressjs-framework.rst | 10 ++++++---- docs/tutorial/expressjs.rst | 6 +++--- .../templates/expressjs-framework/rockcraft.yaml.j2 | 2 +- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/docs/reference/extensions/expressjs-framework.rst b/docs/reference/extensions/expressjs-framework.rst index 4b8697ed6..795541a5f 100644 --- a/docs/reference/extensions/expressjs-framework.rst +++ b/docs/reference/extensions/expressjs-framework.rst @@ -17,10 +17,12 @@ Node and NPM, inside the rock. Additionally, it transfers your project files to Project requirements ==================== -There is 1 requirement to be able to use the ``expressjs-framework`` extension: +There are 3 requirements to be able to use the ``expressjs-framework`` +extension: -1. There must be a ``package.json`` file in the ``app`` directory of the project - with ``start`` script defined. +1. The application should reside in the ``app`` directory. +2. The application should have a ``package.json`` file. +3. The ``package.json`` file should defined the ``start`` script. ``parts`` > ``expressjs-framework/dependencies:`` > ``stage-packages`` ====================================================================== @@ -57,7 +59,7 @@ example: Some files/directories, if they exist, are included by default. These include: ````, ``app.js``, ``migrate``, ``migrate.sh``, ``migrate.py``, ``bin``, ``public``, ``routes``, ``views``, ``package.json``, -``package-lock.json``. +``package-lock.json``, ``.npmrc``. Useful links ============ diff --git a/docs/tutorial/expressjs.rst b/docs/tutorial/expressjs.rst index 1fa02d626..694293ebc 100644 --- a/docs/tutorial/expressjs.rst +++ b/docs/tutorial/expressjs.rst @@ -109,7 +109,7 @@ the ``.rock`` extension: :end-before: [docs:ls-rock-end] :dedent: 2 -The created rock is about 75MB in size. We will reduce its size later in this +The created rock is about 92MB in size. We will reduce its size later in this tutorial. .. note:: @@ -240,8 +240,8 @@ In ``rockcraft.yaml``, change the ``base`` to ``bare`` and add .. note:: The ``sed`` command replaces the current ``base`` in ``rockcraft.yaml`` with - the ``bare`` base. The command also adds a ``build-base`` which is required - when using the ``bare`` base. + the ``bare`` base. Note that ``build-base`` is also required when using the + ``bare`` base. So that we can compare the size after chiselling, open the ``rockcraft.yaml`` file and change the ``version`` (e.g. to ``0.1-chiselled``). Pack the rock with diff --git a/rockcraft/templates/expressjs-framework/rockcraft.yaml.j2 b/rockcraft/templates/expressjs-framework/rockcraft.yaml.j2 index b7dabdb73..ec5abc16e 100644 --- a/rockcraft/templates/expressjs-framework/rockcraft.yaml.j2 +++ b/rockcraft/templates/expressjs-framework/rockcraft.yaml.j2 @@ -30,7 +30,7 @@ extensions: # expressjs-framework/install-app: # prime: # # by default, only the files in bin/, public/, routes/, views/, app.js, -# # package.json package-lock.json are copied into the image. +# # package.json package-lock.json are copied into the image. # # You can modify the list below to override the default list and # # include or exclude specific files/directories in your project. # # note: prefix each entry with "app/" followed by the local path. From c8d9ce535401949058d8a56ec6a83eb52eacf64e Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Tue, 14 Jan 2025 03:14:20 +0000 Subject: [PATCH 38/64] test: add corner cases --- tests/unit/extensions/test_expressjs.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/tests/unit/extensions/test_expressjs.py b/tests/unit/extensions/test_expressjs.py index 24ddd75a8..07c217170 100644 --- a/tests/unit/extensions/test_expressjs.py +++ b/tests/unit/extensions/test_expressjs.py @@ -173,7 +173,30 @@ def test_expressjs_override_node_version( "lib/node_modules/test-expressjs-project/package.json": "app/package.json", "lib/node_modules/test-expressjs-project/node_modules": "app/node_modules", }, - id="single file defined", + id="single file defined, no missing files", + ), + pytest.param( + ["app/app.js", "app/test.js"], + [], + { + "lib/node_modules/test-expressjs-project/.npmrc": "app/.npmrc", + "lib/node_modules/test-expressjs-project/app.js": "app/app.js", + "lib/node_modules/test-expressjs-project/test.js": "app/test.js", + "lib/node_modules/test-expressjs-project/package.json": "app/package.json", + "lib/node_modules/test-expressjs-project/node_modules": "app/node_modules", + }, + id="multiple files defined, no missing files", + ), + pytest.param( + ["app/app.js"], + ["app/test.js"], + { + "lib/node_modules/test-expressjs-project/.npmrc": "app/.npmrc", + "lib/node_modules/test-expressjs-project/app.js": "app/app.js", + "lib/node_modules/test-expressjs-project/package.json": "app/package.json", + "lib/node_modules/test-expressjs-project/node_modules": "app/node_modules", + }, + id="single file defined, missing test.js file", ), ], ) From c319aa2ff5ffc4bbab3f57cd3feb95a1a32f8aa3 Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Tue, 14 Jan 2025 08:07:25 +0000 Subject: [PATCH 39/64] chore: inline constants & match service def --- rockcraft/extensions/expressjs.py | 67 ++++++++++++------------- tests/unit/extensions/test_expressjs.py | 7 ++- 2 files changed, 34 insertions(+), 40 deletions(-) diff --git a/rockcraft/extensions/expressjs.py b/rockcraft/extensions/expressjs.py index d34d35173..29e2ea9bd 100644 --- a/rockcraft/extensions/expressjs.py +++ b/rockcraft/extensions/expressjs.py @@ -29,26 +29,7 @@ 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", - ] - EXPRESS_PACKAGE_DIRS = [ - "package.json", - "package-lock.json", - ] - BUILD_GENERATED_DIRS = ["node_modules", ".npmrc"] - RUNTIME_SLICES = ["ca-certificates_data"] - BARE_RUNTIME_SLICES = [ - "bash_bins", - "coreutils_bins", - "libc6_libs", - "libnode109_libs", - ] + IMAGE_BASE_DIR = "app" @staticmethod @override @@ -78,16 +59,16 @@ def get_root_snippet(self) -> dict[str, Any]: snippet: dict[str, Any] = { "run-user": "_daemon_", "services": { - "app": { + "expressjs": { "override": "replace", - "command": "npm start", "startup": "enabled", - "on-success": "shutdown", - "on-failure": "shutdown", - "working-dir": "/app", + "user": "_daemon_", + "working-dir": f"/{self.IMAGE_BASE_DIR}", } }, } + if not self.yaml_data.get("services", {}).get("expressjs", {}).get("command"): + snippet["services"]["expressjs"]["command"] = "npm start" snippet["parts"] = { "expressjs-framework/install-app": self._gen_install_app_part() @@ -112,9 +93,14 @@ def get_parts_snippet(self) -> dict[str, Any]: def _gen_install_app_part(self) -> dict: """Generate the install app part using NPM plugin.""" - runtime_packages = [*self.RUNTIME_SLICES] + runtime_packages = ["ca-certificates_data"] if self._rock_base == "bare": - runtime_packages += [*self.BARE_RUNTIME_SLICES] + runtime_packages += [ + "bash_bins", + "coreutils_bins", + "libc6_libs", + "libnode109_libs", + ] return { "plugin": "npm", "npm-include-node": True, @@ -174,7 +160,7 @@ def _app_organize(self) -> dict: """ user_prime: list[str] = self._user_install_app_part.get("prime", []) - if not all(re.match(f"-? *{self.IMAGE_BASE_DIR}", p) for p in 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 " f"expressjs-framework/install-app part to start with {self.IMAGE_BASE_DIR}", @@ -182,20 +168,29 @@ def _app_organize(self) -> dict: logpath_report=False, ) if not user_prime: - user_prime = self.EXPRESS_GENERATOR_DIRS + # default paths generated by express-generator + user_prime = [ + "bin", + "public", + "routes", + "views", + "app.js", + ] + project_relative_file_paths = [ - prime_path.removeprefix(self.IMAGE_BASE_DIR) - for prime_path in user_prime + self.EXPRESS_PACKAGE_DIRS + prime_path.removeprefix(f"{self.IMAGE_BASE_DIR}/") + for prime_path in [*user_prime, "package.json", "package-lock.json"] ] lib_dir = f"lib/node_modules/{self._app_name}" - app_dir = "app" organize_mappings = { - f"{lib_dir}/{f}": f"{app_dir}/{f}" + f"{lib_dir}/{f}": f"{self.IMAGE_BASE_DIR}/{f}" for f in project_relative_file_paths - if (self.project_root / app_dir / f).exists() + if (self.project_root / self.IMAGE_BASE_DIR / f).exists() } - for file_or_dir in self.BUILD_GENERATED_DIRS: - organize_mappings[f"{lib_dir}/{file_or_dir}"] = f"{app_dir}/{file_or_dir}" + for build_artifact in ["node_modules", ".npmrc"]: + organize_mappings[f"{lib_dir}/{build_artifact}"] = ( + f"{self.IMAGE_BASE_DIR}/{build_artifact}" + ) return organize_mappings diff --git a/tests/unit/extensions/test_expressjs.py b/tests/unit/extensions/test_expressjs.py index 07c217170..ae6cb179f 100644 --- a/tests/unit/extensions/test_expressjs.py +++ b/tests/unit/extensions/test_expressjs.py @@ -96,13 +96,12 @@ def test_expressjs_extension_default( }, }, "services": { - "app": { - "command": "npm start", - "on-failure": "shutdown", - "on-success": "shutdown", + "expressjs": { "override": "replace", "startup": "enabled", + "user": "_daemon_", "working-dir": "/app", + "command": "npm start", }, }, } From a3c080a34b2658f8a6ed7e5c96413f20e97e420b Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Tue, 14 Jan 2025 09:16:05 +0000 Subject: [PATCH 40/64] chore: update template guildeline --- rockcraft/templates/expressjs-framework/rockcraft.yaml.j2 | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/rockcraft/templates/expressjs-framework/rockcraft.yaml.j2 b/rockcraft/templates/expressjs-framework/rockcraft.yaml.j2 index ec5abc16e..e39e7f412 100644 --- a/rockcraft/templates/expressjs-framework/rockcraft.yaml.j2 +++ b/rockcraft/templates/expressjs-framework/rockcraft.yaml.j2 @@ -19,9 +19,8 @@ platforms: # s390x: # to ensure the expressjs-framework extension works properly, your ExpressJS -# application should be inside the app, src or a directory with the project -# name, and have inside the app directory a `app.js`file with the starting -# script defined in the package.json's scripts section under the "run" key. +# application should be inside the app directory and the "start" script defined +# in the package.json's scripts section. extensions: - expressjs-framework From dee32bf291a1470f1fd8ec95651bb8bea7dd3c46 Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Wed, 15 Jan 2025 01:40:05 +0000 Subject: [PATCH 41/64] docs: tutorial docs wording improvements & code highlighting --- docs/tutorial/expressjs.rst | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/tutorial/expressjs.rst b/docs/tutorial/expressjs.rst index 694293ebc..4f86194c0 100644 --- a/docs/tutorial/expressjs.rst +++ b/docs/tutorial/expressjs.rst @@ -304,18 +304,19 @@ Update the ExpressJS application As a final step, let's update our application. For example, we want to add a new ``/time`` endpoint which returns the current time. -Start by opening the ``time.js`` file in a text editor and update the code to +Start by creating the ``time.js`` file in a text editor and update the code to look like the following: .. literalinclude:: code/expressjs/time.js - :language: python + :language: javascript -Place ``time.js`` file into the appropriate ``routes/`` directory. Import the -time route from the the main ``app.js`` file and update the code to look like -the following: +Place ``time.js`` file into the appropriate ``app/routes/`` directory. Import +the time route from the the main ``app.js`` file and update the code to look +like the following: .. literalinclude:: code/expressjs/app.js :language: javascript + :emphasize-lines: 9,25 Notice the addition of timerouter import and the registration of the ``/time`` endpoint. From 24ae4198246bdf8726b02336b1c934d02784a8e1 Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Mon, 20 Jan 2025 08:31:18 +0000 Subject: [PATCH 42/64] feat: expressjs user override node with different bases --- rockcraft/extensions/expressjs.py | 114 +++++++---------- tests/unit/extensions/test_expressjs.py | 158 ++++++++++-------------- 2 files changed, 109 insertions(+), 163 deletions(-) diff --git a/rockcraft/extensions/expressjs.py b/rockcraft/extensions/expressjs.py index 29e2ea9bd..da0dceed4 100644 --- a/rockcraft/extensions/expressjs.py +++ b/rockcraft/extensions/expressjs.py @@ -17,7 +17,6 @@ """An extension for the NodeJS based Javascript application extension.""" import json -import re from typing import Any from overrides import override @@ -71,7 +70,8 @@ def get_root_snippet(self) -> dict[str, Any]: snippet["services"]["expressjs"]["command"] = "npm start" snippet["parts"] = { - "expressjs-framework/install-app": self._gen_install_app_part() + "expressjs-framework/install-app": self._gen_install_app_part(), + "expressjs-framework/runtime": self._gen_runtime_part(), } return snippet @@ -93,32 +93,46 @@ def get_parts_snippet(self) -> dict[str, Any]: def _gen_install_app_part(self) -> dict: """Generate the install app part using NPM plugin.""" - runtime_packages = ["ca-certificates_data"] - if self._rock_base == "bare": - runtime_packages += [ - "bash_bins", - "coreutils_bins", - "libc6_libs", - "libnode109_libs", - ] - return { + install_app_part: dict[str, Any] = { "plugin": "npm", - "npm-include-node": True, - "npm-node-version": self._install_app_node_version, - "source": "app/", - "organize": self._app_organize, + "source": f"{self.IMAGE_BASE_DIR}/", + "build-packages": self._install_app_build_packages, + "stage-packages": self._install_app_stage_packages, "override-build": ( "craftctl default\n" "npm config set script-shell=bash --location project\n" "cp ${CRAFT_PART_BUILD}/.npmrc ${CRAFT_PART_INSTALL}/lib/node_modules/" - f"{self._app_name}/.npmrc" + f"{self._app_name}/.npmrc\n" + f"ln -s /lib/node_modules/{self._app_name} ${{CRAFT_PART_INSTALL}}/app\n" ), - "override-prime": ( - "craftctl default\n" - f"rm -rf ${{CRAFT_PRIME}}/lib/node_modules/{self._app_name}\n" - ), - "stage-packages": runtime_packages, } + if self._user_npm_include_node: + install_app_part["npm-include-node"] = self._user_npm_include_node + install_app_part["npm-node-version"] = self._user_install_app_part.get( + "npm-node-version" + ) + return install_app_part + + @property + def _install_app_build_packages(self) -> list[str]: + """Return the build packages for the install app part.""" + if self._user_npm_include_node: + return [] + return ["nodejs", "npm"] + + @property + def _install_app_stage_packages(self) -> list[str]: + """Return the stage packages for the install app part.""" + if self._rock_base == "bare": + return [ + "bash_bins", + "ca-certificates_data", + "nodejs_bins", + "coreutils_bins", + ] + if not self._user_npm_include_node: + return ["ca-certificates_data", "nodejs_bins"] + return ["ca-certificates_data"] @property def _rock_base(self) -> str: @@ -151,55 +165,17 @@ def _user_install_app_part(self) -> dict: ) @property - def _app_organize(self) -> dict: - """Return the organized 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._user_install_app_part.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: - # default paths generated by express-generator - user_prime = [ - "bin", - "public", - "routes", - "views", - "app.js", - ] - - project_relative_file_paths = [ - prime_path.removeprefix(f"{self.IMAGE_BASE_DIR}/") - for prime_path in [*user_prime, "package.json", "package-lock.json"] - ] - lib_dir = f"lib/node_modules/{self._app_name}" - organize_mappings = { - f"{lib_dir}/{f}": f"{self.IMAGE_BASE_DIR}/{f}" - for f in project_relative_file_paths - if (self.project_root / self.IMAGE_BASE_DIR / f).exists() + def _user_npm_include_node(self) -> bool: + """Return the user defined npm include node flag.""" + return self._user_install_app_part.get("npm-include-node", False) + + def _gen_runtime_part(self) -> dict: + """Generate the runtime part.""" + runtime_part: dict[str, Any] = { + "plugin": "nil", + "stage-packages": [] if self._user_npm_include_node else ["npm"], } - for build_artifact in ["node_modules", ".npmrc"]: - organize_mappings[f"{lib_dir}/{build_artifact}"] = ( - f"{self.IMAGE_BASE_DIR}/{build_artifact}" - ) - - return organize_mappings - - @property - def _install_app_node_version(self) -> str: - node_version = self._user_install_app_part.get("npm-node-version", "node") - if not node_version: - return "node" - return node_version + return runtime_part def _check_project(self) -> None: """Ensure this extension can apply to the current rockcraft project. diff --git a/tests/unit/extensions/test_expressjs.py b/tests/unit/extensions/test_expressjs.py index ae6cb179f..ebf4caf7d 100644 --- a/tests/unit/extensions/test_expressjs.py +++ b/tests/unit/extensions/test_expressjs.py @@ -23,6 +23,7 @@ def expressjs_input_yaml_fixture(): return { "name": "foo-bar", "base": "ubuntu@24.04", + "build-base": "ubuntu@24.04", "platforms": {"amd64": {}}, "extensions": ["expressjs-framework"], } @@ -66,6 +67,7 @@ def test_expressjs_extension_default( assert applied == { "base": "ubuntu@24.04", + "build-base": "ubuntu@24.04", "name": "foo-bar", "platforms": { "amd64": {}, @@ -74,26 +76,19 @@ def test_expressjs_extension_default( "parts": { "expressjs-framework/install-app": { "plugin": "npm", - "npm-node-version": "node", - "npm-include-node": True, "source": "app/", - "organize": { - f"lib/node_modules/{expressjs_project_name}/package.json": "app/package.json", - f"lib/node_modules/{expressjs_project_name}/node_modules": "app/node_modules", - f"lib/node_modules/{expressjs_project_name}/.npmrc": "app/.npmrc", - }, "override-build": ( "craftctl default\n" "npm config set script-shell=bash --location project\n" "cp ${CRAFT_PART_BUILD}/.npmrc ${CRAFT_PART_INSTALL}/lib/node_modules/" - f"{expressjs_project_name}/.npmrc" + f"{expressjs_project_name}/.npmrc\n" + f"ln -s /lib/node_modules/{expressjs_project_name} " + "${CRAFT_PART_INSTALL}/app\n" ), - "override-prime": ( - "craftctl default\n" - f"rm -rf ${{CRAFT_PRIME}}/lib/node_modules/{expressjs_project_name}\n" - ), - "stage-packages": ["ca-certificates_data"], + "build-packages": ["nodejs", "npm"], + "stage-packages": ["ca-certificates_data", "nodejs_bins"], }, + "expressjs-framework/runtime": {"plugin": "nil", "stage-packages": ["npm"]}, }, "services": { "expressjs": { @@ -135,112 +130,87 @@ def test_expressjs_invalid_package_json_scripts_error( @pytest.mark.parametrize( - "input_version, expected_version", + "base, expected_build_packages, expected_stage_packages", [ - pytest.param("20.12.2", "20.12.2", id="exact version"), - pytest.param("20.12", "20.12", id="major minor version"), - pytest.param("20", "20", id="major version"), - pytest.param("lts/iron", "lts/iron", id="LTS code name"), - pytest.param("node", "node", id="latest mainline"), - pytest.param("", "node", id="default"), + pytest.param( + "ubuntu@24.04", + ["nodejs", "npm"], + ["ca-certificates_data", "nodejs_bins"], + id="ubuntu@24.04", + ), + pytest.param( + "bare", + ["nodejs", "npm"], + ["bash_bins", "ca-certificates_data", "nodejs_bins", "coreutils_bins"], + id="bare", + ), ], ) @pytest.mark.usefixtures("expressjs_extension", "package_json_file") -def test_expressjs_override_node_version( - tmp_path, expressjs_input_yaml, input_version, expected_version +def test_expressjs_install_app_default_node_install( + tmp_path, + expressjs_input_yaml, + base, + expected_build_packages, + expected_stage_packages, ): - expressjs_input_yaml["parts"] = { - "expressjs-framework/install-app": {"npm-node-version": input_version} - } - print(expressjs_input_yaml) + expressjs_input_yaml["base"] = base applied = extensions.apply_extensions(tmp_path, expressjs_input_yaml) - assert ( - applied["parts"]["expressjs-framework/install-app"]["npm-node-version"] - == expected_version + + assert applied["parts"]["expressjs-framework/install-app"]["build-packages"] == ( + expected_build_packages + ) + assert applied["parts"]["expressjs-framework/install-app"]["stage-packages"] == ( + expected_stage_packages ) @pytest.mark.parametrize( - "existing_files, missing_files, expected_organize", + "base, expected_stage_packages", [ pytest.param( - ["app/app.js"], - [], - { - "lib/node_modules/test-expressjs-project/.npmrc": "app/.npmrc", - "lib/node_modules/test-expressjs-project/app.js": "app/app.js", - "lib/node_modules/test-expressjs-project/package.json": "app/package.json", - "lib/node_modules/test-expressjs-project/node_modules": "app/node_modules", - }, - id="single file defined, no missing files", + "ubuntu@24.04", + ["ca-certificates_data"], + id="24.04 base", ), pytest.param( - ["app/app.js", "app/test.js"], - [], - { - "lib/node_modules/test-expressjs-project/.npmrc": "app/.npmrc", - "lib/node_modules/test-expressjs-project/app.js": "app/app.js", - "lib/node_modules/test-expressjs-project/test.js": "app/test.js", - "lib/node_modules/test-expressjs-project/package.json": "app/package.json", - "lib/node_modules/test-expressjs-project/node_modules": "app/node_modules", - }, - id="multiple files defined, no missing files", - ), - pytest.param( - ["app/app.js"], - ["app/test.js"], - { - "lib/node_modules/test-expressjs-project/.npmrc": "app/.npmrc", - "lib/node_modules/test-expressjs-project/app.js": "app/app.js", - "lib/node_modules/test-expressjs-project/package.json": "app/package.json", - "lib/node_modules/test-expressjs-project/node_modules": "app/node_modules", - }, - id="single file defined, missing test.js file", + "bare", + ["bash_bins", "ca-certificates_data", "nodejs_bins", "coreutils_bins"], + id="bare base", ), ], ) @pytest.mark.usefixtures("expressjs_extension", "package_json_file") -def test_expressjs_install_app_prime_to_organize_map( - tmp_path, expressjs_input_yaml, existing_files, missing_files, expected_organize +def test_expressjs_install_app_user_defined_node_install( + tmp_path, + expressjs_input_yaml, + base, + expected_stage_packages, ): - for file in existing_files: - (tmp_path / file).parent.mkdir(parents=True, exist_ok=True) - (tmp_path / file).touch() - prime_files = [*existing_files, *missing_files] + expressjs_input_yaml["base"] = base expressjs_input_yaml["parts"] = { - "expressjs-framework/install-app": {"prime": prime_files} + "expressjs-framework/install-app": { + "npm-include-node": True, + "npm-node-version": "node", + } } applied = extensions.apply_extensions(tmp_path, expressjs_input_yaml) + + assert applied["parts"]["expressjs-framework/install-app"]["build-packages"] == [] assert ( - applied["parts"]["expressjs-framework/install-app"]["organize"] - == expected_organize + applied["parts"]["expressjs-framework/install-app"]["stage-packages"] + == expected_stage_packages ) -@pytest.mark.parametrize( - "rock_base, expected_runtime_packages", - [ - pytest.param( - "bare", - [ - "ca-certificates_data", - "bash_bins", - "coreutils_bins", - "libc6_libs", - "libnode109_libs", - ], - id="bare", - ), - pytest.param("ubuntu@24.04", ["ca-certificates_data"], id="ubuntu@24.04"), - ], -) @pytest.mark.usefixtures("expressjs_extension", "package_json_file") -def test_expressjs_gen_runtime_part( - tmp_path, expressjs_input_yaml, rock_base, expected_runtime_packages -): - expressjs_input_yaml["base"] = rock_base +def test_expressjs_runtime_user_defined_node_install(tmp_path, expressjs_input_yaml): + expressjs_input_yaml["parts"] = { + "expressjs-framework/install-app": { + "npm-include-node": True, + "npm-node-version": "node", + } + } applied = extensions.apply_extensions(tmp_path, expressjs_input_yaml) - assert ( - applied["parts"]["expressjs-framework/install-app"]["stage-packages"] - == expected_runtime_packages - ) + + assert applied["parts"]["expressjs-framework/runtime"]["stage-packages"] == [] From 908581ab1dac375c4936a3c9673fc640df49a6fb Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Mon, 20 Jan 2025 08:38:23 +0000 Subject: [PATCH 43/64] docs: expressjs user-override node version template --- .../expressjs-framework/rockcraft.yaml.j2 | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/rockcraft/templates/expressjs-framework/rockcraft.yaml.j2 b/rockcraft/templates/expressjs-framework/rockcraft.yaml.j2 index e39e7f412..e174651fa 100644 --- a/rockcraft/templates/expressjs-framework/rockcraft.yaml.j2 +++ b/rockcraft/templates/expressjs-framework/rockcraft.yaml.j2 @@ -26,16 +26,12 @@ extensions: # uncomment the sections you need and adjust according to your requirements. # parts: # you need to uncomment this line to add or update any part. -# expressjs-framework/install-app: -# prime: -# # by default, only the files in bin/, public/, routes/, views/, app.js, -# # package.json package-lock.json are copied into the image. -# # You can modify the list below to override the default list and -# # include or exclude specific files/directories in your project. -# # note: prefix each entry with "app/" followed by the local path. -# - app/app -# - app/templates -# - app/static +# expressjs-framework/install-app: +# # to specify the version of node to be installed, uncomment the following +# # line and set the version to the desired one. +# # see https://documentation.ubuntu.com/rockcraft/en/latest/common/craft-parts/reference/plugins/npm_plugin/ +# npm-include-node: true +# npm-node-version: node # you can add package slices or Debian packages to the image. # package slices are subsets of Debian packages, which result @@ -47,6 +43,9 @@ extensions: # expressjs-framework/runtime: # plugin: nil # stage-packages: +# # when using the default provided node version, the npm package is +# # required. +# - npm # # list the required package slices for your expressjs application below. # # for example, for the slice libs of libpq5: # - libpq5_libs From a5423c6e101683a7ce88805f885c9592b74171eb Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Mon, 20 Jan 2025 08:44:01 +0000 Subject: [PATCH 44/64] docs: reference to node-version override --- .../extensions/expressjs-framework.rst | 40 ++++++++----------- 1 file changed, 17 insertions(+), 23 deletions(-) diff --git a/docs/reference/extensions/expressjs-framework.rst b/docs/reference/extensions/expressjs-framework.rst index 795541a5f..f3244e78a 100644 --- a/docs/reference/extensions/expressjs-framework.rst +++ b/docs/reference/extensions/expressjs-framework.rst @@ -24,42 +24,36 @@ extension: 2. The application should have a ``package.json`` file. 3. The ``package.json`` file should defined the ``start`` script. -``parts`` > ``expressjs-framework/dependencies:`` > ``stage-packages`` + +``parts`` > ``expressjs-framework/install-app`` > ``npm-include-node`` ====================================================================== -You can use this key to specify any dependencies required for your ExpressJS -application. In the following example we use it to specify ``libpq-dev``: +You can use this field to specify the version of Node to be installed. For +example: .. code-block:: yaml parts: - expressjs-framework/dependencies: - stage-packages: - # list required packages or slices for your ExpressJS application below. - - libpq-dev + expressjs-framework/install-app: + npm-include-node: true + npm-node-version: node +A corressponding Ubuntu packaged Node version will be provided by default if +not specified. -``parts`` > ``expressjs-framework/install-app`` > ``prime`` -=========================================================== +``parts`` > ``expressjs-framework/runtime:`` > ``stage-packages`` +================================================================= -You can use this field to specify the files to be included or excluded from -your rock upon ``rockcraft pack``. Follow the ``app/`` notation. For -example: +You can use this key to specify any dependencies required for your ExpressJS +application. In the following example we use it to specify ``libpq-dev``: .. code-block:: yaml parts: - expressjs-framework/install-app: - prime: - - app/.env - - app/script.js - - app/templates - - app/static - -Some files/directories, if they exist, are included by default. These include: -````, ``app.js``, ``migrate``, ``migrate.sh``, ``migrate.py``, -``bin``, ``public``, ``routes``, ``views``, ``package.json``, -``package-lock.json``, ``.npmrc``. + expressjs-framework/runtime: + stage-packages: + # list required packages or slices for your ExpressJS application below. + - libpq-dev Useful links ============ From f33666f5866d8fc22bcd18d4a2f731920607b5d6 Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Mon, 20 Jan 2025 08:50:50 +0000 Subject: [PATCH 45/64] docs: update expressjs runtime codeblock to match template --- docs/reference/extensions/expressjs-framework.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/reference/extensions/expressjs-framework.rst b/docs/reference/extensions/expressjs-framework.rst index f3244e78a..b47b65390 100644 --- a/docs/reference/extensions/expressjs-framework.rst +++ b/docs/reference/extensions/expressjs-framework.rst @@ -52,6 +52,9 @@ application. In the following example we use it to specify ``libpq-dev``: parts: expressjs-framework/runtime: stage-packages: + # when using the default provided node version, the npm package is + # required. + # - npm # list required packages or slices for your ExpressJS application below. - libpq-dev From 37fa47c11f4e3b7ff366dc1922d1d727199c4b73 Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Mon, 20 Jan 2025 08:58:16 +0000 Subject: [PATCH 46/64] docs: expressjs tutorial specify run directory --- docs/tutorial/expressjs.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/tutorial/expressjs.rst b/docs/tutorial/expressjs.rst index 4f86194c0..93d05e7e8 100644 --- a/docs/tutorial/expressjs.rst +++ b/docs/tutorial/expressjs.rst @@ -38,8 +38,8 @@ Create the ExpressJS application by running the express-generator. :end-before: [docs:init-app-end] :dedent: 2 -Run the ExpressJS application using ``npm start`` to verify -that it works. +Run the ExpressJS application from within the ``app/`` directory using +``npm start`` to verify that it works. Test the ExpressJS application by using ``curl`` to send a request to the root endpoint. We'll need a new terminal for this -- if we're using Multipass, run @@ -109,7 +109,7 @@ the ``.rock`` extension: :end-before: [docs:ls-rock-end] :dedent: 2 -The created rock is about 92MB in size. We will reduce its size later in this +The created rock is about 100MB in size. We will reduce its size later in this tutorial. .. note:: @@ -147,7 +147,7 @@ size: :class: log-snippets REPOSITORY TAG IMAGE ID CREATED SIZE - expressjs-hello-world 0.1 30c7e5aed202 2 weeks ago 193MB + expressjs-hello-world 0.1 30c7e5aed202 2 weeks ago 304MB .. note:: The size of the image reported by Docker is the uncompressed size which is @@ -261,7 +261,7 @@ As before, verify that the new rock was created: :end-before: [docs:ls-bare-rock-end] :dedent: 2 -We'll verify that the new ExpressJS rock is now approximately **20% smaller** +We'll verify that the new ExpressJS rock is now approximately **15% smaller** in size! And that's just because of the simple change of ``base``. And the functionality is still the same. As before, we can confirm this by From c1c8e2850e324880c99370851f53bcf52969910a Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Mon, 20 Jan 2025 08:58:27 +0000 Subject: [PATCH 47/64] docs: expressjs tutorial fix sumaary --- docs/tutorial/code/expressjs/task.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/code/expressjs/task.yaml b/docs/tutorial/code/expressjs/task.yaml index 5628be503..06c306b00 100644 --- a/docs/tutorial/code/expressjs/task.yaml +++ b/docs/tutorial/code/expressjs/task.yaml @@ -5,7 +5,7 @@ # markers for including said instructions # as snippets in the docs. ########################################### -summary: Getting started with FastAPI tutorial +summary: Getting started with ExpressJS tutorial environment: From 02675c75b5273e39246aa32cc63ad9d099e8e916 Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Mon, 20 Jan 2025 09:16:41 +0000 Subject: [PATCH 48/64] chore: organize expressjs extension code --- rockcraft/extensions/expressjs.py | 115 +++++++++++++++--------------- 1 file changed, 59 insertions(+), 56 deletions(-) diff --git a/rockcraft/extensions/expressjs.py b/rockcraft/extensions/expressjs.py index da0dceed4..b69cbd945 100644 --- a/rockcraft/extensions/expressjs.py +++ b/rockcraft/extensions/expressjs.py @@ -42,6 +42,22 @@ def is_experimental(base: str | None) -> bool: """Check if the extension is in an experimental state.""" return True + @override + def get_part_snippet(self) -> dict[str, Any]: + """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. + + This is unused but is required by the ABC. + """ + return {} + @override def get_root_snippet(self) -> dict[str, Any]: """Fill in some default root components. @@ -63,6 +79,9 @@ def get_root_snippet(self) -> dict[str, Any]: "startup": "enabled", "user": "_daemon_", "working-dir": f"/{self.IMAGE_BASE_DIR}", + "environment": { + "NODE_ENV": "production", + }, } }, } @@ -75,21 +94,28 @@ def get_root_snippet(self) -> dict[str, Any]: } return snippet - @override - def get_part_snippet(self) -> dict[str, Any]: - """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. + def _check_project(self) -> None: + """Ensure this extension can apply to the current rockcraft project. - This is unused but is required by the ABC. + The ExpressJS framework assumes that: + - The npm start script exists. + - The application name is defined. """ - return {} + if ( + "scripts" not in self._app_package_json + or "start" not in self._app_package_json["scripts"] + ): + 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, + ) def _gen_install_app_part(self) -> dict: """Generate the install app part using NPM plugin.""" @@ -134,6 +160,26 @@ def _install_app_stage_packages(self) -> list[str]: return ["ca-certificates_data", "nodejs_bins"] return ["ca-certificates_data"] + def _gen_runtime_part(self) -> dict: + """Generate the runtime part.""" + runtime_part: dict[str, Any] = { + "plugin": "nil", + "stage-packages": [] if self._user_npm_include_node else ["npm"], + } + return runtime_part + + @property + def _user_install_app_part(self) -> dict: + """Return the user defined install app part.""" + return self.yaml_data.get("parts", {}).get( + "expressjs-framework/install-app", {} + ) + + @property + def _user_npm_include_node(self) -> bool: + """Return the user defined npm include node flag.""" + return self._user_install_app_part.get("npm-include-node", False) + @property def _rock_base(self) -> str: """Return the base of the rockcraft project.""" @@ -156,46 +202,3 @@ def _app_package_json(self) -> dict: def _app_name(self) -> str: """Return the application name as defined on package.json.""" return self._app_package_json["name"] - - @property - def _user_install_app_part(self) -> dict: - """Return the user defined install app part.""" - return self.yaml_data.get("parts", {}).get( - "expressjs-framework/install-app", {} - ) - - @property - def _user_npm_include_node(self) -> bool: - """Return the user defined npm include node flag.""" - return self._user_install_app_part.get("npm-include-node", False) - - def _gen_runtime_part(self) -> dict: - """Generate the runtime part.""" - runtime_part: dict[str, Any] = { - "plugin": "nil", - "stage-packages": [] if self._user_npm_include_node else ["npm"], - } - return runtime_part - - def _check_project(self) -> None: - """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"] - ): - 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 c3af5c2c704c9487628889d286eaabfe1d0c1712 Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Mon, 20 Jan 2025 09:21:13 +0000 Subject: [PATCH 49/64] test: set node env to production --- tests/unit/extensions/test_expressjs.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/extensions/test_expressjs.py b/tests/unit/extensions/test_expressjs.py index ebf4caf7d..edbaf04f6 100644 --- a/tests/unit/extensions/test_expressjs.py +++ b/tests/unit/extensions/test_expressjs.py @@ -97,6 +97,7 @@ def test_expressjs_extension_default( "user": "_daemon_", "working-dir": "/app", "command": "npm start", + "environment": {"NODE_ENV": "production"}, }, }, } From fd934b9780d9d33b1dc0dc3f8a7d61fe90f8c574 Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Tue, 21 Jan 2025 07:50:54 +0000 Subject: [PATCH 50/64] chore: remove user custom packages part --- .../expressjs-framework/rockcraft.yaml.j2 | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/rockcraft/templates/expressjs-framework/rockcraft.yaml.j2 b/rockcraft/templates/expressjs-framework/rockcraft.yaml.j2 index e174651fa..9fad77a70 100644 --- a/rockcraft/templates/expressjs-framework/rockcraft.yaml.j2 +++ b/rockcraft/templates/expressjs-framework/rockcraft.yaml.j2 @@ -32,20 +32,3 @@ extensions: # # see https://documentation.ubuntu.com/rockcraft/en/latest/common/craft-parts/reference/plugins/npm_plugin/ # npm-include-node: true # npm-node-version: node - -# you can add package slices or Debian packages to the image. -# package slices are subsets of Debian packages, which result -# in smaller and more secure images. -# see https://documentation.ubuntu.com/rockcraft/en/latest/explanation/chisel/ - -# add this part if you want to add packages slices to your image. -# you can find a list of packages slices at https://github.com/canonical/chisel-releases -# expressjs-framework/runtime: -# plugin: nil -# stage-packages: -# # when using the default provided node version, the npm package is -# # required. -# - npm -# # list the required package slices for your expressjs application below. -# # for example, for the slice libs of libpq5: -# - libpq5_libs From e3ea01b8044b0aa082552e4ae1acc4ac2fda6895 Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Tue, 21 Jan 2025 07:55:57 +0000 Subject: [PATCH 51/64] chore: remove empty parts from rock gen --- rockcraft/extensions/expressjs.py | 21 ++++++++++++++------- tests/unit/extensions/test_expressjs.py | 4 ++-- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/rockcraft/extensions/expressjs.py b/rockcraft/extensions/expressjs.py index b69cbd945..a6f73d55f 100644 --- a/rockcraft/extensions/expressjs.py +++ b/rockcraft/extensions/expressjs.py @@ -90,8 +90,10 @@ def get_root_snippet(self) -> dict[str, Any]: snippet["parts"] = { "expressjs-framework/install-app": self._gen_install_app_part(), - "expressjs-framework/runtime": self._gen_runtime_part(), } + runtime_part = self._gen_runtime_part() + if runtime_part: + snippet["parts"]["expressjs-framework/runtime"] = runtime_part return snippet def _check_project(self) -> None: @@ -122,8 +124,6 @@ def _gen_install_app_part(self) -> dict: install_app_part: dict[str, Any] = { "plugin": "npm", "source": f"{self.IMAGE_BASE_DIR}/", - "build-packages": self._install_app_build_packages, - "stage-packages": self._install_app_stage_packages, "override-build": ( "craftctl default\n" "npm config set script-shell=bash --location project\n" @@ -132,6 +132,12 @@ def _gen_install_app_part(self) -> dict: f"ln -s /lib/node_modules/{self._app_name} ${{CRAFT_PART_INSTALL}}/app\n" ), } + build_packages = self._install_app_build_packages + if build_packages: + install_app_part["build-packages"] = build_packages + stage_packages = self._install_app_stage_packages + if stage_packages: + install_app_part["stage-packages"] = stage_packages if self._user_npm_include_node: install_app_part["npm-include-node"] = self._user_npm_include_node install_app_part["npm-node-version"] = self._user_install_app_part.get( @@ -160,13 +166,14 @@ def _install_app_stage_packages(self) -> list[str]: return ["ca-certificates_data", "nodejs_bins"] return ["ca-certificates_data"] - def _gen_runtime_part(self) -> dict: + def _gen_runtime_part(self) -> dict | None: """Generate the runtime part.""" - runtime_part: dict[str, Any] = { + if self._user_npm_include_node: + return None + return { "plugin": "nil", - "stage-packages": [] if self._user_npm_include_node else ["npm"], + "stage-packages": ["npm"], } - return runtime_part @property def _user_install_app_part(self) -> dict: diff --git a/tests/unit/extensions/test_expressjs.py b/tests/unit/extensions/test_expressjs.py index edbaf04f6..ec9820dfb 100644 --- a/tests/unit/extensions/test_expressjs.py +++ b/tests/unit/extensions/test_expressjs.py @@ -197,7 +197,7 @@ def test_expressjs_install_app_user_defined_node_install( } applied = extensions.apply_extensions(tmp_path, expressjs_input_yaml) - assert applied["parts"]["expressjs-framework/install-app"]["build-packages"] == [] + assert "build-packages" not in applied["parts"]["expressjs-framework/install-app"] assert ( applied["parts"]["expressjs-framework/install-app"]["stage-packages"] == expected_stage_packages @@ -214,4 +214,4 @@ def test_expressjs_runtime_user_defined_node_install(tmp_path, expressjs_input_y } applied = extensions.apply_extensions(tmp_path, expressjs_input_yaml) - assert applied["parts"]["expressjs-framework/runtime"]["stage-packages"] == [] + assert "expressjs-framework/runtime" not in applied["parts"] From 35897570dac3bc9be6a7bcf8239e0c2b9b98f1a8 Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Wed, 22 Jan 2025 03:19:05 +0000 Subject: [PATCH 52/64] docs: re-order framework extensions list --- docs/reference/extensions/index.rst | 2 +- docs/reference/rockcraft.yaml.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reference/extensions/index.rst b/docs/reference/extensions/index.rst index 74f405d56..73a46dcce 100644 --- a/docs/reference/extensions/index.rst +++ b/docs/reference/extensions/index.rst @@ -11,8 +11,8 @@ initiating a new rock. .. toctree:: :maxdepth: 1 - expressjs-framework flask-framework django-framework fastapi-framework go-framework + expressjs-framework diff --git a/docs/reference/rockcraft.yaml.rst b/docs/reference/rockcraft.yaml.rst index d9bdf8959..8a6ed18d6 100644 --- a/docs/reference/rockcraft.yaml.rst +++ b/docs/reference/rockcraft.yaml.rst @@ -262,11 +262,11 @@ Extensions to enable when building the ROCK. Currently supported extensions: -- ``expressjs-framework`` - ``flask-framework`` - ``django-framework`` - ``go-framework`` - ``fastapi-framework`` +- ``expressjs-framework`` Example ======= From 752ce93172b3c866deee4704bd8109c5b0a80a96 Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Wed, 22 Jan 2025 03:19:30 +0000 Subject: [PATCH 53/64] docs: remove customizing runtime deps part --- .../extensions/expressjs-framework.rst | 25 +++++++------------ 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/docs/reference/extensions/expressjs-framework.rst b/docs/reference/extensions/expressjs-framework.rst index b47b65390..b1a1d4384 100644 --- a/docs/reference/extensions/expressjs-framework.rst +++ b/docs/reference/extensions/expressjs-framework.rst @@ -36,27 +36,20 @@ example: parts: expressjs-framework/install-app: npm-include-node: true - npm-node-version: node + npm-node-version: 20.12.2 -A corressponding Ubuntu packaged Node version will be provided by default if -not specified. +For more examples of npm-node-version options, see: +https://documentation.ubuntu.com/rockcraft/en/1.5.3/common/craft-parts/reference/plugins/npm_plugin/#examples + +If you don't customise the version of node, it will be installed from the Ubuntu +package repository. ``parts`` > ``expressjs-framework/runtime:`` > ``stage-packages`` ================================================================= -You can use this key to specify any dependencies required for your ExpressJS -application. In the following example we use it to specify ``libpq-dev``: - -.. code-block:: yaml - - parts: - expressjs-framework/runtime: - stage-packages: - # when using the default provided node version, the npm package is - # required. - # - npm - # list required packages or slices for your ExpressJS application below. - - libpq-dev +Customizing additional runtime dependencies is not supported at the moment due +to an issue with the +`NPM plugin `_. Useful links ============ From c14591953cef3a39bafdfa353f38c7816756ea5d Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Wed, 22 Jan 2025 03:24:52 +0000 Subject: [PATCH 54/64] docs: explain unsupported runtime dependencies issue --- .../extensions/expressjs-framework.rst | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/docs/reference/extensions/expressjs-framework.rst b/docs/reference/extensions/expressjs-framework.rst index b1a1d4384..efe25baae 100644 --- a/docs/reference/extensions/expressjs-framework.rst +++ b/docs/reference/extensions/expressjs-framework.rst @@ -47,9 +47,23 @@ package repository. ``parts`` > ``expressjs-framework/runtime:`` > ``stage-packages`` ================================================================= -Customizing additional runtime dependencies is not supported at the moment due -to an issue with the -`NPM plugin `_. +You can use this key to specify any dependencies required for your ExpressJS +application. In the following example we use it to specify ``libpq-dev``: + +.. code-block:: yaml + + parts: + expressjs-framework/runtime: + stage-packages: + # when using the default provided node version, the npm package is + # required. + # - npm + # list required packages or slices for your ExpressJS application below. + - libpq-dev + +When using the NPM plugin installed Node and NPM, additional runtime packages +is currently unsupported due to an issue with ``lib`` path permission error. See +https://github.com/canonical/rockcraft/issues/790 for more information. Useful links ============ From a90e80b0e75afd1a9a2392c72f7a93738da12434 Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Wed, 22 Jan 2025 03:54:35 +0000 Subject: [PATCH 55/64] docs: expressjs explaination of different combinations --- .../extensions/expressjs-framework.rst | 100 ++++++++++++++++++ docs/explanation/extensions/index.rst | 12 +++ docs/explanation/index.rst | 1 + 3 files changed, 113 insertions(+) create mode 100644 docs/explanation/extensions/expressjs-framework.rst create mode 100644 docs/explanation/extensions/index.rst diff --git a/docs/explanation/extensions/expressjs-framework.rst b/docs/explanation/extensions/expressjs-framework.rst new file mode 100644 index 000000000..b670d84c5 --- /dev/null +++ b/docs/explanation/extensions/expressjs-framework.rst @@ -0,0 +1,100 @@ +.. _expressjs-framework-explanation: + +expressjs-framework +=================== + +When using the expressjs-framework extension, there are four different cases for +customizing the Ubuntu base and the Node version to be included. +The main difference is +- whether the bare base is used or the Ubuntu 24.04 base is used. +- whether the Node is installed from Ubuntu packages or the NPM plugin. + +Ubuntu base and Node combinations +--------------------------------- + +Bare base and Node from Ubuntu packages +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: yaml + + base: bare + build-base: ubuntu@24.04 + parts: + expressjs-framework/install-app: + plugin: npm + npm-include-node: false + build-packages: + - nodejs + - npm + stage-packages: + - bash_bins + - coreutils_bins + - nodejs_bins + expressjs-framework/runtime: + plugin: nil + stage-packages: + - npm + +In this case, the ``npm`` package is installed in a separate ``expressjs-framework/runtime`` part. +This is due to ``expressjs-framework/install-app > stage-packages`` part only being able to install +slices rather than packages as a design choice of Rockcraft. See the comment +https://github.com/canonical/rockcraft/issues/785#issuecomment-2572990545 for more explanation. + +Bare base and Node from NPM plugin +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: yaml + + base: bare + build-base: ubuntu@24.04 + parts: + expressjs-framework/install-app: + plugin: npm + npm-include-node: true + npm-node-version: 20.12 + stage-packages: + - bash_bins + - coreutils_bins + - nodejs_bins + +In this case, the ``expressjs-framework/install-app > build-packages`` part is empty. The +application is is installed using Node and NPM installed by the NPM plugin. The application is run +using the NPM installed by the NPM plugin. + +24.04 base and Node from Ubuntu packages +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: yaml + + base: ubuntu@24.04 + parts: + expressjs-framework/install-app: + plugin: npm + npm-include-node: false + build-packages: + - nodejs + - npm + stage-packages: + - nodejs_bins + expressjs-framework/runtime: + plugin: nil + stage-packages: + - npm + +In this case, the ``expressjs-framework/install-app > stage-packages`` part does not include the +``bash_bins`` and ``coreutils_bins`` slices as they are already included in the Ubuntu 24.04 base. +The application is built and installed using Node and NPM from the Ubuntu packages. + +24.04 base and Node from NPM plugin +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: yaml + + base: ubuntu@24.04 + parts: + expressjs-framework/install-app: + plugin: npm + npm-include-node: true + npm-node-version: 20.12 + +In this case, the application is installed and run via Node and NPM installed by the NPM plugin. diff --git a/docs/explanation/extensions/index.rst b/docs/explanation/extensions/index.rst new file mode 100644 index 000000000..ed980ca49 --- /dev/null +++ b/docs/explanation/extensions/index.rst @@ -0,0 +1,12 @@ +.. _extensions: + +Extensions +********** + +This section of the documentation covers the concepts used by Rockcraft +extensions and the decisions behind its development. + +.. toctree:: + :maxdepth: 1 + + expressjs-framework diff --git a/docs/explanation/index.rst b/docs/explanation/index.rst index 6e3726810..8ff55e307 100644 --- a/docs/explanation/index.rst +++ b/docs/explanation/index.rst @@ -21,3 +21,4 @@ the motivations behind its development. /common/craft-parts/explanation/parts lifecycle /common/craft-parts/explanation/dump_plugin + Extensions From 81c4abbc195f9ad9ef93bc9074d987426c2760df Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Thu, 23 Jan 2025 02:52:23 +0000 Subject: [PATCH 56/64] docs: pin tutorial express generator version --- docs/tutorial/code/expressjs/task.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/code/expressjs/task.yaml b/docs/tutorial/code/expressjs/task.yaml index 06c306b00..96d2bf5a1 100644 --- a/docs/tutorial/code/expressjs/task.yaml +++ b/docs/tutorial/code/expressjs/task.yaml @@ -12,7 +12,7 @@ environment: execute: | # [docs:init-app] sudo apt-get update -y && sudo apt-get install npm -y - npm install -g express-generator + npm install -g express-generator@4.16 echo "y" | npx express-generator app cd app && npm install # [docs:init-app-end] From eca73e38e80ae3e718682e708abb87a2ea21d854 Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Thu, 23 Jan 2025 02:52:47 +0000 Subject: [PATCH 57/64] docs: reword parts --- docs/explanation/extensions/index.rst | 6 +++--- docs/tutorial/expressjs.rst | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/explanation/extensions/index.rst b/docs/explanation/extensions/index.rst index ed980ca49..64c164180 100644 --- a/docs/explanation/extensions/index.rst +++ b/docs/explanation/extensions/index.rst @@ -1,7 +1,7 @@ -.. _extensions: +.. _extensions_explanation: -Extensions -********** +Extensions Explanation +********************** This section of the documentation covers the concepts used by Rockcraft extensions and the decisions behind its development. diff --git a/docs/tutorial/expressjs.rst b/docs/tutorial/expressjs.rst index 93d05e7e8..58b7f5501 100644 --- a/docs/tutorial/expressjs.rst +++ b/docs/tutorial/expressjs.rst @@ -31,7 +31,6 @@ throughout this tutorial. Create the ExpressJS application by running the express-generator. - .. literalinclude:: code/expressjs/task.yaml :language: bash :start-after: [docs:init-app] From da5b5fe6e21678de6aeefd02c42705547a655429 Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Thu, 23 Jan 2025 02:59:27 +0000 Subject: [PATCH 58/64] docs: formatting & spelling --- docs/explanation/extensions/expressjs-framework.rst | 2 +- docs/reference/extensions/expressjs-framework.rst | 6 +++--- docs/tutorial/expressjs.rst | 8 +++++--- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/explanation/extensions/expressjs-framework.rst b/docs/explanation/extensions/expressjs-framework.rst index b670d84c5..ab5c327ff 100644 --- a/docs/explanation/extensions/expressjs-framework.rst +++ b/docs/explanation/extensions/expressjs-framework.rst @@ -4,7 +4,7 @@ expressjs-framework =================== When using the expressjs-framework extension, there are four different cases for -customizing the Ubuntu base and the Node version to be included. +customising the Ubuntu base and the Node version to be included. The main difference is - whether the bare base is used or the Ubuntu 24.04 base is used. - whether the Node is installed from Ubuntu packages or the NPM plugin. diff --git a/docs/reference/extensions/expressjs-framework.rst b/docs/reference/extensions/expressjs-framework.rst index efe25baae..77db41b6d 100644 --- a/docs/reference/extensions/expressjs-framework.rst +++ b/docs/reference/extensions/expressjs-framework.rst @@ -38,8 +38,7 @@ example: npm-include-node: true npm-node-version: 20.12.2 -For more examples of npm-node-version options, see: -https://documentation.ubuntu.com/rockcraft/en/1.5.3/common/craft-parts/reference/plugins/npm_plugin/#examples +For more examples of npm-node-version options, see: https://documentation.ubuntu.com/rockcraft/en/1.5.3/common/craft-parts/reference/plugins/npm_plugin/#examples If you don't customise the version of node, it will be installed from the Ubuntu package repository. @@ -63,7 +62,8 @@ application. In the following example we use it to specify ``libpq-dev``: When using the NPM plugin installed Node and NPM, additional runtime packages is currently unsupported due to an issue with ``lib`` path permission error. See -https://github.com/canonical/rockcraft/issues/790 for more information. +[Issue #790](https://github.com/canonical/rockcraft/issues/790) for more +information. Useful links ============ diff --git a/docs/tutorial/expressjs.rst b/docs/tutorial/expressjs.rst index 58b7f5501..98ec7a05c 100644 --- a/docs/tutorial/expressjs.rst +++ b/docs/tutorial/expressjs.rst @@ -12,9 +12,11 @@ Setup .. include:: /reuse/tutorial/setup.rst -Before we go any further, for this tutorial we'll need the most recent version -of Rockcraft on the edge channel. Run ``sudo snap refresh rockcraft --channel -latest/edge`` to switch to it. +.. important:: + + Before we go any further, for this tutorial we'll need the most recent version + of Rockcraft on the edge channel. Run ``sudo snap refresh rockcraft --channel + latest/edge`` to switch to it. Finally, create a new directory for this tutorial and go inside it: From 4128d2d369875944839f9530efd3d6c632a06ea8 Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Thu, 23 Jan 2025 03:39:41 +0000 Subject: [PATCH 59/64] docs: fix linting --- .../extensions/expressjs-framework.rst | 26 +++++++++++-------- .../extensions/expressjs-framework.rst | 4 ++- docs/tutorial/expressjs.rst | 6 ++--- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/docs/explanation/extensions/expressjs-framework.rst b/docs/explanation/extensions/expressjs-framework.rst index ab5c327ff..e7f56d614 100644 --- a/docs/explanation/extensions/expressjs-framework.rst +++ b/docs/explanation/extensions/expressjs-framework.rst @@ -35,10 +35,12 @@ Bare base and Node from Ubuntu packages stage-packages: - npm -In this case, the ``npm`` package is installed in a separate ``expressjs-framework/runtime`` part. -This is due to ``expressjs-framework/install-app > stage-packages`` part only being able to install -slices rather than packages as a design choice of Rockcraft. See the comment -https://github.com/canonical/rockcraft/issues/785#issuecomment-2572990545 for more explanation. +In this case, the ``npm`` package is installed in a separate +``expressjs-framework/runtime`` part. This is due to +``expressjs-framework/install-app > stage-packages`` part only being able to +install slices rather than packages as a design choice of Rockcraft. See the +[issue comment](https://github.com/canonical/rockcraft/issues/785#issuecomment\ +-2572990545) for more explanation. Bare base and Node from NPM plugin ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -57,9 +59,9 @@ Bare base and Node from NPM plugin - coreutils_bins - nodejs_bins -In this case, the ``expressjs-framework/install-app > build-packages`` part is empty. The -application is is installed using Node and NPM installed by the NPM plugin. The application is run -using the NPM installed by the NPM plugin. +In this case, the ``expressjs-framework/install-app > build-packages`` part is +empty. The application is is installed using Node and NPM installed by the NPM +plugin. The application is run using the NPM installed by the NPM plugin. 24.04 base and Node from Ubuntu packages ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -81,9 +83,10 @@ using the NPM installed by the NPM plugin. stage-packages: - npm -In this case, the ``expressjs-framework/install-app > stage-packages`` part does not include the -``bash_bins`` and ``coreutils_bins`` slices as they are already included in the Ubuntu 24.04 base. -The application is built and installed using Node and NPM from the Ubuntu packages. +In this case, the ``expressjs-framework/install-app > stage-packages`` part does +not include the ``bash_bins`` and ``coreutils_bins`` slices as they are already +included in the Ubuntu 24.04 base. The application is built and installed using +Node and NPM from the Ubuntu packages. 24.04 base and Node from NPM plugin ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -97,4 +100,5 @@ The application is built and installed using Node and NPM from the Ubuntu packag npm-include-node: true npm-node-version: 20.12 -In this case, the application is installed and run via Node and NPM installed by the NPM plugin. +In this case, the application is installed and run via Node and NPM installed by +the NPM plugin. diff --git a/docs/reference/extensions/expressjs-framework.rst b/docs/reference/extensions/expressjs-framework.rst index 77db41b6d..b41395d49 100644 --- a/docs/reference/extensions/expressjs-framework.rst +++ b/docs/reference/extensions/expressjs-framework.rst @@ -38,7 +38,9 @@ example: npm-include-node: true npm-node-version: 20.12.2 -For more examples of npm-node-version options, see: https://documentation.ubuntu.com/rockcraft/en/1.5.3/common/craft-parts/reference/plugins/npm_plugin/#examples +For more examples of npm-node-version options, see: https://documentation.\ +ubuntu.com/rockcraft/en/1.5.3/common/craft-parts/reference/plugins/npm_plugin/\ +#examples If you don't customise the version of node, it will be installed from the Ubuntu package repository. diff --git a/docs/tutorial/expressjs.rst b/docs/tutorial/expressjs.rst index 98ec7a05c..06fb241eb 100644 --- a/docs/tutorial/expressjs.rst +++ b/docs/tutorial/expressjs.rst @@ -14,9 +14,9 @@ Setup .. important:: - Before we go any further, for this tutorial we'll need the most recent version - of Rockcraft on the edge channel. Run ``sudo snap refresh rockcraft --channel - latest/edge`` to switch to it. + Before we go any further, for this tutorial we'll need the most recent + version of Rockcraft on the edge channel. Run ``sudo snap refresh rockcraft + --channel latest/edge`` to switch to it. Finally, create a new directory for this tutorial and go inside it: From 05313618897ca40d9fbf851aac61aad38c58de4e Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Fri, 24 Jan 2025 07:23:16 +0000 Subject: [PATCH 60/64] docs: update tutorial documentation --- .../extensions/expressjs-framework.rst | 126 +++++++++--------- .../extensions/expressjs-framework.rst | 20 +-- docs/tutorial/code/expressjs/app.js | 43 ------ docs/tutorial/code/expressjs/task.yaml | 16 ++- docs/tutorial/code/expressjs/time_app.js | 2 + docs/tutorial/expressjs.rst | 20 ++- 6 files changed, 83 insertions(+), 144 deletions(-) delete mode 100644 docs/tutorial/code/expressjs/app.js create mode 100644 docs/tutorial/code/expressjs/time_app.js diff --git a/docs/explanation/extensions/expressjs-framework.rst b/docs/explanation/extensions/expressjs-framework.rst index e7f56d614..3be32dec9 100644 --- a/docs/explanation/extensions/expressjs-framework.rst +++ b/docs/explanation/extensions/expressjs-framework.rst @@ -3,102 +3,98 @@ expressjs-framework =================== -When using the expressjs-framework extension, there are four different cases for -customising the Ubuntu base and the Node version to be included. -The main difference is -- whether the bare base is used or the Ubuntu 24.04 base is used. -- whether the Node is installed from Ubuntu packages or the NPM plugin. +When using the ``expressjs-framework`` extension, there are four different cases +for customising the Ubuntu base and the Node version to be included. +The main differences are +* whether the bare base is used or the Ubuntu 24.04 base is used. +* whether the Node is installed from Ubuntu packages or the NPM plugin. Ubuntu base and Node combinations --------------------------------- -Bare base and Node from Ubuntu packages -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. code-block:: yaml +In this section of the document, we will discuss the possible combinations of +Ubuntu bases and possible usages of NPM plugin. - base: bare - build-base: ubuntu@24.04 - parts: - expressjs-framework/install-app: - plugin: npm - npm-include-node: false - build-packages: - - nodejs - - npm - stage-packages: - - bash_bins - - coreutils_bins - - nodejs_bins - expressjs-framework/runtime: - plugin: nil - stage-packages: - - npm - -In this case, the ``npm`` package is installed in a separate -``expressjs-framework/runtime`` part. This is due to -``expressjs-framework/install-app > stage-packages`` part only being able to -install slices rather than packages as a design choice of Rockcraft. See the -[issue comment](https://github.com/canonical/rockcraft/issues/785#issuecomment\ --2572990545) for more explanation. +24.04 base and Node from NPM plugin +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Bare base and Node from NPM plugin -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +The following example uses the Ubuntu 24.04 base and Node from the NPM plugin. .. code-block:: yaml - base: bare - build-base: ubuntu@24.04 + base: ubuntu@24.04 + extensions: + - expressjs-framework parts: expressjs-framework/install-app: - plugin: npm npm-include-node: true npm-node-version: 20.12 - stage-packages: - - bash_bins - - coreutils_bins - - nodejs_bins -In this case, the ``expressjs-framework/install-app > build-packages`` part is -empty. The application is is installed using Node and NPM installed by the NPM -plugin. The application is run using the NPM installed by the NPM plugin. +In this case, the application is installed and run via Node and NPM installed by +the NPM plugin. 24.04 base and Node from Ubuntu packages ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +The following example uses the Ubuntu 24.04 base and Node from Ubuntu packages. + .. code-block:: yaml base: ubuntu@24.04 + extensions: + - expressjs-framework parts: expressjs-framework/install-app: - plugin: npm npm-include-node: false - build-packages: - - nodejs - - npm - stage-packages: - - nodejs_bins - expressjs-framework/runtime: - plugin: nil - stage-packages: - - npm - -In this case, the ``expressjs-framework/install-app > stage-packages`` part does -not include the ``bash_bins`` and ``coreutils_bins`` slices as they are already -included in the Ubuntu 24.04 base. The application is built and installed using -Node and NPM from the Ubuntu packages. -24.04 base and Node from NPM plugin -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +In this case, the application is installed and run via Node and NPM installed by +the Ubuntu packages. The NPM and Node versions are determined by the versions of +NPM and NodeJS shipped with the Ubuntu base. See the NodeJS version shipped with +the corressponding Ubuntu base from the chilsel-slices repository. This +[link to the slices repository](https://github.com/canonical/chisel-releases/\ +blob/ubuntu-24.04/slices/nodejs.yaml) is an example of the NodeJS version +shipped with the Ubuntu 24.04 base. + +Bare base and Node from NPM plugin +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The following example uses the bare base and Node from the NPM plugin. .. code-block:: yaml - base: ubuntu@24.04 + base: bare + build-base: ubuntu@24.04 parts: expressjs-framework/install-app: - plugin: npm npm-include-node: true npm-node-version: 20.12 In this case, the application is installed and run via Node and NPM installed by -the NPM plugin. +the NPM plugin. For different possible inputs for npm-node-version, refer to the +[NPM plugin documentation](https://documentation.ubuntu.com/rockcraft/en/\ +latest/common/craft-parts/reference/plugins/npm_plugin). + +Bare base and Node from Ubuntu packages +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The following example uses the bare base and Node from Ubuntu packages. + +.. code-block:: yaml + + base: bare + build-base: ubuntu@24.04 + parts: + expressjs-framework/install-app: + npm-include-node: false + +In this case, the application is installed and run via Node and NPM installed by +the Ubuntu packages. The NPM and Node versions are determined by the versions of +NPM and NodeJS shipped with the Ubuntu base. +See the NodeJS version shipped with +the corressponding Ubuntu base from the chilsel-slices repository. This +[link to the slices repository](https://github.com/canonical/chisel-releases/\ +blob/ubuntu-24.04/slices/nodejs.yaml) is an example of the NodeJS version +shipped with the Ubuntu 24.04 base. +See the NPM version shipped with the corressponding Ubuntu base from the Ubuntu +packages archive from the [Ubuntu packages search](https://packages.ubuntu.com/search?suite=\ +default§ion=all&arch=any&keywords=npm&searchon=names) diff --git a/docs/reference/extensions/expressjs-framework.rst b/docs/reference/extensions/expressjs-framework.rst index b41395d49..7a0e35adb 100644 --- a/docs/reference/extensions/expressjs-framework.rst +++ b/docs/reference/extensions/expressjs-framework.rst @@ -48,24 +48,8 @@ package repository. ``parts`` > ``expressjs-framework/runtime:`` > ``stage-packages`` ================================================================= -You can use this key to specify any dependencies required for your ExpressJS -application. In the following example we use it to specify ``libpq-dev``: - -.. code-block:: yaml - - parts: - expressjs-framework/runtime: - stage-packages: - # when using the default provided node version, the npm package is - # required. - # - npm - # list required packages or slices for your ExpressJS application below. - - libpq-dev - -When using the NPM plugin installed Node and NPM, additional runtime packages -is currently unsupported due to an issue with ``lib`` path permission error. See -[Issue #790](https://github.com/canonical/rockcraft/issues/790) for more -information. +Installing additional runtime packages is currently unsupported due to some +limitations of the Rockcraft tool. Useful links ============ diff --git a/docs/tutorial/code/expressjs/app.js b/docs/tutorial/code/expressjs/app.js deleted file mode 100644 index 803c96b76..000000000 --- a/docs/tutorial/code/expressjs/app.js +++ /dev/null @@ -1,43 +0,0 @@ -var createError = require('http-errors'); -var express = require('express'); -var path = require('path'); -var cookieParser = require('cookie-parser'); -var logger = require('morgan'); - -var indexRouter = require('./routes/index'); -var usersRouter = require('./routes/users'); -var timeRouter = require('./routes/time'); - -var app = express(); - -// view engine setup -app.set('views', path.join(__dirname, 'views')); -app.set('view engine', 'jade'); - -app.use(logger('dev')); -app.use(express.json()); -app.use(express.urlencoded({ extended: false })); -app.use(cookieParser()); -app.use(express.static(path.join(__dirname, 'public'))); - -app.use('/', indexRouter); -app.use('/users', usersRouter); -app.use('/time', timeRouter); - -// catch 404 and forward to error handler -app.use(function (req, res, next) { - next(createError(404)); -}); - -// error handler -app.use(function (err, req, res, next) { - // set locals, only providing error in development - res.locals.message = err.message; - res.locals.error = req.app.get('env') === 'development' ? err : {}; - - // render the error page - res.status(err.status || 500); - res.render('error'); -}); - -module.exports = app; diff --git a/docs/tutorial/code/expressjs/task.yaml b/docs/tutorial/code/expressjs/task.yaml index 96d2bf5a1..82c9223ce 100644 --- a/docs/tutorial/code/expressjs/task.yaml +++ b/docs/tutorial/code/expressjs/task.yaml @@ -12,8 +12,8 @@ environment: execute: | # [docs:init-app] sudo apt-get update -y && sudo apt-get install npm -y - npm install -g express-generator@4.16 - echo "y" | npx express-generator app + npm install -g express-generator@4 + express app cd app && npm install # [docs:init-app-end] @@ -25,6 +25,7 @@ execute: | curl localhost:3000 # [docs:curl-expressjs-end] + kill $! kill $(lsof -t -i:3000) # [docs:create-rockcraft-yaml] @@ -101,9 +102,11 @@ execute: | sudo docker rmi expressjs-hello-world:0.1-chiselled # [docs:stop-docker-chisel-end] - ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=True rockcraft clean mv time.js app/routes/ - mv app.js app/ + original_line="var app = express();" + append_line="var timeRouter = require('./routes/time');\ + app.use('/time', timeRouter);" + sed -i "s@$original_line@&\n$append_line@g" app.js sed -i "s/version: .*/version: 0.2/g" rockcraft.yaml # [docs:docker-run-update] @@ -118,7 +121,7 @@ execute: | retry -n 5 --wait 2 curl --fail localhost:3000/time # [docs:curl-time] - curl localhost:3000/time + curl --fail localhost:3000/time # [docs:curl-time-end] # [docs:stop-docker-updated] @@ -127,8 +130,9 @@ execute: | # [docs:stop-docker-updated-end] # [docs:cleanup] - # exit and delete the virtual environment # delete all the files created during the tutorial + sudo apt-get remove npm -y + rm -rf app rm expressjs-hello-world_0.1_amd64.rock \ expressjs-hello-world_0.1-chiselled_amd64.rock \ expressjs-hello-world_0.2_amd64.rock \ diff --git a/docs/tutorial/code/expressjs/time_app.js b/docs/tutorial/code/expressjs/time_app.js new file mode 100644 index 000000000..faa2c6139 --- /dev/null +++ b/docs/tutorial/code/expressjs/time_app.js @@ -0,0 +1,2 @@ +var timeRouter = require('./routes/time'); +app.use('/time', timeRouter); diff --git a/docs/tutorial/expressjs.rst b/docs/tutorial/expressjs.rst index 06fb241eb..58c2e8e0b 100644 --- a/docs/tutorial/expressjs.rst +++ b/docs/tutorial/expressjs.rst @@ -31,7 +31,7 @@ Create the ExpressJS application Let's start by creating the "Hello, world" ExpressJS application that we'll use throughout this tutorial. -Create the ExpressJS application by running the express-generator. +Create the ExpressJS application using the express-generator: .. literalinclude:: code/expressjs/task.yaml :language: bash @@ -172,7 +172,8 @@ application's root endpoint which is running inside the container: :end-before: [docs:curl-expressjs-rock-end] :dedent: 2 -The ExpressJS application should again respond with ``Welcome to Express`` HTML. +The ExpressJS application should again respond with the ``Welcome to Express`` +HTML. View the application logs ~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -305,22 +306,17 @@ Update the ExpressJS application As a final step, let's update our application. For example, we want to add a new ``/time`` endpoint which returns the current time. -Start by creating the ``time.js`` file in a text editor and update the code to -look like the following: +Start by creating the ``app/routes/time.js`` file in a text editor and paste the +code from the snippet below: .. literalinclude:: code/expressjs/time.js :language: javascript -Place ``time.js`` file into the appropriate ``app/routes/`` directory. Import -the time route from the the main ``app.js`` file and update the code to look -like the following: +Place the code snippet below in ``app.js`` under routes registration section. +It will register the new ``/time`` endpoint: -.. literalinclude:: code/expressjs/app.js +.. literalinclude:: code/expressjs/time_app.js :language: javascript - :emphasize-lines: 9,25 - -Notice the addition of timerouter import and the registration of the ``/time`` -endpoint. Since we are creating a new version of the application, open the ``rockcraft.yaml`` file and change the ``version`` (e.g. to ``0.2``). From b62355ee0cb33960fe54680446786a33f16afeef Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Fri, 24 Jan 2025 07:25:01 +0000 Subject: [PATCH 61/64] docs: tutorial curl --fail flag --- docs/tutorial/code/django/task.yaml | 2 +- docs/tutorial/code/fastapi/task.yaml | 6 +++--- docs/tutorial/code/flask/task.yaml | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/tutorial/code/django/task.yaml b/docs/tutorial/code/django/task.yaml index 265517ecc..a79758280 100644 --- a/docs/tutorial/code/django/task.yaml +++ b/docs/tutorial/code/django/task.yaml @@ -131,7 +131,7 @@ execute: | retry -n 5 --wait 2 curl --fail localhost:8000/time/ # [docs:curl-time] - curl localhost:8000/time/ + curl --fail localhost:8000/time/ # [docs:curl-time-end] # [docs:stop-docker-updated] diff --git a/docs/tutorial/code/fastapi/task.yaml b/docs/tutorial/code/fastapi/task.yaml index 69b152e9f..9a7fdb122 100644 --- a/docs/tutorial/code/fastapi/task.yaml +++ b/docs/tutorial/code/fastapi/task.yaml @@ -1,8 +1,8 @@ ########################################### # IMPORTANT # Comments matter! -# The docs use the wrapping comments as -# markers for including said instructions +# The docs use the wrapping comments as +# markers for including said instructions # as snippets in the docs. ########################################### summary: Getting started with FastAPI tutorial @@ -117,7 +117,7 @@ execute: | retry -n 5 --wait 2 curl --fail localhost:8000/time # [docs:curl-time] - curl localhost:8000/time + curl --fail localhost:8000/time # [docs:curl-time-end] # [docs:stop-docker-updated] diff --git a/docs/tutorial/code/flask/task.yaml b/docs/tutorial/code/flask/task.yaml index 95ca74dcf..d1051a188 100644 --- a/docs/tutorial/code/flask/task.yaml +++ b/docs/tutorial/code/flask/task.yaml @@ -1,8 +1,8 @@ ########################################### # IMPORTANT # Comments matter! -# The docs use the wrapping comments as -# markers for including said instructions +# The docs use the wrapping comments as +# markers for including said instructions # as snippets in the docs. ########################################### summary: Getting started with Flask tutorial @@ -118,7 +118,7 @@ execute: | retry -n 5 --wait 2 curl --fail localhost:8000/time # [docs:curl-time] - curl localhost:8000/time + curl --fail localhost:8000/time # [docs:curl-time-end] # [docs:stop-docker-updated] From 9c2dfb96d3678216c7e084ae66d0b12e18ce1223 Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Fri, 24 Jan 2025 07:28:36 +0000 Subject: [PATCH 62/64] docs: lint fixes for tutorial --- docs/explanation/extensions/expressjs-framework.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/explanation/extensions/expressjs-framework.rst b/docs/explanation/extensions/expressjs-framework.rst index 3be32dec9..82097c8f7 100644 --- a/docs/explanation/extensions/expressjs-framework.rst +++ b/docs/explanation/extensions/expressjs-framework.rst @@ -96,5 +96,5 @@ the corressponding Ubuntu base from the chilsel-slices repository. This blob/ubuntu-24.04/slices/nodejs.yaml) is an example of the NodeJS version shipped with the Ubuntu 24.04 base. See the NPM version shipped with the corressponding Ubuntu base from the Ubuntu -packages archive from the [Ubuntu packages search](https://packages.ubuntu.com/search?suite=\ -default§ion=all&arch=any&keywords=npm&searchon=names) +packages archive from the [Ubuntu packages search](https://packages.ubuntu.com/\ +search?suite=default§ion=all&arch=any&keywords=npm&searchon=names) From 7f9aeb715b2132c95d1e67a865c57022c436c974 Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Mon, 27 Jan 2025 21:51:56 +0000 Subject: [PATCH 63/64] chore: clean up code --- .../extensions/expressjs-framework.rst | 58 ++- docs/tutorial/code/expressjs/package.json | 16 - rockcraft/extensions/expressjs.py | 69 ++-- .../expressjs-framework/rockcraft.yaml.j2 | 1 - .../rockcraft/extension-expressjs/app/app.js | 41 -- .../rockcraft/extension-expressjs/app/bin/www | 90 ----- .../extension-expressjs/app/package.json | 16 - .../app/public/stylesheets/style.css | 8 - .../extension-expressjs/app/routes/index.js | 9 - .../extension-expressjs/app/routes/users.js | 9 - .../extension-expressjs/app/views/error.jade | 6 - .../extension-expressjs/app/views/index.jade | 5 - .../extension-expressjs/app/views/layout.jade | 7 - .../rockcraft/extension-expressjs/task.yaml | 55 ++- tests/unit/extensions/test_expressjs.py | 365 +++++++++++------- 15 files changed, 340 insertions(+), 415 deletions(-) delete mode 100644 docs/tutorial/code/expressjs/package.json delete mode 100644 tests/spread/rockcraft/extension-expressjs/app/app.js delete mode 100755 tests/spread/rockcraft/extension-expressjs/app/bin/www delete mode 100644 tests/spread/rockcraft/extension-expressjs/app/package.json delete mode 100644 tests/spread/rockcraft/extension-expressjs/app/public/stylesheets/style.css delete mode 100644 tests/spread/rockcraft/extension-expressjs/app/routes/index.js delete mode 100644 tests/spread/rockcraft/extension-expressjs/app/routes/users.js delete mode 100644 tests/spread/rockcraft/extension-expressjs/app/views/error.jade delete mode 100644 tests/spread/rockcraft/extension-expressjs/app/views/index.jade delete mode 100644 tests/spread/rockcraft/extension-expressjs/app/views/layout.jade diff --git a/docs/explanation/extensions/expressjs-framework.rst b/docs/explanation/extensions/expressjs-framework.rst index 82097c8f7..5eab0e28c 100644 --- a/docs/explanation/extensions/expressjs-framework.rst +++ b/docs/explanation/extensions/expressjs-framework.rst @@ -15,24 +15,6 @@ Ubuntu base and Node combinations In this section of the document, we will discuss the possible combinations of Ubuntu bases and possible usages of NPM plugin. -24.04 base and Node from NPM plugin -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The following example uses the Ubuntu 24.04 base and Node from the NPM plugin. - -.. code-block:: yaml - - base: ubuntu@24.04 - extensions: - - expressjs-framework - parts: - expressjs-framework/install-app: - npm-include-node: true - npm-node-version: 20.12 - -In this case, the application is installed and run via Node and NPM installed by -the NPM plugin. - 24.04 base and Node from Ubuntu packages ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -43,9 +25,6 @@ The following example uses the Ubuntu 24.04 base and Node from Ubuntu packages. base: ubuntu@24.04 extensions: - expressjs-framework - parts: - expressjs-framework/install-app: - npm-include-node: false In this case, the application is installed and run via Node and NPM installed by the Ubuntu packages. The NPM and Node versions are determined by the versions of @@ -55,24 +34,23 @@ the corressponding Ubuntu base from the chilsel-slices repository. This blob/ubuntu-24.04/slices/nodejs.yaml) is an example of the NodeJS version shipped with the Ubuntu 24.04 base. -Bare base and Node from NPM plugin -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +24.04 base and Node from NPM plugin +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The following example uses the bare base and Node from the NPM plugin. +The following example uses the Ubuntu 24.04 base and Node from the NPM plugin. .. code-block:: yaml - base: bare - build-base: ubuntu@24.04 + base: ubuntu@24.04 + extensions: + - expressjs-framework parts: expressjs-framework/install-app: npm-include-node: true npm-node-version: 20.12 In this case, the application is installed and run via Node and NPM installed by -the NPM plugin. For different possible inputs for npm-node-version, refer to the -[NPM plugin documentation](https://documentation.ubuntu.com/rockcraft/en/\ -latest/common/craft-parts/reference/plugins/npm_plugin). +the NPM plugin. Bare base and Node from Ubuntu packages ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -83,9 +61,6 @@ The following example uses the bare base and Node from Ubuntu packages. base: bare build-base: ubuntu@24.04 - parts: - expressjs-framework/install-app: - npm-include-node: false In this case, the application is installed and run via Node and NPM installed by the Ubuntu packages. The NPM and Node versions are determined by the versions of @@ -98,3 +73,22 @@ shipped with the Ubuntu 24.04 base. See the NPM version shipped with the corressponding Ubuntu base from the Ubuntu packages archive from the [Ubuntu packages search](https://packages.ubuntu.com/\ search?suite=default§ion=all&arch=any&keywords=npm&searchon=names) + +Bare base and Node from NPM plugin +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The following example uses the bare base and Node from the NPM plugin. + +.. code-block:: yaml + + base: bare + build-base: ubuntu@24.04 + parts: + expressjs-framework/install-app: + npm-include-node: true + npm-node-version: 20.12 + +In this case, the application is installed and run via Node and NPM installed by +the NPM plugin. For different possible inputs for npm-node-version, refer to the +[NPM plugin documentation](https://documentation.ubuntu.com/rockcraft/en/\ +latest/common/craft-parts/reference/plugins/npm_plugin). diff --git a/docs/tutorial/code/expressjs/package.json b/docs/tutorial/code/expressjs/package.json deleted file mode 100644 index a7226591b..000000000 --- a/docs/tutorial/code/expressjs/package.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "app", - "version": "0.0.0", - "private": true, - "scripts": { - "start": "node ./bin/www" - }, - "dependencies": { - "cookie-parser": "~1.4.4", - "debug": "~2.6.9", - "express": "~4.16.1", - "http-errors": "~1.6.3", - "jade": "~1.11.0", - "morgan": "~1.9.1" - } -} diff --git a/rockcraft/extensions/expressjs.py b/rockcraft/extensions/expressjs.py index a6f73d55f..bed1ac2ff 100644 --- a/rockcraft/extensions/expressjs.py +++ b/rockcraft/extensions/expressjs.py @@ -42,22 +42,6 @@ def is_experimental(base: str | None) -> bool: """Check if the extension is in an experimental state.""" return True - @override - def get_part_snippet(self) -> dict[str, Any]: - """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. - - This is unused but is required by the ABC. - """ - return {} - @override def get_root_snippet(self) -> dict[str, Any]: """Fill in some default root components. @@ -96,6 +80,22 @@ def get_root_snippet(self) -> dict[str, Any]: snippet["parts"]["expressjs-framework/runtime"] = runtime_part return snippet + @override + def get_part_snippet(self) -> dict[str, Any]: + """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. + + This is unused but is required by the ABC. + """ + return {} + def _check_project(self) -> None: """Ensure this extension can apply to the current rockcraft project. @@ -112,7 +112,9 @@ def _check_project(self) -> None: doc_slug="/reference/extensions/expressjs-framework", logpath_report=False, ) - if "name" not in self._app_package_json: + if "name" not in self._app_package_json or not isinstance( + self._app_package_json["name"], str + ): raise ExtensionError( "missing application name", doc_slug="/reference/extensions/expressjs-framework", @@ -120,7 +122,12 @@ def _check_project(self) -> None: ) def _gen_install_app_part(self) -> dict: - """Generate the install app part using NPM plugin.""" + """Generate the install app part using NPM plugin. + + Set the script shell to bash and copy the .npmrc file to the app + directory. This is to ensure that the ExpressJS run in bare container + can use the shell to launch itself. + """ install_app_part: dict[str, Any] = { "plugin": "npm", "source": f"{self.IMAGE_BASE_DIR}/", @@ -132,10 +139,10 @@ def _gen_install_app_part(self) -> dict: f"ln -s /lib/node_modules/{self._app_name} ${{CRAFT_PART_INSTALL}}/app\n" ), } - build_packages = self._install_app_build_packages + build_packages = self._gen_app_build_packages() if build_packages: install_app_part["build-packages"] = build_packages - stage_packages = self._install_app_stage_packages + stage_packages = self._gen_app_stage_packages() if stage_packages: install_app_part["stage-packages"] = stage_packages if self._user_npm_include_node: @@ -145,15 +152,13 @@ def _gen_install_app_part(self) -> dict: ) return install_app_part - @property - def _install_app_build_packages(self) -> list[str]: + def _gen_app_build_packages(self) -> list[str]: """Return the build packages for the install app part.""" if self._user_npm_include_node: return [] return ["nodejs", "npm"] - @property - def _install_app_stage_packages(self) -> list[str]: + def _gen_app_stage_packages(self) -> list[str]: """Return the stage packages for the install app part.""" if self._rock_base == "bare": return [ @@ -203,7 +208,21 @@ def _app_package_json(self) -> dict: logpath_report=False, ) package_json_contents = package_json_file.read_text(encoding="utf-8") - return json.loads(package_json_contents) + try: + app_package_json = json.loads(package_json_contents) + if not isinstance(app_package_json, dict): + raise ExtensionError( + "invalid package.json file", + doc_slug="/reference/extensions/expressjs-framework", + logpath_report=False, + ) + return app_package_json + except json.JSONDecodeError as exc: + raise ExtensionError( + "failed to parse package.json file", + doc_slug="/reference/extensions/expressjs-framework", + logpath_report=False, + ) from exc @property def _app_name(self) -> str: diff --git a/rockcraft/templates/expressjs-framework/rockcraft.yaml.j2 b/rockcraft/templates/expressjs-framework/rockcraft.yaml.j2 index 9fad77a70..cf1d834d1 100644 --- a/rockcraft/templates/expressjs-framework/rockcraft.yaml.j2 +++ b/rockcraft/templates/expressjs-framework/rockcraft.yaml.j2 @@ -2,7 +2,6 @@ name: {{name}} # see {{versioned_url}}/explanation/bases/ # for more information about bases and using 'bare' bases for chiselled rocks base: ubuntu@24.04 # as an alternative, a 'bare' base can be used -build-base: ubuntu@24.04 # build-base is required when the base is bare version: '0.1' # just for humans. Semantic versioning is recommended summary: A summary of your ExpresssJS application # 79 char long summary description: | diff --git a/tests/spread/rockcraft/extension-expressjs/app/app.js b/tests/spread/rockcraft/extension-expressjs/app/app.js deleted file mode 100644 index 662bcc927..000000000 --- a/tests/spread/rockcraft/extension-expressjs/app/app.js +++ /dev/null @@ -1,41 +0,0 @@ -var createError = require('http-errors'); -var express = require('express'); -var path = require('path'); -var cookieParser = require('cookie-parser'); -var logger = require('morgan'); - -var indexRouter = require('./routes/index'); -var usersRouter = require('./routes/users'); - -var app = express(); - -// view engine setup -app.set('views', path.join(__dirname, 'views')); -app.set('view engine', 'jade'); - -app.use(logger('dev')); -app.use(express.json()); -app.use(express.urlencoded({ extended: false })); -app.use(cookieParser()); -app.use(express.static(path.join(__dirname, 'public'))); - -app.use('/', indexRouter); -app.use('/users', usersRouter); - -// catch 404 and forward to error handler -app.use(function(req, res, next) { - next(createError(404)); -}); - -// error handler -app.use(function(err, req, res, next) { - // set locals, only providing error in development - res.locals.message = err.message; - res.locals.error = req.app.get('env') === 'development' ? err : {}; - - // render the error page - res.status(err.status || 500); - res.render('error'); -}); - -module.exports = app; diff --git a/tests/spread/rockcraft/extension-expressjs/app/bin/www b/tests/spread/rockcraft/extension-expressjs/app/bin/www deleted file mode 100755 index a8c2d36e0..000000000 --- a/tests/spread/rockcraft/extension-expressjs/app/bin/www +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/env node - -/** - * Module dependencies. - */ - -var app = require('../app'); -var debug = require('debug')('app:server'); -var http = require('http'); - -/** - * Get port from environment and store in Express. - */ - -var port = normalizePort(process.env.PORT || '3000'); -app.set('port', port); - -/** - * Create HTTP server. - */ - -var server = http.createServer(app); - -/** - * Listen on provided port, on all network interfaces. - */ - -server.listen(port); -server.on('error', onError); -server.on('listening', onListening); - -/** - * Normalize a port into a number, string, or false. - */ - -function normalizePort(val) { - var port = parseInt(val, 10); - - if (isNaN(port)) { - // named pipe - return val; - } - - if (port >= 0) { - // port number - return port; - } - - return false; -} - -/** - * Event listener for HTTP server "error" event. - */ - -function onError(error) { - if (error.syscall !== 'listen') { - throw error; - } - - var bind = typeof port === 'string' - ? 'Pipe ' + port - : 'Port ' + port; - - // handle specific listen errors with friendly messages - switch (error.code) { - case 'EACCES': - console.error(bind + ' requires elevated privileges'); - process.exit(1); - break; - case 'EADDRINUSE': - console.error(bind + ' is already in use'); - process.exit(1); - break; - default: - throw error; - } -} - -/** - * Event listener for HTTP server "listening" event. - */ - -function onListening() { - var addr = server.address(); - var bind = typeof addr === 'string' - ? 'pipe ' + addr - : 'port ' + addr.port; - debug('Listening on ' + bind); -} diff --git a/tests/spread/rockcraft/extension-expressjs/app/package.json b/tests/spread/rockcraft/extension-expressjs/app/package.json deleted file mode 100644 index 5d49060cc..000000000 --- a/tests/spread/rockcraft/extension-expressjs/app/package.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "app", - "version": "0.0.0", - "private": true, - "scripts": { - "start": "node ./bin/www" - }, - "dependencies": { - "cookie-parser": "~1.4.4", - "debug": "~2.6.9", - "express": "~4.16.1", - "http-errors": "~1.6.3", - "jade": "~1.11.0", - "morgan": "~1.9.1" - } -} diff --git a/tests/spread/rockcraft/extension-expressjs/app/public/stylesheets/style.css b/tests/spread/rockcraft/extension-expressjs/app/public/stylesheets/style.css deleted file mode 100644 index 9453385b9..000000000 --- a/tests/spread/rockcraft/extension-expressjs/app/public/stylesheets/style.css +++ /dev/null @@ -1,8 +0,0 @@ -body { - padding: 50px; - font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; -} - -a { - color: #00B7FF; -} diff --git a/tests/spread/rockcraft/extension-expressjs/app/routes/index.js b/tests/spread/rockcraft/extension-expressjs/app/routes/index.js deleted file mode 100644 index ecca96a56..000000000 --- a/tests/spread/rockcraft/extension-expressjs/app/routes/index.js +++ /dev/null @@ -1,9 +0,0 @@ -var express = require('express'); -var router = express.Router(); - -/* GET home page. */ -router.get('/', function(req, res, next) { - res.render('index', { title: 'Express' }); -}); - -module.exports = router; diff --git a/tests/spread/rockcraft/extension-expressjs/app/routes/users.js b/tests/spread/rockcraft/extension-expressjs/app/routes/users.js deleted file mode 100644 index 623e4302b..000000000 --- a/tests/spread/rockcraft/extension-expressjs/app/routes/users.js +++ /dev/null @@ -1,9 +0,0 @@ -var express = require('express'); -var router = express.Router(); - -/* GET users listing. */ -router.get('/', function(req, res, next) { - res.send('respond with a resource'); -}); - -module.exports = router; diff --git a/tests/spread/rockcraft/extension-expressjs/app/views/error.jade b/tests/spread/rockcraft/extension-expressjs/app/views/error.jade deleted file mode 100644 index 51ec12c6a..000000000 --- a/tests/spread/rockcraft/extension-expressjs/app/views/error.jade +++ /dev/null @@ -1,6 +0,0 @@ -extends layout - -block content - h1= message - h2= error.status - pre #{error.stack} diff --git a/tests/spread/rockcraft/extension-expressjs/app/views/index.jade b/tests/spread/rockcraft/extension-expressjs/app/views/index.jade deleted file mode 100644 index 3d63b9a04..000000000 --- a/tests/spread/rockcraft/extension-expressjs/app/views/index.jade +++ /dev/null @@ -1,5 +0,0 @@ -extends layout - -block content - h1= title - p Welcome to #{title} diff --git a/tests/spread/rockcraft/extension-expressjs/app/views/layout.jade b/tests/spread/rockcraft/extension-expressjs/app/views/layout.jade deleted file mode 100644 index 15af079bf..000000000 --- a/tests/spread/rockcraft/extension-expressjs/app/views/layout.jade +++ /dev/null @@ -1,7 +0,0 @@ -doctype html -html - head - title= title - link(rel='stylesheet', href='/stylesheets/style.css') - body - block content diff --git a/tests/spread/rockcraft/extension-expressjs/task.yaml b/tests/spread/rockcraft/extension-expressjs/task.yaml index 02d86da2b..9c840a2d0 100644 --- a/tests/spread/rockcraft/extension-expressjs/task.yaml +++ b/tests/spread/rockcraft/extension-expressjs/task.yaml @@ -9,6 +9,11 @@ execute: | ROCK_FILE="${NAME}_0.1_amd64.rock" IMAGE="${NAME}:0.1" + sudo apt-get update -y && sudo apt-get install npm -y + npm install -g express-generator@4 + express app + cd app && npm install && cd .. + run_rockcraft init --name "${NAME}" --profile expressjs-framework sed -i "s/^name: .*/name: ${NAME}/g" rockcraft.yaml sed -i "s/^base: .*/base: ${SCENARIO//-/@}/g" rockcraft.yaml @@ -16,27 +21,43 @@ execute: | sed -i "s/^build-base: .*/build-base: ${SCENARIO//-/@}/g" rockcraft.yaml fi - run_rockcraft pack + function run_test() { + # rockcraft clean is required here because the cached layer writes to npmrc + # multiple times, causing the app to crash on first run. + rockcraft clean + run_rockcraft pack + + test -f "${ROCK_FILE}" + test ! -d work + + # Ensure docker does not have this container image + docker rmi --force "${IMAGE}" + # Install container + sudo rockcraft.skopeo --insecure-policy copy "oci-archive:${ROCK_FILE}" \ + "docker-daemon:${IMAGE}" + # Ensure container exists + docker images "${IMAGE}" | MATCH "${NAME}" - test -f "${ROCK_FILE}" - test ! -d work + # Ensure container doesn't exist + docker rm -f "${NAME}-container" - # Ensure docker does not have this container image - docker rmi --force "${IMAGE}" - # Install container - sudo rockcraft.skopeo --insecure-policy copy "oci-archive:${ROCK_FILE}" \ - "docker-daemon:${IMAGE}" - # Ensure container exists - docker images "${IMAGE}" | MATCH "${NAME}" + # test the default expressjs service + docker run --name "${NAME}-container" -d -p 8137:3000 "${IMAGE}" + retry -n 5 --wait 2 curl localhost:8137 + http_status=$(curl -s -o /dev/null -w "%{http_code}" localhost:8137) + [ "${http_status}" -eq 200 ] + } - # ensure container doesn't exist - docker rm -f "${NAME}-container" + run_test - # test the default expressjs service - docker run --name "${NAME}-container" -d -p 8137:3000 "${IMAGE}" - retry -n 5 --wait 2 curl localhost:8137 - http_status=$(curl -s -o /dev/null -w "%{http_code}" localhost:8137) - [ "${http_status}" -eq 200 ] + # test the expressjs service with a Node version specified + cat <> rockcraft.yaml + parts: + expressjs-framework/install-app: + npm-include-node: true + npm-node-version: 20.18.2 + EOF + run_test restore: | NAME="expressjs-${SCENARIO//./-}" diff --git a/tests/unit/extensions/test_expressjs.py b/tests/unit/extensions/test_expressjs.py index ec9820dfb..0cd42c761 100644 --- a/tests/unit/extensions/test_expressjs.py +++ b/tests/unit/extensions/test_expressjs.py @@ -17,6 +17,8 @@ from rockcraft import extensions from rockcraft.errors import ExtensionError +_expressjs_project_name = "test-expressjs-project" + @pytest.fixture(name="expressjs_input_yaml") def expressjs_input_yaml_fixture(): @@ -35,11 +37,6 @@ def expressjs_extension(mock_extensions, monkeypatch): extensions.register("expressjs-framework", extensions.ExpressJSFramework) -@pytest.fixture -def expressjs_project_name(): - return "test-expressjs-project" - - @pytest.fixture def app_path(tmp_path): app_path = tmp_path / "app" @@ -48,10 +45,10 @@ def app_path(tmp_path): @pytest.fixture -def package_json_file(app_path, expressjs_project_name): +def package_json_file(app_path): (app_path / "package.json").write_text( f"""{{ - "name": "{expressjs_project_name}", + "name": "{_expressjs_project_name}", "scripts": {{ "start": "node ./bin/www" }} @@ -59,159 +56,261 @@ def package_json_file(app_path, expressjs_project_name): ) -@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", - "build-base": "ubuntu@24.04", - "name": "foo-bar", - "platforms": { - "amd64": {}, - }, - "run-user": "_daemon_", - "parts": { - "expressjs-framework/install-app": { - "plugin": "npm", - "source": "app/", - "override-build": ( - "craftctl default\n" - "npm config set script-shell=bash --location project\n" - "cp ${CRAFT_PART_BUILD}/.npmrc ${CRAFT_PART_INSTALL}/lib/node_modules/" - f"{expressjs_project_name}/.npmrc\n" - f"ln -s /lib/node_modules/{expressjs_project_name} " - "${CRAFT_PART_INSTALL}/app\n" - ), - "build-packages": ["nodejs", "npm"], - "stage-packages": ["ca-certificates_data", "nodejs_bins"], - }, - "expressjs-framework/runtime": {"plugin": "nil", "stage-packages": ["npm"]}, - }, - "services": { - "expressjs": { - "override": "replace", - "startup": "enabled", - "user": "_daemon_", - "working-dir": "/app", - "command": "npm start", - "environment": {"NODE_ENV": "production"}, - }, - }, - } - - -@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, app_path, expressjs_input_yaml, package_json_contents, error_message -): - (app_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( - "base, expected_build_packages, expected_stage_packages", + "base, npm_include_node, node_version, expected_yaml_dict", [ pytest.param( "ubuntu@24.04", - ["nodejs", "npm"], - ["ca-certificates_data", "nodejs_bins"], + False, + None, + { + "base": "ubuntu@24.04", + "build-base": "ubuntu@24.04", + "name": "foo-bar", + "platforms": { + "amd64": {}, + }, + "run-user": "_daemon_", + "parts": { + "expressjs-framework/install-app": { + "plugin": "npm", + "source": "app/", + "npm-include-node": False, + "npm-node-version": None, + "override-build": ( + "craftctl default\n" + "npm config set script-shell=bash --location project\n" + "cp ${CRAFT_PART_BUILD}/.npmrc ${CRAFT_PART_INSTALL}/lib/node_modules/" + f"{_expressjs_project_name}/.npmrc\n" + f"ln -s /lib/node_modules/{_expressjs_project_name} " + "${CRAFT_PART_INSTALL}/app\n" + ), + "build-packages": ["nodejs", "npm"], + "stage-packages": ["ca-certificates_data", "nodejs_bins"], + }, + "expressjs-framework/runtime": { + "plugin": "nil", + "stage-packages": ["npm"], + }, + }, + "services": { + "expressjs": { + "override": "replace", + "startup": "enabled", + "user": "_daemon_", + "working-dir": "/app", + "command": "npm start", + "environment": {"NODE_ENV": "production"}, + }, + }, + }, id="ubuntu@24.04", ), pytest.param( - "bare", - ["nodejs", "npm"], - ["bash_bins", "ca-certificates_data", "nodejs_bins", "coreutils_bins"], - id="bare", + "ubuntu@24.04", + True, + "1.0.0", + { + "base": "ubuntu@24.04", + "build-base": "ubuntu@24.04", + "name": "foo-bar", + "parts": { + "expressjs-framework/install-app": { + "npm-include-node": True, + "npm-node-version": "1.0.0", + "override-build": "craftctl default\n" + "npm config set script-shell=bash --location project\n" + "cp ${CRAFT_PART_BUILD}/.npmrc " + "${CRAFT_PART_INSTALL}/lib/node_modules/test-expressjs-project/.npmrc\n" + "ln -s /lib/node_modules/test-expressjs-project " + "${CRAFT_PART_INSTALL}/app\n", + "plugin": "npm", + "source": "app/", + "stage-packages": [ + "ca-certificates_data", + ], + }, + }, + "platforms": { + "amd64": {}, + }, + "run-user": "_daemon_", + "services": { + "expressjs": { + "command": "npm start", + "environment": { + "NODE_ENV": "production", + }, + "override": "replace", + "startup": "enabled", + "user": "_daemon_", + "working-dir": "/app", + }, + }, + }, + id="ubuntu@24.04", ), - ], -) -@pytest.mark.usefixtures("expressjs_extension", "package_json_file") -def test_expressjs_install_app_default_node_install( - tmp_path, - expressjs_input_yaml, - base, - expected_build_packages, - expected_stage_packages, -): - expressjs_input_yaml["base"] = base - applied = extensions.apply_extensions(tmp_path, expressjs_input_yaml) - - assert applied["parts"]["expressjs-framework/install-app"]["build-packages"] == ( - expected_build_packages - ) - assert applied["parts"]["expressjs-framework/install-app"]["stage-packages"] == ( - expected_stage_packages - ) - - -@pytest.mark.parametrize( - "base, expected_stage_packages", - [ pytest.param( - "ubuntu@24.04", - ["ca-certificates_data"], - id="24.04 base", + "bare", + False, + None, + { + "base": "bare", + "build-base": "ubuntu@24.04", + "name": "foo-bar", + "parts": { + "expressjs-framework/install-app": { + "build-packages": [ + "nodejs", + "npm", + ], + "npm-include-node": False, + "npm-node-version": None, + "override-build": "craftctl default\n" + "npm config set script-shell=bash --location project\n" + "cp ${CRAFT_PART_BUILD}/.npmrc " + "${CRAFT_PART_INSTALL}/lib/node_modules/test-expressjs-project/.npmrc\n" + "ln -s /lib/node_modules/test-expressjs-project " + "${CRAFT_PART_INSTALL}/app\n", + "plugin": "npm", + "source": "app/", + "stage-packages": [ + "bash_bins", + "ca-certificates_data", + "nodejs_bins", + "coreutils_bins", + ], + }, + "expressjs-framework/runtime": { + "plugin": "nil", + "stage-packages": [ + "npm", + ], + }, + }, + "platforms": { + "amd64": {}, + }, + "run-user": "_daemon_", + "services": { + "expressjs": { + "command": "npm start", + "environment": { + "NODE_ENV": "production", + }, + "override": "replace", + "startup": "enabled", + "user": "_daemon_", + "working-dir": "/app", + }, + }, + }, + id="ubuntu@24.04", ), pytest.param( "bare", - ["bash_bins", "ca-certificates_data", "nodejs_bins", "coreutils_bins"], - id="bare base", + True, + "1.0.0", + { + "base": "bare", + "build-base": "ubuntu@24.04", + "name": "foo-bar", + "parts": { + "expressjs-framework/install-app": { + "npm-include-node": True, + "npm-node-version": "1.0.0", + "override-build": "craftctl default\n" + "npm config set script-shell=bash --location project\n" + "cp ${CRAFT_PART_BUILD}/.npmrc " + "${CRAFT_PART_INSTALL}/lib/node_modules/test-expressjs-project/.npmrc\n" + "ln -s /lib/node_modules/test-expressjs-project " + "${CRAFT_PART_INSTALL}/app\n", + "plugin": "npm", + "source": "app/", + "stage-packages": [ + "bash_bins", + "ca-certificates_data", + "nodejs_bins", + "coreutils_bins", + ], + }, + }, + "platforms": { + "amd64": {}, + }, + "run-user": "_daemon_", + "services": { + "expressjs": { + "command": "npm start", + "environment": { + "NODE_ENV": "production", + }, + "override": "replace", + "startup": "enabled", + "user": "_daemon_", + "working-dir": "/app", + }, + }, + }, + id="ubuntu@24.04", ), ], ) @pytest.mark.usefixtures("expressjs_extension", "package_json_file") -def test_expressjs_install_app_user_defined_node_install( +def test_expressjs_extension_default( tmp_path, expressjs_input_yaml, base, - expected_stage_packages, + npm_include_node, + node_version, + expected_yaml_dict, ): expressjs_input_yaml["base"] = base expressjs_input_yaml["parts"] = { "expressjs-framework/install-app": { - "npm-include-node": True, - "npm-node-version": "node", + "npm-include-node": npm_include_node, + "npm-node-version": node_version, } } applied = extensions.apply_extensions(tmp_path, expressjs_input_yaml) - assert "build-packages" not in applied["parts"]["expressjs-framework/install-app"] - assert ( - applied["parts"]["expressjs-framework/install-app"]["stage-packages"] - == expected_stage_packages - ) + assert applied == expected_yaml_dict -@pytest.mark.usefixtures("expressjs_extension", "package_json_file") -def test_expressjs_runtime_user_defined_node_install(tmp_path, expressjs_input_yaml): - expressjs_input_yaml["parts"] = { - "expressjs-framework/install-app": { - "npm-include-node": True, - "npm-node-version": "node", - } - } - applied = extensions.apply_extensions(tmp_path, expressjs_input_yaml) +@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" + - assert "expressjs-framework/runtime" not in applied["parts"] +@pytest.mark.parametrize( + "package_json_path, package_json_contents, error_message", + [ + ("invalid-path", "", "missing package.json file"), + ("package.json", "[]", "invalid package.json file"), + ("package.json", "{", "failed to parse package.json file"), + ("package.json", "{}", "missing start script"), + ("package.json", '{"scripts":{}}', "missing start script"), + ( + "package.json", + '{"scripts":{"start":"node ./bin/www"}}', + "missing application name", + ), + ], +) +@pytest.mark.usefixtures("expressjs_extension") +def test_expressjs_invalid_package_json_scripts_error( + tmp_path, + app_path, + expressjs_input_yaml, + package_json_path, + package_json_contents, + error_message, +): + (app_path / package_json_path).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" From 2ca47b22063493621a7ade239b7610711fda9e25 Mon Sep 17 00:00:00 2001 From: Yanks Yoon Date: Fri, 31 Jan 2025 12:17:30 +0000 Subject: [PATCH 64/64] chore: trigger CI