quoll-cli 0.2.0__py3-none-any.whl

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.
Files changed (191) hide show
  1. quoll/__init__.py +0 -0
  2. quoll/cd/__init__.py +1 -0
  3. quoll/cd/config.py +75 -0
  4. quoll/ci/__init__.py +0 -0
  5. quoll/ci/config.py +213 -0
  6. quoll/ci/custom.py +32 -0
  7. quoll/ci/detect.py +139 -0
  8. quoll/ci/steps.py +85 -0
  9. quoll/ci/tools.py +104 -0
  10. quoll/commands/__init__.py +0 -0
  11. quoll/commands/_common.py +145 -0
  12. quoll/commands/_workspace.py +83 -0
  13. quoll/commands/add.py +104 -0
  14. quoll/commands/add_secret.py +75 -0
  15. quoll/commands/cd.py +214 -0
  16. quoll/commands/ci.py +521 -0
  17. quoll/commands/docker.py +244 -0
  18. quoll/commands/k8s.py +211 -0
  19. quoll/commands/new.py +337 -0
  20. quoll/commands/remove.py +79 -0
  21. quoll/config/__init__.py +0 -0
  22. quoll/config/quoll_toml.py +196 -0
  23. quoll/config/settings.py +22 -0
  24. quoll/config/store.py +39 -0
  25. quoll/console.py +3 -0
  26. quoll/docker/__init__.py +1 -0
  27. quoll/docker/config.py +64 -0
  28. quoll/docker/detect.py +41 -0
  29. quoll/docker/render.py +162 -0
  30. quoll/files.py +64 -0
  31. quoll/k8s/__init__.py +0 -0
  32. quoll/k8s/chart/templates/_helpers.tpl +38 -0
  33. quoll/k8s/chart/templates/deployment.yaml +93 -0
  34. quoll/k8s/chart/templates/hpa.yaml +24 -0
  35. quoll/k8s/chart/templates/ingress.yaml +35 -0
  36. quoll/k8s/chart/templates/pdb.yaml +16 -0
  37. quoll/k8s/chart/templates/service.yaml +18 -0
  38. quoll/k8s/chart/templates/serviceaccount.yaml +10 -0
  39. quoll/k8s/ci.py +39 -0
  40. quoll/k8s/config.py +97 -0
  41. quoll/k8s/deploy.py +22 -0
  42. quoll/k8s/detect.py +32 -0
  43. quoll/k8s/render.py +94 -0
  44. quoll/languages/__init__.py +121 -0
  45. quoll/languages/base.py +350 -0
  46. quoll/languages/cpp/Dockerfile.j2 +37 -0
  47. quoll/languages/cpp/__init__.py +17 -0
  48. quoll/languages/cpp/ci.py +192 -0
  49. quoll/languages/cpp/detect.py +84 -0
  50. quoll/languages/cpp/docker.py +47 -0
  51. quoll/languages/dotnet/Dockerfile.j2 +21 -0
  52. quoll/languages/dotnet/__init__.py +17 -0
  53. quoll/languages/dotnet/ci.py +88 -0
  54. quoll/languages/dotnet/detect.py +53 -0
  55. quoll/languages/dotnet/docker.py +47 -0
  56. quoll/languages/go/Dockerfile.j2 +25 -0
  57. quoll/languages/go/__init__.py +17 -0
  58. quoll/languages/go/ci.py +126 -0
  59. quoll/languages/go/detect.py +30 -0
  60. quoll/languages/go/docker.py +40 -0
  61. quoll/languages/java/Dockerfile.j2 +28 -0
  62. quoll/languages/java/__init__.py +20 -0
  63. quoll/languages/java/ci.py +163 -0
  64. quoll/languages/java/detect.py +77 -0
  65. quoll/languages/java/docker.py +45 -0
  66. quoll/languages/java/frameworks/__init__.py +1 -0
  67. quoll/languages/java/frameworks/spring_boot/Dockerfile.j2 +18 -0
  68. quoll/languages/java/frameworks/spring_boot/__init__.py +10 -0
  69. quoll/languages/java/frameworks/spring_boot/detect.py +9 -0
  70. quoll/languages/java/frameworks/spring_boot/docker.py +19 -0
  71. quoll/languages/java/frameworks/spring_boot/services.py +24 -0
  72. quoll/languages/node/Dockerfile.j2 +98 -0
  73. quoll/languages/node/__init__.py +29 -0
  74. quoll/languages/node/ci.py +200 -0
  75. quoll/languages/node/detect.py +164 -0
  76. quoll/languages/node/docker.py +141 -0
  77. quoll/languages/node/frameworks/__init__.py +1 -0
  78. quoll/languages/node/frameworks/angular/__init__.py +18 -0
  79. quoll/languages/node/frameworks/angular/ci.py +34 -0
  80. quoll/languages/node/frameworks/angular/detect.py +20 -0
  81. quoll/languages/node/frameworks/angular/docker.py +31 -0
  82. quoll/languages/node/frameworks/angular/starter.py +74 -0
  83. quoll/languages/node/frameworks/astro/__init__.py +10 -0
  84. quoll/languages/node/frameworks/astro/ci.py +17 -0
  85. quoll/languages/node/frameworks/astro/detect.py +14 -0
  86. quoll/languages/node/frameworks/astro/docker.py +17 -0
  87. quoll/languages/node/frameworks/nestjs/__init__.py +7 -0
  88. quoll/languages/node/frameworks/nestjs/detect.py +9 -0
  89. quoll/languages/node/frameworks/nestjs/docker.py +10 -0
  90. quoll/languages/node/frameworks/nextjs/Dockerfile.j2 +14 -0
  91. quoll/languages/node/frameworks/nextjs/__init__.py +8 -0
  92. quoll/languages/node/frameworks/nextjs/ci.py +29 -0
  93. quoll/languages/node/frameworks/nextjs/detect.py +13 -0
  94. quoll/languages/node/frameworks/nextjs/docker.py +41 -0
  95. quoll/languages/node/frameworks/nuxt/__init__.py +10 -0
  96. quoll/languages/node/frameworks/nuxt/ci.py +17 -0
  97. quoll/languages/node/frameworks/nuxt/detect.py +17 -0
  98. quoll/languages/node/frameworks/nuxt/docker.py +24 -0
  99. quoll/languages/node/frameworks/sveltekit/__init__.py +15 -0
  100. quoll/languages/node/frameworks/sveltekit/ci.py +15 -0
  101. quoll/languages/node/frameworks/sveltekit/detect.py +18 -0
  102. quoll/languages/node/frameworks/sveltekit/docker.py +18 -0
  103. quoll/languages/node/starter.py +42 -0
  104. quoll/languages/node/templates/express/README.md +11 -0
  105. quoll/languages/node/templates/express/env.example +2 -0
  106. quoll/languages/node/templates/express/eslint.config.js +15 -0
  107. quoll/languages/node/templates/express/gitignore +4 -0
  108. quoll/languages/node/templates/express/nvmrc +1 -0
  109. quoll/languages/node/templates/express/package.json +16 -0
  110. quoll/languages/node/templates/express/prettierignore +6 -0
  111. quoll/languages/node/templates/express/src/app.ts +27 -0
  112. quoll/languages/node/templates/express/src/server.ts +11 -0
  113. quoll/languages/node/templates/express/test/app.test.ts +29 -0
  114. quoll/languages/node/templates/express/tsconfig.build.json +8 -0
  115. quoll/languages/node/templates/express/tsconfig.json +14 -0
  116. quoll/languages/node/templates/express/vitest.config.ts +12 -0
  117. quoll/languages/php/Dockerfile.j2 +40 -0
  118. quoll/languages/php/__init__.py +19 -0
  119. quoll/languages/php/ci.py +147 -0
  120. quoll/languages/php/detect.py +75 -0
  121. quoll/languages/php/docker.py +59 -0
  122. quoll/languages/php/frameworks/__init__.py +1 -0
  123. quoll/languages/php/frameworks/laravel/__init__.py +17 -0
  124. quoll/languages/php/frameworks/laravel/ci.py +24 -0
  125. quoll/languages/php/frameworks/laravel/detect.py +13 -0
  126. quoll/languages/php/frameworks/laravel/docker.py +29 -0
  127. quoll/languages/php/frameworks/laravel/services.py +25 -0
  128. quoll/languages/php/frameworks/symfony/__init__.py +15 -0
  129. quoll/languages/php/frameworks/symfony/ci.py +22 -0
  130. quoll/languages/php/frameworks/symfony/detect.py +17 -0
  131. quoll/languages/php/frameworks/symfony/docker.py +25 -0
  132. quoll/languages/python/Dockerfile.j2 +61 -0
  133. quoll/languages/python/__init__.py +19 -0
  134. quoll/languages/python/ci.py +157 -0
  135. quoll/languages/python/detect.py +175 -0
  136. quoll/languages/python/docker.py +80 -0
  137. quoll/languages/python/frameworks/__init__.py +1 -0
  138. quoll/languages/python/frameworks/django/__init__.py +16 -0
  139. quoll/languages/python/frameworks/django/ci.py +43 -0
  140. quoll/languages/python/frameworks/django/detect.py +17 -0
  141. quoll/languages/python/frameworks/django/docker.py +20 -0
  142. quoll/languages/python/frameworks/fastapi/__init__.py +7 -0
  143. quoll/languages/python/frameworks/fastapi/detect.py +9 -0
  144. quoll/languages/python/frameworks/fastapi/docker.py +21 -0
  145. quoll/languages/python/frameworks/flask/__init__.py +7 -0
  146. quoll/languages/python/frameworks/flask/detect.py +9 -0
  147. quoll/languages/python/frameworks/flask/docker.py +22 -0
  148. quoll/languages/ruby/Dockerfile.j2 +45 -0
  149. quoll/languages/ruby/__init__.py +19 -0
  150. quoll/languages/ruby/ci.py +110 -0
  151. quoll/languages/ruby/detect.py +72 -0
  152. quoll/languages/ruby/docker.py +63 -0
  153. quoll/languages/ruby/frameworks/__init__.py +1 -0
  154. quoll/languages/ruby/frameworks/rails/Dockerfile.j2 +16 -0
  155. quoll/languages/ruby/frameworks/rails/__init__.py +18 -0
  156. quoll/languages/ruby/frameworks/rails/ci.py +26 -0
  157. quoll/languages/ruby/frameworks/rails/detect.py +15 -0
  158. quoll/languages/ruby/frameworks/rails/docker.py +62 -0
  159. quoll/languages/ruby/frameworks/rails/services.py +10 -0
  160. quoll/languages/rust/Dockerfile.j2 +23 -0
  161. quoll/languages/rust/__init__.py +17 -0
  162. quoll/languages/rust/ci.py +125 -0
  163. quoll/languages/rust/detect.py +46 -0
  164. quoll/languages/rust/docker.py +38 -0
  165. quoll/main.py +50 -0
  166. quoll/platforms/__init__.py +62 -0
  167. quoll/platforms/base.py +103 -0
  168. quoll/platforms/github/__init__.py +4 -0
  169. quoll/platforms/github/actions.py +78 -0
  170. quoll/platforms/github/api.py +124 -0
  171. quoll/platforms/github/cd.py +265 -0
  172. quoll/platforms/github/ci.py +497 -0
  173. quoll/platforms/github/dependabot.yml.j2 +49 -0
  174. quoll/platforms/github/platform.py +52 -0
  175. quoll/platforms/github/workflow.py +74 -0
  176. quoll/platforms/gitlab/__init__.py +4 -0
  177. quoll/platforms/gitlab/api.py +158 -0
  178. quoll/platforms/gitlab/cd.py +226 -0
  179. quoll/platforms/gitlab/ci.py +400 -0
  180. quoll/platforms/gitlab/pipeline.py +136 -0
  181. quoll/platforms/gitlab/platform.py +54 -0
  182. quoll/project.py +60 -0
  183. quoll/services.py +117 -0
  184. quoll/starters.py +117 -0
  185. quoll/workspace.py +232 -0
  186. quoll/yaml_dump.py +137 -0
  187. quoll_cli-0.2.0.dist-info/METADATA +641 -0
  188. quoll_cli-0.2.0.dist-info/RECORD +191 -0
  189. quoll_cli-0.2.0.dist-info/WHEEL +4 -0
  190. quoll_cli-0.2.0.dist-info/entry_points.txt +2 -0
  191. quoll_cli-0.2.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,74 @@
1
+ """A new Angular application, from the official CLI of the chosen major (`angular@18`), the latest by default."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import subprocess
6
+ from pathlib import Path
7
+ from typing import List
8
+
9
+ from quoll.languages.base import Starter, StarterOptions
10
+ from quoll.languages.node.ci import QUOLL_FILES
11
+ from quoll.languages.node.starter import NODE_DEFAULT
12
+ from quoll.starters import install_locally, run, write
13
+
14
+ LATEST = "22"
15
+ # Angular supports its current major and the two before it (https://angular.dev/reference/releases).
16
+ SUPPORTED = 3
17
+
18
+
19
+ def create(directory: Path, options: StarterOptions) -> List[str]:
20
+ major = options.version or LATEST
21
+ cli = f"@angular/cli@{major}"
22
+
23
+ def ng(*args: str) -> None:
24
+ # Run with the asked Node.js: the CLI checks it supports it, the lockfile matches it.
25
+ run(["npx", "-y", *args], directory, "node", options.runtime)
26
+
27
+ new = [cli, "new", options.name, "--directory", ".", "--defaults", "--skip-git", "--style", "css",
28
+ "--ssr", "false", "--package-manager", "npm"]
29
+ if int(major) >= 20:
30
+ # Options of recent CLIs: no AI tools configuration; Vitest is the default runner since 21.
31
+ new += ["--ai-config", "none"]
32
+ ng(*new)
33
+ # ESLint with Angular's rules: the CI's lint job runs `ng lint`.
34
+ ng(f"@angular/cli@{major}", "add", f"angular-eslint@{major}", "--skip-confirmation")
35
+ # The CLI's own files aren't all formatted the way its Prettier config says (when it has one).
36
+ if "prettier" in (directory / "package.json").read_text():
37
+ ng("prettier", "--write", ".")
38
+ ignore = directory / ".prettierignore"
39
+ existing = ignore.read_text() if ignore.exists() else ""
40
+ write(directory, {
41
+ ".nvmrc": f"{options.runtime or NODE_DEFAULT}\n",
42
+ # quoll's files keep quoll's format when the project's Prettier runs.
43
+ ".prettierignore": existing + "# Written by quoll: their format is quoll's.\n" + "\n".join(QUOLL_FILES) + "\n",
44
+ })
45
+ notes = install_locally(directory, options.runtime, ["npm", "ci"], "node_modules")
46
+ if int(major) <= int(LATEST) - SUPPORTED:
47
+ notes.append(
48
+ f"Angular {major} no longer gets security fixes: the CI's dependency audit will fail on its known "
49
+ f"vulnerabilities. Supported: {int(LATEST) - SUPPORTED + 1} to {LATEST}."
50
+ )
51
+ return notes
52
+
53
+
54
+ def proxy(directory: Path, path: str, url: str) -> None:
55
+ """`ng serve` forwards `path` to the API (https://angular.dev/tools/cli/serve#proxying-to-a-backend-server)."""
56
+ write(directory, {"proxy.conf.json": json.dumps({path: {"target": url, "secure": False}}, indent=2) + "\n"})
57
+ config = directory / "angular.json"
58
+ workspace = json.loads(config.read_text())
59
+ for project in workspace["projects"].values():
60
+ serve = project.get("architect", {}).get("serve")
61
+ if serve:
62
+ serve.setdefault("options", {})["proxyConfig"] = "proxy.conf.json"
63
+ config.write_text(json.dumps(workspace, indent=2) + "\n")
64
+ # In the project's format: the CI checks it.
65
+ prettier = directory / "node_modules" / ".bin" / "prettier"
66
+ if prettier.exists():
67
+ subprocess.run([str(prettier), "--write", "angular.json", "proxy.conf.json"], cwd=directory,
68
+ capture_output=True, check=True)
69
+
70
+
71
+ starter = Starter(
72
+ name="angular", label="Angular", role="front", create=create, runtime="node", runtime_default=NODE_DEFAULT,
73
+ proxy=proxy,
74
+ )
@@ -0,0 +1,10 @@
1
+ """Astro: `astro check` type-checks .astro files too; opt out of telemetry."""
2
+
3
+ from quoll.languages.base import Framework
4
+ from quoll.languages.node.frameworks.astro.ci import AstroCI
5
+ from quoll.languages.node.frameworks.astro.detect import detect
6
+ from quoll.languages.node.frameworks.astro.docker import AstroImage
7
+
8
+ framework = Framework(
9
+ name="astro", language="node", detect=detect, ci=AstroCI(), lint_tools=("astro-check",), docker=AstroImage()
10
+ )
@@ -0,0 +1,17 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Dict, List
4
+
5
+ from quoll.ci.steps import Item, step
6
+ from quoll.languages.base import FrameworkCI
7
+
8
+ if TYPE_CHECKING:
9
+ from quoll.ci.config import CIConfig
10
+
11
+
12
+ class AstroCI(FrameworkCI):
13
+ def env(self, cfg: CIConfig) -> Dict[str, str]:
14
+ return {"ASTRO_TELEMETRY_DISABLED": "1"}
15
+
16
+ def lint(self, cfg: CIConfig, tool: str) -> List[Item]:
17
+ return [step("astro check", run=cfg.language.ci.exec(cfg, "astro check"))]
@@ -0,0 +1,14 @@
1
+ from __future__ import annotations
2
+
3
+ from quoll.languages.base import Detection
4
+ from quoll.languages.node.detect import dependencies, package_json
5
+ from quoll.project import Project
6
+
7
+
8
+ def detect(project: Project, det: Detection) -> bool:
9
+ deps = dependencies(package_json(project))
10
+ if "astro" not in deps:
11
+ return False
12
+ if "@astrojs/check" in deps:
13
+ det.lint_tools = [t for t in det.lint_tools if t != "tsc"] + ["astro-check"]
14
+ return True
@@ -0,0 +1,17 @@
1
+ from __future__ import annotations
2
+
3
+ from quoll.languages.base import Detection, DockerDefaults
4
+ from quoll.languages.node.detect import dependencies, package_json
5
+ from quoll.languages.node.docker import NodeImage
6
+ from quoll.project import Project
7
+
8
+
9
+ class AstroImage(NodeImage):
10
+ # Astro builds one HTML file per page and a 404.html.
11
+ spa = False
12
+
13
+ def defaults(self, project: Project, det: Detection) -> DockerDefaults:
14
+ build = det.build_command or "npm run build"
15
+ if "@astrojs/node" in dependencies(package_json(project)):
16
+ return DockerDefaults(port=4321, command="node ./dist/server/entry.mjs", build_command=build)
17
+ return DockerDefaults(port=8080, build_command=build, output_dir="dist", static=True)
@@ -0,0 +1,7 @@
1
+ """NestJS: CI is plain Node; the framework matters for packaging (nest build → dist/main.js)."""
2
+
3
+ from quoll.languages.base import Framework
4
+ from quoll.languages.node.frameworks.nestjs.detect import detect
5
+ from quoll.languages.node.frameworks.nestjs.docker import NestjsImage
6
+
7
+ framework = Framework(name="nestjs", language="node", detect=detect, docker=NestjsImage())
@@ -0,0 +1,9 @@
1
+ from __future__ import annotations
2
+
3
+ from quoll.languages.base import Detection
4
+ from quoll.languages.node.detect import dependencies, package_json
5
+ from quoll.project import Project
6
+
7
+
8
+ def detect(project: Project, det: Detection) -> bool:
9
+ return "@nestjs/core" in dependencies(package_json(project))
@@ -0,0 +1,10 @@
1
+ from __future__ import annotations
2
+
3
+ from quoll.languages.base import Detection, DockerDefaults
4
+ from quoll.languages.node.docker import NodeImage
5
+ from quoll.project import Project
6
+
7
+
8
+ class NestjsImage(NodeImage):
9
+ def defaults(self, project: Project, det: Detection) -> DockerDefaults:
10
+ return DockerDefaults(port=3000, command="node dist/main", build_command=det.build_command or "npm run build")
@@ -0,0 +1,14 @@
1
+ {% extends "node/Dockerfile.j2" %}
2
+
3
+ {% block app_files %}
4
+ {% if standalone %}
5
+ # output: "standalone" bundles the server with only the dependencies it uses.
6
+ {% if public %}
7
+ COPY --from=build --chown={{ user }}:{{ user }} /app/public ./public
8
+ {% endif %}
9
+ COPY --from=build --chown={{ user }}:{{ user }} /app/.next/standalone ./
10
+ COPY --from=build --chown={{ user }}:{{ user }} /app/.next/static ./.next/static
11
+ {% else %}
12
+ {{ super() }}
13
+ {%- endif %}
14
+ {% endblock %}
@@ -0,0 +1,8 @@
1
+ """Next.js: persist the build cache between runs and opt out of telemetry."""
2
+
3
+ from quoll.languages.base import Framework
4
+ from quoll.languages.node.frameworks.nextjs.ci import NextjsCI
5
+ from quoll.languages.node.frameworks.nextjs.detect import detect
6
+ from quoll.languages.node.frameworks.nextjs.docker import NextjsImage
7
+
8
+ framework = Framework(name="nextjs", language="node", detect=detect, ci=NextjsCI(), docker=NextjsImage())
@@ -0,0 +1,29 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Dict, List
4
+
5
+ from quoll.ci.steps import Cache, Item
6
+ from quoll.languages.base import FrameworkCI
7
+ from quoll.languages.node.ci import LOCKFILES
8
+
9
+ if TYPE_CHECKING:
10
+ from quoll.ci.config import CIConfig
11
+
12
+ _SOURCES = ("**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx", "!node_modules/**")
13
+
14
+
15
+ class NextjsCI(FrameworkCI):
16
+ def env(self, cfg: CIConfig) -> Dict[str, str]:
17
+ return {"NEXT_TELEMETRY_DISABLED": "1"}
18
+
19
+ def before_build(self, cfg: CIConfig) -> List[Item]:
20
+ # https://nextjs.org/docs/app/guides/ci-build-caching
21
+ return [
22
+ Cache(
23
+ "nextjs",
24
+ (".next/cache",),
25
+ key_files=(LOCKFILES[cfg.package_manager],),
26
+ revision_files=_SOURCES,
27
+ name="Cache Next.js build",
28
+ )
29
+ ]
@@ -0,0 +1,13 @@
1
+ from __future__ import annotations
2
+
3
+ from quoll.languages.base import Detection
4
+ from quoll.languages.node.detect import dependencies, package_json
5
+ from quoll.project import Project
6
+
7
+
8
+ def detect(project: Project, det: Detection) -> bool:
9
+ if "next" not in dependencies(package_json(project)):
10
+ return False
11
+ # `next build` type-checks, and plain tsc fails without the generated next-env.d.ts.
12
+ det.lint_tools = [t for t in det.lint_tools if t != "tsc"]
13
+ return True
@@ -0,0 +1,41 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional
4
+
5
+ from quoll.languages.base import Detection, DockerDefaults
6
+ from quoll.languages.node.docker import NodeImage
7
+ from quoll.project import Project
8
+
9
+ if TYPE_CHECKING:
10
+ from quoll.docker.config import DockerConfig
11
+
12
+ STANDALONE_DIR = ".next/standalone"
13
+ _CONFIGS = ("next.config.js", "next.config.mjs", "next.config.ts")
14
+
15
+
16
+ class NextjsImage(NodeImage):
17
+ def defaults(self, project: Project, det: Detection) -> DockerDefaults:
18
+ build = det.build_command or "npm run build"
19
+ if any("standalone" in project.read_text(c) for c in _CONFIGS):
20
+ return DockerDefaults(port=3000, command="node server.js", build_command=build, output_dir=STANDALONE_DIR)
21
+ return DockerDefaults(
22
+ port=3000,
23
+ command="next start",
24
+ build_command=build,
25
+ warnings=['Set `output: "standalone"` in next.config for a much smaller image.'],
26
+ )
27
+
28
+ def context(self, project: Project, cfg: DockerConfig) -> Dict[str, Any]:
29
+ standalone = cfg.output_dir == STANDALONE_DIR
30
+ return {**super().context(project, cfg), "standalone": standalone, "public": project.is_dir("public")}
31
+
32
+ def writable_paths(self, cfg: DockerConfig) -> Optional[List[str]]:
33
+ # The image optimization and ISR cache.
34
+ return ["/tmp", "/app/.next/cache"]
35
+
36
+ def build_env(self, cfg: DockerConfig) -> Dict[str, str]:
37
+ return {"NEXT_TELEMETRY_DISABLED": "1"}
38
+
39
+ def env(self, cfg: DockerConfig) -> Dict[str, str]:
40
+ # The standalone server listens on HOSTNAME.
41
+ return {"HOSTNAME": "0.0.0.0", "NEXT_TELEMETRY_DISABLED": "1"}
@@ -0,0 +1,10 @@
1
+ """Nuxt: type-check through nuxi (plain tsc misses the generated .nuxt types) and opt out of telemetry."""
2
+
3
+ from quoll.languages.base import Framework
4
+ from quoll.languages.node.frameworks.nuxt.ci import NuxtCI
5
+ from quoll.languages.node.frameworks.nuxt.detect import detect
6
+ from quoll.languages.node.frameworks.nuxt.docker import NuxtImage
7
+
8
+ framework = Framework(
9
+ name="nuxt", language="node", detect=detect, ci=NuxtCI(), lint_tools=("nuxt-typecheck",), docker=NuxtImage()
10
+ )
@@ -0,0 +1,17 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Dict, List
4
+
5
+ from quoll.ci.steps import Item, step
6
+ from quoll.languages.base import FrameworkCI
7
+
8
+ if TYPE_CHECKING:
9
+ from quoll.ci.config import CIConfig
10
+
11
+
12
+ class NuxtCI(FrameworkCI):
13
+ def env(self, cfg: CIConfig) -> Dict[str, str]:
14
+ return {"NUXT_TELEMETRY_DISABLED": "1"}
15
+
16
+ def lint(self, cfg: CIConfig, tool: str) -> List[Item]:
17
+ return [step("Type check", run=cfg.language.ci.exec(cfg, "nuxi typecheck"))]
@@ -0,0 +1,17 @@
1
+ from __future__ import annotations
2
+
3
+ from quoll.languages.base import SCRIPT_PREFIX, Detection
4
+ from quoll.languages.node.detect import dependencies, package_json
5
+ from quoll.project import Project
6
+
7
+
8
+ def detect(project: Project, det: Detection) -> bool:
9
+ deps = dependencies(package_json(project))
10
+ if "nuxt" not in deps:
11
+ return False
12
+ had_tsc = "tsc" in det.lint_tools
13
+ det.lint_tools = [t for t in det.lint_tools if t != "tsc"]
14
+ has_typecheck_script = any(t.startswith(f"{SCRIPT_PREFIX}type") for t in det.lint_tools)
15
+ if (had_tsc or "vue-tsc" in deps) and not has_typecheck_script:
16
+ det.lint_tools.append("nuxt-typecheck")
17
+ return True
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Dict
4
+
5
+ from quoll.languages.base import Detection, DockerDefaults
6
+ from quoll.languages.node.docker import NodeImage
7
+ from quoll.project import Project
8
+
9
+ if TYPE_CHECKING:
10
+ from quoll.docker.config import DockerConfig
11
+
12
+
13
+ class NuxtImage(NodeImage):
14
+ def defaults(self, project: Project, det: Detection) -> DockerDefaults:
15
+ # Nitro bundles the server and its dependencies into .output.
16
+ return DockerDefaults(
17
+ port=3000,
18
+ command="node .output/server/index.mjs",
19
+ build_command=det.build_command or "npm run build",
20
+ output_dir=".output",
21
+ )
22
+
23
+ def build_env(self, cfg: DockerConfig) -> Dict[str, str]:
24
+ return {"NUXT_TELEMETRY_DISABLED": "1"}
@@ -0,0 +1,15 @@
1
+ """SvelteKit: svelte-check after `svelte-kit sync` (plain tsc misses the generated .svelte-kit types)."""
2
+
3
+ from quoll.languages.base import Framework
4
+ from quoll.languages.node.frameworks.sveltekit.ci import SvelteKitCI
5
+ from quoll.languages.node.frameworks.sveltekit.detect import detect
6
+ from quoll.languages.node.frameworks.sveltekit.docker import SvelteKitImage
7
+
8
+ framework = Framework(
9
+ name="sveltekit",
10
+ language="node",
11
+ detect=detect,
12
+ ci=SvelteKitCI(),
13
+ lint_tools=("svelte-check",),
14
+ docker=SvelteKitImage(),
15
+ )
@@ -0,0 +1,15 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, List
4
+
5
+ from quoll.ci.steps import Item, step
6
+ from quoll.languages.base import FrameworkCI
7
+
8
+ if TYPE_CHECKING:
9
+ from quoll.ci.config import CIConfig
10
+
11
+
12
+ class SvelteKitCI(FrameworkCI):
13
+ def lint(self, cfg: CIConfig, tool: str) -> List[Item]:
14
+ exec_ = cfg.language.ci.exec
15
+ return [step("svelte-check", run=f"{exec_(cfg, 'svelte-kit sync')}\n{exec_(cfg, 'svelte-check')}")]
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+ from quoll.languages.base import SCRIPT_PREFIX, Detection
4
+ from quoll.languages.node.detect import dependencies, package_json, scripts_of
5
+ from quoll.project import Project
6
+
7
+
8
+ def detect(project: Project, det: Detection) -> bool:
9
+ pkg = package_json(project)
10
+ if "@sveltejs/kit" not in dependencies(pkg):
11
+ return False
12
+ det.lint_tools = [t for t in det.lint_tools if t != "tsc"]
13
+ # The SvelteKit template ships a `check` script running svelte-kit sync + svelte-check.
14
+ if "check" in scripts_of(pkg):
15
+ det.lint_tools.append(f"{SCRIPT_PREFIX}check")
16
+ elif "svelte-check" in dependencies(pkg):
17
+ det.lint_tools.append("svelte-check")
18
+ return True
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+ from quoll.languages.base import Detection, DockerDefaults
4
+ from quoll.languages.node.detect import dependencies, package_json
5
+ from quoll.languages.node.docker import NodeImage
6
+ from quoll.project import Project
7
+
8
+
9
+ class SvelteKitImage(NodeImage):
10
+ def defaults(self, project: Project, det: Detection) -> DockerDefaults:
11
+ deps = dependencies(package_json(project))
12
+ build = det.build_command or "npm run build"
13
+ if "@sveltejs/adapter-static" in deps:
14
+ return DockerDefaults(port=8080, build_command=build, output_dir="build", static=True)
15
+ warnings = []
16
+ if "@sveltejs/adapter-node" not in deps:
17
+ warnings.append("Use @sveltejs/adapter-node (or adapter-static) in svelte.config.js to run in a container.")
18
+ return DockerDefaults(port=3000, command="node build", build_command=build, warnings=warnings)
@@ -0,0 +1,42 @@
1
+ """A new Node.js API: Express 5 and TypeScript, tested with Vitest and Supertest, linted with ESLint.
2
+
3
+ Copied from templates/express/, then `npm install` resolves the current version of each dependency (Express's
4
+ major can be chosen: `express@4`). The Node.js version goes to .nvmrc, which the CI and the Dockerfile follow.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from pathlib import Path
9
+
10
+ from quoll.languages.base import Starter
11
+ from quoll.starters import from_template
12
+
13
+ # Node.js version when none is asked for: the active LTS.
14
+ NODE_DEFAULT = "24"
15
+
16
+ DEPENDENCIES = ["express@{{version}}"]
17
+ DEV_DEPENDENCIES = [
18
+ "typescript", "tsx", "@types/express@{{version}}",
19
+ "vitest", "@vitest/coverage-v8", "supertest", "@types/supertest",
20
+ "eslint", "@eslint/js", "typescript-eslint", "globals", "prettier",
21
+ ]
22
+
23
+ starter = Starter(
24
+ name="express",
25
+ label="Express (TypeScript)",
26
+ role="back",
27
+ create=from_template(
28
+ Path(__file__).parent / "templates" / "express",
29
+ [
30
+ ["npm", "install", "--save", *DEPENDENCIES],
31
+ # Types of the Node.js the project runs on.
32
+ ["npm", "install", "--save-dev", *DEV_DEPENDENCIES, "@types/node@{{runtime}}"],
33
+ ["npx", "prettier", "--write", "."],
34
+ ],
35
+ runtime="node",
36
+ runtime_default=NODE_DEFAULT,
37
+ local_install=["npm", "ci"],
38
+ local_folder="node_modules",
39
+ ),
40
+ runtime="node",
41
+ runtime_default=NODE_DEFAULT,
42
+ )
@@ -0,0 +1,11 @@
1
+ # {{name}}
2
+
3
+ Node.js API: Express 5 and TypeScript.
4
+
5
+ ```bash
6
+ npm install
7
+ npm run dev # http://localhost:3000/health, reloads on change
8
+ npm test # Vitest
9
+ npm run lint # ESLint
10
+ npm run build && npm start
11
+ ```
@@ -0,0 +1,2 @@
1
+ # Copied to .env for local runs (npm run dev); never commit .env.
2
+ PORT=3000
@@ -0,0 +1,15 @@
1
+ import js from "@eslint/js";
2
+ import globals from "globals";
3
+ import tseslint from "typescript-eslint";
4
+
5
+ export default tseslint.config(
6
+ { ignores: ["dist", "coverage"] },
7
+ js.configs.recommended,
8
+ ...tseslint.configs.recommended,
9
+ {
10
+ languageOptions: { globals: globals.node },
11
+ rules: {
12
+ "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
13
+ },
14
+ },
15
+ );
@@ -0,0 +1,4 @@
1
+ node_modules/
2
+ dist/
3
+ coverage/
4
+ .env
@@ -0,0 +1 @@
1
+ {{runtime}}
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "{{name}}",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">={{runtime}}"
8
+ },
9
+ "scripts": {
10
+ "dev": "tsx watch --env-file-if-exists=.env src/server.ts",
11
+ "build": "tsc -p tsconfig.build.json",
12
+ "start": "node dist/server.js",
13
+ "test": "vitest run",
14
+ "lint": "eslint ."
15
+ }
16
+ }
@@ -0,0 +1,6 @@
1
+ # Written by quoll: their format is quoll's.
2
+ compose.yaml
3
+ .github/
4
+ .gitlab-ci.yml
5
+ .gitlab/
6
+ chart/
@@ -0,0 +1,27 @@
1
+ import express, { type NextFunction, type Request, type Response } from "express";
2
+
3
+ export function createApp() {
4
+ const app = express();
5
+ app.disable("x-powered-by");
6
+ app.use(express.json());
7
+
8
+ app.get("/health", (_req, res) => {
9
+ res.json({ status: "ok" });
10
+ });
11
+
12
+ app.get("/api/hello", (req, res) => {
13
+ const name = typeof req.query.name === "string" ? req.query.name : "world";
14
+ res.json({ message: `Hello, ${name}!` });
15
+ });
16
+
17
+ app.use((_req, res) => {
18
+ res.status(404).json({ error: "Not found" });
19
+ });
20
+
21
+ app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
22
+ console.error(err);
23
+ res.status(500).json({ error: "Internal server error" });
24
+ });
25
+
26
+ return app;
27
+ }
@@ -0,0 +1,11 @@
1
+ import { createApp } from "./app.js";
2
+
3
+ const port = Number(process.env.PORT ?? 3000);
4
+ const host = process.env.HOST ?? "0.0.0.0";
5
+
6
+ const server = createApp().listen(port, host, () => {
7
+ console.log(`Listening on http://${host}:${port}`);
8
+ });
9
+
10
+ // Containers stop with SIGTERM: finish the requests in flight, then exit.
11
+ process.on("SIGTERM", () => server.close(() => process.exit(0)));
@@ -0,0 +1,29 @@
1
+ import request from "supertest";
2
+ import { describe, expect, it } from "vitest";
3
+
4
+ import { createApp } from "../src/app.js";
5
+
6
+ const app = createApp();
7
+
8
+ describe("API", () => {
9
+ it("reports its health", async () => {
10
+ const res = await request(app).get("/health");
11
+ expect(res.status).toBe(200);
12
+ expect(res.body).toEqual({ status: "ok" });
13
+ });
14
+
15
+ it("says hello", async () => {
16
+ const res = await request(app).get("/api/hello?name=quoll");
17
+ expect(res.body).toEqual({ message: "Hello, quoll!" });
18
+ });
19
+
20
+ it("says hello to the world by default", async () => {
21
+ const res = await request(app).get("/api/hello");
22
+ expect(res.body).toEqual({ message: "Hello, world!" });
23
+ });
24
+
25
+ it("answers 404 to unknown routes", async () => {
26
+ const res = await request(app).get("/nope");
27
+ expect(res.status).toBe(404);
28
+ });
29
+ });
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "rootDir": "src",
5
+ "sourceMap": true
6
+ },
7
+ "include": ["src"]
8
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2023",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "forceConsistentCasingInFileNames": true,
10
+ "outDir": "dist",
11
+ "types": ["node"]
12
+ },
13
+ "include": ["src", "test", "vitest.config.ts"]
14
+ }
@@ -0,0 +1,12 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ coverage: {
6
+ provider: "v8",
7
+ include: ["src/**/*.ts"],
8
+ // The entry point only starts the server.
9
+ exclude: ["src/server.ts"],
10
+ },
11
+ },
12
+ });