Skip to content

Commit

Permalink
Allow site generation to watch for changes
Browse files Browse the repository at this point in the history
  • Loading branch information
bartfeenstra committed Dec 1, 2024
1 parent f0b1930 commit 0ae3a3a
Show file tree
Hide file tree
Showing 14 changed files with 200 additions and 83 deletions.
2 changes: 1 addition & 1 deletion betty/assets/locale/nl-NL/betty.po
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,7 @@ msgid ""
"Created {created_derivations} additional {event_type} events based on "
"existing information."
msgstr ""
"{created_derivations} nieuwe {event-type}-gebeurtenissen toegevoegd "
"{created_derivations} nieuwe {event_type}-gebeurtenissen toegevoegd "
"gebaseerd op bestaande informatie."

msgid "Cremation"
Expand Down
15 changes: 12 additions & 3 deletions betty/cli/commands/demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,16 @@
import asyncio
from typing import TYPE_CHECKING, final, Self

import asyncclick as click
from typing_extensions import override

from betty import about
from betty.app.factory import AppDependentFactory
from betty.cli.commands import command, Command
from betty.locale.localizable import _
from betty.plugin import ShorthandPluginBase

if TYPE_CHECKING:
import asyncclick as click
from betty.app import App


Expand Down Expand Up @@ -44,12 +45,20 @@ async def click_command(self) -> click.Command:
if description
else self.plugin_label().localize(localizer),
)
async def demo() -> None:
async def demo(*, dev_watch: bool = False) -> None:
from betty.project.extension.demo.serve import DemoServer

async with DemoServer(app=self._app) as server:
async with DemoServer(app=self._app, watch=dev_watch) as server:
await server.show()
while True:
await asyncio.sleep(999)

if about.is_development():
demo = click.option(
"--dev-watch",
help="Watch for changes, and regenerate automatically",
default=False,
is_flag=True,
)(demo)

return demo
8 changes: 7 additions & 1 deletion betty/cli/commands/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
from betty.cli.commands import command, Command, project_option
from betty.locale.localizable import _
from betty.plugin import ShorthandPluginBase
import asyncclick as click

if TYPE_CHECKING:
from betty.project import Project
import asyncclick as click
from betty.app import App


Expand Down Expand Up @@ -45,6 +45,12 @@ async def click_command(self) -> click.Command:
else self.plugin_label().localize(localizer),
)
@project_option
@click.option(
"--watch",
help="Watch your project for changes, and regenerate automatically",
default=False,
is_flag=True,
)
async def generate(project: Project) -> None:
from betty.project import generate, load

Expand Down
5 changes: 3 additions & 2 deletions betty/project/extension/demo/serve.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,10 @@ class DemoServer(Server):
Serve the Betty demonstration site.
"""

def __init__(self, app: App):
def __init__(self, app: App, *, watch: bool = False):
super().__init__(localizer=DEFAULT_LOCALIZER)
self._app = app
self._watch = watch
self._server: Server | None = None
self._exit_stack = AsyncExitStack()

Expand All @@ -49,7 +50,7 @@ async def start(self) -> None:
await load.load(project)
if not project_directory_path.is_dir():
try:
await generate.generate(project)
await generate.generate(project, watch=self._watch)
except BaseException:
# Ensure that we never leave a partial build.
await to_thread(rmtree, project_directory_path)
Expand Down
7 changes: 5 additions & 2 deletions betty/project/extension/webpack/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ async def _prebuild_webpack_assets() -> None:
await webpack.prebuild(job_context=job_context)


@internal
class WebpackEntryPointProvider(Extension):
"""
An extension that provides Webpack entry points.
Expand All @@ -97,6 +98,7 @@ def webpack_entry_point_cache_keys(self) -> Sequence[str]:
pass


@internal
class PrebuiltAssetsRequirement(Requirement):
"""
Check if prebuilt assets are available.
Expand All @@ -116,6 +118,7 @@ def summary(self) -> Localizable:


async def _generate_assets(event: GenerateSiteEvent) -> None:
# @todo Implement event.watch
project = event.project
extensions = await project.extensions
webpack = extensions[Webpack]
Expand Down Expand Up @@ -218,10 +221,10 @@ async def _new_builder(
*,
job_context: Context,
) -> build.Builder:
return build.Builder(
return await build.Builder.new(
working_directory_path,
await self._project_entry_point_providers(),
self._project.configuration.debug,
await self._project_entry_point_providers(),
await self._project.renderer,
job_context=job_context,
localizer=await self._project.app.localizer,
Expand Down
160 changes: 110 additions & 50 deletions betty/project/extension/webpack/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@

from __future__ import annotations

from asyncio import to_thread, gather
from asyncio import to_thread, gather, create_task
from json import dumps, loads
from logging import getLogger
from pathlib import Path
from shutil import copy2
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, final, Self

import aiofiles
from aiofiles.os import makedirs
Expand All @@ -18,6 +18,8 @@
from betty.fs import ROOT_DIRECTORY_PATH
from betty.hashid import hashid, hashid_sequence, hashid_file_content
from betty.os import copy_tree
from betty.subprocess import run_process, console_args
from betty.typing import internal

if TYPE_CHECKING:
from betty.project.extension import Extension
Expand All @@ -27,7 +29,6 @@
from collections.abc import Sequence, MutableMapping
from betty.project.extension.webpack import WebpackEntryPointProvider


_NPM_PROJECT_DIRECTORIES_PATH = Path(__file__).parent / "webpack"


Expand Down Expand Up @@ -84,28 +85,93 @@ def _webpack_build_directory_path(
)


@internal
@final
class Builder:
"""
Build Webpack assets.
"""

def __init__(
self,
working_directory_path: Path,
entry_point_providers: Sequence[WebpackEntryPointProvider & Extension],
npm_project_directory_path: Path,
webpack_build_directory_path: Path,
debug: bool,
entry_point_providers: Sequence[WebpackEntryPointProvider & Extension],
renderer: Renderer,
*,
job_context: Context,
localizer: Localizer,
) -> None:
self._working_directory_path = working_directory_path
self._entry_point_providers = entry_point_providers
self._debug = debug
self._npm_project_directory_path = npm_project_directory_path
self._webpack_build_directory_path = webpack_build_directory_path
self._entry_point_providers = entry_point_providers
self._renderer = renderer
self._job_context = job_context
self._localizer = localizer

@classmethod
async def new(
cls,
working_directory_path: Path,
debug: bool,
entry_point_providers: Sequence[WebpackEntryPointProvider & Extension],
renderer: Renderer,
*,
job_context: Context,
localizer: Localizer,
) -> Self:
"""
Create a new instance.
"""
npm_project_directory_path = await _npm_project_directory_path(
working_directory_path, entry_point_providers
)
return cls(
npm_project_directory_path,
_webpack_build_directory_path(
npm_project_directory_path, entry_point_providers, debug
),
debug,
entry_point_providers,
renderer,
job_context=job_context,
localizer=localizer,
)

async def build(self, *, watch: bool = False) -> Path:
"""
Build the Webpack assets.
:return: The path to the directory from which the assets can be copied to their
final destination.
"""
(
npm_project_directory_path,
webpack_build_directory_path,
) = await self._prepare_build()

if watch:
webpack_task = create_task(
run_process(
[*console_args(), "npm", "run", "build-watch"],
cwd=npm_project_directory_path,
shell=True,
)
)
try:
# @todo Finish this
pass
finally:
webpack_task.cancel()
else:
await _npm.npm(("run", "build"), cwd=npm_project_directory_path)
getLogger(__name__).info(
self._localizer._("Built the Webpack front-end assets.")
)
return webpack_build_directory_path

async def _prepare_webpack_extension(
self, npm_project_directory_path: Path
) -> None:
Expand All @@ -125,6 +191,24 @@ async def _prepare_webpack_extension(
]
)

async def _do_prepare_working_directory(
self,
npm_project_directory_path: Path,
npm_project_package_json_dependencies: MutableMapping[str, str],
webpack_entry: MutableMapping[str, str],
) -> None:
await gather(
*(
self._prepare_webpack_entry_point_provider(
npm_project_directory_path,
type(entry_point_provider),
npm_project_package_json_dependencies,
webpack_entry,
)
for entry_point_provider in self._entry_point_providers
),
)

async def _prepare_webpack_entry_point_provider(
self,
npm_project_directory_path: Path,
Expand Down Expand Up @@ -161,22 +245,18 @@ async def _prepare_webpack_entry_point_provider(
)
)

async def _prepare_npm_project_directory(
async def _prepare_working_directory(
self, npm_project_directory_path: Path, webpack_build_directory_path: Path
) -> None:
npm_project_package_json_dependencies: MutableMapping[str, str] = {}
webpack_entry: MutableMapping[str, str] = {}
await makedirs(npm_project_directory_path, exist_ok=True)
await gather(
self._prepare_webpack_extension(npm_project_directory_path),
*(
self._prepare_webpack_entry_point_provider(
npm_project_directory_path,
type(entry_point_provider),
npm_project_package_json_dependencies,
webpack_entry,
)
for entry_point_provider in self._entry_point_providers
self._do_prepare_working_directory(
npm_project_directory_path,
npm_project_package_json_dependencies,
webpack_entry,
),
)
webpack_configuration_json = dumps(
Expand Down Expand Up @@ -212,40 +292,20 @@ async def _prepare_npm_project_directory(
async def _npm_install(self, npm_project_directory_path: Path) -> None:
await _npm.npm(("install", "--production"), cwd=npm_project_directory_path)

async def _webpack_build(
self, npm_project_directory_path: Path, webpack_build_directory_path: Path
) -> None:
await _npm.npm(("run", "webpack"), cwd=npm_project_directory_path)

# Ensure there is always a vendor.css. This makes for easy and unconditional importing.
await makedirs(webpack_build_directory_path / "css", exist_ok=True)
await to_thread((webpack_build_directory_path / "css" / "vendor.css").touch)

async def build(self) -> Path:
"""
Build the Webpack assets.
:return: The path to the directory from which the assets can be copied to their
final destination.
"""
npm_project_directory_path = await _npm_project_directory_path(
self._working_directory_path, self._entry_point_providers
)
webpack_build_directory_path = _webpack_build_directory_path(
npm_project_directory_path, self._entry_point_providers, self._debug
)
if webpack_build_directory_path.exists():
return webpack_build_directory_path
npm_install_required = not npm_project_directory_path.exists()
await self._prepare_npm_project_directory(
npm_project_directory_path, webpack_build_directory_path
async def _prepare_build(self) -> tuple[Path, Path]:
if self._webpack_build_directory_path.exists():
return self._npm_project_directory_path, self._webpack_build_directory_path
npm_install_required = not self._npm_project_directory_path.exists()
await self._prepare_working_directory(
self._npm_project_directory_path, self._webpack_build_directory_path
)
if npm_install_required:
await self._npm_install(npm_project_directory_path)
await self._webpack_build(
npm_project_directory_path, webpack_build_directory_path
)
getLogger(__name__).info(
self._localizer._("Built the Webpack front-end assets.")
await self._npm_install(self._npm_project_directory_path)

# Ensure there is always a vendor.css. This makes for easy and unconditional importing.
await makedirs(self._webpack_build_directory_path / "css", exist_ok=True)
await to_thread(
(self._webpack_build_directory_path / "css" / "vendor.css").touch
)
return webpack_build_directory_path

return self._npm_project_directory_path, self._webpack_build_directory_path
6 changes: 4 additions & 2 deletions betty/project/extension/webpack/webpack/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@
"terser-webpack-plugin": "^5.3.10",
"typescript": "^5.6.3",
"webpack": "^5.96.1",
"webpack-cli": "^5.1.4"
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^5.1.0"
},
"scripts": {
"webpack": "webpack --config webpack.config.js"
"build": "webpack --config webpack.config.js",
"build-watch": "webpack serve --config webpack.config.js --open"
},
"type": "module"
}
Loading

0 comments on commit 0ae3a3a

Please sign in to comment.