make-cloudflare 0.2.0__tar.gz → 0.4.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.4.0}/PKG-INFO +24 -5
- {make_cloudflare-0.2.0 → make_cloudflare-0.4.0}/README.md +23 -4
- {make_cloudflare-0.2.0 → make_cloudflare-0.4.0}/pyproject.toml +1 -1
- {make_cloudflare-0.2.0 → make_cloudflare-0.4.0}/src/make_cloudflare/__init__.py +1 -1
- {make_cloudflare-0.2.0 → make_cloudflare-0.4.0}/src/make_cloudflare/cloudflare.py +6 -2
- {make_cloudflare-0.2.0 → make_cloudflare-0.4.0}/src/make_cloudflare/pages.py +130 -11
- {make_cloudflare-0.2.0 → make_cloudflare-0.4.0}/tests/test_pages.py +97 -0
- {make_cloudflare-0.2.0 → make_cloudflare-0.4.0}/.gitignore +0 -0
- {make_cloudflare-0.2.0 → make_cloudflare-0.4.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.4.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>
|
|
@@ -21,7 +21,7 @@ in the pipeline.
|
|
|
21
21
|
# Makefile.py in a consuming repo
|
|
22
22
|
# /// script
|
|
23
23
|
# requires-python = ">=3.11"
|
|
24
|
-
# dependencies = ["mkrun>=0.4", "make-cloudflare>=0.
|
|
24
|
+
# dependencies = ["mkrun>=0.4.1", "make-cloudflare>=0.3"]
|
|
25
25
|
# ///
|
|
26
26
|
from make_cloudflare import cloudflare # importing is what registers the group
|
|
27
27
|
```
|
|
@@ -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,24 @@ 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
|
+
|
|
65
|
+
**Those settings are one call.** `pages.configure(project, d1={"DB": "<id>"},
|
|
66
|
+
vars={...}, compatibility_date="2026-10-01")` PATCHes the project's
|
|
67
|
+
`production` (or `preview`) configuration. Only what you pass is sent, and
|
|
68
|
+
Cloudflare merges it in. It takes effect on the **next** deployment. Secrets
|
|
69
|
+
are not set this way: a plain variable is shown in the dashboard, so a secret
|
|
70
|
+
goes through `wrangler pages secret put`.
|
|
71
|
+
|
|
53
72
|
## Why this exists
|
|
54
73
|
|
|
55
74
|
`wrangler pages deploy` is four HTTPS calls and a hash. Reaching them through
|
|
@@ -80,9 +99,9 @@ call returns 200 and the site serves the old bytes, or none:
|
|
|
80
99
|
|
|
81
100
|
## What it does not do
|
|
82
101
|
|
|
83
|
-
**
|
|
84
|
-
refused by name
|
|
85
|
-
|
|
102
|
+
**Advanced-mode `_worker.js`.** A `_worker.js` (or a `functions/`) *inside*
|
|
103
|
+
the built directory is refused by name: it would otherwise be served as a
|
|
104
|
+
static file. Pages Functions go beside it, through `--functions`.
|
|
86
105
|
|
|
87
106
|
**Steps 2 and 3 are undocumented.** They exist because wrangler uses them, and
|
|
88
107
|
Cloudflare can change them without a changelog; the documented `deployments`
|
|
@@ -8,7 +8,7 @@ in the pipeline.
|
|
|
8
8
|
# Makefile.py in a consuming repo
|
|
9
9
|
# /// script
|
|
10
10
|
# requires-python = ">=3.11"
|
|
11
|
-
# dependencies = ["mkrun>=0.4", "make-cloudflare>=0.
|
|
11
|
+
# dependencies = ["mkrun>=0.4.1", "make-cloudflare>=0.3"]
|
|
12
12
|
# ///
|
|
13
13
|
from make_cloudflare import cloudflare # importing is what registers the group
|
|
14
14
|
```
|
|
@@ -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,24 @@ 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
|
+
|
|
52
|
+
**Those settings are one call.** `pages.configure(project, d1={"DB": "<id>"},
|
|
53
|
+
vars={...}, compatibility_date="2026-10-01")` PATCHes the project's
|
|
54
|
+
`production` (or `preview`) configuration. Only what you pass is sent, and
|
|
55
|
+
Cloudflare merges it in. It takes effect on the **next** deployment. Secrets
|
|
56
|
+
are not set this way: a plain variable is shown in the dashboard, so a secret
|
|
57
|
+
goes through `wrangler pages secret put`.
|
|
58
|
+
|
|
40
59
|
## Why this exists
|
|
41
60
|
|
|
42
61
|
`wrangler pages deploy` is four HTTPS calls and a hash. Reaching them through
|
|
@@ -67,9 +86,9 @@ call returns 200 and the site serves the old bytes, or none:
|
|
|
67
86
|
|
|
68
87
|
## What it does not do
|
|
69
88
|
|
|
70
|
-
**
|
|
71
|
-
refused by name
|
|
72
|
-
|
|
89
|
+
**Advanced-mode `_worker.js`.** A `_worker.js` (or a `functions/`) *inside*
|
|
90
|
+
the built directory is refused by name: it would otherwise be served as a
|
|
91
|
+
static file. Pages Functions go beside it, through `--functions`.
|
|
73
92
|
|
|
74
93
|
**Steps 2 and 3 are undocumented.** They exist because wrangler uses them, and
|
|
75
94
|
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,21 @@ 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",
|
|
64
|
+
"configure",
|
|
53
65
|
"collect",
|
|
54
66
|
"deploy",
|
|
55
67
|
"deployments",
|
|
@@ -84,9 +96,10 @@ PER_PAGE = 25
|
|
|
84
96
|
#: Handled by Pages itself, as fields on the deployment -- never as assets.
|
|
85
97
|
SPECIAL_FILES = ("_headers", "_redirects", "_routes.json")
|
|
86
98
|
|
|
87
|
-
#: Never uploaded.
|
|
88
|
-
#:
|
|
89
|
-
#:
|
|
99
|
+
#: Never uploaded. Pages Functions live *beside* the built directory and are
|
|
100
|
+
#: handed to `deploy(functions=...)`; inside it they would be served as static
|
|
101
|
+
#: files -- the source of the API, readable by anyone. An advanced-mode
|
|
102
|
+
#: `_worker.js` is not built here at all.
|
|
90
103
|
SKIP_NAMES = {".DS_Store", "Thumbs.db", ".gitkeep", *SPECIAL_FILES}
|
|
91
104
|
SKIP_DIRS = {".git", "node_modules", "__pycache__", ".wrangler"}
|
|
92
105
|
FUNCTIONS = ("_worker.js", "_worker.js.map", "_worker.bundle", "functions")
|
|
@@ -146,8 +159,9 @@ def collect(directory: str | Path) -> tuple[list[Asset], dict[str, str]]:
|
|
|
146
159
|
for name in FUNCTIONS:
|
|
147
160
|
if (root / name).exists():
|
|
148
161
|
raise MakeError(
|
|
149
|
-
f"{root / name} is
|
|
150
|
-
hint="
|
|
162
|
+
f"{root / name} is Pages Functions code inside the built directory",
|
|
163
|
+
hint="keep `functions/` beside the built directory and pass `functions=`; "
|
|
164
|
+
"an advanced-mode `_worker.js` is not supported",
|
|
151
165
|
)
|
|
152
166
|
|
|
153
167
|
assets: list[Asset] = []
|
|
@@ -260,7 +274,9 @@ def _json_call(method: str, url: str, *, auth: str, payload: object, **kwargs: A
|
|
|
260
274
|
)
|
|
261
275
|
|
|
262
276
|
|
|
263
|
-
def _multipart(
|
|
277
|
+
def _multipart(
|
|
278
|
+
fields: Mapping[str, str], files: Mapping[str, str | bytes] | None = None
|
|
279
|
+
) -> tuple[bytes, str]:
|
|
264
280
|
"""Encode a deployment's form: plain fields, then the special files as file parts.
|
|
265
281
|
|
|
266
282
|
⚠ `_headers` and `_redirects` must be FILE parts (a `filename=` on the part),
|
|
@@ -279,7 +295,7 @@ def _multipart(fields: Mapping[str, str], files: Mapping[str, str] | None = None
|
|
|
279
295
|
chunks.append(f"--{boundary}\r\n".encode())
|
|
280
296
|
chunks.append(f'Content-Disposition: form-data; name="{name}"; filename="{name}"\r\n'.encode())
|
|
281
297
|
chunks.append(b"Content-Type: application/octet-stream\r\n\r\n")
|
|
282
|
-
chunks.append(value.encode())
|
|
298
|
+
chunks.append(value if isinstance(value, bytes) else value.encode())
|
|
283
299
|
chunks.append(b"\r\n")
|
|
284
300
|
chunks.append(f"--{boundary}--\r\n".encode())
|
|
285
301
|
return b"".join(chunks), f"multipart/form-data; boundary={boundary}"
|
|
@@ -299,6 +315,55 @@ def _batches(assets: list[Asset]) -> Iterator[list[Asset]]:
|
|
|
299
315
|
yield batch
|
|
300
316
|
|
|
301
317
|
|
|
318
|
+
# -- Pages Functions -------------------------------------------------------
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _wrangler() -> list[str]:
|
|
322
|
+
found = shutil.which("wrangler")
|
|
323
|
+
return [found] if found else ["npx", "--yes", "wrangler"]
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def build_functions(functions: str | Path, directory: str | Path, out: str | Path) -> dict[str, bytes]:
|
|
327
|
+
"""Bundle a `functions/` tree; return the deployment's file parts.
|
|
328
|
+
|
|
329
|
+
`_worker.bundle` (the Worker, already in the upload form Pages takes),
|
|
330
|
+
`functions-filepath-routing-config.json`, and `_routes.json` unless the
|
|
331
|
+
built directory brings its own -- the same three `wrangler pages deploy`
|
|
332
|
+
sends. Empty under `--dry-run`, where nothing was built.
|
|
333
|
+
"""
|
|
334
|
+
source = Path(functions).resolve()
|
|
335
|
+
root = Path(directory).resolve()
|
|
336
|
+
if not source.is_dir():
|
|
337
|
+
raise MakeError(f"{source} is not a directory", hint="`functions=` names the Pages Functions tree")
|
|
338
|
+
out = Path(out)
|
|
339
|
+
bundle, config, routes = out / "_worker.bundle", out / "routing-config.json", out / "_routes.json"
|
|
340
|
+
sh(
|
|
341
|
+
*_wrangler(),
|
|
342
|
+
"pages",
|
|
343
|
+
"functions",
|
|
344
|
+
"build",
|
|
345
|
+
source,
|
|
346
|
+
"--outfile",
|
|
347
|
+
bundle,
|
|
348
|
+
"--output-config-path",
|
|
349
|
+
config,
|
|
350
|
+
"--output-routes-path",
|
|
351
|
+
routes,
|
|
352
|
+
"--build-output-directory",
|
|
353
|
+
root,
|
|
354
|
+
cwd=source.parent,
|
|
355
|
+
)
|
|
356
|
+
if not bundle.is_file():
|
|
357
|
+
return {}
|
|
358
|
+
parts = {
|
|
359
|
+
"_worker.bundle": bundle.read_bytes(),
|
|
360
|
+
"functions-filepath-routing-config.json": config.read_bytes(),
|
|
361
|
+
}
|
|
362
|
+
if not (root / "_routes.json").is_file() and routes.is_file():
|
|
363
|
+
parts["_routes.json"] = routes.read_bytes()
|
|
364
|
+
return parts
|
|
365
|
+
|
|
366
|
+
|
|
302
367
|
# -- the tasks' behaviour --------------------------------------------------
|
|
303
368
|
|
|
304
369
|
|
|
@@ -308,6 +373,7 @@ def deploy(
|
|
|
308
373
|
project: str,
|
|
309
374
|
branch: str = "main",
|
|
310
375
|
keep: int = KEEP,
|
|
376
|
+
functions: str | Path | None = None,
|
|
311
377
|
account: str | None = None,
|
|
312
378
|
token: str | None = None,
|
|
313
379
|
) -> str:
|
|
@@ -318,11 +384,17 @@ def deploy(
|
|
|
318
384
|
other name as a preview on its own URL. There is no `--production` flag to
|
|
319
385
|
forget; there is a branch name to get right.
|
|
320
386
|
|
|
387
|
+
`functions` names a Pages Functions tree (`functions/`, beside the built
|
|
388
|
+
directory): it is bundled by `build_functions` and goes up with the
|
|
389
|
+
deployment, so `/api/...` answers from the same project.
|
|
390
|
+
|
|
321
391
|
Afterwards the project is pruned to its newest `keep` deployments (see
|
|
322
392
|
`prune`); `keep=0` leaves every one in place.
|
|
323
393
|
"""
|
|
324
394
|
account, token = _credentials(account, token)
|
|
325
395
|
assets, specials = collect(directory)
|
|
396
|
+
with tempfile.TemporaryDirectory(prefix="mk-pages-") as scratch:
|
|
397
|
+
worker = build_functions(functions, directory, scratch) if functions is not None else {}
|
|
326
398
|
plural = "" if len(assets) == 1 else "s"
|
|
327
399
|
step(f"{len(assets)} file{plural}, {sum(a.size for a in assets) / 1024:.0f} KiB -> {project} ({branch})")
|
|
328
400
|
|
|
@@ -377,7 +449,7 @@ def deploy(
|
|
|
377
449
|
)
|
|
378
450
|
|
|
379
451
|
fields = {"manifest": json.dumps(manifest_of(assets)), "branch": branch}
|
|
380
|
-
body, content_type = _multipart(fields, specials)
|
|
452
|
+
body, content_type = _multipart(fields, {**specials, **worker})
|
|
381
453
|
created = _call(
|
|
382
454
|
"POST",
|
|
383
455
|
f"{API}/accounts/{account}/pages/projects/{project}/deployments",
|
|
@@ -385,8 +457,8 @@ def deploy(
|
|
|
385
457
|
body=body,
|
|
386
458
|
content_type=content_type,
|
|
387
459
|
)
|
|
388
|
-
if specials:
|
|
389
|
-
note("as deployment files, not assets: " + ", ".join(sorted(specials)))
|
|
460
|
+
if specials or worker:
|
|
461
|
+
note("as deployment files, not assets: " + ", ".join(sorted({**specials, **worker})))
|
|
390
462
|
url = created.get("url") if isinstance(created, dict) else None
|
|
391
463
|
if keep:
|
|
392
464
|
prune(project, keep=keep, account=account, token=token)
|
|
@@ -423,6 +495,53 @@ def create_project(
|
|
|
423
495
|
return created if isinstance(created, dict) else {"name": project}
|
|
424
496
|
|
|
425
497
|
|
|
498
|
+
def configure(
|
|
499
|
+
project: str,
|
|
500
|
+
*,
|
|
501
|
+
d1: Mapping[str, str] | None = None,
|
|
502
|
+
vars: Mapping[str, str] | None = None,
|
|
503
|
+
compatibility_date: str | None = None,
|
|
504
|
+
compatibility_flags: Iterable[str] | None = None,
|
|
505
|
+
environment: str = "production",
|
|
506
|
+
account: str | None = None,
|
|
507
|
+
token: str | None = None,
|
|
508
|
+
) -> dict:
|
|
509
|
+
"""Set what a project's Functions run with; return the project as Cloudflare now has it.
|
|
510
|
+
|
|
511
|
+
`d1` maps a binding name to a D1 database id (`{"DB": "<uuid>"}`), `vars` sets plain
|
|
512
|
+
environment variables, and the compatibility date and flags pick the runtime.
|
|
513
|
+
`environment` is `production` or `preview`. Only what is passed is sent, and
|
|
514
|
+
Cloudflare merges it into what the project already has -- nothing unnamed is removed.
|
|
515
|
+
|
|
516
|
+
⚠ Settings take effect on the **next deployment**, never on the live one. Secrets are
|
|
517
|
+
not set here: a value sent this way is stored in plain text and shown in the dashboard,
|
|
518
|
+
so a secret goes through `wrangler pages secret put`.
|
|
519
|
+
"""
|
|
520
|
+
if environment not in ("production", "preview"):
|
|
521
|
+
raise MakeError(f"no Pages environment {environment!r}", hint="production or preview")
|
|
522
|
+
settings: dict[str, Any] = {}
|
|
523
|
+
if d1:
|
|
524
|
+
settings["d1_databases"] = {name: {"id": ident} for name, ident in d1.items()}
|
|
525
|
+
if vars:
|
|
526
|
+
settings["env_vars"] = {name: {"type": "plain_text", "value": value} for name, value in vars.items()}
|
|
527
|
+
if compatibility_date:
|
|
528
|
+
settings["compatibility_date"] = compatibility_date
|
|
529
|
+
if compatibility_flags is not None:
|
|
530
|
+
settings["compatibility_flags"] = list(compatibility_flags)
|
|
531
|
+
if not settings:
|
|
532
|
+
raise MakeError(f"nothing to set on {project}", hint="pass d1=, vars= or a compatibility date")
|
|
533
|
+
|
|
534
|
+
account, token = _credentials(account, token)
|
|
535
|
+
step(f"{project} ({environment}): " + ", ".join(sorted(settings)))
|
|
536
|
+
result = _json_call(
|
|
537
|
+
"PATCH",
|
|
538
|
+
f"{API}/accounts/{account}/pages/projects/{project}",
|
|
539
|
+
auth=token,
|
|
540
|
+
payload={"deployment_configs": {environment: settings}},
|
|
541
|
+
)
|
|
542
|
+
return result if isinstance(result, dict) else {"name": project}
|
|
543
|
+
|
|
544
|
+
|
|
426
545
|
def deployments(project: str, *, account: str | None = None, token: str | None = None) -> list[dict]:
|
|
427
546
|
"""A project's deployments, newest first -- every page of them.
|
|
428
547
|
|
|
@@ -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,93 @@ 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")
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
# -- project settings --------------------------------------------------------
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def test_configure_patches_only_what_it_is_given(monkeypatch):
|
|
432
|
+
fake = FakeHTTP()
|
|
433
|
+
monkeypatch.setattr(pages.http, "request", fake)
|
|
434
|
+
pages.configure(
|
|
435
|
+
"demo",
|
|
436
|
+
d1={"DB": "uuid-1"},
|
|
437
|
+
vars={"ZONE": "example.dev"},
|
|
438
|
+
compatibility_date="2026-10-01",
|
|
439
|
+
account="acct",
|
|
440
|
+
token="tok",
|
|
441
|
+
)
|
|
442
|
+
method, url, call = fake.calls[0]
|
|
443
|
+
assert (method, url) == ("PATCH", f"{pages.API}/accounts/acct/pages/projects/demo")
|
|
444
|
+
assert json.loads(call["data"]) == {
|
|
445
|
+
"deployment_configs": {
|
|
446
|
+
"production": {
|
|
447
|
+
"d1_databases": {"DB": {"id": "uuid-1"}},
|
|
448
|
+
"env_vars": {"ZONE": {"type": "plain_text", "value": "example.dev"}},
|
|
449
|
+
"compatibility_date": "2026-10-01",
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def test_configure_refuses_an_empty_or_unknown_request():
|
|
456
|
+
with pytest.raises(MakeError, match="nothing to set"):
|
|
457
|
+
pages.configure("demo", account="acct", token="tok")
|
|
458
|
+
with pytest.raises(MakeError, match="no Pages environment"):
|
|
459
|
+
pages.configure("demo", vars={"A": "1"}, environment="staging", account="acct", token="tok")
|
|
File without changes
|
|
File without changes
|