Skip to content

Commit

Permalink
Feat/improve vite manifest handling (#21)
Browse files Browse the repository at this point in the history
- Cache vite manifest content
- Better type vite manifest
- Allow for multiple css files to be in the manifest file
- Remove use_typescript in favour of `entrypoint_filename`
- Introduce root_directory to InertiaConfig instead of assuming it
- Introduce assets_prefix to InertiaConfig instead of assuming it
  • Loading branch information
hxjo authored Jul 17, 2024
1 parent 7d378cb commit b868a29
Show file tree
Hide file tree
Showing 13 changed files with 358 additions and 116 deletions.
Binary file modified .coverage
Binary file not shown.
28 changes: 22 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,40 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.1.6] - 2024-07-17

- Cache vite manifest content
- Better type vite manifest
- Allow for multiple css files to be in the manifest file
- Deprecate use_typescript in favour of entrypoint_filename
- Will be removed in 1.0.0
- Introduce root_directory to InertiaConfig instead of assuming it
- Introduce assets_prefix to InertiaConfig instead of assuming it

## [0.1.5] - 2024-07-17

- Introduce new dev dependency: BeautifulSoup
- Use it instead of manual parsing as this is more reliable and less painful

## [0.1.4] - 2024-05-31

* Handle better JSONification of Pydantic models
* Use `json.loads(model.model_dump.json())` rather than `model.model_dump()`
- Handle better JSONification of Pydantic models
- Use `json.loads(model.model_dump.json())` rather than `model.model_dump()`
To avoid issues with some common field types (UUID, datetime, etc.)
* Expose a `_render_json` method on inertia to allow easier overriding.
- Expose a `_render_json` method on inertia to allow easier overriding.

## [0.1.3] - 2024-05-08

* Bump FastAPI version from 0.110.2 to 0.111.0
- Bump FastAPI version from 0.110.2 to 0.111.0

## [0.1.2] - 2024-04-23

* Update `README.md` and available versions
- Update `README.md` and available versions

## [0.1.1] - 2024-04-23

* Initial release.
- Initial release.
165 changes: 101 additions & 64 deletions README.md

Large diffs are not rendered by default.

41 changes: 38 additions & 3 deletions inertia/config.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
from typing import Literal, Type
from functools import lru_cache
import json
from typing import Literal, Type, Optional, TypedDict, Dict, Union, cast
import warnings
from json import JSONEncoder
from .utils import InertiaJsonEncoder
from dataclasses import dataclass
Expand All @@ -13,12 +16,44 @@ class InertiaConfig:
environment: Literal["development", "production"] = "development"
version: str = "1.0"
json_encoder: Type[JSONEncoder] = InertiaJsonEncoder
manifest_json_path: str = ""
dev_url: str = "http://localhost:5173"
ssr_url: str = "http://localhost:13714"
ssr_enabled: bool = False
use_typescript: bool = False
manifest_json_path: str = ""
root_directory: str = "src"
entrypoint_filename: str = "main.js"
use_flash_messages: bool = False
use_flash_errors: bool = False
flash_message_key: str = "messages"
flash_error_key: str = "errors"
assets_prefix: str = ""
use_typescript: Union[bool, None] = None

def __post_init__(self) -> None:
if self.use_typescript is not None:
warnings.warn(
"use_typescript is deprecated: Please use entrypoint_filename instead. It will be removed in 1.0.0",
DeprecationWarning,
stacklevel=2,
)
self.entrypoint_filename = "main.ts" if self.use_typescript else "main.js"


class ViteManifestChunk(TypedDict):
file: str
src: Optional[str]
isEntry: Optional[bool]
isDynamicEntry: Optional[bool]
dynamicImports: Optional[list[str]]
css: Optional[list[str]]
assets: Optional[list[str]]
imports: Optional[list[str]]


ViteManifest = Dict[str, ViteManifestChunk]


@lru_cache
def _read_manifest_file(path: str) -> ViteManifest:
with open(path, "r") as manifest_file:
return cast(ViteManifest, json.load(manifest_file))
52 changes: 32 additions & 20 deletions inertia/inertia.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import logging
import os

from fastapi import Request, Response, status
from fastapi.responses import JSONResponse, HTMLResponse
from typing import Any, Callable, Dict, Optional, TypeVar, TypedDict, Union, cast
from typing import Any, Callable, Dict, List, Optional, TypeVar, TypedDict, Union, cast
import json
from pydantic import BaseModel
from starlette.responses import RedirectResponse

from .config import InertiaConfig
from .config import InertiaConfig, _read_manifest_file
from .exceptions import InertiaVersionConflictException
from .utils import LazyProp
from dataclasses import dataclass
Expand Down Expand Up @@ -39,11 +40,11 @@ class Inertia:
@dataclass
class InertiaFiles:
"""
Helper class to store the CSS and JS files for Inertia.js
Helper class to store the CSS and JS file urls for Inertia.js
"""

css_file: Union[str, None]
js_file: str
css_file_urls: List[str]
js_file_url: str

_request: Request
_component: str
Expand Down Expand Up @@ -143,20 +144,25 @@ def _set_inertia_files(self) -> None:
Set the Inertia files (CSS and JS) based on the configuration
"""
if self._config.environment == "production" or self._config.ssr_enabled:
with open(self._config.manifest_json_path, "r") as manifest_file:
manifest = json.load(manifest_file)
manifest = _read_manifest_file(self._config.manifest_json_path)
asset_manifest = manifest[
f"{self._config.root_directory}/{self._config.entrypoint_filename}"
]
css_file_urls = asset_manifest.get("css", []) or []
js_file_url = asset_manifest["file"]

extension = "ts" if self._config.use_typescript else "js"

css_file = manifest[f"src/main.{extension}"]["css"][0]
js_file = manifest[f"src/main.{extension}"]["file"]
self._inertia_files = self.InertiaFiles(
css_file=f"/src/{css_file}", js_file=f"/{js_file}"
css_file_urls=[
os.path.join("/", self._config.assets_prefix, file)
for file in css_file_urls
],
js_file_url=os.path.join("/", self._config.assets_prefix, js_file_url),
)
else:
extension = "ts" if self._config.use_typescript else "js"
js_file = f"{self._config.dev_url}/src/main.{extension}"
self._inertia_files = self.InertiaFiles(css_file=None, js_file=js_file)
js_file_url = f"{self._config.dev_url}/{self._config.root_directory}/{self._config.entrypoint_filename}"
self._inertia_files = self.InertiaFiles(
css_file_urls=[], js_file_url=js_file_url
)

@classmethod
def _deep_transform_callables(
Expand Down Expand Up @@ -208,23 +214,29 @@ def _get_html_content(self, head: str, body: str) -> str:
:param body: The content for the body tag
:return: The HTML content
"""
css_link = (
f'<link rel="stylesheet" href="{self._inertia_files.css_file}">'
if self._inertia_files.css_file
css_links = (
"\n".join(
[
f'<link rel="stylesheet" href="{url}">'
for url in self._inertia_files.css_file_urls
]
)
if len(self._inertia_files.css_file_urls) > 0
else ""
)

return f"""
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{head}
{css_link}
{css_links}
</head>
<body>
{body}
<script type="module" src="{self._inertia_files.js_file}"></script>
<script type="module" src="{self._inertia_files.js_file_url}"></script>
</body>
</html>
"""
Expand Down
4 changes: 3 additions & 1 deletion inertia/tests/dummy_manifest_js.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
"src": "src/main.js",
"isEntry": true,
"css": [
"assets/main-jJL2BnPz.css"
"assets/main-jJL2BnPz.css",
"assets/main-jJL2BnPy.css",
"assets/main-jJL2BnPx.css"
]
}
}
4 changes: 3 additions & 1 deletion inertia/tests/dummy_manifest_ts.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
"src": "src/main.ts",
"isEntry": true,
"css": [
"assets/main-GJL2BnPz.css"
"assets/main-GJL2BnPz.css",
"assets/main-GJL2BnPy.css",
"assets/main-GJL2BnPx.css"
]
}
}
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import json
import os
from datetime import datetime

from fastapi import FastAPI, Depends
from typing import Annotated, cast

from starlette.testclient import TestClient

from inertia import Inertia, inertia_dependency_factory, InertiaResponse, InertiaConfig

from inertia.tests.utils import assert_response_content

app = FastAPI()
manifest_json_ts = os.path.join(
os.path.dirname(__file__), "..", "dummy_manifest_ts.json"
)


TypescriptInertiaDep = Annotated[
Inertia,
Depends(
inertia_dependency_factory(
InertiaConfig(
use_typescript=True,
)
)
),
]

TypescriptProductionInertiaDep = Annotated[
Inertia,
Depends(
inertia_dependency_factory(
InertiaConfig(
manifest_json_path=manifest_json_ts,
environment="production",
use_typescript=True,
)
)
),
]

PROPS = {"message": "hello from index", "created_at": datetime.now()}

EXPECTED_PROPS = {
**PROPS,
"created_at": cast(datetime, PROPS["created_at"]).isoformat(),
}

COMPONENT = "IndexPage"


@app.get("/typescript", response_model=None)
async def typescript(inertia: TypescriptInertiaDep) -> InertiaResponse:
return await inertia.render(COMPONENT, PROPS)


@app.get("/typescript-production", response_model=None)
async def typescript_production(
inertia: TypescriptProductionInertiaDep,
) -> InertiaResponse:
return await inertia.render(COMPONENT, PROPS)


def test_first_request_returns_html_typescript_still_works() -> None:
with TestClient(app) as client:
response = client.get("/typescript")
assert response.status_code == 200
assert response.headers.get("content-type").split(";")[0] == "text/html"
expected_url = str(client.base_url) + "/typescript"
assert_response_content(
response,
expected_component=COMPONENT,
expected_props=EXPECTED_PROPS,
expected_url=expected_url,
expected_script_asset_url="http://localhost:5173/src/main.ts",
)


def test_first_request_returns_html_production_typescript_still_works() -> None:
with open(manifest_json_ts, "r") as manifest_file:
manifest = json.load(manifest_file)

css_files = [f"/{file}" for file in manifest["src/main.ts"]["css"]]
js_file = manifest["src/main.ts"]["file"]
js_file = f"/{js_file}"
with TestClient(app) as client:
response = client.get("/typescript-production")
assert response.status_code == 200
assert response.headers.get("content-type").split(";")[0] == "text/html"
expected_url = str(client.base_url) + "/typescript-production"
assert_response_content(
response,
expected_component=COMPONENT,
expected_props=EXPECTED_PROPS,
expected_url=expected_url,
expected_script_asset_url=js_file,
expected_css_asset_urls=css_files,
)
Loading

0 comments on commit b868a29

Please sign in to comment.