wexample-wex-addon-dev-javascript 8.1.5__py3-none-any.whl → 8.2.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/common/npm_registry_gateway.py +27 -2
- wexample_wex_addon_dev_javascript/const/registry.py +5 -0
- wexample_wex_addon_dev_javascript/services/node/commands/service/install_local.py +8 -0
- wexample_wex_addon_dev_javascript/workdir/javascript_package_workdir.py +167 -23
- wexample_wex_addon_dev_javascript/workdir/javascript_workdir.py +95 -0
- {wexample_wex_addon_dev_javascript-8.1.5.dist-info → wexample_wex_addon_dev_javascript-8.2.0.dist-info}/METADATA +6 -6
- {wexample_wex_addon_dev_javascript-8.1.5.dist-info → wexample_wex_addon_dev_javascript-8.2.0.dist-info}/RECORD +9 -8
- {wexample_wex_addon_dev_javascript-8.1.5.dist-info → wexample_wex_addon_dev_javascript-8.2.0.dist-info}/WHEEL +1 -1
- {wexample_wex_addon_dev_javascript-8.1.5.dist-info → wexample_wex_addon_dev_javascript-8.2.0.dist-info}/entry_points.txt +0 -0
|
@@ -24,6 +24,28 @@ class NpmRegistryGateway(AbstractGateway):
|
|
|
24
24
|
if self.token:
|
|
25
25
|
self.default_headers["Authorization"] = f"Bearer {self.token}"
|
|
26
26
|
|
|
27
|
+
def has_package(self, package: str) -> bool:
|
|
28
|
+
"""Return True iff `package` is known to this registry, at any version.
|
|
29
|
+
|
|
30
|
+
A 404 means it was never published — the signal the first-publication
|
|
31
|
+
bootstrap reads. An unreachable registry raises instead of answering
|
|
32
|
+
False, so that a network glitch is never mistaken for a new package.
|
|
33
|
+
"""
|
|
34
|
+
response = self.make_request(
|
|
35
|
+
endpoint=self._encode_package_name(package),
|
|
36
|
+
expected_status_codes=[200, 404],
|
|
37
|
+
fatal_if_unexpected=False,
|
|
38
|
+
quiet=True,
|
|
39
|
+
)
|
|
40
|
+
if response is None:
|
|
41
|
+
from wexample_helpers.error.gateway_error import GatewayError
|
|
42
|
+
|
|
43
|
+
raise GatewayError(
|
|
44
|
+
f"Registry {self.base_url} is unreachable: cannot tell whether "
|
|
45
|
+
f"{package} has ever been published."
|
|
46
|
+
)
|
|
47
|
+
return response.status_code == 200
|
|
48
|
+
|
|
27
49
|
def has_version(self, package: str, version: str) -> bool:
|
|
28
50
|
"""Return True iff `package@version` is published on this registry.
|
|
29
51
|
|
|
@@ -32,9 +54,8 @@ class NpmRegistryGateway(AbstractGateway):
|
|
|
32
54
|
registries that don't expose per-version URLs. Returns False on
|
|
33
55
|
404 (unknown package or version not yet propagated).
|
|
34
56
|
"""
|
|
35
|
-
encoded = package.replace("/", "%2F")
|
|
36
57
|
response = self.make_request(
|
|
37
|
-
endpoint=
|
|
58
|
+
endpoint=self._encode_package_name(package),
|
|
38
59
|
expected_status_codes=[200, 404],
|
|
39
60
|
fatal_if_unexpected=False,
|
|
40
61
|
quiet=True,
|
|
@@ -42,3 +63,7 @@ class NpmRegistryGateway(AbstractGateway):
|
|
|
42
63
|
if response is None or response.status_code != 200:
|
|
43
64
|
return False
|
|
44
65
|
return version in response.json().get("versions", {})
|
|
66
|
+
|
|
67
|
+
def _encode_package_name(self, package: str) -> str:
|
|
68
|
+
"""A scoped name carries a slash, which the URL must not read as a path."""
|
|
69
|
+
return package.replace("/", "%2F")
|
|
@@ -54,6 +54,14 @@ def node__service__install_local(
|
|
|
54
54
|
# newer local source exists, so the deterministic wiring is a forced
|
|
55
55
|
# symlink over node_modules/ — resolved by each package's declared scoped
|
|
56
56
|
# name, which differs from its directory name.
|
|
57
|
+
#
|
|
58
|
+
# Only the javascript suite is linked here. The npm face of a PHP package
|
|
59
|
+
# (`@wexample/symfony-loader` → `vendor/wexample/symfony-loader/assets`) is
|
|
60
|
+
# not: the app's package.json must declare it as `link:` so that the
|
|
61
|
+
# `yarn install` above makes the symlink itself. Declared as `file:`, yarn
|
|
62
|
+
# copies it, and the copy is what TypeScript falls back to when a mapped
|
|
63
|
+
# file goes missing — see the symfony-design-system knowledge page
|
|
64
|
+
# contributing/assets-as-npm-package.
|
|
57
65
|
script = f"""
|
|
58
66
|
set -e
|
|
59
67
|
cd {APP_DIR}
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from typing import TYPE_CHECKING
|
|
4
4
|
|
|
5
|
+
from wexample_helpers.classes.private_field import private_field
|
|
5
6
|
from wexample_helpers.decorator.base_class import base_class
|
|
6
7
|
from wexample_helpers.helper.string import string_to_kebab_case
|
|
7
8
|
from wexample_helpers_git.helper.git import (
|
|
@@ -22,9 +23,18 @@ if TYPE_CHECKING:
|
|
|
22
23
|
FrameworkPackageSuiteWorkdir,
|
|
23
24
|
)
|
|
24
25
|
|
|
26
|
+
from wexample_wex_addon_dev_javascript.common.npm_registry_gateway import (
|
|
27
|
+
NpmRegistryGateway,
|
|
28
|
+
)
|
|
29
|
+
|
|
25
30
|
|
|
26
31
|
@base_class
|
|
27
32
|
class JavascriptPackageWorkdir(JavascriptWorkdir):
|
|
33
|
+
_first_publication: bool = private_field(
|
|
34
|
+
default=False,
|
|
35
|
+
description="Set when this run created the package on the registry",
|
|
36
|
+
)
|
|
37
|
+
|
|
28
38
|
def get_package_dependency_name(self) -> str:
|
|
29
39
|
return self.get_package_import_name()
|
|
30
40
|
|
|
@@ -131,9 +141,80 @@ class JavascriptPackageWorkdir(JavascriptWorkdir):
|
|
|
131
141
|
|
|
132
142
|
return UPGRADE_TYPE_MAJOR
|
|
133
143
|
|
|
144
|
+
def _declare_trusted_publisher(self) -> None:
|
|
145
|
+
"""Let the CI workflow publish the next versions, through `npm trust`.
|
|
146
|
+
|
|
147
|
+
The token npm keeps in ~/.npmrc is refused for account changes, so a
|
|
148
|
+
short-lived session is opened for this alone — in its own config, so
|
|
149
|
+
the everyday token is left untouched — and revoked right after. npm
|
|
150
|
+
asks the user to confirm both the login and the declaration through
|
|
151
|
+
links it prints, which any device can open.
|
|
152
|
+
"""
|
|
153
|
+
import os
|
|
154
|
+
import shutil
|
|
155
|
+
import tempfile
|
|
156
|
+
|
|
157
|
+
from wexample_helpers.helper.user import user_get_real_gid, user_get_real_uid
|
|
158
|
+
|
|
159
|
+
repository, workflow = self._get_trusted_publisher_target()
|
|
160
|
+
config_dir = tempfile.mkdtemp(prefix="npm-trust-")
|
|
161
|
+
os.chown(config_dir, user_get_real_uid(), user_get_real_gid())
|
|
162
|
+
env = {"NPM_CONFIG_USERCONFIG": os.path.join(config_dir, ".npmrc")}
|
|
163
|
+
|
|
164
|
+
self.log("Declaring the CI as trusted publisher — confirm the two links npm prints.")
|
|
165
|
+
try:
|
|
166
|
+
self._shell_run_node_command(["npm", "login", "--auth-type=web"], env)
|
|
167
|
+
self._shell_run_node_command(
|
|
168
|
+
[
|
|
169
|
+
"npm",
|
|
170
|
+
"trust",
|
|
171
|
+
"github",
|
|
172
|
+
self.get_project_name(),
|
|
173
|
+
"--file",
|
|
174
|
+
workflow,
|
|
175
|
+
"--repo",
|
|
176
|
+
repository,
|
|
177
|
+
"--allow-publish",
|
|
178
|
+
"--yes",
|
|
179
|
+
],
|
|
180
|
+
env,
|
|
181
|
+
)
|
|
182
|
+
finally:
|
|
183
|
+
try:
|
|
184
|
+
self._shell_run_node_command(["npm", "logout"], env)
|
|
185
|
+
except Exception:
|
|
186
|
+
pass
|
|
187
|
+
shutil.rmtree(config_dir, ignore_errors=True)
|
|
188
|
+
|
|
189
|
+
self.success(f"CI declared as trusted publisher of {self.get_project_name()}.")
|
|
190
|
+
|
|
134
191
|
def _get_critical_directories(self) -> list[str]:
|
|
135
192
|
return ["src"]
|
|
136
193
|
|
|
194
|
+
def _get_first_publication_notice(self) -> str:
|
|
195
|
+
"""Why this first publication is slow, and what comes after it."""
|
|
196
|
+
return (
|
|
197
|
+
f"First publication of {self.get_project_name()}: npm usually needs "
|
|
198
|
+
"5 to 15 minutes to list a brand-new package.\n"
|
|
199
|
+
"Next versions publish from CI, which npm rejects until it is declared "
|
|
200
|
+
"as a trusted publisher. This is done right after the listing, and npm "
|
|
201
|
+
"will ask you to open two links to confirm: a login, then the "
|
|
202
|
+
"declaration itself."
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
def _get_manual_trusted_publisher_notice(self) -> str:
|
|
206
|
+
"""The by-hand fallback when the declaration could not be scripted."""
|
|
207
|
+
repository, workflow = self._get_trusted_publisher_target()
|
|
208
|
+
|
|
209
|
+
return (
|
|
210
|
+
f"Could not declare the trusted publisher of {self.get_project_name()}; "
|
|
211
|
+
"its next version will fail to publish until it is done by hand:\n"
|
|
212
|
+
f" npmjs.com → {self.get_project_name()} → Settings → Trusted Publisher "
|
|
213
|
+
"→ GitHub Actions\n"
|
|
214
|
+
f" repository: {repository}\n"
|
|
215
|
+
f" workflow: {workflow}"
|
|
216
|
+
)
|
|
217
|
+
|
|
137
218
|
def _get_readme_content(self) -> ReadmeContentConfigValue | None:
|
|
138
219
|
from wexample_wex_addon_dev_javascript.config_value.javascript_package_readme_config_value import (
|
|
139
220
|
JavascriptPackageReadmeContentConfigValue,
|
|
@@ -141,6 +222,25 @@ class JavascriptPackageWorkdir(JavascriptWorkdir):
|
|
|
141
222
|
|
|
142
223
|
return JavascriptPackageReadmeContentConfigValue(workdir=self)
|
|
143
224
|
|
|
225
|
+
def _get_registry_gateway(self) -> NpmRegistryGateway:
|
|
226
|
+
"""Gateway over the registry this package publishes to."""
|
|
227
|
+
from wexample_wex_addon_dev_javascript.common.npm_registry_gateway import (
|
|
228
|
+
NpmRegistryGateway,
|
|
229
|
+
)
|
|
230
|
+
from wexample_wex_addon_dev_javascript.const.registry import (
|
|
231
|
+
NPM_REGISTRY_URL_DEFAULT,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
config = self.get_runtime_config()
|
|
235
|
+
|
|
236
|
+
return NpmRegistryGateway(
|
|
237
|
+
base_url=config.search("npm.registry_url").get_str_or_none()
|
|
238
|
+
or NPM_REGISTRY_URL_DEFAULT,
|
|
239
|
+
token=config.search("npm.api_token").get_str_or_none(),
|
|
240
|
+
io=self.io,
|
|
241
|
+
timeout=10,
|
|
242
|
+
)
|
|
243
|
+
|
|
144
244
|
def _get_suite_workdir_class(self) -> type[FrameworkPackageSuiteWorkdir]:
|
|
145
245
|
from wexample_wex_addon_dev_javascript.workdir.javascript_packages_suite_workdir import (
|
|
146
246
|
JavascriptPackagesSuiteWorkdir,
|
|
@@ -148,10 +248,37 @@ class JavascriptPackageWorkdir(JavascriptWorkdir):
|
|
|
148
248
|
|
|
149
249
|
return JavascriptPackagesSuiteWorkdir
|
|
150
250
|
|
|
251
|
+
def _get_trusted_publisher_target(self) -> tuple[str, str]:
|
|
252
|
+
"""The `owner/repo` and workflow file npm must trust to publish."""
|
|
253
|
+
from urllib.parse import urlparse
|
|
254
|
+
|
|
255
|
+
url = self.get_app_config_file().read_parsed().get("repository") or ""
|
|
256
|
+
repository = urlparse(url.removeprefix("git+")).path.strip("/")
|
|
257
|
+
|
|
258
|
+
return repository.removesuffix(".git"), "publish.yml"
|
|
259
|
+
|
|
260
|
+
def _post_publish(self) -> None:
|
|
261
|
+
# After the registry wait on purpose: npm can only trust a package it
|
|
262
|
+
# already lists.
|
|
263
|
+
if not self._first_publication:
|
|
264
|
+
return
|
|
265
|
+
try:
|
|
266
|
+
self._declare_trusted_publisher()
|
|
267
|
+
except Exception:
|
|
268
|
+
self.warning(self._get_manual_trusted_publisher_notice())
|
|
269
|
+
|
|
151
270
|
def _publish(self, force: bool = False) -> None:
|
|
152
271
|
"""Push a git tag to the deployment remote to trigger a CI/CD publication workflow."""
|
|
153
272
|
from wexample_helpers_git.helper.git import git_push_tag
|
|
154
273
|
|
|
274
|
+
# Only public npm has the chicken-and-egg problem: a private registry
|
|
275
|
+
# (GitLab) lets its CI job token create the package itself.
|
|
276
|
+
if self.is_published_publicly() and not self._get_registry_gateway().has_package(
|
|
277
|
+
self.get_project_name()
|
|
278
|
+
):
|
|
279
|
+
self._publish_first_version()
|
|
280
|
+
return
|
|
281
|
+
|
|
155
282
|
remote = self._get_deployment_remote_name()
|
|
156
283
|
if not remote:
|
|
157
284
|
self.log("No deployment remote configured, skipping publication.")
|
|
@@ -167,11 +294,36 @@ class JavascriptPackageWorkdir(JavascriptWorkdir):
|
|
|
167
294
|
|
|
168
295
|
git_push_tag(tag, cwd=cwd, remote=remote, inherit_stdio=True)
|
|
169
296
|
|
|
297
|
+
def _publish_first_version(self) -> None:
|
|
298
|
+
"""Create the package on the registry from here, bypassing the CI workflow.
|
|
299
|
+
|
|
300
|
+
The workflow authenticates by OIDC alone (`id-token: write`, no token),
|
|
301
|
+
which npm only honours once the package has a trusted publisher — and
|
|
302
|
+
that is declared on a package settings page that does not exist until
|
|
303
|
+
the package does. So the very first version cannot come from CI, and
|
|
304
|
+
the CI tag is deliberately not pushed: it would start a run that can
|
|
305
|
+
only fail. Subsequent versions take the normal tag path again.
|
|
306
|
+
"""
|
|
307
|
+
manager = self._get_package_manager()
|
|
308
|
+
package = self.get_project_name()
|
|
309
|
+
|
|
310
|
+
self.log(
|
|
311
|
+
f"{package} is not on the registry yet: publishing this first "
|
|
312
|
+
"version locally, the workflow cannot create it."
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
# Same two steps as the workflow: publishing runs prepublishOnly,
|
|
316
|
+
# which type-checks against dependencies that must be installed.
|
|
317
|
+
self._shell_run_node_command([manager, "install"])
|
|
318
|
+
self._shell_run_node_command([manager, "publish", "--access", "public"])
|
|
319
|
+
|
|
320
|
+
self._first_publication = True
|
|
321
|
+
self.success(f"{package} now exists on the registry.")
|
|
322
|
+
self.warning(self._get_first_publication_notice())
|
|
323
|
+
|
|
170
324
|
def _refresh_lockfile(self) -> None:
|
|
171
325
|
import shutil
|
|
172
326
|
|
|
173
|
-
from wexample_helpers.helper.shell import shell_run
|
|
174
|
-
|
|
175
327
|
for lockfile, command in self._LOCKFILE_REFRESH_COMMANDS.items():
|
|
176
328
|
if not (self.get_path() / lockfile).exists():
|
|
177
329
|
continue
|
|
@@ -182,7 +334,7 @@ class JavascriptPackageWorkdir(JavascriptWorkdir):
|
|
|
182
334
|
f"available on this machine. A stale lockfile would break "
|
|
183
335
|
f"the strict CI install."
|
|
184
336
|
)
|
|
185
|
-
|
|
337
|
+
self._shell_run_node_command(command)
|
|
186
338
|
return
|
|
187
339
|
|
|
188
340
|
def _wait_for_registry(self) -> None:
|
|
@@ -196,25 +348,9 @@ class JavascriptPackageWorkdir(JavascriptWorkdir):
|
|
|
196
348
|
PollingCallbackManager,
|
|
197
349
|
)
|
|
198
350
|
|
|
199
|
-
from wexample_wex_addon_dev_javascript.common.npm_registry_gateway import (
|
|
200
|
-
NpmRegistryGateway,
|
|
201
|
-
)
|
|
202
|
-
|
|
203
351
|
package = self.get_project_name()
|
|
204
352
|
version = self.get_setup_version()
|
|
205
|
-
|
|
206
|
-
registry_base = (
|
|
207
|
-
self.get_runtime_config().search("npm.registry_url").get_str_or_none()
|
|
208
|
-
or "https://registry.npmjs.org"
|
|
209
|
-
)
|
|
210
|
-
token = self.get_runtime_config().search("npm.api_token").get_str_or_none()
|
|
211
|
-
|
|
212
|
-
gateway = NpmRegistryGateway(
|
|
213
|
-
base_url=registry_base,
|
|
214
|
-
token=token,
|
|
215
|
-
io=self.io,
|
|
216
|
-
timeout=10,
|
|
217
|
-
)
|
|
353
|
+
gateway = self._get_registry_gateway()
|
|
218
354
|
|
|
219
355
|
max_attempts = 40
|
|
220
356
|
delay_seconds = 30
|
|
@@ -222,9 +358,16 @@ class JavascriptPackageWorkdir(JavascriptWorkdir):
|
|
|
222
358
|
self.log(f"Waiting for {package}@{version} to appear on registry…")
|
|
223
359
|
|
|
224
360
|
def on_retry(attempt, max_a, delay, _exc, _msg) -> None:
|
|
225
|
-
self.
|
|
226
|
-
|
|
227
|
-
|
|
361
|
+
if not self._first_publication:
|
|
362
|
+
self.log(
|
|
363
|
+
f"Not yet available (attempt {attempt}/{max_a}), retrying in {delay}s…"
|
|
364
|
+
)
|
|
365
|
+
# A first listing is expected to be slow and was announced as
|
|
366
|
+
# such: one line every two minutes is enough to show it is alive.
|
|
367
|
+
elif attempt % 4 == 0:
|
|
368
|
+
self.log(
|
|
369
|
+
f"npm is still listing {package} ({attempt * delay // 60} min)…"
|
|
370
|
+
)
|
|
228
371
|
|
|
229
372
|
PollingCallbackManager(
|
|
230
373
|
callback=lambda: True if gateway.has_version(package, version) else None,
|
|
@@ -238,3 +381,4 @@ class JavascriptPackageWorkdir(JavascriptWorkdir):
|
|
|
238
381
|
).run()
|
|
239
382
|
|
|
240
383
|
self.success(f"{package}@{version} is available.")
|
|
384
|
+
|
|
@@ -24,6 +24,21 @@ if TYPE_CHECKING:
|
|
|
24
24
|
|
|
25
25
|
@base_class
|
|
26
26
|
class JavascriptWorkdir(WithAiWorkdirMixin, CodeBaseWorkdir):
|
|
27
|
+
# What a test file looks like, whatever the runner the package picked.
|
|
28
|
+
_TEST_FILE_SUFFIXES: tuple[str, ...] = tuple(
|
|
29
|
+
f".{kind}.{extension}"
|
|
30
|
+
for kind in ("test", "spec")
|
|
31
|
+
for extension in ("js", "jsx", "mjs", "cjs", "ts", "tsx", "mts", "cts")
|
|
32
|
+
)
|
|
33
|
+
# Lockfile → the package manager that wrote it. Order matters: a repo
|
|
34
|
+
# carrying several lockfiles is read as the first one listed, exactly as
|
|
35
|
+
# JavascriptPackageWorkdir._LOCKFILE_REFRESH_COMMANDS does.
|
|
36
|
+
_LOCKFILE_PACKAGE_MANAGERS: dict[str, str] = {
|
|
37
|
+
"package-lock.json": "npm",
|
|
38
|
+
"pnpm-lock.yaml": "pnpm",
|
|
39
|
+
"yarn.lock": "yarn",
|
|
40
|
+
}
|
|
41
|
+
|
|
27
42
|
def get_app_config_file(self, reload: bool = True) -> JavascriptPackageJsonFile:
|
|
28
43
|
from wexample_wex_addon_dev_javascript.file.javascript_package_json_file import (
|
|
29
44
|
JavascriptPackageJsonFile,
|
|
@@ -51,6 +66,17 @@ class JavascriptWorkdir(WithAiWorkdirMixin, CodeBaseWorkdir):
|
|
|
51
66
|
|
|
52
67
|
return options
|
|
53
68
|
|
|
69
|
+
def has_tests(self) -> bool:
|
|
70
|
+
# The tests/ directory itself is filestate-managed scaffolding and
|
|
71
|
+
# exists for every JavaScript package; only actual test files count.
|
|
72
|
+
tests_path = self.get_path() / "tests"
|
|
73
|
+
if not tests_path.is_dir():
|
|
74
|
+
return False
|
|
75
|
+
return any(
|
|
76
|
+
path.name.endswith(self._TEST_FILE_SUFFIXES)
|
|
77
|
+
for path in tests_path.rglob("*")
|
|
78
|
+
)
|
|
79
|
+
|
|
54
80
|
def prepare_value(self, raw_value: DictConfig | None = None) -> DictConfig:
|
|
55
81
|
from wexample_filestate.const.disk import DiskItemType
|
|
56
82
|
|
|
@@ -124,6 +150,32 @@ class JavascriptWorkdir(WithAiWorkdirMixin, CodeBaseWorkdir):
|
|
|
124
150
|
|
|
125
151
|
return raw_value
|
|
126
152
|
|
|
153
|
+
def test_run(self, format: str | None = None) -> None:
|
|
154
|
+
"""Run the package's own `test` script; raise on failure.
|
|
155
|
+
|
|
156
|
+
A JavaScript package names its runner itself — vitest, jest, plain
|
|
157
|
+
node — so the script declared in package.json is the only portable
|
|
158
|
+
entry point. `format` is ignored: unlike pytest-cov, nothing here
|
|
159
|
+
produces a coverage report to shape.
|
|
160
|
+
"""
|
|
161
|
+
manager = self._get_package_manager()
|
|
162
|
+
scripts = self.get_app_config_file().read_parsed().get("scripts", {})
|
|
163
|
+
|
|
164
|
+
if "test" not in scripts:
|
|
165
|
+
raise NotImplementedError(
|
|
166
|
+
f"{self.get_project_name()} ships test files under tests/ but its "
|
|
167
|
+
"package.json declares no `test` script; add one so they can run."
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# Tests import the dependencies, which the publication flow never
|
|
171
|
+
# installs on its own — a fresh clone would fail on the first import
|
|
172
|
+
# rather than on a real assertion.
|
|
173
|
+
if not any(self.get_path().glob("node_modules/*")):
|
|
174
|
+
self.log(f"Installing {self.get_project_name()} dependencies before tests…")
|
|
175
|
+
self._shell_run_node_command([manager, "install"])
|
|
176
|
+
|
|
177
|
+
self._shell_run_node_command([manager, "run", "test"])
|
|
178
|
+
|
|
127
179
|
def _canonicalize_dep_name(self, name: str) -> str:
|
|
128
180
|
return name
|
|
129
181
|
|
|
@@ -150,3 +202,46 @@ class JavascriptWorkdir(WithAiWorkdirMixin, CodeBaseWorkdir):
|
|
|
150
202
|
|
|
151
203
|
def _default_dependency_operator(self) -> str:
|
|
152
204
|
return "^"
|
|
205
|
+
|
|
206
|
+
def _get_node_command_environment(self) -> dict[str, str]:
|
|
207
|
+
"""Environment a node command needs to be the invoking user's own."""
|
|
208
|
+
import os
|
|
209
|
+
|
|
210
|
+
from wexample_helpers.helper.user import user_get_real_home, user_get_real_path
|
|
211
|
+
|
|
212
|
+
# Under sudo HOME still points at /root: npm would read root's .npmrc
|
|
213
|
+
# (no registry token) and root's cache, which the lifecycle scripts —
|
|
214
|
+
# dropped back to the invoking uid by npm — then cannot even write.
|
|
215
|
+
# PATH is sudo's secure_path: the distribution's node, not nvm's.
|
|
216
|
+
return {
|
|
217
|
+
**os.environ,
|
|
218
|
+
"HOME": user_get_real_home(),
|
|
219
|
+
"PATH": user_get_real_path(),
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
def _get_package_manager(self) -> str:
|
|
223
|
+
"""The package manager this repository uses, told by its lockfile."""
|
|
224
|
+
for lockfile, manager in self._LOCKFILE_PACKAGE_MANAGERS.items():
|
|
225
|
+
if (self.get_path() / lockfile).exists():
|
|
226
|
+
return manager
|
|
227
|
+
return "npm"
|
|
228
|
+
|
|
229
|
+
def _shell_run_node_command(
|
|
230
|
+
self, cmd: list[str], extra_env: dict[str, str] | None = None
|
|
231
|
+
) -> None:
|
|
232
|
+
"""Run a node toolchain command as the user who invoked wex.
|
|
233
|
+
|
|
234
|
+
Publication carries @as_sudo, so the process is root by the time it
|
|
235
|
+
reaches here: npm run as root would use root's node, token and cache.
|
|
236
|
+
Earlier release steps ran as root too, so whatever they wrote in the
|
|
237
|
+
workdir (package.json, lockfile…) is handed back first — npm could not
|
|
238
|
+
rewrite it otherwise.
|
|
239
|
+
"""
|
|
240
|
+
from wexample_helpers.helper.shell import shell_run
|
|
241
|
+
from wexample_helpers.helper.user import user_run_as_real_user
|
|
242
|
+
|
|
243
|
+
self.restore_real_user_ownership()
|
|
244
|
+
env = {**self._get_node_command_environment(), **(extra_env or {})}
|
|
245
|
+
|
|
246
|
+
with user_run_as_real_user():
|
|
247
|
+
shell_run(cmd, cwd=self.get_path(), env=env, inherit_stdio=True)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: wexample-wex-addon-dev-javascript
|
|
3
|
-
Version: 8.
|
|
3
|
+
Version: 8.2.0
|
|
4
4
|
Summary: Extends wex with JavaScript support: enforces JS/TS project layout and symlinks local npm packages into node_modules
|
|
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>=
|
|
17
|
-
Requires-Dist: wexample-wex-addon-app>=
|
|
16
|
+
Requires-Dist: wexample-wex-addon-ai>=16.0.0
|
|
17
|
+
Requires-Dist: wexample-wex-addon-app>=32.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: 8.
|
|
25
|
+
Version: 8.2.0
|
|
26
26
|
|
|
27
27
|
`wex-addon-dev-javascript` extends wex for JavaScript and TypeScript developers: it enforces a standard project layout — requiring `package.json`, `tsconfig.json`, `.npmrc`, `src/`, `tests/`, and the corresponding `.gitignore` rules — and in local environments symlinks locally-developed npm packages directly into a running container's `node_modules/` so monorepo packages resolve without a registry publish cycle. It also contributes a `node` service and a `vite` service, each backed by Docker Compose, where the vite service patches `allowedHosts` in `vite.config.ts` for reverse-proxy use and the node service handles lockfile refresh, annotated-tag release, and registry polling for npm package suites managed through wex.
|
|
28
28
|
|
|
@@ -251,8 +251,8 @@ Visit the [Wexample Suite documentation](https://docs.wexample.com) for the comp
|
|
|
251
251
|
- cattrs: >=23.1.0
|
|
252
252
|
- wexample-api: >=6.8.0
|
|
253
253
|
- wexample-filestate-javascript: >=6.6.0
|
|
254
|
-
- wexample-wex-addon-ai: >=
|
|
255
|
-
- wexample-wex-addon-app: >=
|
|
254
|
+
- wexample-wex-addon-ai: >=16.0.0
|
|
255
|
+
- wexample-wex-addon-app: >=32.0.0
|
|
256
256
|
|
|
257
257
|
## Versioning & Compatibility Policy
|
|
258
258
|
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
wexample_wex_addon_dev_javascript-8.
|
|
2
|
-
wexample_wex_addon_dev_javascript-8.
|
|
3
|
-
wexample_wex_addon_dev_javascript-8.
|
|
1
|
+
wexample_wex_addon_dev_javascript-8.2.0.dist-info/METADATA,sha256=jQSdi-wtyOIrG76xCCSSOrMF4_UTHmj0i24IjqRQpj8,18012
|
|
2
|
+
wexample_wex_addon_dev_javascript-8.2.0.dist-info/WHEEL,sha256=Y8xFLOH-FrS4gLE6ON4ti06sl2W_9QUDiRaTK3tfwig,91
|
|
3
|
+
wexample_wex_addon_dev_javascript-8.2.0.dist-info/entry_points.txt,sha256=WwYdg3RL2Ey1fg4JNNkcQxZcQRgZSgPLLzSDi6lbYgI,143
|
|
4
4
|
wexample_wex_addon_dev_javascript/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
5
|
wexample_wex_addon_dev_javascript/__pycache__/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
6
|
wexample_wex_addon_dev_javascript/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
-
wexample_wex_addon_dev_javascript/common/npm_registry_gateway.py,sha256=
|
|
7
|
+
wexample_wex_addon_dev_javascript/common/npm_registry_gateway.py,sha256=jcAIOZN1eiPeRSjradY095VI93ne86ijbVAZcXzCKpQ,2742
|
|
8
8
|
wexample_wex_addon_dev_javascript/config_value/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
9
|
wexample_wex_addon_dev_javascript/config_value/javascript_package_readme_config_value.py,sha256=FaYjL6A6Zv8sYnz4cWqD4DsrNO9EMYvSi1-8GCbmNvs,522
|
|
10
10
|
wexample_wex_addon_dev_javascript/const/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
wexample_wex_addon_dev_javascript/const/registry.py,sha256=pkGypHvxhPeb6-Quyv8Vodt8LIBmbPYHV7ibL1TemSw,168
|
|
11
12
|
wexample_wex_addon_dev_javascript/const/tags.py,sha256=qirMGuj7UqenempTZeQfJfMa_bC7mt-yLD_BkLnMnKE,307
|
|
12
13
|
wexample_wex_addon_dev_javascript/file/__init__.py,sha256=U4S_2y3zgLZVfMenHRaJFBW8yqh2mUBuI291LGQVOJ8,35
|
|
13
14
|
wexample_wex_addon_dev_javascript/file/javascript_package_json_file.py,sha256=yn1ltv3oBYNg1OvUytJ4PEzH8FSJS2KcIKK8y-cMrl4,2906
|
|
@@ -25,7 +26,7 @@ wexample_wex_addon_dev_javascript/services/__init__.py,sha256=47DEQpj8HBSa-_TImW
|
|
|
25
26
|
wexample_wex_addon_dev_javascript/services/node/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
26
27
|
wexample_wex_addon_dev_javascript/services/node/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
27
28
|
wexample_wex_addon_dev_javascript/services/node/commands/service/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
28
|
-
wexample_wex_addon_dev_javascript/services/node/commands/service/install_local.py,sha256=
|
|
29
|
+
wexample_wex_addon_dev_javascript/services/node/commands/service/install_local.py,sha256=U4WjWabvGhlET5zocCfKIT-1WwQJbRK0GOGD7krwTnE,3283
|
|
29
30
|
wexample_wex_addon_dev_javascript/services/node/commands/service/refresh_lock.py,sha256=qlGmvYxjnXxrr1DILjBBLfp9kTruuBe-5SYEzxvVNIA,2204
|
|
30
31
|
wexample_wex_addon_dev_javascript/services/node/docker/.wex.yml,sha256=JdIF6nGkFcfi76yZYGlXxeWfOK4eAUSV_gAn6i3hk2w,32
|
|
31
32
|
wexample_wex_addon_dev_javascript/services/node/docker/docker-compose.yml,sha256=GF22uk3PDtywP_GAF0RbfJ7lQ5P5nuuwJQE9ohwQh28,362
|
|
@@ -45,7 +46,7 @@ wexample_wex_addon_dev_javascript/services/vite/docker/.wex.yml,sha256=JdIF6nGkF
|
|
|
45
46
|
wexample_wex_addon_dev_javascript/services/vite/docker/docker-compose.yml,sha256=M7ejIcht0GxuEAEEcK3urBIrXA66JwYrr5EPUA9nMMM,595
|
|
46
47
|
wexample_wex_addon_dev_javascript/services/vite/service.yml,sha256=8mFPv49QbKA3klHFbNK73QOB7ryy38zFeLiQB3gfDys,184
|
|
47
48
|
wexample_wex_addon_dev_javascript/workdir/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
48
|
-
wexample_wex_addon_dev_javascript/workdir/javascript_package_workdir.py,sha256=
|
|
49
|
+
wexample_wex_addon_dev_javascript/workdir/javascript_package_workdir.py,sha256=mRTWei7NtmUzr5Ht1ZYVOdv_18tZ6BzruJMTuzy20s4,15612
|
|
49
50
|
wexample_wex_addon_dev_javascript/workdir/javascript_packages_suite_workdir.py,sha256=b0QoTWlNYOYfpqEb8vYzL4WRoY0WwBhYOFUoRc4oySI,928
|
|
50
|
-
wexample_wex_addon_dev_javascript/workdir/javascript_workdir.py,sha256=
|
|
51
|
-
wexample_wex_addon_dev_javascript-8.
|
|
51
|
+
wexample_wex_addon_dev_javascript/workdir/javascript_workdir.py,sha256=Wff6pgdPzsaf2zDfkVKytVHHp8rAXaXesz0G8Svh5z4,9529
|
|
52
|
+
wexample_wex_addon_dev_javascript-8.2.0.dist-info/RECORD,,
|