@beatzball/create-litro 0.1.2 → 0.1.4
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.
- package/CHANGELOG.md +128 -0
- package/dist/recipes/fullstack/template/app.ts +11 -8
- package/dist/recipes/fullstack/template/pages/blog/[slug].ts +17 -6
- package/dist/recipes/fullstack/template/pages/index.ts +4 -5
- package/dist/recipes/fullstack/template/vite.config.ts +9 -0
- package/package.json +1 -1
- package/recipes/fullstack/template/app.ts +11 -8
- package/recipes/fullstack/template/pages/blog/[slug].ts +17 -6
- package/recipes/fullstack/template/pages/index.ts +4 -5
- package/recipes/fullstack/template/vite.config.ts +9 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,133 @@
|
|
|
1
1
|
# create-litro
|
|
2
2
|
|
|
3
|
+
## 0.1.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 76d3bc7: fix: client-side navigation links do not work on first load
|
|
8
|
+
|
|
9
|
+
`<litro-link>` clicks were silently no-ops in scaffolded apps because of
|
|
10
|
+
three compounding bugs.
|
|
11
|
+
|
|
12
|
+
***
|
|
13
|
+
|
|
14
|
+
**Bug 1 — Empty route table on init** (`LitroOutlet`, `app.ts`)
|
|
15
|
+
|
|
16
|
+
`app.ts` set `outlet.routes` inside a `DOMContentLoaded` callback (a
|
|
17
|
+
macrotask). By that point Lit's first-update microtask had already fired,
|
|
18
|
+
so `firstUpdated()` ran with `routes = []` and the router was initialised
|
|
19
|
+
with no routes.
|
|
20
|
+
|
|
21
|
+
_Fix — `LitroOutlet`_: Replace `@property({ type: Array }) routes` with a
|
|
22
|
+
plain getter/setter. The setter calls `router.setRoutes()` directly when
|
|
23
|
+
the router is already initialised, without going through Lit's render cycle
|
|
24
|
+
(which would crash with "ChildPart has no parentNode" because
|
|
25
|
+
`firstUpdated()` removes Lit's internal marker nodes to give the router
|
|
26
|
+
ownership of the outlet's subtree).
|
|
27
|
+
|
|
28
|
+
_Fix — `app.ts`_ (fullstack recipe template + playground): Set
|
|
29
|
+
`outlet.routes` synchronously after imports rather than inside a
|
|
30
|
+
`DOMContentLoaded` callback. Module scripts are deferred by the browser;
|
|
31
|
+
by the time they execute the DOM is fully parsed and `<litro-outlet>` is
|
|
32
|
+
present.
|
|
33
|
+
|
|
34
|
+
***
|
|
35
|
+
|
|
36
|
+
**Bug 2 — Click handler never attached on SSR'd pages** (`LitroLink`)
|
|
37
|
+
|
|
38
|
+
`@lit-labs/ssr` adds `defer-hydration` to custom elements inside shadow
|
|
39
|
+
DOM. `@lit-labs/ssr-client` patches `LitElement.prototype.connectedCallback`
|
|
40
|
+
to block Lit's update cycle when this attribute is present. A `@click`
|
|
41
|
+
binding on the shadow `<a>` is a Lit binding — it is never attached until
|
|
42
|
+
`defer-hydration` is removed, which only happens when the parent component
|
|
43
|
+
hydrates. For page components that are never hydrated client-side (because
|
|
44
|
+
the router replaces the SSR content before they load), `<litro-link>`
|
|
45
|
+
elements inside them never receive a click handler.
|
|
46
|
+
|
|
47
|
+
This is why the playground appeared to work: its home page has no
|
|
48
|
+
`<litro-link>` elements. The fullstack generator template does, so clicks
|
|
49
|
+
on the SSR'd page were silently ignored.
|
|
50
|
+
|
|
51
|
+
_Fix_: Move the click handler from a `@click` binding on the shadow `<a>`
|
|
52
|
+
to the HOST element via `addEventListener('click', ...)` registered in
|
|
53
|
+
`connectedCallback()` (before `super.connectedCallback()`). The host
|
|
54
|
+
listener runs in `LitroLink`'s own `connectedCallback` override, which
|
|
55
|
+
executes before the `@lit-labs/ssr-client` patch checks for
|
|
56
|
+
`defer-hydration`. This ensures the handler is active immediately after the
|
|
57
|
+
element connects to the DOM, even for SSR'd elements on first load.
|
|
58
|
+
|
|
59
|
+
The shadow `<a>` is kept without a `@click` binding — it exists for
|
|
60
|
+
progressive enhancement (no-JS navigation) and accessibility (cursor,
|
|
61
|
+
focus, keyboard navigation).
|
|
62
|
+
|
|
63
|
+
***
|
|
64
|
+
|
|
65
|
+
**Bug 3 — `_resolve()` race condition** (`LitroRouter`)
|
|
66
|
+
|
|
67
|
+
`setRoutes()` calls `_resolve()` immediately for the current URL. If the
|
|
68
|
+
user clicks a link before that initial `_resolve()` completes (e.g. while
|
|
69
|
+
the page action's dynamic import is in flight), a second `_resolve()` call
|
|
70
|
+
starts concurrently. If the first call (for `/`) completes after the second
|
|
71
|
+
(for `/blog`), it overwrites the blog page with the home page.
|
|
72
|
+
|
|
73
|
+
_Fix_: Add a `_resolveToken` monotonic counter. Each `_resolve()` call
|
|
74
|
+
captures its own token at the start and checks it after every `await`. If
|
|
75
|
+
the token has advanced, a newer navigation superseded this one and the call
|
|
76
|
+
returns without touching the DOM.
|
|
77
|
+
|
|
78
|
+
***
|
|
79
|
+
|
|
80
|
+
**Bug 4 — `@property()` decorators silently dropped by esbuild TC39 transform** (`LitroLink`)
|
|
81
|
+
|
|
82
|
+
esbuild 0.21+ uses the TC39 Stage 3 decorator transform. In that mode,
|
|
83
|
+
Lit's `@property()` decorator only handles `accessor` fields; applied to a
|
|
84
|
+
plain field (`href = ''`) it is silently not applied. As a result `href`,
|
|
85
|
+
`target`, and `rel` were absent from `observedAttributes`, so
|
|
86
|
+
`attributeChangedCallback` was never called during element upgrade, leaving
|
|
87
|
+
`this.href = ''` forever regardless of what the HTML attribute said.
|
|
88
|
+
|
|
89
|
+
_Fix_: Replace the three `@property()` field decorators with a
|
|
90
|
+
`static override properties = { href, target, rel }` declaration. Lit reads
|
|
91
|
+
this static field at class-finalization time via `finalize()`, which runs
|
|
92
|
+
before the element is defined in `customElements`, ensuring the properties
|
|
93
|
+
are correctly registered in `observedAttributes`.
|
|
94
|
+
|
|
95
|
+
***
|
|
96
|
+
|
|
97
|
+
Adds a new `LitroOutlet.test.ts` test file (6 tests) covering the
|
|
98
|
+
synchronous and late-assignment code paths, the setter guard, SSR child
|
|
99
|
+
clearing, and the `LitroRouter` constructor call.
|
|
100
|
+
|
|
101
|
+
Updates `LitroLink.test.ts` (12 tests) to dispatch real `MouseEvent`s on
|
|
102
|
+
the host element (exercising the `addEventListener` path) rather than
|
|
103
|
+
calling the private handler directly by name.
|
|
104
|
+
|
|
105
|
+
***
|
|
106
|
+
|
|
107
|
+
**Template fix — `@state() declare serverData` incompatible with jiti/SSG**
|
|
108
|
+
|
|
109
|
+
The fullstack recipe template used `@state() declare serverData: T | null` to
|
|
110
|
+
narrow the `serverData: unknown` type inherited from `LitroPage`. The `declare`
|
|
111
|
+
modifier emits no runtime code, but jiti's oxc-transform (used in SSG mode to
|
|
112
|
+
load page files) throws "Fields with the 'declare' modifier cannot be
|
|
113
|
+
initialized here" under TC39 Stage 3 decorator mode.
|
|
114
|
+
|
|
115
|
+
_Fix_: Remove `@state() declare serverData` from both page templates. Use a
|
|
116
|
+
local type cast in `render()` instead: `const data = this.serverData as T | null`.
|
|
117
|
+
The property is already reactive (declared as `@state() serverData = null` in
|
|
118
|
+
`LitroPage`). Updated `LitroPage.ts` JSDoc and `DECISIONS.md` to document this
|
|
119
|
+
pattern and warn against `declare` fields in subclasses.
|
|
120
|
+
|
|
121
|
+
## 0.1.3
|
|
122
|
+
|
|
123
|
+
### Patch Changes
|
|
124
|
+
|
|
125
|
+
- bfd8f9a: Fix fullstack recipe: add `base: '/_litro/'` to `vite.config.ts` and extend `LitroPage` in `[slug].ts`
|
|
126
|
+
|
|
127
|
+
Without `base: '/_litro/'`, Vite's compiled modulepreload URL resolver emits paths like `/assets/chunk.js` instead of `/_litro/assets/chunk.js`. These requests hit the Nitro catch-all page handler and return HTML, causing a MIME type error that leaves dynamic routes (e.g. `/blog/hello-world`) stuck on "Loading…".
|
|
128
|
+
|
|
129
|
+
Also fixes `pages/blog/[slug].ts` to extend `LitroPage` (not `LitElement`) and implement `fetchData()`, so client-side SPA navigation to different slugs correctly updates `serverData`.
|
|
130
|
+
|
|
3
131
|
## 0.1.2
|
|
4
132
|
|
|
5
133
|
### Patch Changes
|
|
@@ -10,11 +10,14 @@ import '@beatzball/litro/runtime/LitroLink.js';
|
|
|
10
10
|
// emptyOutDir does not delete it between builds.
|
|
11
11
|
import { routes } from './routes.generated.js';
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
13
|
+
// Set routes synchronously. By the time this module-script executes (all
|
|
14
|
+
// module scripts are deferred), the HTML is fully parsed and <litro-outlet>
|
|
15
|
+
// is already in the DOM. Setting outlet.routes here — before Lit's first
|
|
16
|
+
// update microtask fires — ensures firstUpdated() sees the real route table
|
|
17
|
+
// rather than the empty default, so the router starts correctly.
|
|
18
|
+
const outlet = document.querySelector('litro-outlet') as (Element & { routes: unknown }) | null;
|
|
19
|
+
if (outlet) {
|
|
20
|
+
outlet.routes = routes;
|
|
21
|
+
} else {
|
|
22
|
+
console.warn('[litro] <litro-outlet> not found — router will not start.');
|
|
23
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { customElement
|
|
1
|
+
import { html } from 'lit';
|
|
2
|
+
import { customElement } from 'lit/decorators.js';
|
|
3
|
+
import { LitroPage } from '@beatzball/litro/runtime';
|
|
3
4
|
import { definePageData } from '@beatzball/litro';
|
|
5
|
+
import type { LitroLocation } from '@beatzball/litro-router';
|
|
4
6
|
|
|
5
7
|
export interface PostData {
|
|
6
8
|
slug: string;
|
|
@@ -24,14 +26,23 @@ export async function generateRoutes(): Promise<string[]> {
|
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
@customElement('page-blog-slug')
|
|
27
|
-
export class BlogPostPage extends
|
|
28
|
-
|
|
29
|
+
export class BlogPostPage extends LitroPage {
|
|
30
|
+
// Called by LitroRouter on client-side navigation to fetch data for the new slug.
|
|
31
|
+
override async fetchData(location: LitroLocation): Promise<PostData> {
|
|
32
|
+
const slug = location.params['slug'] ?? '';
|
|
33
|
+
return {
|
|
34
|
+
slug,
|
|
35
|
+
title: `Post: ${slug}`,
|
|
36
|
+
content: `This is the content for the "${slug}" post.`,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
29
39
|
|
|
30
40
|
render() {
|
|
41
|
+
const data = this.serverData as PostData | null;
|
|
31
42
|
return html`
|
|
32
43
|
<article>
|
|
33
|
-
<h1>${
|
|
34
|
-
<p>${
|
|
44
|
+
<h1>${data?.title ?? 'Loading…'}</h1>
|
|
45
|
+
<p>${data?.content ?? ''}</p>
|
|
35
46
|
<litro-link href="/blog">← Back to Blog</litro-link>
|
|
36
47
|
|
|
|
37
48
|
<litro-link href="/">← Home</litro-link>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { html } from 'lit';
|
|
2
|
-
import { customElement
|
|
2
|
+
import { customElement } from 'lit/decorators.js';
|
|
3
3
|
import { LitroPage } from '@beatzball/litro/runtime';
|
|
4
4
|
import { definePageData } from '@beatzball/litro';
|
|
5
5
|
|
|
@@ -18,8 +18,6 @@ export const pageData = definePageData(async (_event) => {
|
|
|
18
18
|
|
|
19
19
|
@customElement('page-home')
|
|
20
20
|
export class HomePage extends LitroPage {
|
|
21
|
-
@state() declare serverData: HomeData | null;
|
|
22
|
-
|
|
23
21
|
// Called on client-side navigation (not on the initial SSR load).
|
|
24
22
|
override async fetchData() {
|
|
25
23
|
const res = await fetch('/api/hello');
|
|
@@ -27,11 +25,12 @@ export class HomePage extends LitroPage {
|
|
|
27
25
|
}
|
|
28
26
|
|
|
29
27
|
render() {
|
|
28
|
+
const data = this.serverData as HomeData | null;
|
|
30
29
|
if (this.loading) return html`<p>Loading…</p>`;
|
|
31
30
|
return html`
|
|
32
31
|
<main>
|
|
33
|
-
<h1>${
|
|
34
|
-
<p><small>Rendered at: ${
|
|
32
|
+
<h1>${data?.message ?? 'Welcome to {{projectName}}'}</h1>
|
|
33
|
+
<p><small>Rendered at: ${data?.timestamp ?? '—'}</small></p>
|
|
35
34
|
<nav>
|
|
36
35
|
<litro-link href="/blog">Go to Blog →</litro-link>
|
|
37
36
|
</nav>
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { defineConfig } from 'vite';
|
|
2
2
|
|
|
3
3
|
export default defineConfig({
|
|
4
|
+
// base must match publicAssets.baseURL in nitro.config.ts ('/_litro/').
|
|
5
|
+
// Vite embeds the base into the compiled preload URL resolver so that
|
|
6
|
+
// modulepreload hints resolve to /_litro/assets/... instead of /assets/...
|
|
7
|
+
// Without this, preload requests hit the catch-all page handler and return
|
|
8
|
+
// HTML, causing a MIME type error for module scripts.
|
|
9
|
+
base: '/_litro/',
|
|
10
|
+
resolve: {
|
|
11
|
+
conditions: ['source', 'browser', 'module', 'import', 'default'],
|
|
12
|
+
},
|
|
4
13
|
build: {
|
|
5
14
|
outDir: 'dist/client',
|
|
6
15
|
rollupOptions: {
|
package/package.json
CHANGED
|
@@ -10,11 +10,14 @@ import '@beatzball/litro/runtime/LitroLink.js';
|
|
|
10
10
|
// emptyOutDir does not delete it between builds.
|
|
11
11
|
import { routes } from './routes.generated.js';
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
13
|
+
// Set routes synchronously. By the time this module-script executes (all
|
|
14
|
+
// module scripts are deferred), the HTML is fully parsed and <litro-outlet>
|
|
15
|
+
// is already in the DOM. Setting outlet.routes here — before Lit's first
|
|
16
|
+
// update microtask fires — ensures firstUpdated() sees the real route table
|
|
17
|
+
// rather than the empty default, so the router starts correctly.
|
|
18
|
+
const outlet = document.querySelector('litro-outlet') as (Element & { routes: unknown }) | null;
|
|
19
|
+
if (outlet) {
|
|
20
|
+
outlet.routes = routes;
|
|
21
|
+
} else {
|
|
22
|
+
console.warn('[litro] <litro-outlet> not found — router will not start.');
|
|
23
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { customElement
|
|
1
|
+
import { html } from 'lit';
|
|
2
|
+
import { customElement } from 'lit/decorators.js';
|
|
3
|
+
import { LitroPage } from '@beatzball/litro/runtime';
|
|
3
4
|
import { definePageData } from '@beatzball/litro';
|
|
5
|
+
import type { LitroLocation } from '@beatzball/litro-router';
|
|
4
6
|
|
|
5
7
|
export interface PostData {
|
|
6
8
|
slug: string;
|
|
@@ -24,14 +26,23 @@ export async function generateRoutes(): Promise<string[]> {
|
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
@customElement('page-blog-slug')
|
|
27
|
-
export class BlogPostPage extends
|
|
28
|
-
|
|
29
|
+
export class BlogPostPage extends LitroPage {
|
|
30
|
+
// Called by LitroRouter on client-side navigation to fetch data for the new slug.
|
|
31
|
+
override async fetchData(location: LitroLocation): Promise<PostData> {
|
|
32
|
+
const slug = location.params['slug'] ?? '';
|
|
33
|
+
return {
|
|
34
|
+
slug,
|
|
35
|
+
title: `Post: ${slug}`,
|
|
36
|
+
content: `This is the content for the "${slug}" post.`,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
29
39
|
|
|
30
40
|
render() {
|
|
41
|
+
const data = this.serverData as PostData | null;
|
|
31
42
|
return html`
|
|
32
43
|
<article>
|
|
33
|
-
<h1>${
|
|
34
|
-
<p>${
|
|
44
|
+
<h1>${data?.title ?? 'Loading…'}</h1>
|
|
45
|
+
<p>${data?.content ?? ''}</p>
|
|
35
46
|
<litro-link href="/blog">← Back to Blog</litro-link>
|
|
36
47
|
|
|
|
37
48
|
<litro-link href="/">← Home</litro-link>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { html } from 'lit';
|
|
2
|
-
import { customElement
|
|
2
|
+
import { customElement } from 'lit/decorators.js';
|
|
3
3
|
import { LitroPage } from '@beatzball/litro/runtime';
|
|
4
4
|
import { definePageData } from '@beatzball/litro';
|
|
5
5
|
|
|
@@ -18,8 +18,6 @@ export const pageData = definePageData(async (_event) => {
|
|
|
18
18
|
|
|
19
19
|
@customElement('page-home')
|
|
20
20
|
export class HomePage extends LitroPage {
|
|
21
|
-
@state() declare serverData: HomeData | null;
|
|
22
|
-
|
|
23
21
|
// Called on client-side navigation (not on the initial SSR load).
|
|
24
22
|
override async fetchData() {
|
|
25
23
|
const res = await fetch('/api/hello');
|
|
@@ -27,11 +25,12 @@ export class HomePage extends LitroPage {
|
|
|
27
25
|
}
|
|
28
26
|
|
|
29
27
|
render() {
|
|
28
|
+
const data = this.serverData as HomeData | null;
|
|
30
29
|
if (this.loading) return html`<p>Loading…</p>`;
|
|
31
30
|
return html`
|
|
32
31
|
<main>
|
|
33
|
-
<h1>${
|
|
34
|
-
<p><small>Rendered at: ${
|
|
32
|
+
<h1>${data?.message ?? 'Welcome to {{projectName}}'}</h1>
|
|
33
|
+
<p><small>Rendered at: ${data?.timestamp ?? '—'}</small></p>
|
|
35
34
|
<nav>
|
|
36
35
|
<litro-link href="/blog">Go to Blog →</litro-link>
|
|
37
36
|
</nav>
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { defineConfig } from 'vite';
|
|
2
2
|
|
|
3
3
|
export default defineConfig({
|
|
4
|
+
// base must match publicAssets.baseURL in nitro.config.ts ('/_litro/').
|
|
5
|
+
// Vite embeds the base into the compiled preload URL resolver so that
|
|
6
|
+
// modulepreload hints resolve to /_litro/assets/... instead of /assets/...
|
|
7
|
+
// Without this, preload requests hit the catch-all page handler and return
|
|
8
|
+
// HTML, causing a MIME type error for module scripts.
|
|
9
|
+
base: '/_litro/',
|
|
10
|
+
resolve: {
|
|
11
|
+
conditions: ['source', 'browser', 'module', 'import', 'default'],
|
|
12
|
+
},
|
|
4
13
|
build: {
|
|
5
14
|
outDir: 'dist/client',
|
|
6
15
|
rollupOptions: {
|