shipit-cli 0.18.2__tar.gz → 0.19.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.
- {shipit_cli-0.18.2 → shipit_cli-0.19.0}/PKG-INFO +1 -1
- {shipit_cli-0.18.2 → shipit_cli-0.19.0}/pyproject.toml +1 -1
- {shipit_cli-0.18.2 → shipit_cli-0.19.0}/src/shipit/assets/php/php.ini +1 -1
- {shipit_cli-0.18.2 → shipit_cli-0.19.0}/src/shipit/assets/wordpress/install.sh +7 -2
- {shipit_cli-0.18.2 → shipit_cli-0.19.0}/src/shipit/assets/wordpress/wp-config.php +0 -4
- {shipit_cli-0.18.2 → shipit_cli-0.19.0}/src/shipit/builders/base.py +1 -0
- {shipit_cli-0.18.2 → shipit_cli-0.19.0}/src/shipit/builders/docker.py +13 -0
- {shipit_cli-0.18.2 → shipit_cli-0.19.0}/src/shipit/builders/local.py +11 -0
- {shipit_cli-0.18.2 → shipit_cli-0.19.0}/src/shipit/cli.py +65 -6
- {shipit_cli-0.18.2 → shipit_cli-0.19.0}/src/shipit/providers/mkdocs.py +1 -1
- {shipit_cli-0.18.2 → shipit_cli-0.19.0}/src/shipit/providers/node_static.py +1 -1
- shipit_cli-0.19.0/src/shipit/providers/staticfile.py +328 -0
- {shipit_cli-0.18.2 → shipit_cli-0.19.0}/src/shipit/providers/wordpress.py +3 -2
- {shipit_cli-0.18.2 → shipit_cli-0.19.0}/src/shipit/runners/base.py +1 -0
- {shipit_cli-0.18.2 → shipit_cli-0.19.0}/src/shipit/runners/local.py +28 -0
- {shipit_cli-0.18.2 → shipit_cli-0.19.0}/src/shipit/runners/wasmer.py +57 -22
- {shipit_cli-0.18.2 → shipit_cli-0.19.0}/src/shipit/shipit_types.py +16 -1
- shipit_cli-0.19.0/src/shipit/version.py +5 -0
- shipit_cli-0.19.0/tests/test_cli_after_deploy.py +127 -0
- {shipit_cli-0.18.2 → shipit_cli-0.19.0}/tests/test_e2e.py +58 -16
- shipit_cli-0.19.0/tests/test_php_provider.py +69 -0
- shipit_cli-0.19.0/tests/test_staticfile_provider.py +52 -0
- shipit_cli-0.19.0/tests/test_volumes.py +130 -0
- {shipit_cli-0.18.2 → shipit_cli-0.19.0}/tests/test_wasmer_annotations.py +25 -0
- shipit_cli-0.18.2/src/shipit/providers/staticfile.py +0 -119
- shipit_cli-0.18.2/src/shipit/version.py +0 -5
- {shipit_cli-0.18.2 → shipit_cli-0.19.0}/.gitignore +0 -0
- {shipit_cli-0.18.2 → shipit_cli-0.19.0}/README.md +0 -0
- {shipit_cli-0.18.2 → shipit_cli-0.19.0}/src/shipit/__init__.py +0 -0
- {shipit_cli-0.18.2 → shipit_cli-0.19.0}/src/shipit/assets/wordpress/.htaccess +0 -0
- {shipit_cli-0.18.2 → shipit_cli-0.19.0}/src/shipit/assets/wordpress/start.php +0 -0
- {shipit_cli-0.18.2 → shipit_cli-0.19.0}/src/shipit/builders/__init__.py +0 -0
- {shipit_cli-0.18.2 → shipit_cli-0.19.0}/src/shipit/generator.py +0 -0
- {shipit_cli-0.18.2 → shipit_cli-0.19.0}/src/shipit/procfile.py +0 -0
- {shipit_cli-0.18.2 → shipit_cli-0.19.0}/src/shipit/providers/base.py +0 -0
- {shipit_cli-0.18.2 → shipit_cli-0.19.0}/src/shipit/providers/go.py +0 -0
- {shipit_cli-0.18.2 → shipit_cli-0.19.0}/src/shipit/providers/hugo.py +0 -0
- {shipit_cli-0.18.2 → shipit_cli-0.19.0}/src/shipit/providers/jekyll.py +0 -0
- {shipit_cli-0.18.2 → shipit_cli-0.19.0}/src/shipit/providers/laravel.py +0 -0
- {shipit_cli-0.18.2 → shipit_cli-0.19.0}/src/shipit/providers/php.py +0 -0
- {shipit_cli-0.18.2 → shipit_cli-0.19.0}/src/shipit/providers/python.py +0 -0
- {shipit_cli-0.18.2 → shipit_cli-0.19.0}/src/shipit/providers/registry.py +0 -0
- {shipit_cli-0.18.2 → shipit_cli-0.19.0}/src/shipit/runners/__init__.py +0 -0
- {shipit_cli-0.18.2 → shipit_cli-0.19.0}/src/shipit/ui.py +0 -0
- {shipit_cli-0.18.2 → shipit_cli-0.19.0}/src/shipit/utils.py +0 -0
- {shipit_cli-0.18.2 → shipit_cli-0.19.0}/tests/test_generate_shipit_examples.py +0 -0
- {shipit_cli-0.18.2 → shipit_cli-0.19.0}/tests/test_version.py +0 -0
- {shipit_cli-0.18.2 → shipit_cli-0.19.0}/tests/test_wordpress_phpix.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: shipit-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.19.0
|
|
4
4
|
Summary: Shipit CLI is the best way to build, serve and deploy your projects anywhere.
|
|
5
5
|
Project-URL: homepage, https://wasmer.io
|
|
6
6
|
Project-URL: repository, https://github.com/wasmerio/shipit
|
|
@@ -18,6 +18,10 @@ echo "Creating required directories..."
|
|
|
18
18
|
mkdir -p wp-content/plugins
|
|
19
19
|
mkdir -p wp-content/upgrade
|
|
20
20
|
|
|
21
|
+
if [ -n "${WPCONTENT_BASE_PATH:-}" ] && [ -d "${WPCONTENT_BASE_PATH}" ]; then
|
|
22
|
+
cp -R "${WPCONTENT_BASE_PATH}/." /app/wp-content/
|
|
23
|
+
fi
|
|
24
|
+
|
|
21
25
|
echo "Installing WordPress core"
|
|
22
26
|
|
|
23
27
|
wp core install \
|
|
@@ -28,7 +32,6 @@ wp core install \
|
|
|
28
32
|
--admin_email="$WP_ADMIN_EMAIL" \
|
|
29
33
|
--locale="$WP_LOCALE"
|
|
30
34
|
|
|
31
|
-
|
|
32
35
|
if [ "${WP_UPDATE_DB:-false}" = "true" ]; then
|
|
33
36
|
echo "Updating database..."
|
|
34
37
|
wp core update-db
|
|
@@ -96,11 +99,13 @@ if [ -n "${WP_LOCALE:-}" ]; then
|
|
|
96
99
|
wp site switch-language "$WP_LOCALE"
|
|
97
100
|
fi
|
|
98
101
|
|
|
99
|
-
|
|
102
|
+
if [ ! -f "/app/wp-content/wp-config.php" ]; then
|
|
103
|
+
cat > /app/wp-content/wp-config.php <<EOF
|
|
100
104
|
<?php
|
|
101
105
|
// If you need to set custom configuration, you can place it here.
|
|
102
106
|
// This file will be included by the main wp-config.php after
|
|
103
107
|
// loading environment variables.
|
|
104
108
|
EOF
|
|
109
|
+
fi
|
|
105
110
|
|
|
106
111
|
echo "✅ WordPress Installation complete"
|
|
@@ -11,4 +11,5 @@ class BuildBackend(Protocol):
|
|
|
11
11
|
) -> None: ...
|
|
12
12
|
def get_build_mount_path(self, name: str) -> Path: ...
|
|
13
13
|
def get_artifact_mount_path(self, name: str) -> Path: ...
|
|
14
|
+
def get_volume_path(self, name: str) -> Path: ...
|
|
14
15
|
def get_runtime_path(self) -> Optional[str]: ...
|
|
@@ -21,6 +21,7 @@ from shipit.shipit_types import (
|
|
|
21
21
|
RunStep,
|
|
22
22
|
Step,
|
|
23
23
|
UseStep,
|
|
24
|
+
WriteFileStep,
|
|
24
25
|
WorkdirStep,
|
|
25
26
|
)
|
|
26
27
|
from shipit.ui import console, write_stderr, write_stdout
|
|
@@ -75,6 +76,9 @@ class DockerBuildBackend:
|
|
|
75
76
|
def get_artifact_mount_path(self, name: str) -> Path:
|
|
76
77
|
return self.docker_out_path / self.get_mount_path(name)
|
|
77
78
|
|
|
79
|
+
def get_volume_path(self, name: str) -> Path:
|
|
80
|
+
return self.src_dir / ".shipit" / "volumes" / name
|
|
81
|
+
|
|
78
82
|
@property
|
|
79
83
|
def is_depot(self) -> bool:
|
|
80
84
|
return self.docker_client == "depot"
|
|
@@ -210,6 +214,15 @@ RUN curl https://mise.run | sh
|
|
|
210
214
|
elif isinstance(step, PathStep):
|
|
211
215
|
docker_file_contents += f"ENV PATH={step.path}:$PATH\n"
|
|
212
216
|
env["PATH"] = f"{step.path}{os.pathsep}{env.get('PATH', '')}"
|
|
217
|
+
elif isinstance(step, WriteFileStep):
|
|
218
|
+
content_base64 = base64.b64encode(
|
|
219
|
+
step.content.encode("utf-8")
|
|
220
|
+
).decode("utf-8")
|
|
221
|
+
target_path = Path(step.path)
|
|
222
|
+
docker_file_contents += (
|
|
223
|
+
f"RUN mkdir -p {target_path.parent} "
|
|
224
|
+
f"&& echo '{content_base64}' | base64 -d > {target_path}\n"
|
|
225
|
+
)
|
|
213
226
|
elif isinstance(step, UseStep):
|
|
214
227
|
for dependency in step.dependencies:
|
|
215
228
|
if dependency.name == "pie":
|
|
@@ -17,6 +17,7 @@ from shipit.shipit_types import (
|
|
|
17
17
|
RunStep,
|
|
18
18
|
Step,
|
|
19
19
|
UseStep,
|
|
20
|
+
WriteFileStep,
|
|
20
21
|
WorkdirStep,
|
|
21
22
|
)
|
|
22
23
|
from shipit.ui import console, write_stderr, write_stdout
|
|
@@ -44,6 +45,9 @@ class LocalBuildBackend:
|
|
|
44
45
|
def get_artifact_mount_path(self, name: str) -> Path:
|
|
45
46
|
return self.get_mount_path(name)
|
|
46
47
|
|
|
48
|
+
def get_volume_path(self, name: str) -> Path:
|
|
49
|
+
return self.src_dir / ".shipit" / "volumes" / name
|
|
50
|
+
|
|
47
51
|
def execute_step(self, step: Step, env: Dict[str, str]) -> None:
|
|
48
52
|
build_path = self.workdir
|
|
49
53
|
if isinstance(step, UseStep):
|
|
@@ -132,6 +136,13 @@ class LocalBuildBackend:
|
|
|
132
136
|
console.print(f"[bold]Add {step.path}[/bold] to PATH")
|
|
133
137
|
fullpath = step.path
|
|
134
138
|
env["PATH"] = f"{fullpath}{os.pathsep}{env['PATH']}"
|
|
139
|
+
elif isinstance(step, WriteFileStep):
|
|
140
|
+
target = Path(step.path)
|
|
141
|
+
if not target.is_absolute():
|
|
142
|
+
target = build_path / target
|
|
143
|
+
console.print(f"[bold]Write file {target}[/bold]")
|
|
144
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
145
|
+
target.write_text(step.content)
|
|
135
146
|
else:
|
|
136
147
|
raise Exception(f"Unknown step type: {type(step)}")
|
|
137
148
|
|
|
@@ -31,6 +31,7 @@ from shipit.shipit_types import (
|
|
|
31
31
|
Step,
|
|
32
32
|
UseStep,
|
|
33
33
|
Volume,
|
|
34
|
+
WriteFileStep,
|
|
34
35
|
WorkdirStep,
|
|
35
36
|
)
|
|
36
37
|
from shipit.ui import console
|
|
@@ -40,6 +41,7 @@ app = typer.Typer(invoke_without_command=True)
|
|
|
40
41
|
|
|
41
42
|
DIR_PATH = Path(__file__).resolve().parent
|
|
42
43
|
ASSETS_PATH = DIR_PATH / "assets"
|
|
44
|
+
OPTIONAL_RUN_COMMANDS = {"start", "after_deploy"}
|
|
43
45
|
|
|
44
46
|
|
|
45
47
|
@dataclass
|
|
@@ -195,6 +197,10 @@ class Ctx:
|
|
|
195
197
|
step = EnvStep(env_vars)
|
|
196
198
|
return self.add_step(step)
|
|
197
199
|
|
|
200
|
+
def write(self, path: str, content: str) -> Optional[str]:
|
|
201
|
+
step = WriteFileStep(path, content)
|
|
202
|
+
return self.add_step(step)
|
|
203
|
+
|
|
198
204
|
def add_mount(self, mount: Mount) -> Optional[str]:
|
|
199
205
|
self.mounts.append(mount)
|
|
200
206
|
return f"ref:mount:{len(self.mounts) - 1}"
|
|
@@ -216,12 +222,18 @@ class Ctx:
|
|
|
216
222
|
return f"ref:volume:{len(self.volumes) - 1}"
|
|
217
223
|
|
|
218
224
|
def volume(self, name: str, serve: str) -> Optional[str]:
|
|
219
|
-
volume = Volume(
|
|
225
|
+
volume = Volume(
|
|
226
|
+
name=name,
|
|
227
|
+
path=self.build_backend.get_volume_path(name),
|
|
228
|
+
serve_path=Path(serve),
|
|
229
|
+
)
|
|
220
230
|
ref = self.add_volume(volume)
|
|
221
231
|
return {
|
|
222
232
|
"ref": ref,
|
|
223
233
|
"name": name,
|
|
234
|
+
"path": str(volume.path.absolute()),
|
|
224
235
|
"serve": str(volume.serve_path),
|
|
236
|
+
"serve_path": str(volume.serve_path),
|
|
225
237
|
}
|
|
226
238
|
|
|
227
239
|
|
|
@@ -245,6 +257,7 @@ def evaluate_shipit(
|
|
|
245
257
|
glb.set("volume", ctx.volume)
|
|
246
258
|
glb.set("workdir", ctx.workdir)
|
|
247
259
|
glb.set("copy", ctx.copy)
|
|
260
|
+
glb.set("write", ctx.write)
|
|
248
261
|
glb.set("path", ctx.path)
|
|
249
262
|
glb.set("env", ctx.env)
|
|
250
263
|
glb.set("use", ctx.use)
|
|
@@ -359,9 +372,18 @@ def auto(
|
|
|
359
372
|
False,
|
|
360
373
|
help="Run the prepare command after building (defaults to True).",
|
|
361
374
|
),
|
|
375
|
+
run_commands: Optional[List[str]] = typer.Option(
|
|
376
|
+
None,
|
|
377
|
+
"--run",
|
|
378
|
+
help="Run one or more serve commands after building. Can be passed multiple times.",
|
|
379
|
+
),
|
|
362
380
|
start: bool = typer.Option(
|
|
363
381
|
False,
|
|
364
|
-
help="
|
|
382
|
+
help="Equivalent to `--run=start`.",
|
|
383
|
+
),
|
|
384
|
+
after_deploy: bool = typer.Option(
|
|
385
|
+
False,
|
|
386
|
+
help="Equivalent to `--run=after_deploy`.",
|
|
365
387
|
),
|
|
366
388
|
regenerate: bool = typer.Option(
|
|
367
389
|
None,
|
|
@@ -479,14 +501,16 @@ def auto(
|
|
|
479
501
|
provider=provider,
|
|
480
502
|
config=config,
|
|
481
503
|
)
|
|
482
|
-
if start or wasmer_deploy or wasmer_deploy_config:
|
|
504
|
+
if run_commands or start or after_deploy or wasmer_deploy or wasmer_deploy_config:
|
|
483
505
|
serve(
|
|
484
506
|
path,
|
|
485
507
|
wasmer=wasmer,
|
|
486
508
|
wasmer_bin=wasmer_bin,
|
|
487
509
|
docker=docker,
|
|
488
510
|
docker_client=docker_client,
|
|
511
|
+
run_commands=run_commands,
|
|
489
512
|
start=start,
|
|
513
|
+
after_deploy=after_deploy,
|
|
490
514
|
wasmer_token=wasmer_token,
|
|
491
515
|
wasmer_registry=wasmer_registry,
|
|
492
516
|
wasmer_deploy=wasmer_deploy,
|
|
@@ -619,9 +643,18 @@ def serve(
|
|
|
619
643
|
None,
|
|
620
644
|
help="Additional options to pass to the Docker client.",
|
|
621
645
|
),
|
|
646
|
+
run_commands: Optional[List[str]] = typer.Option(
|
|
647
|
+
None,
|
|
648
|
+
"--run",
|
|
649
|
+
help="Run one or more serve commands. Can be passed multiple times.",
|
|
650
|
+
),
|
|
622
651
|
start: Optional[bool] = typer.Option(
|
|
623
652
|
True,
|
|
624
|
-
help="
|
|
653
|
+
help="Equivalent to `--run=start`.",
|
|
654
|
+
),
|
|
655
|
+
after_deploy: bool = typer.Option(
|
|
656
|
+
False,
|
|
657
|
+
help="Equivalent to `--run=after_deploy`.",
|
|
625
658
|
),
|
|
626
659
|
wasmer_deploy: Optional[bool] = typer.Option(
|
|
627
660
|
False,
|
|
@@ -672,6 +705,12 @@ def serve(
|
|
|
672
705
|
else:
|
|
673
706
|
runner = LocalRunner(build_backend, path)
|
|
674
707
|
|
|
708
|
+
commands_to_run = resolve_run_commands(
|
|
709
|
+
run_commands=run_commands,
|
|
710
|
+
start=bool(start),
|
|
711
|
+
after_deploy=after_deploy,
|
|
712
|
+
)
|
|
713
|
+
|
|
675
714
|
if wasmer_deploy_config:
|
|
676
715
|
if not isinstance(runner, WasmerRunner):
|
|
677
716
|
raise RuntimeError("--wasmer-deploy-config requires the Wasmer runner")
|
|
@@ -680,8 +719,8 @@ def serve(
|
|
|
680
719
|
if not isinstance(runner, WasmerRunner):
|
|
681
720
|
raise RuntimeError("--wasmer-deploy requires the Wasmer runner")
|
|
682
721
|
runner.deploy(app_owner=wasmer_app_owner, app_name=wasmer_app_name)
|
|
683
|
-
elif
|
|
684
|
-
runner
|
|
722
|
+
elif commands_to_run:
|
|
723
|
+
run_serve_commands(runner, commands_to_run)
|
|
685
724
|
|
|
686
725
|
|
|
687
726
|
@app.command(name="plan")
|
|
@@ -1053,6 +1092,26 @@ def get_shipit_path(path: Path, shipit_path: Optional[Path] = None) -> Path:
|
|
|
1053
1092
|
return shipit_path
|
|
1054
1093
|
|
|
1055
1094
|
|
|
1095
|
+
def resolve_run_commands(
|
|
1096
|
+
run_commands: Optional[List[str]],
|
|
1097
|
+
start: bool,
|
|
1098
|
+
after_deploy: bool,
|
|
1099
|
+
) -> List[str]:
|
|
1100
|
+
commands = list(run_commands or [])
|
|
1101
|
+
if after_deploy and "after_deploy" not in commands:
|
|
1102
|
+
commands.append("after_deploy")
|
|
1103
|
+
if start and "start" not in commands:
|
|
1104
|
+
commands.append("start")
|
|
1105
|
+
return commands
|
|
1106
|
+
|
|
1107
|
+
|
|
1108
|
+
def run_serve_commands(runner: Runner, commands: List[str]) -> None:
|
|
1109
|
+
for command in commands:
|
|
1110
|
+
if command in OPTIONAL_RUN_COMMANDS and not runner.has_serve_command(command):
|
|
1111
|
+
continue
|
|
1112
|
+
runner.run_serve_command(command)
|
|
1113
|
+
|
|
1114
|
+
|
|
1056
1115
|
def main() -> None:
|
|
1057
1116
|
args = sys.argv[1:]
|
|
1058
1117
|
# If no subcommand or first token looks like option/path → default to "build"
|
|
@@ -25,7 +25,7 @@ class MkdocsConfig(PythonConfig, StaticFileConfig):
|
|
|
25
25
|
|
|
26
26
|
class MkdocsProvider(StaticFileProvider):
|
|
27
27
|
def __init__(self, path: Path, config: MkdocsConfig):
|
|
28
|
-
|
|
28
|
+
super().__init__(path, config)
|
|
29
29
|
self.python_provider = PythonProvider(path, config, only_build=True)
|
|
30
30
|
|
|
31
31
|
@classmethod
|
|
@@ -432,7 +432,7 @@ class NodeStaticProvider(StaticFileProvider):
|
|
|
432
432
|
'run("cp -R {}/* {}/".format(config.static_dir, static_app.path))'
|
|
433
433
|
if not self.only_build
|
|
434
434
|
else None,
|
|
435
|
-
],
|
|
435
|
+
] + self.build_steps_redirects(),
|
|
436
436
|
)
|
|
437
437
|
|
|
438
438
|
def prepare_steps(self) -> Optional[list[str]]:
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import re
|
|
3
|
+
import shlex
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from functools import cached_property
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Dict, Optional
|
|
8
|
+
|
|
9
|
+
from tomlkit import aot, document, table
|
|
10
|
+
import yaml
|
|
11
|
+
from pydantic_settings import SettingsConfigDict
|
|
12
|
+
|
|
13
|
+
from .base import (
|
|
14
|
+
DetectResult,
|
|
15
|
+
DependencySpec,
|
|
16
|
+
Provider,
|
|
17
|
+
_exists,
|
|
18
|
+
MountSpec,
|
|
19
|
+
ServiceSpec,
|
|
20
|
+
VolumeSpec,
|
|
21
|
+
Config,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class StaticFileConfig(Config):
|
|
26
|
+
model_config = SettingsConfigDict(extra="ignore", env_prefix="SHIPIT_")
|
|
27
|
+
|
|
28
|
+
convert_redirects: bool = True
|
|
29
|
+
sws_version: Optional[str] = "2.38.0"
|
|
30
|
+
static_dir: Optional[str] = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class RedirectRule:
|
|
35
|
+
source: str
|
|
36
|
+
destination: str
|
|
37
|
+
kind: int
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class StaticFileProvider:
|
|
41
|
+
REDIRECTS_CONFIG_FILE = "sws.toml"
|
|
42
|
+
REDIRECTS_CONFIG_MOUNT = "static_config"
|
|
43
|
+
REDIRECTS_SOURCE = "_redirects"
|
|
44
|
+
REDIRECT_STATUS_CODES = {301, 302}
|
|
45
|
+
_PARAM_PATTERN = re.compile(r":([A-Za-z][A-Za-z0-9_]*)")
|
|
46
|
+
_SOURCE_TOKEN_PATTERN = re.compile(r":([A-Za-z][A-Za-z0-9_]*)|\*")
|
|
47
|
+
|
|
48
|
+
config: Optional[dict] = None
|
|
49
|
+
path: Path
|
|
50
|
+
|
|
51
|
+
def __init__(self, path: Path, config: StaticFileConfig):
|
|
52
|
+
self.path = path
|
|
53
|
+
self.config = config
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def load_config(
|
|
57
|
+
cls, path: Path, base_config: Config
|
|
58
|
+
) -> StaticFileConfig:
|
|
59
|
+
if (path / "Staticfile").exists():
|
|
60
|
+
config = None
|
|
61
|
+
try:
|
|
62
|
+
config = yaml.safe_load((path / "Staticfile").read_text())
|
|
63
|
+
except yaml.YAMLError as e:
|
|
64
|
+
print(f"Error loading Staticfile: {e}")
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
if config:
|
|
68
|
+
return StaticFileConfig(
|
|
69
|
+
**base_config.model_dump(),
|
|
70
|
+
static_dir=config.get("root"),
|
|
71
|
+
)
|
|
72
|
+
if _exists(path, "public/index.html") or _exists(path, "public/index.htm"):
|
|
73
|
+
return StaticFileConfig(static_dir="public", **base_config.model_dump())
|
|
74
|
+
|
|
75
|
+
return StaticFileConfig(**base_config.model_dump())
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def name(cls) -> str:
|
|
79
|
+
return "staticfile"
|
|
80
|
+
|
|
81
|
+
@classmethod
|
|
82
|
+
def detect(
|
|
83
|
+
cls, path: Path, config: Config
|
|
84
|
+
) -> Optional[DetectResult]:
|
|
85
|
+
is_python_php_js_project = _exists(
|
|
86
|
+
path, "package.json", "pyproject.toml", "composer.json"
|
|
87
|
+
)
|
|
88
|
+
if _exists(path, "Staticfile"):
|
|
89
|
+
return DetectResult(cls.name(), 50)
|
|
90
|
+
if not is_python_php_js_project:
|
|
91
|
+
if _exists(
|
|
92
|
+
path, "index.html", "index.htm", "public/index.htm", "public/index.html"
|
|
93
|
+
):
|
|
94
|
+
return DetectResult(cls.name(), 10)
|
|
95
|
+
return DetectResult(cls.name(), 10)
|
|
96
|
+
if config.commands.start and config.commands.start.startswith(
|
|
97
|
+
"static-web-server "
|
|
98
|
+
):
|
|
99
|
+
return DetectResult(cls.name(), 70)
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
def dependencies(self) -> list[DependencySpec]:
|
|
103
|
+
return [
|
|
104
|
+
DependencySpec(
|
|
105
|
+
"static-web-server",
|
|
106
|
+
var_name="config.sws_version",
|
|
107
|
+
use_in_serve=True,
|
|
108
|
+
)
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
def build_steps_redirects(self) -> list[str]:
|
|
112
|
+
redirects_config = self.redirects_config
|
|
113
|
+
if not redirects_config:
|
|
114
|
+
return []
|
|
115
|
+
return [
|
|
116
|
+
'write("{}/%s".format(static_config.path), %s)'
|
|
117
|
+
% (
|
|
118
|
+
self.REDIRECTS_CONFIG_FILE,
|
|
119
|
+
json.dumps(redirects_config),
|
|
120
|
+
)
|
|
121
|
+
]
|
|
122
|
+
|
|
123
|
+
def build_steps(self) -> list[str]:
|
|
124
|
+
return [
|
|
125
|
+
'workdir(static_app.path)',
|
|
126
|
+
'copy({}, ".", ignore=[".git"])'.format(
|
|
127
|
+
json.dumps(self.config.static_dir or ".")
|
|
128
|
+
),
|
|
129
|
+
] + self.build_steps_redirects()
|
|
130
|
+
|
|
131
|
+
def prepare_steps(self) -> Optional[list[str]]:
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
def declarations(self) -> Optional[str]:
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
def commands(self) -> Dict[str, str]:
|
|
138
|
+
if self.redirects_config:
|
|
139
|
+
return {
|
|
140
|
+
"start": '"static-web-server --root={} --log-level=info --config-file={}/%s --port={}".format(static_app.serve_path, static_config.serve_path, PORT)'
|
|
141
|
+
% self.REDIRECTS_CONFIG_FILE
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
"start": '"static-web-server --root={} --log-level=info --port={}".format(static_app.serve_path, PORT)'
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
def mounts(self) -> list[MountSpec]:
|
|
148
|
+
mounts = [MountSpec("static_app")]
|
|
149
|
+
if self.redirects_config:
|
|
150
|
+
mounts.append(MountSpec(self.REDIRECTS_CONFIG_MOUNT))
|
|
151
|
+
return mounts
|
|
152
|
+
|
|
153
|
+
def volumes(self) -> list[VolumeSpec]:
|
|
154
|
+
return []
|
|
155
|
+
|
|
156
|
+
def env(self) -> Optional[Dict[str, str]]:
|
|
157
|
+
return None
|
|
158
|
+
|
|
159
|
+
def services(self) -> list[ServiceSpec]:
|
|
160
|
+
return []
|
|
161
|
+
|
|
162
|
+
@cached_property
|
|
163
|
+
def redirects_config(self) -> Optional[str]:
|
|
164
|
+
if not self.config.convert_redirects:
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
redirects_path = self._resolve_redirects_path()
|
|
168
|
+
if not redirects_path.is_file():
|
|
169
|
+
return None
|
|
170
|
+
|
|
171
|
+
rules = self._load_redirect_rules(redirects_path)
|
|
172
|
+
if not rules:
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
doc = document()
|
|
176
|
+
advanced = table()
|
|
177
|
+
redirects = aot()
|
|
178
|
+
for rule in rules:
|
|
179
|
+
entry = table()
|
|
180
|
+
entry.add("source", rule.source)
|
|
181
|
+
entry.add("destination", rule.destination)
|
|
182
|
+
entry.add("kind", rule.kind)
|
|
183
|
+
redirects.append(entry)
|
|
184
|
+
|
|
185
|
+
advanced.add("redirects", redirects)
|
|
186
|
+
doc.add("advanced", advanced)
|
|
187
|
+
return doc.as_string()
|
|
188
|
+
|
|
189
|
+
def _resolve_redirects_path(self) -> Path:
|
|
190
|
+
if self.config.static_dir:
|
|
191
|
+
static_dir_redirects = (
|
|
192
|
+
self.path / self.config.static_dir / self.REDIRECTS_SOURCE
|
|
193
|
+
)
|
|
194
|
+
if static_dir_redirects.is_file():
|
|
195
|
+
return static_dir_redirects
|
|
196
|
+
return self.path / self.REDIRECTS_SOURCE
|
|
197
|
+
|
|
198
|
+
@classmethod
|
|
199
|
+
def _load_redirect_rules(cls, redirects_path: Path) -> list[RedirectRule]:
|
|
200
|
+
rules: list[RedirectRule] = []
|
|
201
|
+
for line_number, raw_line in enumerate(
|
|
202
|
+
redirects_path.read_text().splitlines(), start=1
|
|
203
|
+
):
|
|
204
|
+
line = raw_line.strip()
|
|
205
|
+
if not line or line.startswith("#"):
|
|
206
|
+
continue
|
|
207
|
+
|
|
208
|
+
try:
|
|
209
|
+
parts = shlex.split(line)
|
|
210
|
+
except ValueError as exc:
|
|
211
|
+
raise ValueError(
|
|
212
|
+
f"{redirects_path}:{line_number}: invalid _redirects rule"
|
|
213
|
+
) from exc
|
|
214
|
+
|
|
215
|
+
if len(parts) < 2:
|
|
216
|
+
raise ValueError(
|
|
217
|
+
f"{redirects_path}:{line_number}: expected source and "
|
|
218
|
+
"destination"
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
source, destination, *rest = parts
|
|
222
|
+
kind = 301
|
|
223
|
+
if rest and rest[0].isdigit():
|
|
224
|
+
kind = int(rest[0])
|
|
225
|
+
rest = rest[1:]
|
|
226
|
+
|
|
227
|
+
if kind not in cls.REDIRECT_STATUS_CODES:
|
|
228
|
+
raise ValueError(
|
|
229
|
+
f"{redirects_path}:{line_number}: redirect status {kind} "
|
|
230
|
+
"is not supported by static-web-server"
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
if rest:
|
|
234
|
+
raise ValueError(
|
|
235
|
+
f"{redirects_path}:{line_number}: conditions and forced "
|
|
236
|
+
"redirects are not supported"
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
sws_source, replacements = cls._translate_source(
|
|
240
|
+
redirects_path, line_number, source
|
|
241
|
+
)
|
|
242
|
+
sws_destination = cls._translate_destination(
|
|
243
|
+
redirects_path, line_number, destination, replacements
|
|
244
|
+
)
|
|
245
|
+
rules.append(
|
|
246
|
+
RedirectRule(
|
|
247
|
+
source=sws_source,
|
|
248
|
+
destination=sws_destination,
|
|
249
|
+
kind=kind,
|
|
250
|
+
)
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
return rules
|
|
254
|
+
|
|
255
|
+
@classmethod
|
|
256
|
+
def _translate_source(
|
|
257
|
+
cls, redirects_path: Path, line_number: int, source: str
|
|
258
|
+
) -> tuple[str, dict[str, int]]:
|
|
259
|
+
if "://" in source:
|
|
260
|
+
raise ValueError(
|
|
261
|
+
f"{redirects_path}:{line_number}: redirect sources must be "
|
|
262
|
+
"local paths"
|
|
263
|
+
)
|
|
264
|
+
if "?" in source:
|
|
265
|
+
raise ValueError(
|
|
266
|
+
f"{redirects_path}:{line_number}: query matching is not "
|
|
267
|
+
"supported"
|
|
268
|
+
)
|
|
269
|
+
if not source.startswith("/"):
|
|
270
|
+
raise ValueError(
|
|
271
|
+
f"{redirects_path}:{line_number}: redirect sources must start "
|
|
272
|
+
"with '/'"
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
translated_parts: list[str] = []
|
|
276
|
+
replacements: dict[str, int] = {}
|
|
277
|
+
last_index = 0
|
|
278
|
+
next_index = 1
|
|
279
|
+
|
|
280
|
+
for match in cls._SOURCE_TOKEN_PATTERN.finditer(source):
|
|
281
|
+
translated_parts.append(source[last_index : match.start()])
|
|
282
|
+
param_name = match.group(1)
|
|
283
|
+
if param_name is not None:
|
|
284
|
+
if param_name in replacements:
|
|
285
|
+
raise ValueError(
|
|
286
|
+
f"{redirects_path}:{line_number}: duplicate source "
|
|
287
|
+
f"parameter :{param_name}"
|
|
288
|
+
)
|
|
289
|
+
replacements[param_name] = next_index
|
|
290
|
+
translated_parts.append("{*}")
|
|
291
|
+
else:
|
|
292
|
+
if "splat" in replacements:
|
|
293
|
+
raise ValueError(
|
|
294
|
+
f"{redirects_path}:{line_number}: only one splat "
|
|
295
|
+
"segment is supported"
|
|
296
|
+
)
|
|
297
|
+
replacements["splat"] = next_index
|
|
298
|
+
translated_parts.append("{**}")
|
|
299
|
+
next_index += 1
|
|
300
|
+
last_index = match.end()
|
|
301
|
+
|
|
302
|
+
translated_parts.append(source[last_index:])
|
|
303
|
+
return "".join(translated_parts), replacements
|
|
304
|
+
|
|
305
|
+
@classmethod
|
|
306
|
+
def _translate_destination(
|
|
307
|
+
cls,
|
|
308
|
+
redirects_path: Path,
|
|
309
|
+
line_number: int,
|
|
310
|
+
destination: str,
|
|
311
|
+
replacements: dict[str, int],
|
|
312
|
+
) -> str:
|
|
313
|
+
if "*" in destination:
|
|
314
|
+
raise ValueError(
|
|
315
|
+
f"{redirects_path}:{line_number}: destination splats must use "
|
|
316
|
+
":splat"
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
def replace_param(match: re.Match[str]) -> str:
|
|
320
|
+
param_name = match.group(1)
|
|
321
|
+
if param_name not in replacements:
|
|
322
|
+
raise ValueError(
|
|
323
|
+
f"{redirects_path}:{line_number}: destination references "
|
|
324
|
+
f"unknown parameter :{param_name}"
|
|
325
|
+
)
|
|
326
|
+
return f"${replacements[param_name]}"
|
|
327
|
+
|
|
328
|
+
return cls._PARAM_PATTERN.sub(replace_param, destination)
|
|
@@ -80,7 +80,7 @@ class WordPressProvider(PhpProvider):
|
|
|
80
80
|
extra_ignore=["wp-content"],
|
|
81
81
|
after_install=None,
|
|
82
82
|
after_build=None
|
|
83
|
-
)
|
|
83
|
+
) + ['copy("wp-content", "{}".format(wpcontent_base.path))']
|
|
84
84
|
|
|
85
85
|
def prepare_steps(self) -> Optional[list[str]]:
|
|
86
86
|
return super().prepare_steps()
|
|
@@ -100,7 +100,7 @@ class WordPressProvider(PhpProvider):
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
def mounts(self) -> list[MountSpec]:
|
|
103
|
-
return super().mounts()
|
|
103
|
+
return super().mounts() + [MountSpec("wpcontent_base")]
|
|
104
104
|
|
|
105
105
|
def volumes(self) -> list[VolumeSpec]:
|
|
106
106
|
return [
|
|
@@ -114,6 +114,7 @@ class WordPressProvider(PhpProvider):
|
|
|
114
114
|
def env(self) -> Optional[Dict[str, str]]:
|
|
115
115
|
return {
|
|
116
116
|
"PAGER": '"cat"',
|
|
117
|
+
"WPCONTENT_BASE_PATH": '"{}".format(wpcontent_base.serve_path)',
|
|
117
118
|
**(super().env() or {}),
|
|
118
119
|
}
|
|
119
120
|
|
|
@@ -14,5 +14,6 @@ class Runner(Protocol):
|
|
|
14
14
|
def prepare_build_steps(self, build_steps: List["Step"]) -> List["Step"]: ...
|
|
15
15
|
def build(self, serve: "Serve") -> None: ...
|
|
16
16
|
def prepare(self, env: Dict[str, str], prepare: List["PrepareStep"]) -> None: ...
|
|
17
|
+
def has_serve_command(self, command: str) -> bool: ...
|
|
17
18
|
def run_serve_command(self, command: str) -> None: ...
|
|
18
19
|
def get_serve_mount_path(self, name: str) -> Path: ...
|