make-cloudflare 0.2.0__tar.gz → 0.3.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.
- {make_cloudflare-0.2.0 → make_cloudflare-0.3.0}/PKG-INFO +16 -4
- {make_cloudflare-0.2.0 → make_cloudflare-0.3.0}/README.md +15 -3
- {make_cloudflare-0.2.0 → make_cloudflare-0.3.0}/pyproject.toml +1 -1
- {make_cloudflare-0.2.0 → make_cloudflare-0.3.0}/src/make_cloudflare/__init__.py +1 -1
- {make_cloudflare-0.2.0 → make_cloudflare-0.3.0}/src/make_cloudflare/cloudflare.py +6 -2
- {make_cloudflare-0.2.0 → make_cloudflare-0.3.0}/src/make_cloudflare/pages.py +82 -11
- {make_cloudflare-0.2.0 → make_cloudflare-0.3.0}/tests/test_pages.py +63 -0
- {make_cloudflare-0.2.0 → make_cloudflare-0.3.0}/.gitignore +0 -0
- {make_cloudflare-0.2.0 → make_cloudflare-0.3.0}/tests/conftest.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: make-cloudflare
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Cloudflare Pages direct upload for mkrun -- deploy a built site with no Node and no wrangler.
|
|
5
5
|
Project-URL: Homepage, https://make.optersoft.com
|
|
6
6
|
Author-email: "Optersoft, S.L." <david@optersoft.com>
|
|
@@ -29,6 +29,7 @@ from make_cloudflare import cloudflare # importing is what registers the group
|
|
|
29
29
|
```console
|
|
30
30
|
$ mk cloudflare.deploy site/dist --project mkrun # production (branch main)
|
|
31
31
|
$ mk cloudflare.deploy site/dist --branch try # a preview, on its own URL
|
|
32
|
+
$ mk cloudflare.deploy dist --functions functions # with Pages Functions
|
|
32
33
|
$ mk cloudflare.projects # what the account has
|
|
33
34
|
$ mk cloudflare.deployments --limit 5 # one project's recent ones
|
|
34
35
|
$ mk cloudflare.prune --keep 10 # delete all but the newest ten
|
|
@@ -50,6 +51,17 @@ Python). Pages never deletes one by itself, and each stays reachable on its own
|
|
|
50
51
|
never deleted, however old; a pruned preview that was still its branch's alias
|
|
51
52
|
takes that branch URL with it.
|
|
52
53
|
|
|
54
|
+
**Pages Functions go up with the deployment.** `--functions functions` (or
|
|
55
|
+
`pages.deploy(..., functions=)`) names the tree. It sits **beside** the built
|
|
56
|
+
directory, as wrangler expects, and `/api/...` answers from the same project and
|
|
57
|
+
origin. Bundling it is a build step, not an upload, so that one step is
|
|
58
|
+
`wrangler pages functions build --outfile` (from PATH, or through `npx`). It
|
|
59
|
+
writes exactly the `_worker.bundle` the deployment takes, plus
|
|
60
|
+
`functions-filepath-routing-config.json` and a generated `_routes.json`; the
|
|
61
|
+
site's own `_routes.json` wins. Everything else is still the four calls below.
|
|
62
|
+
Bindings (D1, KV) and secrets are settings on the Pages project, never part of
|
|
63
|
+
the upload.
|
|
64
|
+
|
|
53
65
|
## Why this exists
|
|
54
66
|
|
|
55
67
|
`wrangler pages deploy` is four HTTPS calls and a hash. Reaching them through
|
|
@@ -80,9 +92,9 @@ call returns 200 and the site serves the old bytes, or none:
|
|
|
80
92
|
|
|
81
93
|
## What it does not do
|
|
82
94
|
|
|
83
|
-
**
|
|
84
|
-
refused by name
|
|
85
|
-
|
|
95
|
+
**Advanced-mode `_worker.js`.** A `_worker.js` (or a `functions/`) *inside*
|
|
96
|
+
the built directory is refused by name: it would otherwise be served as a
|
|
97
|
+
static file. Pages Functions go beside it, through `--functions`.
|
|
86
98
|
|
|
87
99
|
**Steps 2 and 3 are undocumented.** They exist because wrangler uses them, and
|
|
88
100
|
Cloudflare can change them without a changelog; the documented `deployments`
|
|
@@ -16,6 +16,7 @@ from make_cloudflare import cloudflare # importing is what registers the group
|
|
|
16
16
|
```console
|
|
17
17
|
$ mk cloudflare.deploy site/dist --project mkrun # production (branch main)
|
|
18
18
|
$ mk cloudflare.deploy site/dist --branch try # a preview, on its own URL
|
|
19
|
+
$ mk cloudflare.deploy dist --functions functions # with Pages Functions
|
|
19
20
|
$ mk cloudflare.projects # what the account has
|
|
20
21
|
$ mk cloudflare.deployments --limit 5 # one project's recent ones
|
|
21
22
|
$ mk cloudflare.prune --keep 10 # delete all but the newest ten
|
|
@@ -37,6 +38,17 @@ Python). Pages never deletes one by itself, and each stays reachable on its own
|
|
|
37
38
|
never deleted, however old; a pruned preview that was still its branch's alias
|
|
38
39
|
takes that branch URL with it.
|
|
39
40
|
|
|
41
|
+
**Pages Functions go up with the deployment.** `--functions functions` (or
|
|
42
|
+
`pages.deploy(..., functions=)`) names the tree. It sits **beside** the built
|
|
43
|
+
directory, as wrangler expects, and `/api/...` answers from the same project and
|
|
44
|
+
origin. Bundling it is a build step, not an upload, so that one step is
|
|
45
|
+
`wrangler pages functions build --outfile` (from PATH, or through `npx`). It
|
|
46
|
+
writes exactly the `_worker.bundle` the deployment takes, plus
|
|
47
|
+
`functions-filepath-routing-config.json` and a generated `_routes.json`; the
|
|
48
|
+
site's own `_routes.json` wins. Everything else is still the four calls below.
|
|
49
|
+
Bindings (D1, KV) and secrets are settings on the Pages project, never part of
|
|
50
|
+
the upload.
|
|
51
|
+
|
|
40
52
|
## Why this exists
|
|
41
53
|
|
|
42
54
|
`wrangler pages deploy` is four HTTPS calls and a hash. Reaching them through
|
|
@@ -67,9 +79,9 @@ call returns 200 and the site serves the old bytes, or none:
|
|
|
67
79
|
|
|
68
80
|
## What it does not do
|
|
69
81
|
|
|
70
|
-
**
|
|
71
|
-
refused by name
|
|
72
|
-
|
|
82
|
+
**Advanced-mode `_worker.js`.** A `_worker.js` (or a `functions/`) *inside*
|
|
83
|
+
the built directory is refused by name: it would otherwise be served as a
|
|
84
|
+
static file. Pages Functions go beside it, through `--functions`.
|
|
73
85
|
|
|
74
86
|
**Steps 2 and 3 are undocumented.** They exist because wrangler uses them, and
|
|
75
87
|
Cloudflare can change them without a changelog; the documented `deployments`
|
|
@@ -24,15 +24,19 @@ def deploy(
|
|
|
24
24
|
project: str | None = None,
|
|
25
25
|
branch: Annotated[str, arg(help="the Pages branch; the production one publishes live")] = "main",
|
|
26
26
|
keep: Annotated[int, arg(help="deployments to keep afterwards; 0 keeps every one")] = pages.KEEP,
|
|
27
|
+
functions: Annotated[str | None, arg(help="a Pages Functions tree to bundle and deploy with it")] = None,
|
|
27
28
|
) -> None:
|
|
28
29
|
"""Publish a built directory to Cloudflare Pages by direct upload.
|
|
29
30
|
|
|
30
|
-
No Node and no wrangler: four HTTPS calls. `--
|
|
31
|
+
No Node and no wrangler: four HTTPS calls. (`--functions` is the exception:
|
|
32
|
+
wrangler bundles that tree, and only bundles it.) `--branch` is what decides
|
|
31
33
|
production -- the project's production branch goes live, every other name
|
|
32
34
|
lands as a preview on its own URL. Then the project is pruned to its
|
|
33
35
|
newest `--keep` deployments; the live one always stays.
|
|
34
36
|
"""
|
|
35
|
-
url = pages.deploy(
|
|
37
|
+
url = pages.deploy(
|
|
38
|
+
Path(directory), project=pages.project_name(project), branch=branch, keep=keep, functions=functions
|
|
39
|
+
)
|
|
36
40
|
note(f"deployed -- {url}")
|
|
37
41
|
|
|
38
42
|
|
|
@@ -8,6 +8,7 @@ a built directory without installing a JavaScript toolchain to do it.
|
|
|
8
8
|
|
|
9
9
|
pages.deploy("site/dist", project="mkrun") # production
|
|
10
10
|
pages.deploy("site/dist", project="mkrun", branch="x") # a preview
|
|
11
|
+
pages.deploy("dist", project="name", functions="functions") # with Pages Functions
|
|
11
12
|
|
|
12
13
|
The flow, and the three things a naive port gets wrong:
|
|
13
14
|
|
|
@@ -30,6 +31,13 @@ reports success while the site's CSP quietly stops applying.
|
|
|
30
31
|
**Steps 2 and 3 are undocumented.** They exist because wrangler uses them;
|
|
31
32
|
Cloudflare can change them without a changelog. The documented `deployments`
|
|
32
33
|
endpoint alone cannot upload a file.
|
|
34
|
+
|
|
35
|
+
**Pages Functions are built by wrangler, and only built.** Bundling a
|
|
36
|
+
`functions/` tree into a Worker is esbuild plus Cloudflare's routing glue, and
|
|
37
|
+
`wrangler pages functions build --outfile` writes exactly the `_worker.bundle`
|
|
38
|
+
the deployment takes. That one build step is the only thing wrangler does here;
|
|
39
|
+
the upload is still these calls. Bindings (D1, KV) and secrets are settings on
|
|
40
|
+
the Pages project, never part of the upload.
|
|
33
41
|
"""
|
|
34
42
|
|
|
35
43
|
from __future__ import annotations
|
|
@@ -39,17 +47,20 @@ import json
|
|
|
39
47
|
import mimetypes
|
|
40
48
|
import os
|
|
41
49
|
import secrets as _random
|
|
50
|
+
import shutil
|
|
51
|
+
import tempfile
|
|
42
52
|
from collections.abc import Iterable, Iterator, Mapping
|
|
43
53
|
from dataclasses import dataclass
|
|
44
54
|
from pathlib import Path
|
|
45
55
|
from typing import Any
|
|
46
56
|
|
|
47
|
-
from make import env, http, note, step
|
|
57
|
+
from make import env, http, note, sh, step
|
|
48
58
|
from make.context import mark_sensitive
|
|
49
59
|
from make.errors import ConfigError, MakeError
|
|
50
60
|
|
|
51
61
|
__all__ = [
|
|
52
62
|
"Asset",
|
|
63
|
+
"build_functions",
|
|
53
64
|
"collect",
|
|
54
65
|
"deploy",
|
|
55
66
|
"deployments",
|
|
@@ -84,9 +95,10 @@ PER_PAGE = 25
|
|
|
84
95
|
#: Handled by Pages itself, as fields on the deployment -- never as assets.
|
|
85
96
|
SPECIAL_FILES = ("_headers", "_redirects", "_routes.json")
|
|
86
97
|
|
|
87
|
-
#: Never uploaded.
|
|
88
|
-
#:
|
|
89
|
-
#:
|
|
98
|
+
#: Never uploaded. Pages Functions live *beside* the built directory and are
|
|
99
|
+
#: handed to `deploy(functions=...)`; inside it they would be served as static
|
|
100
|
+
#: files -- the source of the API, readable by anyone. An advanced-mode
|
|
101
|
+
#: `_worker.js` is not built here at all.
|
|
90
102
|
SKIP_NAMES = {".DS_Store", "Thumbs.db", ".gitkeep", *SPECIAL_FILES}
|
|
91
103
|
SKIP_DIRS = {".git", "node_modules", "__pycache__", ".wrangler"}
|
|
92
104
|
FUNCTIONS = ("_worker.js", "_worker.js.map", "_worker.bundle", "functions")
|
|
@@ -146,8 +158,9 @@ def collect(directory: str | Path) -> tuple[list[Asset], dict[str, str]]:
|
|
|
146
158
|
for name in FUNCTIONS:
|
|
147
159
|
if (root / name).exists():
|
|
148
160
|
raise MakeError(
|
|
149
|
-
f"{root / name} is
|
|
150
|
-
hint="
|
|
161
|
+
f"{root / name} is Pages Functions code inside the built directory",
|
|
162
|
+
hint="keep `functions/` beside the built directory and pass `functions=`; "
|
|
163
|
+
"an advanced-mode `_worker.js` is not supported",
|
|
151
164
|
)
|
|
152
165
|
|
|
153
166
|
assets: list[Asset] = []
|
|
@@ -260,7 +273,9 @@ def _json_call(method: str, url: str, *, auth: str, payload: object, **kwargs: A
|
|
|
260
273
|
)
|
|
261
274
|
|
|
262
275
|
|
|
263
|
-
def _multipart(
|
|
276
|
+
def _multipart(
|
|
277
|
+
fields: Mapping[str, str], files: Mapping[str, str | bytes] | None = None
|
|
278
|
+
) -> tuple[bytes, str]:
|
|
264
279
|
"""Encode a deployment's form: plain fields, then the special files as file parts.
|
|
265
280
|
|
|
266
281
|
⚠ `_headers` and `_redirects` must be FILE parts (a `filename=` on the part),
|
|
@@ -279,7 +294,7 @@ def _multipart(fields: Mapping[str, str], files: Mapping[str, str] | None = None
|
|
|
279
294
|
chunks.append(f"--{boundary}\r\n".encode())
|
|
280
295
|
chunks.append(f'Content-Disposition: form-data; name="{name}"; filename="{name}"\r\n'.encode())
|
|
281
296
|
chunks.append(b"Content-Type: application/octet-stream\r\n\r\n")
|
|
282
|
-
chunks.append(value.encode())
|
|
297
|
+
chunks.append(value if isinstance(value, bytes) else value.encode())
|
|
283
298
|
chunks.append(b"\r\n")
|
|
284
299
|
chunks.append(f"--{boundary}--\r\n".encode())
|
|
285
300
|
return b"".join(chunks), f"multipart/form-data; boundary={boundary}"
|
|
@@ -299,6 +314,55 @@ def _batches(assets: list[Asset]) -> Iterator[list[Asset]]:
|
|
|
299
314
|
yield batch
|
|
300
315
|
|
|
301
316
|
|
|
317
|
+
# -- Pages Functions -------------------------------------------------------
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _wrangler() -> list[str]:
|
|
321
|
+
found = shutil.which("wrangler")
|
|
322
|
+
return [found] if found else ["npx", "--yes", "wrangler"]
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def build_functions(functions: str | Path, directory: str | Path, out: str | Path) -> dict[str, bytes]:
|
|
326
|
+
"""Bundle a `functions/` tree; return the deployment's file parts.
|
|
327
|
+
|
|
328
|
+
`_worker.bundle` (the Worker, already in the upload form Pages takes),
|
|
329
|
+
`functions-filepath-routing-config.json`, and `_routes.json` unless the
|
|
330
|
+
built directory brings its own -- the same three `wrangler pages deploy`
|
|
331
|
+
sends. Empty under `--dry-run`, where nothing was built.
|
|
332
|
+
"""
|
|
333
|
+
source = Path(functions).resolve()
|
|
334
|
+
root = Path(directory).resolve()
|
|
335
|
+
if not source.is_dir():
|
|
336
|
+
raise MakeError(f"{source} is not a directory", hint="`functions=` names the Pages Functions tree")
|
|
337
|
+
out = Path(out)
|
|
338
|
+
bundle, config, routes = out / "_worker.bundle", out / "routing-config.json", out / "_routes.json"
|
|
339
|
+
sh(
|
|
340
|
+
*_wrangler(),
|
|
341
|
+
"pages",
|
|
342
|
+
"functions",
|
|
343
|
+
"build",
|
|
344
|
+
source,
|
|
345
|
+
"--outfile",
|
|
346
|
+
bundle,
|
|
347
|
+
"--output-config-path",
|
|
348
|
+
config,
|
|
349
|
+
"--output-routes-path",
|
|
350
|
+
routes,
|
|
351
|
+
"--build-output-directory",
|
|
352
|
+
root,
|
|
353
|
+
cwd=source.parent,
|
|
354
|
+
)
|
|
355
|
+
if not bundle.is_file():
|
|
356
|
+
return {}
|
|
357
|
+
parts = {
|
|
358
|
+
"_worker.bundle": bundle.read_bytes(),
|
|
359
|
+
"functions-filepath-routing-config.json": config.read_bytes(),
|
|
360
|
+
}
|
|
361
|
+
if not (root / "_routes.json").is_file() and routes.is_file():
|
|
362
|
+
parts["_routes.json"] = routes.read_bytes()
|
|
363
|
+
return parts
|
|
364
|
+
|
|
365
|
+
|
|
302
366
|
# -- the tasks' behaviour --------------------------------------------------
|
|
303
367
|
|
|
304
368
|
|
|
@@ -308,6 +372,7 @@ def deploy(
|
|
|
308
372
|
project: str,
|
|
309
373
|
branch: str = "main",
|
|
310
374
|
keep: int = KEEP,
|
|
375
|
+
functions: str | Path | None = None,
|
|
311
376
|
account: str | None = None,
|
|
312
377
|
token: str | None = None,
|
|
313
378
|
) -> str:
|
|
@@ -318,11 +383,17 @@ def deploy(
|
|
|
318
383
|
other name as a preview on its own URL. There is no `--production` flag to
|
|
319
384
|
forget; there is a branch name to get right.
|
|
320
385
|
|
|
386
|
+
`functions` names a Pages Functions tree (`functions/`, beside the built
|
|
387
|
+
directory): it is bundled by `build_functions` and goes up with the
|
|
388
|
+
deployment, so `/api/...` answers from the same project.
|
|
389
|
+
|
|
321
390
|
Afterwards the project is pruned to its newest `keep` deployments (see
|
|
322
391
|
`prune`); `keep=0` leaves every one in place.
|
|
323
392
|
"""
|
|
324
393
|
account, token = _credentials(account, token)
|
|
325
394
|
assets, specials = collect(directory)
|
|
395
|
+
with tempfile.TemporaryDirectory(prefix="mk-pages-") as scratch:
|
|
396
|
+
worker = build_functions(functions, directory, scratch) if functions is not None else {}
|
|
326
397
|
plural = "" if len(assets) == 1 else "s"
|
|
327
398
|
step(f"{len(assets)} file{plural}, {sum(a.size for a in assets) / 1024:.0f} KiB -> {project} ({branch})")
|
|
328
399
|
|
|
@@ -377,7 +448,7 @@ def deploy(
|
|
|
377
448
|
)
|
|
378
449
|
|
|
379
450
|
fields = {"manifest": json.dumps(manifest_of(assets)), "branch": branch}
|
|
380
|
-
body, content_type = _multipart(fields, specials)
|
|
451
|
+
body, content_type = _multipart(fields, {**specials, **worker})
|
|
381
452
|
created = _call(
|
|
382
453
|
"POST",
|
|
383
454
|
f"{API}/accounts/{account}/pages/projects/{project}/deployments",
|
|
@@ -385,8 +456,8 @@ def deploy(
|
|
|
385
456
|
body=body,
|
|
386
457
|
content_type=content_type,
|
|
387
458
|
)
|
|
388
|
-
if specials:
|
|
389
|
-
note("as deployment files, not assets: " + ", ".join(sorted(specials)))
|
|
459
|
+
if specials or worker:
|
|
460
|
+
note("as deployment files, not assets: " + ", ".join(sorted({**specials, **worker})))
|
|
390
461
|
url = created.get("url") if isinstance(created, dict) else None
|
|
391
462
|
if keep:
|
|
392
463
|
prune(project, keep=keep, account=account, token=token)
|
|
@@ -109,6 +109,13 @@ def test_a_functions_project_is_refused(tmp_path):
|
|
|
109
109
|
pages.collect(tmp_path)
|
|
110
110
|
|
|
111
111
|
|
|
112
|
+
def test_functions_inside_the_built_directory_are_refused(tmp_path):
|
|
113
|
+
"""Served from there, the API's source would be a static file anyone can read."""
|
|
114
|
+
build(tmp_path, {"index.html": "x", "functions/api/x.ts": "export const onRequest = 1"})
|
|
115
|
+
with pytest.raises(MakeError, match="inside the built directory"):
|
|
116
|
+
pages.collect(tmp_path)
|
|
117
|
+
|
|
118
|
+
|
|
112
119
|
def test_an_oversized_asset_is_named(tmp_path):
|
|
113
120
|
(tmp_path / "big.bin").write_bytes(b"x" * (pages.MAX_ASSET_SIZE + 1))
|
|
114
121
|
with pytest.raises(MakeError, match=r"big\.bin"):
|
|
@@ -360,3 +367,59 @@ def test_deploy_prunes_unless_told_to_keep_everything(monkeypatch, tmp_path):
|
|
|
360
367
|
calls.clear()
|
|
361
368
|
pages.deploy(tmp_path, project="demo", account="acct", token="tok", keep=0)
|
|
362
369
|
assert deleted(calls) == [] and not any("/deployments?" in url for _, url in calls)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
# -- Pages Functions -------------------------------------------------------
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def fake_wrangler(monkeypatch):
|
|
376
|
+
"""Stands in for `wrangler pages functions build`, writing the three outputs."""
|
|
377
|
+
seen = []
|
|
378
|
+
|
|
379
|
+
def run(*argv, cwd=None, **kwargs):
|
|
380
|
+
argv = [str(a) for a in argv]
|
|
381
|
+
seen.append(argv)
|
|
382
|
+
out = {
|
|
383
|
+
flag: argv[argv.index(flag) + 1]
|
|
384
|
+
for flag in ("--outfile", "--output-config-path", "--output-routes-path")
|
|
385
|
+
}
|
|
386
|
+
pages.Path(out["--outfile"]).write_bytes(b"--bundle--")
|
|
387
|
+
pages.Path(out["--output-config-path"]).write_text('{"routes": []}')
|
|
388
|
+
pages.Path(out["--output-routes-path"]).write_text('{"include": ["/api/*"]}')
|
|
389
|
+
|
|
390
|
+
monkeypatch.setattr(pages, "sh", run)
|
|
391
|
+
return seen
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def test_functions_go_up_as_file_parts_of_the_deployment(monkeypatch, tmp_path):
|
|
395
|
+
site = build(tmp_path / "dist", {"index.html": "x"})
|
|
396
|
+
functions = build(tmp_path / "functions", {"api/[[path]].ts": "export const onRequest = 1"})
|
|
397
|
+
seen = fake_wrangler(monkeypatch)
|
|
398
|
+
fake = FakeHTTP()
|
|
399
|
+
deploy(monkeypatch, site, fake, functions=functions)
|
|
400
|
+
|
|
401
|
+
assert seen[0][1:4] == ["pages", "functions", "build"] or seen[0][3:6] == ["pages", "functions", "build"]
|
|
402
|
+
body = fake.body("/deployments").decode()
|
|
403
|
+
assert 'name="_worker.bundle"; filename="_worker.bundle"' in body and "--bundle--" in body
|
|
404
|
+
assert 'name="functions-filepath-routing-config.json"; filename=' in body
|
|
405
|
+
assert 'name="_routes.json"; filename="_routes.json"' in body and "/api/*" in body
|
|
406
|
+
# The functions are never assets.
|
|
407
|
+
assert "[[path]]" not in fake.body("/check-missing").decode()
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def test_the_sites_own_routes_beat_the_generated_ones(monkeypatch, tmp_path):
|
|
411
|
+
site = build(tmp_path / "dist", {"index.html": "x", "_routes.json": '{"include": ["/mine/*"]}'})
|
|
412
|
+
functions = build(tmp_path / "functions", {"api/x.ts": "export const onRequest = 1"})
|
|
413
|
+
fake_wrangler(monkeypatch)
|
|
414
|
+
fake = FakeHTTP()
|
|
415
|
+
deploy(monkeypatch, site, fake, functions=functions)
|
|
416
|
+
|
|
417
|
+
body = fake.body("/deployments").decode()
|
|
418
|
+
assert body.count('filename="_routes.json"') == 1
|
|
419
|
+
assert "/mine/*" in body and "/api/*" not in body
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def test_a_missing_functions_tree_is_named(monkeypatch, tmp_path):
|
|
423
|
+
site = build(tmp_path / "dist", {"index.html": "x"})
|
|
424
|
+
with pytest.raises(MakeError, match="not a directory"):
|
|
425
|
+
deploy(monkeypatch, site, FakeHTTP(), functions=tmp_path / "nope")
|
|
File without changes
|
|
File without changes
|