wexample-wex-addon-dev-javascript 7.16.1__py3-none-any.whl → 8.1.0__py3-none-any.whl
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.
- wexample_wex_addon_dev_javascript/config_value/javascript_package_readme_config_value.py +5 -2
- wexample_wex_addon_dev_javascript/file/javascript_package_json_file.py +11 -60
- wexample_wex_addon_dev_javascript/file/node_package_json_file.py +75 -0
- wexample_wex_addon_dev_javascript/formatter/__init__.py +0 -0
- wexample_wex_addon_dev_javascript/formatter/javascript_code_formatter.py +43 -0
- wexample_wex_addon_dev_javascript/javascript_addon_manager.py +32 -2
- wexample_wex_addon_dev_javascript/resources/design_rules/javascript_code.md +17 -0
- wexample_wex_addon_dev_javascript/resources/writing_rules/javascript_code.md +26 -0
- wexample_wex_addon_dev_javascript/services/node/commands/__init__.py +0 -0
- wexample_wex_addon_dev_javascript/services/node/commands/service/__init__.py +0 -0
- wexample_wex_addon_dev_javascript/services/node/commands/service/install_local.py +81 -0
- wexample_wex_addon_dev_javascript/services/node/commands/service/refresh_lock.py +71 -0
- wexample_wex_addon_dev_javascript/workdir/javascript_package_workdir.py +38 -0
- wexample_wex_addon_dev_javascript-8.1.0.dist-info/METADATA +309 -0
- {wexample_wex_addon_dev_javascript-7.16.1.dist-info → wexample_wex_addon_dev_javascript-8.1.0.dist-info}/RECORD +17 -8
- wexample_wex_addon_dev_javascript-7.16.1.dist-info/METADATA +0 -475
- {wexample_wex_addon_dev_javascript-7.16.1.dist-info → wexample_wex_addon_dev_javascript-8.1.0.dist-info}/WHEEL +0 -0
- {wexample_wex_addon_dev_javascript-7.16.1.dist-info → wexample_wex_addon_dev_javascript-8.1.0.dist-info}/entry_points.txt +0 -0
|
@@ -10,5 +10,8 @@ from wexample_wex_addon_app.config_value.app_readme_config_value import (
|
|
|
10
10
|
class JavascriptPackageReadmeContentConfigValue(AppReadmeConfigValue):
|
|
11
11
|
"""README generation for Javascript packages."""
|
|
12
12
|
|
|
13
|
-
def _get_app_description(self) -> str:
|
|
14
|
-
return
|
|
13
|
+
def _get_app_description(self) -> str | None:
|
|
14
|
+
return (
|
|
15
|
+
self.workdir.get_app_config().get("description")
|
|
16
|
+
or super()._get_app_description()
|
|
17
|
+
)
|
|
@@ -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
|
-
|
|
6
|
-
|
|
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(
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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}
|
|
File without changes
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, ClassVar
|
|
4
|
+
|
|
5
|
+
from wexample_helpers.decorator.base_class import base_class
|
|
6
|
+
from wexample_wex_addon_ai.formatter.abstract_formatter import AbstractFormatter
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@base_class
|
|
13
|
+
class JavascriptCodeFormatter(AbstractFormatter):
|
|
14
|
+
"""TypeScript and JavaScript shipped by an npm package, under its `src/`.
|
|
15
|
+
|
|
16
|
+
Covers both extensions because the packages are TypeScript sources published as
|
|
17
|
+
ESM, and a `.js` under `src/` is the same code with the types left out. Build
|
|
18
|
+
output, tests and config files answer elsewhere.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
FORMATTER_NAME: ClassVar[str] = "javascript-code"
|
|
22
|
+
SOURCE_SUFFIXES: ClassVar[set[str]] = {".ts", ".tsx", ".js", ".jsx", ".mts", ".cts"}
|
|
23
|
+
|
|
24
|
+
def get_design_rules(self) -> str:
|
|
25
|
+
return self.read_resource("design_rules", "javascript_code.md")
|
|
26
|
+
|
|
27
|
+
def get_writing_rules(self) -> str:
|
|
28
|
+
return self.read_resource("writing_rules", "javascript_code.md")
|
|
29
|
+
|
|
30
|
+
def matches_path(self, path: Path) -> bool:
|
|
31
|
+
"""A source file under the `src/` of a directory holding a `package.json`.
|
|
32
|
+
|
|
33
|
+
`node_modules` is excluded outright: every dependency in it carries its own
|
|
34
|
+
`package.json` and would otherwise match exactly like the package that
|
|
35
|
+
installed it.
|
|
36
|
+
"""
|
|
37
|
+
if path.suffix not in self.SOURCE_SUFFIXES or "node_modules" in path.parts:
|
|
38
|
+
return False
|
|
39
|
+
|
|
40
|
+
for parent in path.parents:
|
|
41
|
+
if parent.name == "src" and (parent.parent / "package.json").exists():
|
|
42
|
+
return True
|
|
43
|
+
return False
|
|
@@ -1,7 +1,37 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from wexample_wex_addon_ai.formatter.formatter_contributing_addon_mixin import (
|
|
6
|
+
FormatterContributingAddonMixin,
|
|
7
|
+
)
|
|
3
8
|
from wexample_wex_core.common.abstract_addon_manager import AbstractAddonManager
|
|
4
9
|
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from wexample_wex_addon_ai.formatter.abstract_formatter import AbstractFormatter
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class JavascriptAddonManager(FormatterContributingAddonMixin, AbstractAddonManager):
|
|
15
|
+
def get_formatter_classes(self) -> list[type[AbstractFormatter]]:
|
|
16
|
+
from wexample_wex_addon_dev_javascript.formatter.javascript_code_formatter import (
|
|
17
|
+
JavascriptCodeFormatter,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
return [JavascriptCodeFormatter]
|
|
21
|
+
|
|
22
|
+
def get_workdir_types(self) -> dict[str, type]:
|
|
23
|
+
from wexample_wex_addon_dev_javascript.workdir.javascript_package_workdir import (
|
|
24
|
+
JavascriptPackageWorkdir,
|
|
25
|
+
)
|
|
26
|
+
from wexample_wex_addon_dev_javascript.workdir.javascript_packages_suite_workdir import (
|
|
27
|
+
JavascriptPackagesSuiteWorkdir,
|
|
28
|
+
)
|
|
29
|
+
from wexample_wex_addon_dev_javascript.workdir.javascript_workdir import (
|
|
30
|
+
JavascriptWorkdir,
|
|
31
|
+
)
|
|
5
32
|
|
|
6
|
-
|
|
7
|
-
|
|
33
|
+
return {
|
|
34
|
+
"javascript": JavascriptWorkdir,
|
|
35
|
+
"javascript-package": JavascriptPackageWorkdir,
|
|
36
|
+
"javascript-packages-suite": JavascriptPackagesSuiteWorkdir,
|
|
37
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
This file is design rules for TypeScript shipped by an npm package, under its `src/`.
|
|
2
|
+
Decide the layout below before opening an editor — by the time a file is being written,
|
|
3
|
+
its path is already a decision made.
|
|
4
|
+
|
|
5
|
+
- One class per file, one file per class, and the file name is the class name in
|
|
6
|
+
PascalCase — `Common/AsyncConstructor.ts` holds `AsyncConstructor` and nothing else.
|
|
7
|
+
The class is the file's `export default`.
|
|
8
|
+
- Helper functions are grouped by the type they act on, one file per type, named after
|
|
9
|
+
it: `Helper/Array.ts`, `Helper/String.ts`, `Helper/Dom.ts`. They are named exports,
|
|
10
|
+
never a default, and each name is prefixed with the file's subject —
|
|
11
|
+
`arrayShallowCopy`, `stringToKebabCase`. The prefix is what makes the import readable
|
|
12
|
+
at the call site, where the file name is no longer visible.
|
|
13
|
+
- Kinds live apart, each in its own directory under `src/`: classes in `Common/`, helper
|
|
14
|
+
functions in `Helper/`. A class dropped next to the helpers because it serves them is
|
|
15
|
+
the case to move.
|
|
16
|
+
- No barrel files. Import from the module that defines the symbol, so that a consumer
|
|
17
|
+
bundling one helper does not pull the package in.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
This file is TypeScript shipped by an npm package, under its `src/`. The file's path is
|
|
2
|
+
already decided by the time these rules apply — see
|
|
3
|
+
`ai::design/rules --formatter javascript-code` for the layout decisions that come before
|
|
4
|
+
it.
|
|
5
|
+
|
|
6
|
+
- Code, names, comments and doc comments are in English, whatever language the
|
|
7
|
+
conversation is held in.
|
|
8
|
+
- Relative imports carry the `.js` extension even though the source is `.ts` —
|
|
9
|
+
`import { functionIsType } from '../Helper/Function.js'`. The packages publish native
|
|
10
|
+
ESM, where the specifier is resolved as written at runtime and an extensionless path
|
|
11
|
+
has nothing to resolve.
|
|
12
|
+
- The packages compile under `strict`, so every parameter and every return is typed, and
|
|
13
|
+
`any` is a decision to justify rather than a default. Prefer a generic that keeps the
|
|
14
|
+
caller's type (`<T>(array: T[]): T[]`) over widening to `unknown[]` and casting back.
|
|
15
|
+
- A `try` is occasional and justified: it catches a failure somewhere that knows what to do
|
|
16
|
+
about it. Wrapping a call defensively, or writing a `catch` that swallows and a `finally`
|
|
17
|
+
that lets the failure pass, turns a bug into a wrong result reported nowhere. Let it
|
|
18
|
+
throw — a stack trace at the point of failure is worth more than a silent fallback
|
|
19
|
+
discovered three layers later.
|
|
20
|
+
- Defensive code goes the same way: a guard against a state that cannot happen, a `typeof`
|
|
21
|
+
check on something the type already guarantees, a `?.` or `|| fallback` on a value that
|
|
22
|
+
is never missing. Validate at the boundary — user input, an external API, a parsed
|
|
23
|
+
payload — and trust what our own code hands you.
|
|
24
|
+
- Comment only what the code cannot say: a constraint, an invariant, the reason a
|
|
25
|
+
surprising line is the way it is. A comment that no longer matches the code it
|
|
26
|
+
describes is deleted, not updated around.
|
|
File without changes
|
|
File without changes
|
|
@@ -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,71 @@
|
|
|
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=False,
|
|
24
|
+
default="",
|
|
25
|
+
description="Space-separated npm package names whose constraint changed in package.json",
|
|
26
|
+
)
|
|
27
|
+
@command(
|
|
28
|
+
type=COMMAND_TYPE_SERVICE,
|
|
29
|
+
description="Refresh the javascript lock file after a package.json dependency change",
|
|
30
|
+
tags=[
|
|
31
|
+
DomainTag.FRONTEND,
|
|
32
|
+
EffectTag.WRITE,
|
|
33
|
+
AudienceTag.AGENT_SAFE,
|
|
34
|
+
ScopeTag.APP,
|
|
35
|
+
ScopeTag.CONTAINER,
|
|
36
|
+
ScopeTag.LOCAL,
|
|
37
|
+
],
|
|
38
|
+
)
|
|
39
|
+
def node__service__refresh_lock(
|
|
40
|
+
context: ExecutionContext,
|
|
41
|
+
service: AppService,
|
|
42
|
+
npm_packages: str = "",
|
|
43
|
+
) -> None:
|
|
44
|
+
"""Generic javascript lock refresh, reusable by any service whose
|
|
45
|
+
container runs the app's javascript toolchain.
|
|
46
|
+
|
|
47
|
+
The lock file present in the app tells which package manager owns it.
|
|
48
|
+
Yarn classic has no lock-only mode, so node_modules is (re)installed
|
|
49
|
+
along the way — harmless inside the app container.
|
|
50
|
+
"""
|
|
51
|
+
# The hook reaches every service, carrying only the manifests that moved.
|
|
52
|
+
if not npm_packages:
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
script = f"""
|
|
56
|
+
set -e
|
|
57
|
+
cd {APP_DIR}
|
|
58
|
+
if [ -f yarn.lock ]; then yarn install
|
|
59
|
+
elif [ -f pnpm-lock.yaml ]; then pnpm install --lockfile-only
|
|
60
|
+
elif [ -f package-lock.json ]; then npm install --package-lock-only
|
|
61
|
+
else
|
|
62
|
+
echo "No javascript lock file found in {APP_DIR}" >&2
|
|
63
|
+
exit 1
|
|
64
|
+
fi
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
context.io.log(f"Refreshing javascript lock file ({npm_packages})…")
|
|
68
|
+
output = service.addon_manager.docker_exec(
|
|
69
|
+
service.name, ["/bin/sh", "-c", script]
|
|
70
|
+
)
|
|
71
|
+
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
|
|