Flask-Node 0.1.2__tar.gz → 0.2.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.
- {flask_node-0.1.2/src/Flask_Node.egg-info → flask_node-0.2.0}/PKG-INFO +75 -2
- {flask_node-0.1.2 → flask_node-0.2.0}/README.md +74 -1
- {flask_node-0.1.2 → flask_node-0.2.0}/pyproject.toml +1 -1
- {flask_node-0.1.2 → flask_node-0.2.0/src/Flask_Node.egg-info}/PKG-INFO +75 -2
- {flask_node-0.1.2 → flask_node-0.2.0}/src/Flask_Node.egg-info/SOURCES.txt +2 -0
- {flask_node-0.1.2 → flask_node-0.2.0}/src/flask_node/__init__.py +2 -0
- flask_node-0.2.0/src/flask_node/entry.py +101 -0
- {flask_node-0.1.2 → flask_node-0.2.0}/src/flask_node/exceptions.py +4 -0
- {flask_node-0.1.2 → flask_node-0.2.0}/src/flask_node/extension.py +3 -0
- {flask_node-0.1.2 → flask_node-0.2.0}/src/flask_node/manager.py +11 -0
- flask_node-0.2.0/tests/test_entry.py +364 -0
- {flask_node-0.1.2 → flask_node-0.2.0}/LICENSE +0 -0
- {flask_node-0.1.2 → flask_node-0.2.0}/MANIFEST.in +0 -0
- {flask_node-0.1.2 → flask_node-0.2.0}/setup.cfg +0 -0
- {flask_node-0.1.2 → flask_node-0.2.0}/src/Flask_Node.egg-info/dependency_links.txt +0 -0
- {flask_node-0.1.2 → flask_node-0.2.0}/src/Flask_Node.egg-info/requires.txt +0 -0
- {flask_node-0.1.2 → flask_node-0.2.0}/src/Flask_Node.egg-info/top_level.txt +0 -0
- {flask_node-0.1.2 → flask_node-0.2.0}/src/flask_node/assets.py +0 -0
- {flask_node-0.1.2 → flask_node-0.2.0}/src/flask_node/cli.py +0 -0
- {flask_node-0.1.2 → flask_node-0.2.0}/src/flask_node/config.py +0 -0
- {flask_node-0.1.2 → flask_node-0.2.0}/src/flask_node/package.py +0 -0
- {flask_node-0.1.2 → flask_node-0.2.0}/src/flask_node/py.typed +0 -0
- {flask_node-0.1.2 → flask_node-0.2.0}/src/flask_node/runner.py +0 -0
- {flask_node-0.1.2 → flask_node-0.2.0}/tests/conftest.py +0 -0
- {flask_node-0.1.2 → flask_node-0.2.0}/tests/test_assets.py +0 -0
- {flask_node-0.1.2 → flask_node-0.2.0}/tests/test_cli.py +0 -0
- {flask_node-0.1.2 → flask_node-0.2.0}/tests/test_declarative.py +0 -0
- {flask_node-0.1.2 → flask_node-0.2.0}/tests/test_extension.py +0 -0
- {flask_node-0.1.2 → flask_node-0.2.0}/tests/test_manager.py +0 -0
- {flask_node-0.1.2 → flask_node-0.2.0}/tests/test_package.py +0 -0
- {flask_node-0.1.2 → flask_node-0.2.0}/tests/test_runner.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: Flask-Node
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Isolated Node/npm infrastructure for Flask extensions
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Requires-Python: >=3.10
|
|
@@ -224,6 +224,78 @@ packages raise `PackageNotFoundError`; unsafe or missing assets raise
|
|
|
224
224
|
`AssetResolutionError`. This is filesystem path validation, not protection
|
|
225
225
|
against concurrent malicious filesystem changes.
|
|
226
226
|
|
|
227
|
+
## Installed package entries
|
|
228
|
+
|
|
229
|
+
After packages have been installed explicitly, consumers can resolve their
|
|
230
|
+
JavaScript entry points through either the manager or the `Node` facade:
|
|
231
|
+
|
|
232
|
+
```python
|
|
233
|
+
entry = manager.resolve_entry("example")
|
|
234
|
+
plugin = manager.resolve_entry("example", "plugin")
|
|
235
|
+
scoped_entry = manager.resolve_entry("@scope/plugin")
|
|
236
|
+
# Equivalent, inside the current application's context:
|
|
237
|
+
entry = node.resolve_entry("example")
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
`resolve_entry(name, subpath=None)` returns an absolute, existing regular-file
|
|
241
|
+
`pathlib.Path`. Package and subpath are separate arguments; do not put a subpath
|
|
242
|
+
in `name`. Only locally installed packages under the manager's `node_modules`
|
|
243
|
+
are accepted. Resolution is anchored to that manager, independent of the Python
|
|
244
|
+
process's working directory or the location of consumer-owned source files.
|
|
245
|
+
It does not require or generate a managed project manifest.
|
|
246
|
+
|
|
247
|
+
The resolver invokes the configured `NODE_BIN` through `CommandRunner` and uses
|
|
248
|
+
Node's `module.createRequire(...).resolve(...)`. It never loads or evaluates the
|
|
249
|
+
target package, installs packages, invokes npm/npx, downloads files, copies
|
|
250
|
+
assets, or writes helper files. Invoke it during an explicit consumer operation
|
|
251
|
+
after installation; Flask-Node does not resolve entries or run processes during
|
|
252
|
+
normal Flask initialization.
|
|
253
|
+
|
|
254
|
+
Resolution follows Node's **require** semantics:
|
|
255
|
+
|
|
256
|
+
- `exports`, when present, takes precedence over `main` and controls which root
|
|
257
|
+
and subpath entries are accessible. Blocked/private subpaths fail even when a
|
|
258
|
+
matching file exists.
|
|
259
|
+
- Conditional exports use Node's active require conditions, including `node`,
|
|
260
|
+
`require`, and applicable `default` branches, in manifest order. An
|
|
261
|
+
`import`-only entry is unavailable. Additional conditions supplied by the Node
|
|
262
|
+
runtime/configuration, such as version-dependent `module-sync`, follow Node's
|
|
263
|
+
own behavior; Flask-Node does not emulate or override them.
|
|
264
|
+
- Without `exports`, Node uses `main` and its legacy file/directory lookup,
|
|
265
|
+
including `.js`, `.json`, `.node` extension probing and index fallbacks.
|
|
266
|
+
Export targets require their exact files; legacy extension/index probing does
|
|
267
|
+
not apply to those targets. Nonstandard fields such as `module` are not used.
|
|
268
|
+
|
|
269
|
+
These semantics suit consumers using require-style JavaScript plugin resolution.
|
|
270
|
+
Consumers needing import-condition selection must not assume this API selects
|
|
271
|
+
that branch. Resolution is separate from loading: a returned path may identify
|
|
272
|
+
ESM, JSON, or a native addon, and does not guarantee that a consumer's loader can
|
|
273
|
+
execute it. See Node's [resolution documentation](https://nodejs.org/api/modules.html#requireresolverequest-options)
|
|
274
|
+
and [conditional exports](https://nodejs.org/api/packages.html#conditional-exports).
|
|
275
|
+
|
|
276
|
+
Package names use the existing registry-name validation. A supplied subpath must
|
|
277
|
+
be a nonempty relative string with no empty, `.` or `..` segments, absolute or
|
|
278
|
+
Windows drive paths, backslashes, NULs, or percent encoding. The package,
|
|
279
|
+
`node_modules`, and package manifest follow the existing symlink containment
|
|
280
|
+
checks. The resolved entry must remain within that installed package's resolved
|
|
281
|
+
root; internal symlinks are allowed, escaping symlinks are rejected. Ancestor or
|
|
282
|
+
global packages, built-ins, directories, and other non-file results are
|
|
283
|
+
unsupported. These are filesystem checks, not protection against concurrent
|
|
284
|
+
malicious filesystem changes.
|
|
285
|
+
|
|
286
|
+
Invalid package names raise `ConfigurationError`; missing or malformed installed
|
|
287
|
+
packages raise `PackageNotFoundError`; package containment failures retain
|
|
288
|
+
`AssetResolutionError`. Entry validation, Node resolution failures, and invalid
|
|
289
|
+
results raise the publicly exported `EntryResolutionError`, derived from
|
|
290
|
+
`NodeError`, with the requested entry and Node error code/message where available.
|
|
291
|
+
Missing Node and runner failures propagate as `ExecutableNotFoundError` and
|
|
292
|
+
`CommandExecutionError`; command diagnostics remain intact. Invalid resolver
|
|
293
|
+
responses include captured output in the error. Existing `resolve(name, asset)`
|
|
294
|
+
continues to resolve explicit filesystem paths and does not consult `exports`.
|
|
295
|
+
|
|
296
|
+
Package-entry resolution is available starting with **0.2.0**. Consumer
|
|
297
|
+
extensions using this API should declare `Flask-Node>=0.2.0`.
|
|
298
|
+
|
|
227
299
|
## Publishing browser assets
|
|
228
300
|
|
|
229
301
|
Consuming extensions choose which npm dependencies and browser assets they need.
|
|
@@ -317,7 +389,8 @@ python -m venv .venv
|
|
|
317
389
|
```
|
|
318
390
|
|
|
319
391
|
Tests inject/mock the runner and subprocess boundary. They never download npm
|
|
320
|
-
packages
|
|
392
|
+
packages. Offline integration tests use temporary fake packages and real Node
|
|
393
|
+
when available; pytest reports explicit skips when Node is unavailable. A custom runner can be supplied to `Node(runner=...)`
|
|
321
394
|
or `NodeManager(..., runner=...)` for testing. This dependency injection is not a
|
|
322
395
|
consumer plugin protocol.
|
|
323
396
|
|
|
@@ -210,6 +210,78 @@ packages raise `PackageNotFoundError`; unsafe or missing assets raise
|
|
|
210
210
|
`AssetResolutionError`. This is filesystem path validation, not protection
|
|
211
211
|
against concurrent malicious filesystem changes.
|
|
212
212
|
|
|
213
|
+
## Installed package entries
|
|
214
|
+
|
|
215
|
+
After packages have been installed explicitly, consumers can resolve their
|
|
216
|
+
JavaScript entry points through either the manager or the `Node` facade:
|
|
217
|
+
|
|
218
|
+
```python
|
|
219
|
+
entry = manager.resolve_entry("example")
|
|
220
|
+
plugin = manager.resolve_entry("example", "plugin")
|
|
221
|
+
scoped_entry = manager.resolve_entry("@scope/plugin")
|
|
222
|
+
# Equivalent, inside the current application's context:
|
|
223
|
+
entry = node.resolve_entry("example")
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
`resolve_entry(name, subpath=None)` returns an absolute, existing regular-file
|
|
227
|
+
`pathlib.Path`. Package and subpath are separate arguments; do not put a subpath
|
|
228
|
+
in `name`. Only locally installed packages under the manager's `node_modules`
|
|
229
|
+
are accepted. Resolution is anchored to that manager, independent of the Python
|
|
230
|
+
process's working directory or the location of consumer-owned source files.
|
|
231
|
+
It does not require or generate a managed project manifest.
|
|
232
|
+
|
|
233
|
+
The resolver invokes the configured `NODE_BIN` through `CommandRunner` and uses
|
|
234
|
+
Node's `module.createRequire(...).resolve(...)`. It never loads or evaluates the
|
|
235
|
+
target package, installs packages, invokes npm/npx, downloads files, copies
|
|
236
|
+
assets, or writes helper files. Invoke it during an explicit consumer operation
|
|
237
|
+
after installation; Flask-Node does not resolve entries or run processes during
|
|
238
|
+
normal Flask initialization.
|
|
239
|
+
|
|
240
|
+
Resolution follows Node's **require** semantics:
|
|
241
|
+
|
|
242
|
+
- `exports`, when present, takes precedence over `main` and controls which root
|
|
243
|
+
and subpath entries are accessible. Blocked/private subpaths fail even when a
|
|
244
|
+
matching file exists.
|
|
245
|
+
- Conditional exports use Node's active require conditions, including `node`,
|
|
246
|
+
`require`, and applicable `default` branches, in manifest order. An
|
|
247
|
+
`import`-only entry is unavailable. Additional conditions supplied by the Node
|
|
248
|
+
runtime/configuration, such as version-dependent `module-sync`, follow Node's
|
|
249
|
+
own behavior; Flask-Node does not emulate or override them.
|
|
250
|
+
- Without `exports`, Node uses `main` and its legacy file/directory lookup,
|
|
251
|
+
including `.js`, `.json`, `.node` extension probing and index fallbacks.
|
|
252
|
+
Export targets require their exact files; legacy extension/index probing does
|
|
253
|
+
not apply to those targets. Nonstandard fields such as `module` are not used.
|
|
254
|
+
|
|
255
|
+
These semantics suit consumers using require-style JavaScript plugin resolution.
|
|
256
|
+
Consumers needing import-condition selection must not assume this API selects
|
|
257
|
+
that branch. Resolution is separate from loading: a returned path may identify
|
|
258
|
+
ESM, JSON, or a native addon, and does not guarantee that a consumer's loader can
|
|
259
|
+
execute it. See Node's [resolution documentation](https://nodejs.org/api/modules.html#requireresolverequest-options)
|
|
260
|
+
and [conditional exports](https://nodejs.org/api/packages.html#conditional-exports).
|
|
261
|
+
|
|
262
|
+
Package names use the existing registry-name validation. A supplied subpath must
|
|
263
|
+
be a nonempty relative string with no empty, `.` or `..` segments, absolute or
|
|
264
|
+
Windows drive paths, backslashes, NULs, or percent encoding. The package,
|
|
265
|
+
`node_modules`, and package manifest follow the existing symlink containment
|
|
266
|
+
checks. The resolved entry must remain within that installed package's resolved
|
|
267
|
+
root; internal symlinks are allowed, escaping symlinks are rejected. Ancestor or
|
|
268
|
+
global packages, built-ins, directories, and other non-file results are
|
|
269
|
+
unsupported. These are filesystem checks, not protection against concurrent
|
|
270
|
+
malicious filesystem changes.
|
|
271
|
+
|
|
272
|
+
Invalid package names raise `ConfigurationError`; missing or malformed installed
|
|
273
|
+
packages raise `PackageNotFoundError`; package containment failures retain
|
|
274
|
+
`AssetResolutionError`. Entry validation, Node resolution failures, and invalid
|
|
275
|
+
results raise the publicly exported `EntryResolutionError`, derived from
|
|
276
|
+
`NodeError`, with the requested entry and Node error code/message where available.
|
|
277
|
+
Missing Node and runner failures propagate as `ExecutableNotFoundError` and
|
|
278
|
+
`CommandExecutionError`; command diagnostics remain intact. Invalid resolver
|
|
279
|
+
responses include captured output in the error. Existing `resolve(name, asset)`
|
|
280
|
+
continues to resolve explicit filesystem paths and does not consult `exports`.
|
|
281
|
+
|
|
282
|
+
Package-entry resolution is available starting with **0.2.0**. Consumer
|
|
283
|
+
extensions using this API should declare `Flask-Node>=0.2.0`.
|
|
284
|
+
|
|
213
285
|
## Publishing browser assets
|
|
214
286
|
|
|
215
287
|
Consuming extensions choose which npm dependencies and browser assets they need.
|
|
@@ -303,7 +375,8 @@ python -m venv .venv
|
|
|
303
375
|
```
|
|
304
376
|
|
|
305
377
|
Tests inject/mock the runner and subprocess boundary. They never download npm
|
|
306
|
-
packages
|
|
378
|
+
packages. Offline integration tests use temporary fake packages and real Node
|
|
379
|
+
when available; pytest reports explicit skips when Node is unavailable. A custom runner can be supplied to `Node(runner=...)`
|
|
307
380
|
or `NodeManager(..., runner=...)` for testing. This dependency injection is not a
|
|
308
381
|
consumer plugin protocol.
|
|
309
382
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: Flask-Node
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Isolated Node/npm infrastructure for Flask extensions
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Requires-Python: >=3.10
|
|
@@ -224,6 +224,78 @@ packages raise `PackageNotFoundError`; unsafe or missing assets raise
|
|
|
224
224
|
`AssetResolutionError`. This is filesystem path validation, not protection
|
|
225
225
|
against concurrent malicious filesystem changes.
|
|
226
226
|
|
|
227
|
+
## Installed package entries
|
|
228
|
+
|
|
229
|
+
After packages have been installed explicitly, consumers can resolve their
|
|
230
|
+
JavaScript entry points through either the manager or the `Node` facade:
|
|
231
|
+
|
|
232
|
+
```python
|
|
233
|
+
entry = manager.resolve_entry("example")
|
|
234
|
+
plugin = manager.resolve_entry("example", "plugin")
|
|
235
|
+
scoped_entry = manager.resolve_entry("@scope/plugin")
|
|
236
|
+
# Equivalent, inside the current application's context:
|
|
237
|
+
entry = node.resolve_entry("example")
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
`resolve_entry(name, subpath=None)` returns an absolute, existing regular-file
|
|
241
|
+
`pathlib.Path`. Package and subpath are separate arguments; do not put a subpath
|
|
242
|
+
in `name`. Only locally installed packages under the manager's `node_modules`
|
|
243
|
+
are accepted. Resolution is anchored to that manager, independent of the Python
|
|
244
|
+
process's working directory or the location of consumer-owned source files.
|
|
245
|
+
It does not require or generate a managed project manifest.
|
|
246
|
+
|
|
247
|
+
The resolver invokes the configured `NODE_BIN` through `CommandRunner` and uses
|
|
248
|
+
Node's `module.createRequire(...).resolve(...)`. It never loads or evaluates the
|
|
249
|
+
target package, installs packages, invokes npm/npx, downloads files, copies
|
|
250
|
+
assets, or writes helper files. Invoke it during an explicit consumer operation
|
|
251
|
+
after installation; Flask-Node does not resolve entries or run processes during
|
|
252
|
+
normal Flask initialization.
|
|
253
|
+
|
|
254
|
+
Resolution follows Node's **require** semantics:
|
|
255
|
+
|
|
256
|
+
- `exports`, when present, takes precedence over `main` and controls which root
|
|
257
|
+
and subpath entries are accessible. Blocked/private subpaths fail even when a
|
|
258
|
+
matching file exists.
|
|
259
|
+
- Conditional exports use Node's active require conditions, including `node`,
|
|
260
|
+
`require`, and applicable `default` branches, in manifest order. An
|
|
261
|
+
`import`-only entry is unavailable. Additional conditions supplied by the Node
|
|
262
|
+
runtime/configuration, such as version-dependent `module-sync`, follow Node's
|
|
263
|
+
own behavior; Flask-Node does not emulate or override them.
|
|
264
|
+
- Without `exports`, Node uses `main` and its legacy file/directory lookup,
|
|
265
|
+
including `.js`, `.json`, `.node` extension probing and index fallbacks.
|
|
266
|
+
Export targets require their exact files; legacy extension/index probing does
|
|
267
|
+
not apply to those targets. Nonstandard fields such as `module` are not used.
|
|
268
|
+
|
|
269
|
+
These semantics suit consumers using require-style JavaScript plugin resolution.
|
|
270
|
+
Consumers needing import-condition selection must not assume this API selects
|
|
271
|
+
that branch. Resolution is separate from loading: a returned path may identify
|
|
272
|
+
ESM, JSON, or a native addon, and does not guarantee that a consumer's loader can
|
|
273
|
+
execute it. See Node's [resolution documentation](https://nodejs.org/api/modules.html#requireresolverequest-options)
|
|
274
|
+
and [conditional exports](https://nodejs.org/api/packages.html#conditional-exports).
|
|
275
|
+
|
|
276
|
+
Package names use the existing registry-name validation. A supplied subpath must
|
|
277
|
+
be a nonempty relative string with no empty, `.` or `..` segments, absolute or
|
|
278
|
+
Windows drive paths, backslashes, NULs, or percent encoding. The package,
|
|
279
|
+
`node_modules`, and package manifest follow the existing symlink containment
|
|
280
|
+
checks. The resolved entry must remain within that installed package's resolved
|
|
281
|
+
root; internal symlinks are allowed, escaping symlinks are rejected. Ancestor or
|
|
282
|
+
global packages, built-ins, directories, and other non-file results are
|
|
283
|
+
unsupported. These are filesystem checks, not protection against concurrent
|
|
284
|
+
malicious filesystem changes.
|
|
285
|
+
|
|
286
|
+
Invalid package names raise `ConfigurationError`; missing or malformed installed
|
|
287
|
+
packages raise `PackageNotFoundError`; package containment failures retain
|
|
288
|
+
`AssetResolutionError`. Entry validation, Node resolution failures, and invalid
|
|
289
|
+
results raise the publicly exported `EntryResolutionError`, derived from
|
|
290
|
+
`NodeError`, with the requested entry and Node error code/message where available.
|
|
291
|
+
Missing Node and runner failures propagate as `ExecutableNotFoundError` and
|
|
292
|
+
`CommandExecutionError`; command diagnostics remain intact. Invalid resolver
|
|
293
|
+
responses include captured output in the error. Existing `resolve(name, asset)`
|
|
294
|
+
continues to resolve explicit filesystem paths and does not consult `exports`.
|
|
295
|
+
|
|
296
|
+
Package-entry resolution is available starting with **0.2.0**. Consumer
|
|
297
|
+
extensions using this API should declare `Flask-Node>=0.2.0`.
|
|
298
|
+
|
|
227
299
|
## Publishing browser assets
|
|
228
300
|
|
|
229
301
|
Consuming extensions choose which npm dependencies and browser assets they need.
|
|
@@ -317,7 +389,8 @@ python -m venv .venv
|
|
|
317
389
|
```
|
|
318
390
|
|
|
319
391
|
Tests inject/mock the runner and subprocess boundary. They never download npm
|
|
320
|
-
packages
|
|
392
|
+
packages. Offline integration tests use temporary fake packages and real Node
|
|
393
|
+
when available; pytest reports explicit skips when Node is unavailable. A custom runner can be supplied to `Node(runner=...)`
|
|
321
394
|
or `NodeManager(..., runner=...)` for testing. This dependency injection is not a
|
|
322
395
|
consumer plugin protocol.
|
|
323
396
|
|
|
@@ -11,6 +11,7 @@ src/flask_node/__init__.py
|
|
|
11
11
|
src/flask_node/assets.py
|
|
12
12
|
src/flask_node/cli.py
|
|
13
13
|
src/flask_node/config.py
|
|
14
|
+
src/flask_node/entry.py
|
|
14
15
|
src/flask_node/exceptions.py
|
|
15
16
|
src/flask_node/extension.py
|
|
16
17
|
src/flask_node/manager.py
|
|
@@ -21,6 +22,7 @@ tests/conftest.py
|
|
|
21
22
|
tests/test_assets.py
|
|
22
23
|
tests/test_cli.py
|
|
23
24
|
tests/test_declarative.py
|
|
25
|
+
tests/test_entry.py
|
|
24
26
|
tests/test_extension.py
|
|
25
27
|
tests/test_manager.py
|
|
26
28
|
tests/test_package.py
|
|
@@ -8,6 +8,7 @@ from .exceptions import (
|
|
|
8
8
|
CommandExecutionError,
|
|
9
9
|
ConfigurationError,
|
|
10
10
|
DependencyConflictError,
|
|
11
|
+
EntryResolutionError,
|
|
11
12
|
EnvironmentError,
|
|
12
13
|
ExecutableNotFoundError,
|
|
13
14
|
NodeError,
|
|
@@ -27,6 +28,7 @@ __all__ = [
|
|
|
27
28
|
"CommandRunner",
|
|
28
29
|
"ConfigurationError",
|
|
29
30
|
"DependencyConflictError",
|
|
31
|
+
"EntryResolutionError",
|
|
30
32
|
"EnvironmentError",
|
|
31
33
|
"ExecutableNotFoundError",
|
|
32
34
|
"Node",
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Resolve installed entries using Node, without loading package code."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path, PureWindowsPath
|
|
5
|
+
|
|
6
|
+
from .exceptions import EntryResolutionError
|
|
7
|
+
from .runner import CommandRunner
|
|
8
|
+
|
|
9
|
+
# Only built-in modules are loaded. The synthetic anchor need not exist.
|
|
10
|
+
_SCRIPT = """
|
|
11
|
+
const { createRequire, isBuiltin } = require('node:module');
|
|
12
|
+
const [anchor, specifier] = process.argv.slice(1);
|
|
13
|
+
try {
|
|
14
|
+
if (isBuiltin(specifier)) {
|
|
15
|
+
throw Object.assign(new Error('Built-in modules are unsupported'), {
|
|
16
|
+
code: 'ERR_UNSUPPORTED_BUILTIN'
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
const resolved = createRequire(anchor).resolve(specifier);
|
|
20
|
+
process.stdout.write(JSON.stringify({path: resolved}));
|
|
21
|
+
} catch (error) {
|
|
22
|
+
process.stdout.write(JSON.stringify({error: {
|
|
23
|
+
code: error.code || 'ERR_ENTRY_RESOLUTION', message: error.message
|
|
24
|
+
}}));
|
|
25
|
+
}
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def validate_subpath(subpath: str | None) -> str | None:
|
|
30
|
+
if subpath is None:
|
|
31
|
+
return None
|
|
32
|
+
if (
|
|
33
|
+
not isinstance(subpath, str)
|
|
34
|
+
or not subpath
|
|
35
|
+
or Path(subpath).is_absolute()
|
|
36
|
+
or PureWindowsPath(subpath).drive
|
|
37
|
+
or "\\" in subpath
|
|
38
|
+
or "\x00" in subpath
|
|
39
|
+
or "%" in subpath
|
|
40
|
+
or any(part in ("", ".", "..") for part in subpath.split("/"))
|
|
41
|
+
):
|
|
42
|
+
raise EntryResolutionError(f"Unsafe package entry subpath: {subpath!r}")
|
|
43
|
+
return subpath
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def resolve_entry(
|
|
47
|
+
runner: CommandRunner,
|
|
48
|
+
node_bin: str,
|
|
49
|
+
directory: Path,
|
|
50
|
+
root: Path,
|
|
51
|
+
specifier: str,
|
|
52
|
+
) -> Path:
|
|
53
|
+
result = runner.run(
|
|
54
|
+
node_bin,
|
|
55
|
+
(
|
|
56
|
+
"--input-type=commonjs",
|
|
57
|
+
"--eval",
|
|
58
|
+
_SCRIPT,
|
|
59
|
+
"--",
|
|
60
|
+
str(directory / ".flask-node-resolver.cjs"),
|
|
61
|
+
specifier,
|
|
62
|
+
),
|
|
63
|
+
cwd=directory,
|
|
64
|
+
capture_output=True,
|
|
65
|
+
)
|
|
66
|
+
try:
|
|
67
|
+
payload = json.loads(result.stdout or "")
|
|
68
|
+
if not isinstance(payload, dict):
|
|
69
|
+
raise TypeError("expected an object")
|
|
70
|
+
if "error" in payload:
|
|
71
|
+
error = payload["error"]
|
|
72
|
+
if not isinstance(error, dict):
|
|
73
|
+
raise ValueError("invalid error response")
|
|
74
|
+
detail = (
|
|
75
|
+
f"Cannot resolve entry {specifier!r}: "
|
|
76
|
+
f"{error.get('code', 'ERR_ENTRY_RESOLUTION')}: "
|
|
77
|
+
f"{error.get('message', 'unknown resolution failure')}"
|
|
78
|
+
)
|
|
79
|
+
if result.stderr:
|
|
80
|
+
detail += f"; stderr={result.stderr!r}"
|
|
81
|
+
raise EntryResolutionError(detail)
|
|
82
|
+
value = payload.get("path")
|
|
83
|
+
if not isinstance(value, str) or not Path(value).is_absolute():
|
|
84
|
+
raise ValueError(
|
|
85
|
+
"expected an absolute file path; built-ins are unsupported"
|
|
86
|
+
)
|
|
87
|
+
target = Path(value).resolve(strict=True)
|
|
88
|
+
if not target.is_relative_to(root):
|
|
89
|
+
raise EntryResolutionError(
|
|
90
|
+
f"Entry {specifier!r} escapes its installed package: {target}"
|
|
91
|
+
)
|
|
92
|
+
if not target.is_file():
|
|
93
|
+
raise EntryResolutionError(
|
|
94
|
+
f"Entry {specifier!r} is not a regular file: {target}"
|
|
95
|
+
)
|
|
96
|
+
return target
|
|
97
|
+
except (ValueError, OSError, RuntimeError, TypeError) as exc:
|
|
98
|
+
raise EntryResolutionError(
|
|
99
|
+
f"Cannot resolve entry {specifier!r}: {exc}; "
|
|
100
|
+
f"stdout={result.stdout!r}; stderr={result.stderr!r}"
|
|
101
|
+
) from exc
|
|
@@ -113,6 +113,9 @@ class Node:
|
|
|
113
113
|
def resolve(self, name: str, asset: str | Path) -> Path:
|
|
114
114
|
return self.get_manager().resolve(name, asset)
|
|
115
115
|
|
|
116
|
+
def resolve_entry(self, name: str, subpath: str | None = None) -> Path:
|
|
117
|
+
return self.get_manager().resolve_entry(name, subpath)
|
|
118
|
+
|
|
116
119
|
@property
|
|
117
120
|
def assets(self) -> tuple[NodeAsset, ...]:
|
|
118
121
|
return self.get_manager().assets
|
|
@@ -8,6 +8,7 @@ from typing import Any
|
|
|
8
8
|
|
|
9
9
|
from .assets import NodeAsset, destination_path, publish, validate_tree
|
|
10
10
|
from .config import read_configuration
|
|
11
|
+
from .entry import resolve_entry, validate_subpath
|
|
11
12
|
from .exceptions import (
|
|
12
13
|
AssetConflictError,
|
|
13
14
|
AssetPublicationError,
|
|
@@ -319,6 +320,16 @@ class NodeManager:
|
|
|
319
320
|
def resolve(self, name: str, asset: str | Path) -> Path:
|
|
320
321
|
return self.package(name).resolve(asset)
|
|
321
322
|
|
|
323
|
+
def resolve_entry(self, name: str, subpath: str | None = None) -> Path:
|
|
324
|
+
"""Resolve an installed entry using Node's require conditions, without loading it."""
|
|
325
|
+
validate_name(name)
|
|
326
|
+
subpath = validate_subpath(subpath)
|
|
327
|
+
package = self.package(name)
|
|
328
|
+
specifier = name if subpath is None else f"{name}/{subpath}"
|
|
329
|
+
return resolve_entry(
|
|
330
|
+
self.runner, self.node_bin, self.directory, package.root, specifier
|
|
331
|
+
)
|
|
332
|
+
|
|
322
333
|
def status(self) -> dict[str, Any]:
|
|
323
334
|
return {
|
|
324
335
|
"directory": str(self.directory),
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import shutil
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from flask import Flask
|
|
7
|
+
|
|
8
|
+
from flask_node import (
|
|
9
|
+
AssetResolutionError,
|
|
10
|
+
CommandExecutionError,
|
|
11
|
+
CommandResult,
|
|
12
|
+
CommandRunner,
|
|
13
|
+
ConfigurationError,
|
|
14
|
+
EntryResolutionError,
|
|
15
|
+
ExecutableNotFoundError,
|
|
16
|
+
Node,
|
|
17
|
+
NodeManager,
|
|
18
|
+
PackageNotFoundError,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def installed(manager, name="example", **manifest):
|
|
23
|
+
root = manager.directory / "node_modules" / name
|
|
24
|
+
root.mkdir(parents=True)
|
|
25
|
+
(root / "package.json").write_text(json.dumps({"version": "1", **manifest}))
|
|
26
|
+
for filename in ("index.js", "plugin.js", "require.cjs", "import.mjs", "main.js"):
|
|
27
|
+
# Loading this code would fail, proving resolution does not evaluate it.
|
|
28
|
+
(root / filename).write_text("throw new Error('PACKAGE EXECUTED');")
|
|
29
|
+
return root
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def response(runner, path):
|
|
33
|
+
runner.run.return_value = CommandResult(
|
|
34
|
+
("node",), 0, json.dumps({"path": str(path)})
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@pytest.mark.parametrize(
|
|
39
|
+
"name,subpath", [("example", None), ("@scope/example", None), ("example", "plugin")]
|
|
40
|
+
)
|
|
41
|
+
def test_boundary(manager, runner, name, subpath):
|
|
42
|
+
root = installed(manager, name)
|
|
43
|
+
target = root / ("plugin.js" if subpath else "index.js")
|
|
44
|
+
response(runner, target)
|
|
45
|
+
assert manager.resolve_entry(name, subpath) == target
|
|
46
|
+
executable, args = runner.run.call_args.args
|
|
47
|
+
assert executable == manager.node_bin
|
|
48
|
+
assert args[-2:] == (
|
|
49
|
+
str(manager.directory / ".flask-node-resolver.cjs"),
|
|
50
|
+
name + (f"/{subpath}" if subpath else ""),
|
|
51
|
+
)
|
|
52
|
+
assert runner.run.call_args.kwargs == {
|
|
53
|
+
"cwd": manager.directory,
|
|
54
|
+
"capture_output": True,
|
|
55
|
+
}
|
|
56
|
+
assert not manager.manifest_path.exists()
|
|
57
|
+
assert not (manager.directory / ".flask-node-resolver.cjs").exists()
|
|
58
|
+
runner.locate.assert_not_called()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@pytest.mark.parametrize(
|
|
62
|
+
"name", [None, "", "../x", "/x", "x/plugin", "node:fs", "@scope/../x", "C:\\x"]
|
|
63
|
+
)
|
|
64
|
+
def test_invalid_name(manager, runner, name):
|
|
65
|
+
with pytest.raises(ConfigurationError):
|
|
66
|
+
manager.resolve_entry(name)
|
|
67
|
+
runner.run.assert_not_called()
|
|
68
|
+
assert not manager.directory.exists()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@pytest.mark.parametrize(
|
|
72
|
+
"subpath",
|
|
73
|
+
[
|
|
74
|
+
"",
|
|
75
|
+
".",
|
|
76
|
+
"..",
|
|
77
|
+
"../x",
|
|
78
|
+
"x/../y",
|
|
79
|
+
"/x",
|
|
80
|
+
"C:/x",
|
|
81
|
+
"x\\y",
|
|
82
|
+
"x//y",
|
|
83
|
+
"./x",
|
|
84
|
+
"x/",
|
|
85
|
+
"\x00",
|
|
86
|
+
"%2e%2e/x",
|
|
87
|
+
"x%2fy",
|
|
88
|
+
1,
|
|
89
|
+
Path("plugin"),
|
|
90
|
+
],
|
|
91
|
+
)
|
|
92
|
+
def test_invalid_subpath(manager, runner, subpath):
|
|
93
|
+
with pytest.raises(EntryResolutionError):
|
|
94
|
+
manager.resolve_entry("example", subpath)
|
|
95
|
+
runner.run.assert_not_called()
|
|
96
|
+
assert not manager.directory.exists()
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_missing_package(manager, runner):
|
|
100
|
+
with pytest.raises(PackageNotFoundError):
|
|
101
|
+
manager.resolve_entry("example")
|
|
102
|
+
runner.run.assert_not_called()
|
|
103
|
+
assert not manager.directory.exists()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@pytest.mark.parametrize("value", ["fs", "node:fs", "relative.js", None])
|
|
107
|
+
def test_unsupported_results(manager, runner, value):
|
|
108
|
+
installed(manager)
|
|
109
|
+
runner.run.return_value = CommandResult(("node",), 0, json.dumps({"path": value}))
|
|
110
|
+
with pytest.raises(EntryResolutionError):
|
|
111
|
+
manager.resolve_entry("example")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@pytest.mark.parametrize("output", ["", "garbage", "[]", '{"error": null}'])
|
|
115
|
+
def test_malformed_response(manager, runner, output):
|
|
116
|
+
installed(manager)
|
|
117
|
+
runner.run.return_value = CommandResult(("node",), 0, output, "diagnostic")
|
|
118
|
+
with pytest.raises(EntryResolutionError, match="diagnostic"):
|
|
119
|
+
manager.resolve_entry("example")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@pytest.mark.parametrize("code", ["MODULE_NOT_FOUND", "ERR_PACKAGE_PATH_NOT_EXPORTED"])
|
|
123
|
+
def test_node_error(manager, runner, code):
|
|
124
|
+
installed(manager)
|
|
125
|
+
runner.run.return_value = CommandResult(
|
|
126
|
+
("node",), 0, json.dumps({"error": {"code": code, "message": "blocked entry"}})
|
|
127
|
+
)
|
|
128
|
+
with pytest.raises(EntryResolutionError, match=f"{code}: blocked entry"):
|
|
129
|
+
manager.resolve_entry("example", "plugin")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@pytest.mark.parametrize(
|
|
133
|
+
"error",
|
|
134
|
+
[
|
|
135
|
+
ExecutableNotFoundError("missing node"),
|
|
136
|
+
CommandExecutionError(
|
|
137
|
+
"failed",
|
|
138
|
+
args=("node",),
|
|
139
|
+
cwd="managed",
|
|
140
|
+
returncode=7,
|
|
141
|
+
stdout="out",
|
|
142
|
+
stderr="err",
|
|
143
|
+
),
|
|
144
|
+
],
|
|
145
|
+
)
|
|
146
|
+
def test_runner_errors_preserved(manager, runner, error):
|
|
147
|
+
installed(manager)
|
|
148
|
+
runner.run.side_effect = error
|
|
149
|
+
with pytest.raises(type(error)) as caught:
|
|
150
|
+
manager.resolve_entry("example")
|
|
151
|
+
assert caught.value is error
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def test_missing_executable(manager, monkeypatch):
|
|
155
|
+
installed(manager)
|
|
156
|
+
manager.runner = CommandRunner()
|
|
157
|
+
monkeypatch.setattr("flask_node.runner.shutil.which", lambda executable: None)
|
|
158
|
+
with pytest.raises(ExecutableNotFoundError):
|
|
159
|
+
manager.resolve_entry("example")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@pytest.mark.parametrize(
|
|
163
|
+
"kind", ["outside", "sibling", "directory", "missing", "symlink"]
|
|
164
|
+
)
|
|
165
|
+
def test_containment_and_files(manager, runner, tmp_path, kind):
|
|
166
|
+
root = installed(manager)
|
|
167
|
+
outside = tmp_path / "outside.js"
|
|
168
|
+
outside.write_text("outside")
|
|
169
|
+
if kind == "sibling":
|
|
170
|
+
target = installed(manager, "other") / "index.js"
|
|
171
|
+
elif kind == "directory":
|
|
172
|
+
target = root
|
|
173
|
+
elif kind == "missing":
|
|
174
|
+
target = root / "missing.js"
|
|
175
|
+
elif kind == "symlink":
|
|
176
|
+
target = root / "link.js"
|
|
177
|
+
target.symlink_to(outside)
|
|
178
|
+
else:
|
|
179
|
+
target = outside
|
|
180
|
+
response(runner, target)
|
|
181
|
+
with pytest.raises(EntryResolutionError):
|
|
182
|
+
manager.resolve_entry("example")
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@pytest.mark.parametrize("kind", ["modules", "package", "manifest"])
|
|
186
|
+
def test_preflight_symlink_escape(manager, runner, tmp_path, kind):
|
|
187
|
+
root = installed(manager)
|
|
188
|
+
if kind == "modules":
|
|
189
|
+
shutil.rmtree(manager.directory / "node_modules")
|
|
190
|
+
(manager.directory / "node_modules").symlink_to(
|
|
191
|
+
tmp_path, target_is_directory=True
|
|
192
|
+
)
|
|
193
|
+
elif kind == "package":
|
|
194
|
+
shutil.rmtree(root)
|
|
195
|
+
root.symlink_to(tmp_path, target_is_directory=True)
|
|
196
|
+
else:
|
|
197
|
+
(root / "package.json").unlink()
|
|
198
|
+
(root / "package.json").symlink_to(tmp_path / "outside.json")
|
|
199
|
+
with pytest.raises(AssetResolutionError):
|
|
200
|
+
manager.resolve_entry("example")
|
|
201
|
+
runner.run.assert_not_called()
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def test_facade_isolation(tmp_path, runner):
|
|
205
|
+
node = Node(runner=runner)
|
|
206
|
+
for name in ("one", "two"):
|
|
207
|
+
app = Flask(name, root_path=str(tmp_path / name))
|
|
208
|
+
app.config["NODE_DIR"] = "custom"
|
|
209
|
+
node.init_app(app)
|
|
210
|
+
runner.run.assert_not_called()
|
|
211
|
+
manager = node.get_manager(app)
|
|
212
|
+
assert not manager.directory.exists()
|
|
213
|
+
root = installed(manager)
|
|
214
|
+
response(runner, root / "plugin.js")
|
|
215
|
+
with app.app_context():
|
|
216
|
+
assert node.resolve_entry("example", "plugin") == root / "plugin.js"
|
|
217
|
+
assert runner.run.call_args.kwargs["cwd"] == tmp_path / name / "custom"
|
|
218
|
+
runner.reset_mock()
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@pytest.fixture
|
|
222
|
+
def real_manager(tmp_path):
|
|
223
|
+
executable = shutil.which("node")
|
|
224
|
+
if executable is None:
|
|
225
|
+
pytest.skip("Node executable unavailable: offline entry integration skipped")
|
|
226
|
+
return NodeManager(tmp_path / "custom-managed", node_bin=executable)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@pytest.mark.parametrize(
|
|
230
|
+
"name,subpath,manifest,expected",
|
|
231
|
+
[
|
|
232
|
+
("example", None, {}, "index.js"),
|
|
233
|
+
("@scope/example", None, {"main": "main"}, "main.js"),
|
|
234
|
+
("example", "plugin", {}, "plugin.js"),
|
|
235
|
+
(
|
|
236
|
+
"example",
|
|
237
|
+
None,
|
|
238
|
+
{"main": "main.js", "exports": "./require.cjs"},
|
|
239
|
+
"require.cjs",
|
|
240
|
+
),
|
|
241
|
+
(
|
|
242
|
+
"example",
|
|
243
|
+
None,
|
|
244
|
+
{
|
|
245
|
+
"exports": {
|
|
246
|
+
"import": "./import.mjs",
|
|
247
|
+
"require": "./require.cjs",
|
|
248
|
+
"default": "./main.js",
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
"require.cjs",
|
|
252
|
+
),
|
|
253
|
+
(
|
|
254
|
+
"example",
|
|
255
|
+
None,
|
|
256
|
+
{"exports": {"node": "./main.js", "default": "./index.js"}},
|
|
257
|
+
"main.js",
|
|
258
|
+
),
|
|
259
|
+
("example", None, {"exports": {"default": "./main.js"}}, "main.js"),
|
|
260
|
+
(
|
|
261
|
+
"@scope/example",
|
|
262
|
+
"plugin",
|
|
263
|
+
{"exports": {"./plugin": "./require.cjs"}},
|
|
264
|
+
"require.cjs",
|
|
265
|
+
),
|
|
266
|
+
],
|
|
267
|
+
)
|
|
268
|
+
def test_offline_real_resolution(
|
|
269
|
+
real_manager, tmp_path, monkeypatch, name, subpath, manifest, expected
|
|
270
|
+
):
|
|
271
|
+
root = installed(real_manager, name, **manifest)
|
|
272
|
+
unrelated = tmp_path / "unrelated"
|
|
273
|
+
unrelated.mkdir()
|
|
274
|
+
monkeypatch.chdir(unrelated)
|
|
275
|
+
before = {
|
|
276
|
+
p: p.read_bytes() for p in real_manager.directory.rglob("*") if p.is_file()
|
|
277
|
+
}
|
|
278
|
+
assert real_manager.resolve_entry(name, subpath) == root / expected
|
|
279
|
+
after = {
|
|
280
|
+
p: p.read_bytes() for p in real_manager.directory.rglob("*") if p.is_file()
|
|
281
|
+
}
|
|
282
|
+
assert before == after
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
@pytest.mark.parametrize(
|
|
286
|
+
"manifest,subpath",
|
|
287
|
+
[
|
|
288
|
+
({}, "missing"),
|
|
289
|
+
({"exports": {".": "./index.js"}}, "plugin"),
|
|
290
|
+
({"exports": {"import": "./import.mjs"}}, None),
|
|
291
|
+
({"exports": "./missing.js"}, None),
|
|
292
|
+
({"exports": "./main"}, None),
|
|
293
|
+
({"exports": {"./plugin": None}}, "plugin"),
|
|
294
|
+
],
|
|
295
|
+
)
|
|
296
|
+
def test_offline_missing_or_blocked(real_manager, manifest, subpath):
|
|
297
|
+
installed(real_manager, **manifest)
|
|
298
|
+
with pytest.raises(EntryResolutionError, match="example"):
|
|
299
|
+
real_manager.resolve_entry("example", subpath)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def test_offline_builtin(real_manager):
|
|
303
|
+
installed(real_manager, "fs")
|
|
304
|
+
with pytest.raises(EntryResolutionError, match="ERR_UNSUPPORTED_BUILTIN"):
|
|
305
|
+
real_manager.resolve_entry("fs")
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def test_offline_internal_symlink(real_manager):
|
|
309
|
+
root = installed(real_manager)
|
|
310
|
+
(root / "plugin.js").unlink()
|
|
311
|
+
(root / "plugin.js").symlink_to(root / "index.js")
|
|
312
|
+
assert real_manager.resolve_entry("example", "plugin") == root / "index.js"
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def test_offline_directory_index(real_manager):
|
|
316
|
+
root = installed(real_manager)
|
|
317
|
+
(root / "plugin.js").unlink()
|
|
318
|
+
(root / "plugin").mkdir()
|
|
319
|
+
target = root / "plugin/index.js"
|
|
320
|
+
target.write_text("throw new Error('PACKAGE EXECUTED');")
|
|
321
|
+
assert real_manager.resolve_entry("example", "plugin") == target
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def test_offline_entry_symlink_escape(real_manager, tmp_path):
|
|
325
|
+
root = installed(real_manager)
|
|
326
|
+
outside = tmp_path / "outside.js"
|
|
327
|
+
outside.write_text("throw new Error('PACKAGE EXECUTED');")
|
|
328
|
+
(root / "index.js").unlink()
|
|
329
|
+
(root / "index.js").symlink_to(outside)
|
|
330
|
+
with pytest.raises(EntryResolutionError, match="escapes"):
|
|
331
|
+
real_manager.resolve_entry("example")
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def test_no_ancestor_fallback(real_manager, tmp_path):
|
|
335
|
+
ancestor = NodeManager(tmp_path)
|
|
336
|
+
installed(ancestor)
|
|
337
|
+
with pytest.raises(PackageNotFoundError):
|
|
338
|
+
real_manager.resolve_entry("example")
|
|
339
|
+
assert not real_manager.directory.exists()
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def test_offline_exports_directory_rejected(real_manager):
|
|
343
|
+
installed(real_manager, exports="./")
|
|
344
|
+
with pytest.raises(EntryResolutionError):
|
|
345
|
+
real_manager.resolve_entry("example")
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def test_resolution_error_retains_stderr(manager, runner):
|
|
349
|
+
installed(manager)
|
|
350
|
+
runner.run.return_value = CommandResult(
|
|
351
|
+
("node",),
|
|
352
|
+
0,
|
|
353
|
+
json.dumps({"error": {"code": "MODULE_NOT_FOUND", "message": "missing"}}),
|
|
354
|
+
"runtime diagnostic",
|
|
355
|
+
)
|
|
356
|
+
with pytest.raises(EntryResolutionError, match="runtime diagnostic"):
|
|
357
|
+
manager.resolve_entry("example")
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def test_explicit_asset_ignores_exports(real_manager):
|
|
361
|
+
root = installed(real_manager, exports={".": "./index.js"})
|
|
362
|
+
assert real_manager.resolve("example", "plugin.js") == root / "plugin.js"
|
|
363
|
+
with pytest.raises(EntryResolutionError, match="ERR_PACKAGE_PATH_NOT_EXPORTED"):
|
|
364
|
+
real_manager.resolve_entry("example", "plugin")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|