wexample-wex-addon-dev-javascript 7.16.1__tar.gz → 8.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. {wexample_wex_addon_dev_javascript-7.16.1 → wexample_wex_addon_dev_javascript-8.0.0}/PKG-INFO +6 -6
  2. {wexample_wex_addon_dev_javascript-7.16.1 → wexample_wex_addon_dev_javascript-8.0.0}/README.md +3 -3
  3. {wexample_wex_addon_dev_javascript-7.16.1 → wexample_wex_addon_dev_javascript-8.0.0}/pyproject.toml +3 -3
  4. {wexample_wex_addon_dev_javascript-7.16.1 → wexample_wex_addon_dev_javascript-8.0.0}/src/wexample_wex_addon_dev_javascript/file/javascript_package_json_file.py +11 -60
  5. wexample_wex_addon_dev_javascript-8.0.0/src/wexample_wex_addon_dev_javascript/file/node_package_json_file.py +75 -0
  6. {wexample_wex_addon_dev_javascript-7.16.1/src/wexample_wex_addon_dev_javascript/services/vite → wexample_wex_addon_dev_javascript-8.0.0/src/wexample_wex_addon_dev_javascript/services/node/commands}/__init__.py +0 -0
  7. wexample_wex_addon_dev_javascript-8.0.0/src/wexample_wex_addon_dev_javascript/services/node/commands/service/install_local.py +81 -0
  8. wexample_wex_addon_dev_javascript-8.0.0/src/wexample_wex_addon_dev_javascript/services/node/commands/service/refresh_lock.py +66 -0
  9. {wexample_wex_addon_dev_javascript-7.16.1/src/wexample_wex_addon_dev_javascript/services/vite/ai/agents → wexample_wex_addon_dev_javascript-8.0.0/src/wexample_wex_addon_dev_javascript/services/vite}/__init__.py +0 -0
  10. {wexample_wex_addon_dev_javascript-7.16.1/src/wexample_wex_addon_dev_javascript/services/vite/commands → wexample_wex_addon_dev_javascript-8.0.0/src/wexample_wex_addon_dev_javascript/services/vite/ai/agents}/__init__.py +0 -0
  11. {wexample_wex_addon_dev_javascript-7.16.1/src/wexample_wex_addon_dev_javascript/services/vite/commands/service → wexample_wex_addon_dev_javascript-8.0.0/src/wexample_wex_addon_dev_javascript/services/vite/ai/agents/main}/__init__.py +0 -0
  12. wexample_wex_addon_dev_javascript-8.0.0/src/wexample_wex_addon_dev_javascript/services/vite/commands/service/__init__.py +0 -0
  13. wexample_wex_addon_dev_javascript-8.0.0/src/wexample_wex_addon_dev_javascript/workdir/__init__.py +0 -0
  14. {wexample_wex_addon_dev_javascript-7.16.1 → wexample_wex_addon_dev_javascript-8.0.0}/src/wexample_wex_addon_dev_javascript/workdir/javascript_package_workdir.py +38 -0
  15. {wexample_wex_addon_dev_javascript-7.16.1 → wexample_wex_addon_dev_javascript-8.0.0}/src/wexample_wex_addon_dev_javascript/__init__.py +0 -0
  16. {wexample_wex_addon_dev_javascript-7.16.1 → wexample_wex_addon_dev_javascript-8.0.0}/src/wexample_wex_addon_dev_javascript/__pycache__/__init__.py +0 -0
  17. {wexample_wex_addon_dev_javascript-7.16.1 → wexample_wex_addon_dev_javascript-8.0.0}/src/wexample_wex_addon_dev_javascript/common/__init__.py +0 -0
  18. {wexample_wex_addon_dev_javascript-7.16.1 → wexample_wex_addon_dev_javascript-8.0.0}/src/wexample_wex_addon_dev_javascript/common/npm_registry_gateway.py +0 -0
  19. {wexample_wex_addon_dev_javascript-7.16.1 → wexample_wex_addon_dev_javascript-8.0.0}/src/wexample_wex_addon_dev_javascript/config_value/__init__.py +0 -0
  20. {wexample_wex_addon_dev_javascript-7.16.1 → wexample_wex_addon_dev_javascript-8.0.0}/src/wexample_wex_addon_dev_javascript/config_value/javascript_package_readme_config_value.py +0 -0
  21. {wexample_wex_addon_dev_javascript-7.16.1 → wexample_wex_addon_dev_javascript-8.0.0}/src/wexample_wex_addon_dev_javascript/const/__init__.py +0 -0
  22. {wexample_wex_addon_dev_javascript-7.16.1 → wexample_wex_addon_dev_javascript-8.0.0}/src/wexample_wex_addon_dev_javascript/const/tags.py +0 -0
  23. {wexample_wex_addon_dev_javascript-7.16.1 → wexample_wex_addon_dev_javascript-8.0.0}/src/wexample_wex_addon_dev_javascript/file/__init__.py +0 -0
  24. {wexample_wex_addon_dev_javascript-7.16.1 → wexample_wex_addon_dev_javascript-8.0.0}/src/wexample_wex_addon_dev_javascript/file/javascript_tsconfig_json_file.py +0 -0
  25. {wexample_wex_addon_dev_javascript-7.16.1 → wexample_wex_addon_dev_javascript-8.0.0}/src/wexample_wex_addon_dev_javascript/javascript_addon_manager.py +0 -0
  26. {wexample_wex_addon_dev_javascript-7.16.1 → wexample_wex_addon_dev_javascript-8.0.0}/src/wexample_wex_addon_dev_javascript/py.typed +0 -0
  27. {wexample_wex_addon_dev_javascript-7.16.1 → wexample_wex_addon_dev_javascript-8.0.0}/src/wexample_wex_addon_dev_javascript/resources/.wex.yml +0 -0
  28. {wexample_wex_addon_dev_javascript-7.16.1 → wexample_wex_addon_dev_javascript-8.0.0}/src/wexample_wex_addon_dev_javascript/resources/package_publish.yml +0 -0
  29. {wexample_wex_addon_dev_javascript-7.16.1 → wexample_wex_addon_dev_javascript-8.0.0}/src/wexample_wex_addon_dev_javascript/services/__init__.py +0 -0
  30. {wexample_wex_addon_dev_javascript-7.16.1 → wexample_wex_addon_dev_javascript-8.0.0}/src/wexample_wex_addon_dev_javascript/services/node/__init__.py +0 -0
  31. {wexample_wex_addon_dev_javascript-7.16.1/src/wexample_wex_addon_dev_javascript/services/vite/ai → wexample_wex_addon_dev_javascript-8.0.0/src/wexample_wex_addon_dev_javascript/services/node/commands/service}/__init__.py +0 -0
  32. {wexample_wex_addon_dev_javascript-7.16.1 → wexample_wex_addon_dev_javascript-8.0.0}/src/wexample_wex_addon_dev_javascript/services/node/docker/.wex.yml +0 -0
  33. {wexample_wex_addon_dev_javascript-7.16.1 → wexample_wex_addon_dev_javascript-8.0.0}/src/wexample_wex_addon_dev_javascript/services/node/docker/docker-compose.yml +0 -0
  34. {wexample_wex_addon_dev_javascript-7.16.1 → wexample_wex_addon_dev_javascript-8.0.0}/src/wexample_wex_addon_dev_javascript/services/node/service.yml +0 -0
  35. {wexample_wex_addon_dev_javascript-7.16.1/src/wexample_wex_addon_dev_javascript/services/vite/ai/agents/main → wexample_wex_addon_dev_javascript-8.0.0/src/wexample_wex_addon_dev_javascript/services/vite/ai}/__init__.py +0 -0
  36. {wexample_wex_addon_dev_javascript-7.16.1 → wexample_wex_addon_dev_javascript-8.0.0}/src/wexample_wex_addon_dev_javascript/services/vite/ai/agents/main/about-service.md +0 -0
  37. {wexample_wex_addon_dev_javascript-7.16.1 → wexample_wex_addon_dev_javascript-8.0.0}/src/wexample_wex_addon_dev_javascript/services/vite/ai/agents/main/agent.yml +0 -0
  38. {wexample_wex_addon_dev_javascript-7.16.1 → wexample_wex_addon_dev_javascript-8.0.0}/src/wexample_wex_addon_dev_javascript/services/vite/app_service.py +0 -0
  39. {wexample_wex_addon_dev_javascript-7.16.1/src/wexample_wex_addon_dev_javascript/workdir → wexample_wex_addon_dev_javascript-8.0.0/src/wexample_wex_addon_dev_javascript/services/vite/commands}/__init__.py +0 -0
  40. {wexample_wex_addon_dev_javascript-7.16.1 → wexample_wex_addon_dev_javascript-8.0.0}/src/wexample_wex_addon_dev_javascript/services/vite/commands/service/install.py +0 -0
  41. {wexample_wex_addon_dev_javascript-7.16.1 → wexample_wex_addon_dev_javascript-8.0.0}/src/wexample_wex_addon_dev_javascript/services/vite/commands/service/ready.py +0 -0
  42. {wexample_wex_addon_dev_javascript-7.16.1 → wexample_wex_addon_dev_javascript-8.0.0}/src/wexample_wex_addon_dev_javascript/services/vite/docker/.wex.yml +0 -0
  43. {wexample_wex_addon_dev_javascript-7.16.1 → wexample_wex_addon_dev_javascript-8.0.0}/src/wexample_wex_addon_dev_javascript/services/vite/docker/docker-compose.yml +0 -0
  44. {wexample_wex_addon_dev_javascript-7.16.1 → wexample_wex_addon_dev_javascript-8.0.0}/src/wexample_wex_addon_dev_javascript/services/vite/service.yml +0 -0
  45. {wexample_wex_addon_dev_javascript-7.16.1 → wexample_wex_addon_dev_javascript-8.0.0}/src/wexample_wex_addon_dev_javascript/workdir/javascript_packages_suite_workdir.py +0 -0
  46. {wexample_wex_addon_dev_javascript-7.16.1 → wexample_wex_addon_dev_javascript-8.0.0}/src/wexample_wex_addon_dev_javascript/workdir/javascript_workdir.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: wexample-wex-addon-dev-javascript
3
- Version: 7.16.1
3
+ Version: 8.0.0
4
4
  Summary: Python dev addon for wex
5
5
  Author-Email: weeger <contact@wexample.com>
6
6
  License: MIT
@@ -13,8 +13,8 @@ Requires-Dist: attrs>=23.1.0
13
13
  Requires-Dist: cattrs>=23.1.0
14
14
  Requires-Dist: wexample-api>=6.8.0
15
15
  Requires-Dist: wexample-filestate-javascript>=6.6.0
16
- Requires-Dist: wexample-wex-addon-ai>=12.0.0
17
- Requires-Dist: wexample-wex-addon-app>=28.0.0
16
+ Requires-Dist: wexample-wex-addon-ai>=12.1.0
17
+ Requires-Dist: wexample-wex-addon-app>=29.0.0
18
18
  Provides-Extra: dev
19
19
  Requires-Dist: pytest; extra == "dev"
20
20
  Requires-Dist: pytest-cov; extra == "dev"
@@ -22,7 +22,7 @@ Description-Content-Type: text/markdown
22
22
 
23
23
  # wex_addon_dev_javascript
24
24
 
25
- Version: 7.16.1
25
+ Version: 8.0.0
26
26
 
27
27
  Python dev addon for wex
28
28
 
@@ -106,8 +106,8 @@ Visit the [Wexample Suite documentation](https://docs.wexample.com) for the comp
106
106
  - cattrs: >=23.1.0
107
107
  - wexample-api: >=6.8.0
108
108
  - wexample-filestate-javascript: >=6.6.0
109
- - wexample-wex-addon-ai: >=12.0.0
110
- - wexample-wex-addon-app: >=28.0.0
109
+ - wexample-wex-addon-ai: >=12.1.0
110
+ - wexample-wex-addon-app: >=29.0.0
111
111
 
112
112
  ## Versioning & Compatibility Policy
113
113
 
@@ -1,6 +1,6 @@
1
1
  # wex_addon_dev_javascript
2
2
 
3
- Version: 7.16.1
3
+ Version: 8.0.0
4
4
 
5
5
  Python dev addon for wex
6
6
 
@@ -84,8 +84,8 @@ Visit the [Wexample Suite documentation](https://docs.wexample.com) for the comp
84
84
  - cattrs: >=23.1.0
85
85
  - wexample-api: >=6.8.0
86
86
  - wexample-filestate-javascript: >=6.6.0
87
- - wexample-wex-addon-ai: >=12.0.0
88
- - wexample-wex-addon-app: >=28.0.0
87
+ - wexample-wex-addon-ai: >=12.1.0
88
+ - wexample-wex-addon-app: >=29.0.0
89
89
 
90
90
  ## Versioning & Compatibility Policy
91
91
 
@@ -6,7 +6,7 @@ build-backend = "pdm.backend"
6
6
 
7
7
  [project]
8
8
  name = "wexample-wex-addon-dev-javascript"
9
- version = "7.16.1"
9
+ version = "8.0.0"
10
10
  description = "Python dev addon for wex"
11
11
  authors = [
12
12
  { name = "weeger", email = "contact@wexample.com" },
@@ -22,8 +22,8 @@ dependencies = [
22
22
  "cattrs>=23.1.0",
23
23
  "wexample-api>=6.8.0",
24
24
  "wexample-filestate-javascript>=6.6.0",
25
- "wexample-wex-addon-ai>=12.0.0",
26
- "wexample-wex-addon-app>=28.0.0",
25
+ "wexample-wex-addon-ai>=12.1.0",
26
+ "wexample-wex-addon-app>=29.0.0",
27
27
  ]
28
28
 
29
29
  [project.readme]
@@ -1,60 +1,21 @@
1
1
  from __future__ import annotations
2
2
 
3
- from wexample_filestate.item.file.json_file import JsonFile
4
3
  from wexample_helpers.decorator.base_class import base_class
5
- from wexample_wex_addon_app.item.file.mixin.app_dependencies_config_file_mixin import (
6
- AppDependenciesConfigFileMixin,
4
+
5
+ from wexample_wex_addon_dev_javascript.file.node_package_json_file import (
6
+ NodePackageJsonFile,
7
7
  )
8
8
 
9
9
 
10
10
  @base_class
11
- class JavascriptPackageJsonFile(AppDependenciesConfigFileMixin, JsonFile):
12
- def add_dependency(
13
- self,
14
- operator: str = "",
15
- **kwargs,
16
- ) -> bool:
17
- # NPM uses bare versions (or ^/~), not "==".
18
- return super().add_dependency(
19
- operator=operator,
20
- **kwargs,
21
- )
22
-
23
- def add_dependency_from_string(
24
- self,
25
- package_name: str,
26
- version: str,
27
- operator: str = "",
28
- optional: bool = False,
29
- group: None | str = None,
30
- ) -> bool:
31
- """
32
- Add or update an npm dependency entry from a raw package name + version string.
33
- Equivalent to add_dependency() but without requiring a CodeBaseWorkdir.
34
- """
35
- if optional:
36
- section = "optionalDependencies"
37
- elif group == "dev":
38
- section = "devDependencies"
39
- elif group:
40
- section = group
41
- else:
42
- section = "dependencies"
43
-
44
- constraint = f"{operator}{version}".strip()
45
-
46
- config = self.read_config()
47
- deps_node = config.search(path=section, default={})
48
- deps = deps_node.to_dict() if deps_node else {}
49
-
50
- if deps.get(package_name) == constraint:
51
- return False
52
-
53
- deps[package_name] = constraint
54
- config.update_nested({section: deps})
55
- self.write_config(config)
56
-
57
- return True
11
+ class JavascriptPackageJsonFile(NodePackageJsonFile):
12
+ """A published javascript package's package.json.
13
+
14
+ On top of the dependency-manifest API, publication metadata (name,
15
+ version, repository, publishConfig, exports...) is derived from the
16
+ workdir at dump time — which is why this class must never be attached
17
+ to an app manifest (see NodePackageJsonFile).
18
+ """
58
19
 
59
20
  def dumps(self, content: dict | None = None) -> str:
60
21
  content = content or self.read_parsed()
@@ -71,16 +32,6 @@ class JavascriptPackageJsonFile(AppDependenciesConfigFileMixin, JsonFile):
71
32
 
72
33
  return super().dumps(content)
73
34
 
74
- def get_dependencies_versions(
75
- self, optional: bool = False, group: str = "dev"
76
- ) -> dict[str, str]:
77
- config = self.read_config()
78
- # Use to_dict_or_none() (not get_dict_or_default) so nested ConfigValue
79
- # wrappers are unwrapped to native str — matches the dict[str, str] signature.
80
- deps = config.search(path="dependencies").to_dict_or_none() or {}
81
- peer = config.search(path="peerDependencies").to_dict_or_none() or {}
82
- return {**deps, **peer}
83
-
84
35
  def _apply_default_publish_config(self, content: dict) -> None:
85
36
  content.setdefault("type", "module")
86
37
 
@@ -0,0 +1,75 @@
1
+ from __future__ import annotations
2
+
3
+ from wexample_filestate.item.file.json_file import JsonFile
4
+ from wexample_helpers.decorator.base_class import base_class
5
+ from wexample_wex_addon_app.item.file.mixin.app_dependencies_config_file_mixin import (
6
+ AppDependenciesConfigFileMixin,
7
+ )
8
+
9
+
10
+ @base_class
11
+ class NodePackageJsonFile(AppDependenciesConfigFileMixin, JsonFile):
12
+ """A package.json exposing the dependency-manifest API.
13
+
14
+ Suits any workdir shipping a package.json (polyglot apps included);
15
+ JavascriptPackageJsonFile extends it with package-publication concerns
16
+ (name/version stamping, publishConfig...) that must never touch an
17
+ app manifest.
18
+ """
19
+
20
+ def add_dependency(
21
+ self,
22
+ operator: str = "",
23
+ **kwargs,
24
+ ) -> bool:
25
+ # NPM uses bare versions (or ^/~), not "==".
26
+ return super().add_dependency(
27
+ operator=operator,
28
+ **kwargs,
29
+ )
30
+
31
+ def add_dependency_from_string(
32
+ self,
33
+ package_name: str,
34
+ version: str,
35
+ operator: str = "",
36
+ optional: bool = False,
37
+ group: None | str = None,
38
+ ) -> bool:
39
+ """
40
+ Add or update an npm dependency entry from a raw package name + version string.
41
+ Equivalent to add_dependency() but without requiring a CodeBaseWorkdir.
42
+ """
43
+ if optional:
44
+ section = "optionalDependencies"
45
+ elif group == "dev":
46
+ section = "devDependencies"
47
+ elif group:
48
+ section = group
49
+ else:
50
+ section = "dependencies"
51
+
52
+ constraint = f"{operator}{version}".strip()
53
+
54
+ config = self.read_config()
55
+ deps_node = config.search(path=section, default={})
56
+ deps = deps_node.to_dict() if deps_node else {}
57
+
58
+ if deps.get(package_name) == constraint:
59
+ return False
60
+
61
+ deps[package_name] = constraint
62
+ config.update_nested({section: deps})
63
+ self.write_config(config)
64
+
65
+ return True
66
+
67
+ def get_dependencies_versions(
68
+ self, optional: bool = False, group: str = "dev"
69
+ ) -> dict[str, str]:
70
+ config = self.read_config()
71
+ # Use to_dict_or_none() (not get_dict_or_default) so nested ConfigValue
72
+ # wrappers are unwrapped to native str — matches the dict[str, str] signature.
73
+ deps = config.search(path="dependencies").to_dict_or_none() or {}
74
+ peer = config.search(path="peerDependencies").to_dict_or_none() or {}
75
+ return {**deps, **peer}
@@ -0,0 +1,81 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from wexample_cli.const.tags import AudienceTag, EffectTag, ScopeTag
6
+ from wexample_cli.decorator.command import command
7
+ from wexample_wex_core.const.globals import COMMAND_TYPE_SERVICE
8
+
9
+ from wexample_wex_addon_dev_javascript.const.tags import DomainTag
10
+
11
+ if TYPE_CHECKING:
12
+ from wexample_cli.context.execution_context import ExecutionContext
13
+ from wexample_wex_addon_app.service.app_service import AppService
14
+
15
+ # In-container layout convention shared by the local dev composes: the app is
16
+ # served from APP_DIR and each locally-developed javascript suite is mounted
17
+ # under JS_DEV_DIR/<vendor> (see the `local_packages` env config section).
18
+ APP_DIR = "/var/www/html"
19
+ JS_DEV_DIR = "/var/www/javascript-dev"
20
+
21
+
22
+ @command(
23
+ type=COMMAND_TYPE_SERVICE,
24
+ description="Wire locally-developed npm packages into node_modules/ (local env only)",
25
+ tags=[
26
+ DomainTag.FRONTEND,
27
+ EffectTag.WRITE,
28
+ AudienceTag.AGENT_SAFE,
29
+ ScopeTag.APP,
30
+ ScopeTag.CONTAINER,
31
+ ScopeTag.LOCAL,
32
+ ],
33
+ )
34
+ def node__service__install_local(
35
+ context: ExecutionContext,
36
+ service: AppService,
37
+ ) -> None:
38
+ """Generic node_modules wiring, reusable by any service whose container
39
+ runs the app's javascript toolchain (symfony, laravel, vite…).
40
+
41
+ Delegating services call this with their own ``service`` so the work runs
42
+ inside their container.
43
+ """
44
+ js_packages = service.app_workdir.get_runtime_config().search(
45
+ "local_packages.javascript"
46
+ )
47
+ if js_packages.is_none():
48
+ context.io.log("No local_packages.javascript configured, nothing to wire.")
49
+ return
50
+
51
+ vendors = " ".join(sorted(js_packages.to_dict().keys()))
52
+
53
+ # Package managers keep the (possibly stale) registry version even when a
54
+ # newer local source exists, so the deterministic wiring is a forced
55
+ # symlink over node_modules/ — resolved by each package's declared scoped
56
+ # name, which differs from its directory name.
57
+ script = f"""
58
+ set -e
59
+ cd {APP_DIR}
60
+ if [ -f yarn.lock ]; then yarn install
61
+ elif [ -f pnpm-lock.yaml ]; then pnpm install
62
+ else npm install
63
+ fi
64
+ for vendor in {vendors}; do
65
+ [ -d "{JS_DEV_DIR}/$vendor" ] || continue
66
+ for src in {JS_DEV_DIR}/$vendor/*/; do
67
+ [ -f "$src/package.json" ] || continue
68
+ name=$(node -p "require('$src/package.json').name")
69
+ mkdir -p "node_modules/$(dirname "$name")"
70
+ rm -rf "node_modules/$name"
71
+ ln -s "${{src%/}}" "node_modules/$name"
72
+ echo " symlinked $name"
73
+ done
74
+ done
75
+ """
76
+
77
+ context.io.log(f"Wiring local npm packages ({vendors}) into node_modules/…")
78
+ output = service.addon_manager.docker_exec(
79
+ service.name, ["/bin/sh", "-c", script]
80
+ )
81
+ context.io.log(output)
@@ -0,0 +1,66 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from wexample_cli.const.tags import AudienceTag, EffectTag, ScopeTag
6
+ from wexample_cli.decorator.command import command
7
+ from wexample_cli.decorator.option import option
8
+ from wexample_wex_core.const.globals import COMMAND_TYPE_SERVICE
9
+
10
+ from wexample_wex_addon_dev_javascript.const.tags import DomainTag
11
+ from wexample_wex_addon_dev_javascript.services.node.commands.service.install_local import (
12
+ APP_DIR,
13
+ )
14
+
15
+ if TYPE_CHECKING:
16
+ from wexample_cli.context.execution_context import ExecutionContext
17
+ from wexample_wex_addon_app.service.app_service import AppService
18
+
19
+
20
+ @option(
21
+ name="npm_packages",
22
+ type=str,
23
+ required=True,
24
+ description="Space-separated npm package names whose constraint changed in package.json",
25
+ )
26
+ @command(
27
+ type=COMMAND_TYPE_SERVICE,
28
+ description="Refresh the javascript lock file after a package.json dependency change",
29
+ tags=[
30
+ DomainTag.FRONTEND,
31
+ EffectTag.WRITE,
32
+ AudienceTag.AGENT_SAFE,
33
+ ScopeTag.APP,
34
+ ScopeTag.CONTAINER,
35
+ ScopeTag.LOCAL,
36
+ ],
37
+ )
38
+ def node__service__refresh_lock(
39
+ context: ExecutionContext,
40
+ service: AppService,
41
+ npm_packages: str,
42
+ ) -> None:
43
+ """Generic javascript lock refresh, reusable by any service whose
44
+ container runs the app's javascript toolchain.
45
+
46
+ The lock file present in the app tells which package manager owns it.
47
+ Yarn classic has no lock-only mode, so node_modules is (re)installed
48
+ along the way — harmless inside the app container.
49
+ """
50
+ script = f"""
51
+ set -e
52
+ cd {APP_DIR}
53
+ if [ -f yarn.lock ]; then yarn install
54
+ elif [ -f pnpm-lock.yaml ]; then pnpm install --lockfile-only
55
+ elif [ -f package-lock.json ]; then npm install --package-lock-only
56
+ else
57
+ echo "No javascript lock file found in {APP_DIR}" >&2
58
+ exit 1
59
+ fi
60
+ """
61
+
62
+ context.io.log(f"Refreshing javascript lock file ({npm_packages})…")
63
+ output = service.addon_manager.docker_exec(
64
+ service.name, ["/bin/sh", "-c", script]
65
+ )
66
+ context.io.log(output)
@@ -35,6 +35,14 @@ class JavascriptPackageWorkdir(JavascriptWorkdir):
35
35
  def get_project_name(self) -> str:
36
36
  return f"@{self.get_vendor_name()}/{string_to_kebab_case(super().get_project_name())}"
37
37
 
38
+ # Lockfile → package manager + its lockfile-only refresh command. The
39
+ # lockfile present in the repo tells which manager the project uses.
40
+ _LOCKFILE_REFRESH_COMMANDS: dict[str, list[str]] = {
41
+ "package-lock.json": ["npm", "install", "--package-lock-only"],
42
+ "pnpm-lock.yaml": ["pnpm", "install", "--lockfile-only"],
43
+ "yarn.lock": ["yarn", "install", "--mode", "update-lockfile"],
44
+ }
45
+
38
46
  def prepare_value(self, raw_value: DictConfig | None = None) -> DictConfig:
39
47
  from wexample_filestate.const.disk import DiskItemType
40
48
  from wexample_helpers.helper.file import file_read
@@ -76,8 +84,20 @@ class JavascriptPackageWorkdir(JavascriptWorkdir):
76
84
 
77
85
  return raw_value
78
86
 
87
+ def release(self, **kwargs) -> None:
88
+ # The lockfile must follow package.json: CI installs run in strict
89
+ # mode (`npm ci` & co) and fail hard on any desync between the two.
90
+ # A sibling's propagate step may have bumped constraints here, so the
91
+ # refresh happens at release start — NOT at propagate time, when the
92
+ # sibling's new version is not registry-available yet (it publishes,
93
+ # then waits for the registry, before dependents release: the suite
94
+ # loop follows the dependency graph).
95
+ self._refresh_lockfile()
96
+ return super().release(**kwargs)
97
+
79
98
  def _classify_version_bump(self, last_tag: str) -> str:
80
99
  from wexample_helpers.const.types import (
100
+ UPGRADE_TYPE_INTERMEDIATE,
81
101
  UPGRADE_TYPE_MAJOR,
82
102
  UPGRADE_TYPE_MINOR,
83
103
  )
@@ -147,6 +167,24 @@ class JavascriptPackageWorkdir(JavascriptWorkdir):
147
167
 
148
168
  git_push_tag(tag, cwd=cwd, remote=remote, inherit_stdio=True)
149
169
 
170
+ def _refresh_lockfile(self) -> None:
171
+ import shutil
172
+
173
+ from wexample_helpers.helper.shell import shell_run
174
+
175
+ for lockfile, command in self._LOCKFILE_REFRESH_COMMANDS.items():
176
+ if not (self.get_path() / lockfile).exists():
177
+ continue
178
+ if shutil.which(command[0]) is None:
179
+ raise RuntimeError(
180
+ f"{lockfile} needs a refresh after a dependency change in "
181
+ f"{self.get_project_name()}, but '{command[0]}' is not "
182
+ f"available on this machine. A stale lockfile would break "
183
+ f"the strict CI install."
184
+ )
185
+ shell_run(command, cwd=self.get_path())
186
+ return
187
+
150
188
  def _wait_for_registry(self) -> None:
151
189
  """Poll the configured npm registry until the current version is available (max 20 min).
152
190