shipit-cli 0.18.2__tar.gz → 0.19.1__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.1}/PKG-INFO +1 -1
- {shipit_cli-0.18.2 → shipit_cli-0.19.1}/pyproject.toml +1 -1
- {shipit_cli-0.18.2 → shipit_cli-0.19.1}/src/shipit/assets/php/php.ini +1 -1
- {shipit_cli-0.18.2 → shipit_cli-0.19.1}/src/shipit/assets/wordpress/install.sh +9 -2
- {shipit_cli-0.18.2 → shipit_cli-0.19.1}/src/shipit/assets/wordpress/wp-config.php +0 -4
- {shipit_cli-0.18.2 → shipit_cli-0.19.1}/src/shipit/builders/base.py +1 -0
- {shipit_cli-0.18.2 → shipit_cli-0.19.1}/src/shipit/builders/docker.py +13 -0
- {shipit_cli-0.18.2 → shipit_cli-0.19.1}/src/shipit/builders/local.py +11 -0
- {shipit_cli-0.18.2 → shipit_cli-0.19.1}/src/shipit/cli.py +68 -6
- {shipit_cli-0.18.2 → shipit_cli-0.19.1}/src/shipit/providers/mkdocs.py +1 -1
- {shipit_cli-0.18.2 → shipit_cli-0.19.1}/src/shipit/providers/node_static.py +1 -1
- shipit_cli-0.19.1/src/shipit/providers/staticfile.py +328 -0
- {shipit_cli-0.18.2 → shipit_cli-0.19.1}/src/shipit/providers/wordpress.py +3 -2
- {shipit_cli-0.18.2 → shipit_cli-0.19.1}/src/shipit/runners/base.py +7 -2
- {shipit_cli-0.18.2 → shipit_cli-0.19.1}/src/shipit/runners/local.py +9 -2
- {shipit_cli-0.18.2 → shipit_cli-0.19.1}/src/shipit/runners/wasmer.py +47 -23
- {shipit_cli-0.18.2 → shipit_cli-0.19.1}/src/shipit/shipit_types.py +16 -1
- shipit_cli-0.19.1/src/shipit/version.py +5 -0
- shipit_cli-0.19.1/src/shipit/volumes.py +90 -0
- shipit_cli-0.19.1/tests/test_cli_after_deploy.py +159 -0
- {shipit_cli-0.18.2 → shipit_cli-0.19.1}/tests/test_e2e.py +58 -16
- shipit_cli-0.19.1/tests/test_php_provider.py +69 -0
- shipit_cli-0.19.1/tests/test_staticfile_provider.py +52 -0
- shipit_cli-0.19.1/tests/test_volumes.py +134 -0
- {shipit_cli-0.18.2 → shipit_cli-0.19.1}/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.1}/.gitignore +0 -0
- {shipit_cli-0.18.2 → shipit_cli-0.19.1}/README.md +0 -0
- {shipit_cli-0.18.2 → shipit_cli-0.19.1}/src/shipit/__init__.py +0 -0
- {shipit_cli-0.18.2 → shipit_cli-0.19.1}/src/shipit/assets/wordpress/.htaccess +0 -0
- {shipit_cli-0.18.2 → shipit_cli-0.19.1}/src/shipit/assets/wordpress/start.php +0 -0
- {shipit_cli-0.18.2 → shipit_cli-0.19.1}/src/shipit/builders/__init__.py +0 -0
- {shipit_cli-0.18.2 → shipit_cli-0.19.1}/src/shipit/generator.py +0 -0
- {shipit_cli-0.18.2 → shipit_cli-0.19.1}/src/shipit/procfile.py +0 -0
- {shipit_cli-0.18.2 → shipit_cli-0.19.1}/src/shipit/providers/base.py +0 -0
- {shipit_cli-0.18.2 → shipit_cli-0.19.1}/src/shipit/providers/go.py +0 -0
- {shipit_cli-0.18.2 → shipit_cli-0.19.1}/src/shipit/providers/hugo.py +0 -0
- {shipit_cli-0.18.2 → shipit_cli-0.19.1}/src/shipit/providers/jekyll.py +0 -0
- {shipit_cli-0.18.2 → shipit_cli-0.19.1}/src/shipit/providers/laravel.py +0 -0
- {shipit_cli-0.18.2 → shipit_cli-0.19.1}/src/shipit/providers/php.py +0 -0
- {shipit_cli-0.18.2 → shipit_cli-0.19.1}/src/shipit/providers/python.py +0 -0
- {shipit_cli-0.18.2 → shipit_cli-0.19.1}/src/shipit/providers/registry.py +0 -0
- {shipit_cli-0.18.2 → shipit_cli-0.19.1}/src/shipit/runners/__init__.py +0 -0
- {shipit_cli-0.18.2 → shipit_cli-0.19.1}/src/shipit/ui.py +0 -0
- {shipit_cli-0.18.2 → shipit_cli-0.19.1}/src/shipit/utils.py +0 -0
- {shipit_cli-0.18.2 → shipit_cli-0.19.1}/tests/test_generate_shipit_examples.py +0 -0
- {shipit_cli-0.18.2 → shipit_cli-0.19.1}/tests/test_version.py +0 -0
- {shipit_cli-0.18.2 → shipit_cli-0.19.1}/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.1
|
|
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,12 @@ 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
|
+
shopt -s dotglob nullglob
|
|
23
|
+
cp -R "${WPCONTENT_BASE_PATH}"/* /app/wp-content
|
|
24
|
+
shopt -u dotglob nullglob
|
|
25
|
+
fi
|
|
26
|
+
|
|
21
27
|
echo "Installing WordPress core"
|
|
22
28
|
|
|
23
29
|
wp core install \
|
|
@@ -28,7 +34,6 @@ wp core install \
|
|
|
28
34
|
--admin_email="$WP_ADMIN_EMAIL" \
|
|
29
35
|
--locale="$WP_LOCALE"
|
|
30
36
|
|
|
31
|
-
|
|
32
37
|
if [ "${WP_UPDATE_DB:-false}" = "true" ]; then
|
|
33
38
|
echo "Updating database..."
|
|
34
39
|
wp core update-db
|
|
@@ -96,11 +101,13 @@ if [ -n "${WP_LOCALE:-}" ]; then
|
|
|
96
101
|
wp site switch-language "$WP_LOCALE"
|
|
97
102
|
fi
|
|
98
103
|
|
|
99
|
-
|
|
104
|
+
if [ ! -f "/app/wp-content/wp-config.php" ]; then
|
|
105
|
+
cat > /app/wp-content/wp-config.php <<EOF
|
|
100
106
|
<?php
|
|
101
107
|
// If you need to set custom configuration, you can place it here.
|
|
102
108
|
// This file will be included by the main wp-config.php after
|
|
103
109
|
// loading environment variables.
|
|
104
110
|
EOF
|
|
111
|
+
fi
|
|
105
112
|
|
|
106
113
|
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,15 +31,18 @@ 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
|
|
37
38
|
from shipit.version import version as shipit_version
|
|
39
|
+
from shipit.volumes import build_volumes, load_volume_mappings
|
|
38
40
|
|
|
39
41
|
app = typer.Typer(invoke_without_command=True)
|
|
40
42
|
|
|
41
43
|
DIR_PATH = Path(__file__).resolve().parent
|
|
42
44
|
ASSETS_PATH = DIR_PATH / "assets"
|
|
45
|
+
OPTIONAL_RUN_COMMANDS = {"start", "after_deploy"}
|
|
43
46
|
|
|
44
47
|
|
|
45
48
|
@dataclass
|
|
@@ -195,6 +198,10 @@ class Ctx:
|
|
|
195
198
|
step = EnvStep(env_vars)
|
|
196
199
|
return self.add_step(step)
|
|
197
200
|
|
|
201
|
+
def write(self, path: str, content: str) -> Optional[str]:
|
|
202
|
+
step = WriteFileStep(path, content)
|
|
203
|
+
return self.add_step(step)
|
|
204
|
+
|
|
198
205
|
def add_mount(self, mount: Mount) -> Optional[str]:
|
|
199
206
|
self.mounts.append(mount)
|
|
200
207
|
return f"ref:mount:{len(self.mounts) - 1}"
|
|
@@ -216,12 +223,18 @@ class Ctx:
|
|
|
216
223
|
return f"ref:volume:{len(self.volumes) - 1}"
|
|
217
224
|
|
|
218
225
|
def volume(self, name: str, serve: str) -> Optional[str]:
|
|
219
|
-
volume = Volume(
|
|
226
|
+
volume = Volume(
|
|
227
|
+
name=name,
|
|
228
|
+
path=self.build_backend.get_volume_path(name),
|
|
229
|
+
serve_path=Path(serve),
|
|
230
|
+
)
|
|
220
231
|
ref = self.add_volume(volume)
|
|
221
232
|
return {
|
|
222
233
|
"ref": ref,
|
|
223
234
|
"name": name,
|
|
235
|
+
"path": str(volume.path.absolute()),
|
|
224
236
|
"serve": str(volume.serve_path),
|
|
237
|
+
"serve_path": str(volume.serve_path),
|
|
225
238
|
}
|
|
226
239
|
|
|
227
240
|
|
|
@@ -245,6 +258,7 @@ def evaluate_shipit(
|
|
|
245
258
|
glb.set("volume", ctx.volume)
|
|
246
259
|
glb.set("workdir", ctx.workdir)
|
|
247
260
|
glb.set("copy", ctx.copy)
|
|
261
|
+
glb.set("write", ctx.write)
|
|
248
262
|
glb.set("path", ctx.path)
|
|
249
263
|
glb.set("env", ctx.env)
|
|
250
264
|
glb.set("use", ctx.use)
|
|
@@ -359,9 +373,18 @@ def auto(
|
|
|
359
373
|
False,
|
|
360
374
|
help="Run the prepare command after building (defaults to True).",
|
|
361
375
|
),
|
|
376
|
+
run_commands: Optional[List[str]] = typer.Option(
|
|
377
|
+
None,
|
|
378
|
+
"--run",
|
|
379
|
+
help="Run one or more serve commands after building. Can be passed multiple times.",
|
|
380
|
+
),
|
|
362
381
|
start: bool = typer.Option(
|
|
363
382
|
False,
|
|
364
|
-
help="
|
|
383
|
+
help="Equivalent to `--run=start`.",
|
|
384
|
+
),
|
|
385
|
+
after_deploy: bool = typer.Option(
|
|
386
|
+
False,
|
|
387
|
+
help="Equivalent to `--run=after_deploy`.",
|
|
365
388
|
),
|
|
366
389
|
regenerate: bool = typer.Option(
|
|
367
390
|
None,
|
|
@@ -479,14 +502,16 @@ def auto(
|
|
|
479
502
|
provider=provider,
|
|
480
503
|
config=config,
|
|
481
504
|
)
|
|
482
|
-
if start or wasmer_deploy or wasmer_deploy_config:
|
|
505
|
+
if run_commands or start or after_deploy or wasmer_deploy or wasmer_deploy_config:
|
|
483
506
|
serve(
|
|
484
507
|
path,
|
|
485
508
|
wasmer=wasmer,
|
|
486
509
|
wasmer_bin=wasmer_bin,
|
|
487
510
|
docker=docker,
|
|
488
511
|
docker_client=docker_client,
|
|
512
|
+
run_commands=run_commands,
|
|
489
513
|
start=start,
|
|
514
|
+
after_deploy=after_deploy,
|
|
490
515
|
wasmer_token=wasmer_token,
|
|
491
516
|
wasmer_registry=wasmer_registry,
|
|
492
517
|
wasmer_deploy=wasmer_deploy,
|
|
@@ -619,9 +644,18 @@ def serve(
|
|
|
619
644
|
None,
|
|
620
645
|
help="Additional options to pass to the Docker client.",
|
|
621
646
|
),
|
|
647
|
+
run_commands: Optional[List[str]] = typer.Option(
|
|
648
|
+
None,
|
|
649
|
+
"--run",
|
|
650
|
+
help="Run one or more serve commands. Can be passed multiple times.",
|
|
651
|
+
),
|
|
622
652
|
start: Optional[bool] = typer.Option(
|
|
623
653
|
True,
|
|
624
|
-
help="
|
|
654
|
+
help="Equivalent to `--run=start`.",
|
|
655
|
+
),
|
|
656
|
+
after_deploy: bool = typer.Option(
|
|
657
|
+
False,
|
|
658
|
+
help="Equivalent to `--run=after_deploy`.",
|
|
625
659
|
),
|
|
626
660
|
wasmer_deploy: Optional[bool] = typer.Option(
|
|
627
661
|
False,
|
|
@@ -672,6 +706,12 @@ def serve(
|
|
|
672
706
|
else:
|
|
673
707
|
runner = LocalRunner(build_backend, path)
|
|
674
708
|
|
|
709
|
+
commands_to_run = resolve_run_commands(
|
|
710
|
+
run_commands=run_commands,
|
|
711
|
+
start=bool(start),
|
|
712
|
+
after_deploy=after_deploy,
|
|
713
|
+
)
|
|
714
|
+
|
|
675
715
|
if wasmer_deploy_config:
|
|
676
716
|
if not isinstance(runner, WasmerRunner):
|
|
677
717
|
raise RuntimeError("--wasmer-deploy-config requires the Wasmer runner")
|
|
@@ -680,8 +720,8 @@ def serve(
|
|
|
680
720
|
if not isinstance(runner, WasmerRunner):
|
|
681
721
|
raise RuntimeError("--wasmer-deploy requires the Wasmer runner")
|
|
682
722
|
runner.deploy(app_owner=wasmer_app_owner, app_name=wasmer_app_name)
|
|
683
|
-
elif
|
|
684
|
-
runner
|
|
723
|
+
elif commands_to_run:
|
|
724
|
+
run_serve_commands(path, runner, commands_to_run)
|
|
685
725
|
|
|
686
726
|
|
|
687
727
|
@app.command(name="plan")
|
|
@@ -1034,6 +1074,7 @@ def build(
|
|
|
1034
1074
|
|
|
1035
1075
|
# Build and serve
|
|
1036
1076
|
build_backend.build(serve.name, env, serve.mounts or [], build_steps)
|
|
1077
|
+
build_volumes(path, serve)
|
|
1037
1078
|
runner.build(serve)
|
|
1038
1079
|
if serve.prepare and not skip_prepare:
|
|
1039
1080
|
runner.prepare(env, serve.prepare)
|
|
@@ -1053,6 +1094,27 @@ def get_shipit_path(path: Path, shipit_path: Optional[Path] = None) -> Path:
|
|
|
1053
1094
|
return shipit_path
|
|
1054
1095
|
|
|
1055
1096
|
|
|
1097
|
+
def resolve_run_commands(
|
|
1098
|
+
run_commands: Optional[List[str]],
|
|
1099
|
+
start: bool,
|
|
1100
|
+
after_deploy: bool,
|
|
1101
|
+
) -> List[str]:
|
|
1102
|
+
commands = list(run_commands or [])
|
|
1103
|
+
if after_deploy and "after_deploy" not in commands:
|
|
1104
|
+
commands.append("after_deploy")
|
|
1105
|
+
if start and "start" not in commands:
|
|
1106
|
+
commands.append("start")
|
|
1107
|
+
return commands
|
|
1108
|
+
|
|
1109
|
+
|
|
1110
|
+
def run_serve_commands(path: Path, runner: Runner, commands: List[str]) -> None:
|
|
1111
|
+
volume_mappings = load_volume_mappings(path)
|
|
1112
|
+
for command in commands:
|
|
1113
|
+
if command in OPTIONAL_RUN_COMMANDS and not runner.has_serve_command(command):
|
|
1114
|
+
continue
|
|
1115
|
+
runner.run_serve_command(command, volume_mappings=volume_mappings)
|
|
1116
|
+
|
|
1117
|
+
|
|
1056
1118
|
def main() -> None:
|
|
1057
1119
|
args = sys.argv[1:]
|
|
1058
1120
|
# 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)
|