@beatzball/create-litro 0.1.4 → 0.2.0
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 +15 -0
- package/dist/recipes/starlight/recipe.config.d.ts +4 -0
- package/dist/recipes/starlight/recipe.config.d.ts.map +1 -0
- package/dist/recipes/starlight/recipe.config.js +9 -0
- package/dist/recipes/starlight/recipe.config.js.map +1 -0
- package/dist/recipes/starlight/recipe.config.ts +11 -0
- package/dist/recipes/starlight/template/_data/metadata.js +10 -0
- package/dist/recipes/starlight/template/app.ts +18 -0
- package/dist/recipes/starlight/template/content/blog/.11tydata.json +1 -0
- package/dist/recipes/starlight/template/content/blog/release-notes.md +44 -0
- package/dist/recipes/starlight/template/content/blog/welcome.md +44 -0
- package/dist/recipes/starlight/template/content/docs/.11tydata.json +1 -0
- package/dist/recipes/starlight/template/content/docs/configuration.md +77 -0
- package/dist/recipes/starlight/template/content/docs/getting-started.md +53 -0
- package/dist/recipes/starlight/template/content/docs/guides-deploying.md +79 -0
- package/dist/recipes/starlight/template/content/docs/guides-first-page.md +64 -0
- package/dist/recipes/starlight/template/content/docs/installation.md +54 -0
- package/dist/recipes/starlight/template/litro.recipe.json +7 -0
- package/dist/recipes/starlight/template/nitro.config.ts +57 -0
- package/dist/recipes/starlight/template/package.json +26 -0
- package/dist/recipes/starlight/template/pages/blog/[slug].ts +125 -0
- package/dist/recipes/starlight/template/pages/blog/index.ts +114 -0
- package/dist/recipes/starlight/template/pages/blog/tags/[tag].ts +110 -0
- package/dist/recipes/starlight/template/pages/docs/[slug].ts +147 -0
- package/dist/recipes/starlight/template/pages/index.ts +135 -0
- package/dist/recipes/starlight/template/public/styles/starlight.css +215 -0
- package/dist/recipes/starlight/template/server/middleware/vite-dev.ts +29 -0
- package/dist/recipes/starlight/template/server/routes/[...].ts +57 -0
- package/dist/recipes/starlight/template/server/starlight.config.js +29 -0
- package/dist/recipes/starlight/template/src/components/sl-aside.ts +91 -0
- package/dist/recipes/starlight/template/src/components/sl-badge.ts +76 -0
- package/dist/recipes/starlight/template/src/components/sl-card-grid.ts +34 -0
- package/dist/recipes/starlight/template/src/components/sl-card.ts +91 -0
- package/dist/recipes/starlight/template/src/components/sl-tab-item.ts +35 -0
- package/dist/recipes/starlight/template/src/components/sl-tabs.ts +108 -0
- package/dist/recipes/starlight/template/src/components/starlight-header.ts +152 -0
- package/dist/recipes/starlight/template/src/components/starlight-page.ts +168 -0
- package/dist/recipes/starlight/template/src/components/starlight-sidebar.ts +125 -0
- package/dist/recipes/starlight/template/src/components/starlight-toc.ts +133 -0
- package/dist/recipes/starlight/template/src/date-utils.ts +20 -0
- package/dist/recipes/starlight/template/src/extract-headings.ts +68 -0
- package/dist/recipes/starlight/template/src/route-meta.ts +16 -0
- package/dist/recipes/starlight/template/tsconfig.json +14 -0
- package/dist/recipes/starlight/template/vite.config.ts +19 -0
- package/dist/src/scaffold.test.js +134 -0
- package/dist/src/scaffold.test.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/recipes/starlight/recipe.config.ts +11 -0
- package/recipes/starlight/template/_data/metadata.js +10 -0
- package/recipes/starlight/template/app.ts +18 -0
- package/recipes/starlight/template/content/blog/.11tydata.json +1 -0
- package/recipes/starlight/template/content/blog/release-notes.md +44 -0
- package/recipes/starlight/template/content/blog/welcome.md +44 -0
- package/recipes/starlight/template/content/docs/.11tydata.json +1 -0
- package/recipes/starlight/template/content/docs/configuration.md +77 -0
- package/recipes/starlight/template/content/docs/getting-started.md +53 -0
- package/recipes/starlight/template/content/docs/guides-deploying.md +79 -0
- package/recipes/starlight/template/content/docs/guides-first-page.md +64 -0
- package/recipes/starlight/template/content/docs/installation.md +54 -0
- package/recipes/starlight/template/litro.recipe.json +7 -0
- package/recipes/starlight/template/nitro.config.ts +57 -0
- package/recipes/starlight/template/package.json +26 -0
- package/recipes/starlight/template/pages/blog/[slug].ts +125 -0
- package/recipes/starlight/template/pages/blog/index.ts +114 -0
- package/recipes/starlight/template/pages/blog/tags/[tag].ts +110 -0
- package/recipes/starlight/template/pages/docs/[slug].ts +147 -0
- package/recipes/starlight/template/pages/index.ts +135 -0
- package/recipes/starlight/template/public/styles/starlight.css +215 -0
- package/recipes/starlight/template/server/middleware/vite-dev.ts +29 -0
- package/recipes/starlight/template/server/routes/[...].ts +57 -0
- package/recipes/starlight/template/server/starlight.config.js +29 -0
- package/recipes/starlight/template/src/components/sl-aside.ts +91 -0
- package/recipes/starlight/template/src/components/sl-badge.ts +76 -0
- package/recipes/starlight/template/src/components/sl-card-grid.ts +34 -0
- package/recipes/starlight/template/src/components/sl-card.ts +91 -0
- package/recipes/starlight/template/src/components/sl-tab-item.ts +35 -0
- package/recipes/starlight/template/src/components/sl-tabs.ts +108 -0
- package/recipes/starlight/template/src/components/starlight-header.ts +152 -0
- package/recipes/starlight/template/src/components/starlight-page.ts +168 -0
- package/recipes/starlight/template/src/components/starlight-sidebar.ts +125 -0
- package/recipes/starlight/template/src/components/starlight-toc.ts +133 -0
- package/recipes/starlight/template/src/date-utils.ts +20 -0
- package/recipes/starlight/template/src/extract-headings.ts +68 -0
- package/recipes/starlight/template/src/route-meta.ts +16 -0
- package/recipes/starlight/template/tsconfig.json +14 -0
- package/recipes/starlight/template/vite.config.ts +19 -0
- package/src/scaffold.test.ts +148 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { LitElement, html, css } from 'lit';
|
|
2
|
+
import { customElement } from 'lit/decorators.js';
|
|
3
|
+
|
|
4
|
+
export interface SidebarItem {
|
|
5
|
+
label: string;
|
|
6
|
+
slug: string;
|
|
7
|
+
badge?: { text: string; variant?: string };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface SidebarGroup {
|
|
11
|
+
label: string;
|
|
12
|
+
items: SidebarItem[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* <starlight-sidebar .groups=${sidebar} currentSlug="getting-started">
|
|
17
|
+
* Renders grouped navigation links for the docs sidebar.
|
|
18
|
+
* The active item (matching currentSlug) is highlighted with aria-current.
|
|
19
|
+
*/
|
|
20
|
+
@customElement('starlight-sidebar')
|
|
21
|
+
export class StarlightSidebar extends LitElement {
|
|
22
|
+
static override properties = {
|
|
23
|
+
groups: { type: Array },
|
|
24
|
+
currentSlug: { type: String },
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
static override styles = css`
|
|
28
|
+
:host {
|
|
29
|
+
display: block;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
nav {
|
|
33
|
+
padding: 1rem 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.group {
|
|
37
|
+
margin-bottom: 1.5rem;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.group-label {
|
|
41
|
+
font-size: var(--sl-text-xs, 0.75rem);
|
|
42
|
+
font-weight: 700;
|
|
43
|
+
text-transform: uppercase;
|
|
44
|
+
letter-spacing: 0.1em;
|
|
45
|
+
color: var(--sl-color-gray-4, #757575);
|
|
46
|
+
padding: 0 1rem;
|
|
47
|
+
margin: 0 0 0.5rem;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
ul {
|
|
51
|
+
list-style: none;
|
|
52
|
+
padding: 0;
|
|
53
|
+
margin: 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
li {
|
|
57
|
+
margin: 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
a {
|
|
61
|
+
display: flex;
|
|
62
|
+
align-items: center;
|
|
63
|
+
justify-content: space-between;
|
|
64
|
+
padding: 0.35rem 1rem;
|
|
65
|
+
font-size: var(--sl-text-sm, 0.875rem);
|
|
66
|
+
color: var(--sl-color-gray-5, #4b4b4b);
|
|
67
|
+
text-decoration: none;
|
|
68
|
+
border-left: 2px solid transparent;
|
|
69
|
+
transition: color 0.15s, background-color 0.15s;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
a:hover {
|
|
73
|
+
color: var(--sl-color-text, #23262f);
|
|
74
|
+
background-color: var(--sl-color-gray-2, #e8e8e8);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
a[aria-current='page'] {
|
|
78
|
+
color: var(--sl-color-accent, #7c3aed);
|
|
79
|
+
border-left-color: var(--sl-color-accent, #7c3aed);
|
|
80
|
+
background-color: var(--sl-color-accent-low, #ede9fe);
|
|
81
|
+
font-weight: 600;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.badge {
|
|
85
|
+
display: inline-block;
|
|
86
|
+
padding: 0.1em 0.45em;
|
|
87
|
+
font-size: var(--sl-text-xs, 0.75rem);
|
|
88
|
+
font-weight: 600;
|
|
89
|
+
border-radius: 9999px;
|
|
90
|
+
background-color: var(--sl-color-accent-low, #ede9fe);
|
|
91
|
+
color: var(--sl-color-accent-high, #5b21b6);
|
|
92
|
+
margin-left: 0.5rem;
|
|
93
|
+
}
|
|
94
|
+
`;
|
|
95
|
+
|
|
96
|
+
groups: SidebarGroup[] = [];
|
|
97
|
+
currentSlug = '';
|
|
98
|
+
|
|
99
|
+
override render() {
|
|
100
|
+
return html`
|
|
101
|
+
<nav aria-label="Site navigation">
|
|
102
|
+
${this.groups.map(group => html`
|
|
103
|
+
<div class="group">
|
|
104
|
+
<p class="group-label">${group.label}</p>
|
|
105
|
+
<ul>
|
|
106
|
+
${group.items.map(item => html`
|
|
107
|
+
<li>
|
|
108
|
+
<a
|
|
109
|
+
href="/docs/${item.slug}"
|
|
110
|
+
aria-current="${this.currentSlug === item.slug ? 'page' : 'false'}"
|
|
111
|
+
>
|
|
112
|
+
<span>${item.label}</span>
|
|
113
|
+
${item.badge ? html`<span class="badge">${item.badge.text}</span>` : ''}
|
|
114
|
+
</a>
|
|
115
|
+
</li>
|
|
116
|
+
`)}
|
|
117
|
+
</ul>
|
|
118
|
+
</div>
|
|
119
|
+
`)}
|
|
120
|
+
</nav>
|
|
121
|
+
`;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export default StarlightSidebar;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { LitElement, html, css } from 'lit';
|
|
2
|
+
import { customElement } from 'lit/decorators.js';
|
|
3
|
+
import type { TocEntry } from '../extract-headings.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* <starlight-toc .entries=${toc}>
|
|
7
|
+
* Renders the table of contents as anchor links.
|
|
8
|
+
* Active section highlighting is handled by CSS :target pseudo-class.
|
|
9
|
+
*/
|
|
10
|
+
@customElement('starlight-toc')
|
|
11
|
+
export class StarlightToc extends LitElement {
|
|
12
|
+
static override properties = {
|
|
13
|
+
entries: { type: Array },
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
static override styles = css`
|
|
17
|
+
:host {
|
|
18
|
+
display: block;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
nav {
|
|
22
|
+
position: sticky;
|
|
23
|
+
top: calc(var(--sl-nav-height, 3.5rem) + 1rem);
|
|
24
|
+
max-height: calc(100vh - var(--sl-nav-height, 3.5rem) - 2rem);
|
|
25
|
+
overflow-y: auto;
|
|
26
|
+
padding: 0 0.5rem;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
h2 {
|
|
30
|
+
font-size: var(--sl-text-xs, 0.75rem);
|
|
31
|
+
font-weight: 700;
|
|
32
|
+
text-transform: uppercase;
|
|
33
|
+
letter-spacing: 0.1em;
|
|
34
|
+
color: var(--sl-color-gray-4, #757575);
|
|
35
|
+
margin: 0 0 0.75rem;
|
|
36
|
+
padding: 0;
|
|
37
|
+
border: none;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
ul {
|
|
41
|
+
list-style: none;
|
|
42
|
+
padding: 0;
|
|
43
|
+
margin: 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
li {
|
|
47
|
+
margin: 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
a {
|
|
51
|
+
display: block;
|
|
52
|
+
padding: 0.2rem 0;
|
|
53
|
+
font-size: var(--sl-text-sm, 0.875rem);
|
|
54
|
+
color: var(--sl-color-gray-4, #757575);
|
|
55
|
+
text-decoration: none;
|
|
56
|
+
transition: color 0.15s;
|
|
57
|
+
border-left: 2px solid transparent;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
a:hover {
|
|
61
|
+
color: var(--sl-color-text, #23262f);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* Depth indentation */
|
|
65
|
+
.depth-2 a { padding-left: 0.75rem; }
|
|
66
|
+
.depth-3 a { padding-left: 1.5rem; }
|
|
67
|
+
.depth-4 a { padding-left: 2.25rem; }
|
|
68
|
+
|
|
69
|
+
/* Active via [aria-current] set by the click handler */
|
|
70
|
+
li a[aria-current='true'] {
|
|
71
|
+
color: var(--sl-color-accent, #7c3aed);
|
|
72
|
+
border-left-color: var(--sl-color-accent, #7c3aed);
|
|
73
|
+
}
|
|
74
|
+
`;
|
|
75
|
+
|
|
76
|
+
entries: TocEntry[] = [];
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Click handler for TOC anchor links.
|
|
80
|
+
*
|
|
81
|
+
* Heading elements rendered via unsafeHTML live inside shadow roots that
|
|
82
|
+
* native fragment navigation and document.getElementById() cannot reach.
|
|
83
|
+
* Instead of relying on the browser's default hash navigation (which also
|
|
84
|
+
* fires popstate and re-renders the page component with no data), we:
|
|
85
|
+
* 1. Prevent the default navigation.
|
|
86
|
+
* 2. Walk the shadow tree to find the target element.
|
|
87
|
+
* 3. Scroll to it smoothly.
|
|
88
|
+
* 4. Update the URL hash via pushState (does not fire popstate).
|
|
89
|
+
*/
|
|
90
|
+
private _handleClick(e: MouseEvent, slug: string) {
|
|
91
|
+
e.preventDefault();
|
|
92
|
+
const target = this._findDeep(document, slug);
|
|
93
|
+
if (target) target.scrollIntoView({ behavior: 'smooth' });
|
|
94
|
+
history.pushState(null, '', `#${slug}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Recursively searches shadow roots for an element matching the CSS id selector. */
|
|
98
|
+
private _findDeep(root: Document | ShadowRoot | Element, id: string): Element | null {
|
|
99
|
+
const sel = `#${CSS.escape(id)}`;
|
|
100
|
+
const direct = root.querySelector(sel);
|
|
101
|
+
if (direct) return direct;
|
|
102
|
+
for (const el of root.querySelectorAll('*')) {
|
|
103
|
+
if (el.shadowRoot) {
|
|
104
|
+
const found = this._findDeep(el.shadowRoot, id);
|
|
105
|
+
if (found) return found;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
override render() {
|
|
112
|
+
if (!this.entries.length) return html``;
|
|
113
|
+
const currentHash = typeof location !== 'undefined' ? location.hash : '';
|
|
114
|
+
return html`
|
|
115
|
+
<nav aria-label="On this page">
|
|
116
|
+
<h2>On this page</h2>
|
|
117
|
+
<ul>
|
|
118
|
+
${this.entries.map(entry => html`
|
|
119
|
+
<li class="depth-${entry.depth}">
|
|
120
|
+
<a
|
|
121
|
+
href="#${entry.slug}"
|
|
122
|
+
aria-current="${currentHash === '#' + entry.slug ? 'true' : 'false'}"
|
|
123
|
+
@click=${(e: MouseEvent) => this._handleClick(e, entry.slug)}
|
|
124
|
+
>${entry.text}</a>
|
|
125
|
+
</li>
|
|
126
|
+
`)}
|
|
127
|
+
</ul>
|
|
128
|
+
</nav>
|
|
129
|
+
`;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export default StarlightToc;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formats a Date or date string into a human-readable form.
|
|
3
|
+
* Uses the `en-US` locale with month-name style (e.g. "January 15, 2026").
|
|
4
|
+
*/
|
|
5
|
+
export function formatDate(date: Date | string): string {
|
|
6
|
+
const d = date instanceof Date ? date : new Date(date as string);
|
|
7
|
+
return d.toLocaleDateString('en-US', {
|
|
8
|
+
year: 'numeric',
|
|
9
|
+
month: 'long',
|
|
10
|
+
day: 'numeric',
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Returns an ISO 8601 string (e.g. "2026-01-15") suitable for a `datetime` attribute.
|
|
16
|
+
*/
|
|
17
|
+
export function isoDate(date: Date | string): string {
|
|
18
|
+
const d = date instanceof Date ? date : new Date(date as string);
|
|
19
|
+
return d.toISOString().slice(0, 10);
|
|
20
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export interface TocEntry {
|
|
2
|
+
depth: number;
|
|
3
|
+
text: string;
|
|
4
|
+
slug: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Extracts h2–h4 headings from raw Markdown source.
|
|
9
|
+
* Returns an array of TocEntry objects suitable for rendering a table of contents.
|
|
10
|
+
*/
|
|
11
|
+
export function extractHeadings(rawMarkdown: string): TocEntry[] {
|
|
12
|
+
const headings: TocEntry[] = [];
|
|
13
|
+
const lines = rawMarkdown.split('\n');
|
|
14
|
+
|
|
15
|
+
for (const line of lines) {
|
|
16
|
+
const match = line.match(/^(#{2,4})\s+(.+)$/);
|
|
17
|
+
if (!match) continue;
|
|
18
|
+
|
|
19
|
+
const depth = match[1].length;
|
|
20
|
+
const text = match[2].trim().replace(/\s*\{[^}]*\}\s*$/, ''); // strip {#custom-id} syntax
|
|
21
|
+
const slug = text
|
|
22
|
+
.toLowerCase()
|
|
23
|
+
.replace(/[^\w\s-]/g, '')
|
|
24
|
+
.trim()
|
|
25
|
+
.replace(/\s+/g, '-')
|
|
26
|
+
.replace(/-+/g, '-');
|
|
27
|
+
|
|
28
|
+
headings.push({ depth, text, slug });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return headings;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Post-processes rendered HTML to inject `id` attributes on h2–h4 elements,
|
|
36
|
+
* matching the slugs produced by `extractHeadings()`.
|
|
37
|
+
* Enables anchor-link navigation from the table of contents.
|
|
38
|
+
*/
|
|
39
|
+
export function addHeadingIds(html: string): string {
|
|
40
|
+
const counters: Record<string, number> = {};
|
|
41
|
+
|
|
42
|
+
return html.replace(
|
|
43
|
+
/<(h[2-4])([^>]*)>([\s\S]*?)<\/h[2-4]>/gi,
|
|
44
|
+
(_match, tag: string, attrs: string, content: string) => {
|
|
45
|
+
// Strip HTML tags from content to get plain text
|
|
46
|
+
const text = content.replace(/<[^>]+>/g, '').trim().replace(/\s*\{[^}]*\}\s*$/, '');
|
|
47
|
+
let slug = text
|
|
48
|
+
.toLowerCase()
|
|
49
|
+
.replace(/[^\w\s-]/g, '')
|
|
50
|
+
.trim()
|
|
51
|
+
.replace(/\s+/g, '-')
|
|
52
|
+
.replace(/-+/g, '-');
|
|
53
|
+
|
|
54
|
+
// Deduplicate: if the same slug appears twice, append -2, -3, etc.
|
|
55
|
+
counters[slug] = (counters[slug] ?? 0) + 1;
|
|
56
|
+
if (counters[slug] > 1) {
|
|
57
|
+
slug = `${slug}-${counters[slug]}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Avoid double-injecting if an id already exists
|
|
61
|
+
if (/\bid=/.test(attrs)) {
|
|
62
|
+
return `<${tag}${attrs}>${content}</${tag}>`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return `<${tag}${attrs} id="${slug}">${content}</${tag}>`;
|
|
66
|
+
},
|
|
67
|
+
);
|
|
68
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML injected into every page's <head> via routeMeta.head.
|
|
3
|
+
*
|
|
4
|
+
* Order matters:
|
|
5
|
+
* 1. Stylesheet link — loaded asynchronously by the browser; must come before
|
|
6
|
+
* the FOUC-prevention script so --sl-* tokens are available immediately.
|
|
7
|
+
* 2. Inline script — synchronous, runs before first paint to set data-theme
|
|
8
|
+
* from localStorage, preventing a flash of the wrong theme on reload.
|
|
9
|
+
*/
|
|
10
|
+
export const starlightHead = [
|
|
11
|
+
'<link rel="stylesheet" href="/styles/starlight.css" />',
|
|
12
|
+
'<script>(function(){',
|
|
13
|
+
'var t=localStorage.getItem("sl-theme")||"light";',
|
|
14
|
+
'document.documentElement.setAttribute("data-theme",t);',
|
|
15
|
+
'})();</script>',
|
|
16
|
+
].join('');
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"experimentalDecorators": true,
|
|
10
|
+
"useDefineForClassFields": false
|
|
11
|
+
},
|
|
12
|
+
"include": ["**/*.ts", "**/*.tsx"],
|
|
13
|
+
"exclude": ["node_modules", "dist", ".nitro"]
|
|
14
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
import litroContentPlugin from '@beatzball/litro/vite';
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
plugins: [litroContentPlugin()],
|
|
6
|
+
base: '/_litro/',
|
|
7
|
+
resolve: {
|
|
8
|
+
conditions: ['source', 'browser', 'module', 'import', 'default'],
|
|
9
|
+
},
|
|
10
|
+
build: {
|
|
11
|
+
outDir: 'dist/client',
|
|
12
|
+
rollupOptions: {
|
|
13
|
+
input: 'app.ts',
|
|
14
|
+
output: {
|
|
15
|
+
entryFileNames: '[name].js',
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
});
|
package/src/scaffold.test.ts
CHANGED
|
@@ -225,4 +225,152 @@ describe('scaffold', () => {
|
|
|
225
225
|
.rejects.toThrow();
|
|
226
226
|
});
|
|
227
227
|
});
|
|
228
|
+
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
// starlight recipe
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
|
|
233
|
+
it('starlight recipe writes litro.recipe.json with ssg mode', async () => {
|
|
234
|
+
await withTmpDir(async (dir) => {
|
|
235
|
+
const targetDir = join(dir, 'my-docs');
|
|
236
|
+
await scaffold('starlight', { projectName: 'my-docs', mode: 'ssg' }, targetDir);
|
|
237
|
+
|
|
238
|
+
const { existsSync } = await import('node:fs');
|
|
239
|
+
expect(existsSync(join(targetDir, 'litro.recipe.json'))).toBe(true);
|
|
240
|
+
|
|
241
|
+
const manifest = JSON.parse(await readFile(join(targetDir, 'litro.recipe.json'), 'utf-8')) as Record<string, unknown>;
|
|
242
|
+
expect(manifest.recipe).toBe('starlight');
|
|
243
|
+
expect(manifest.mode).toBe('ssg');
|
|
244
|
+
expect(manifest.contentDir).toBe('content');
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('starlight recipe writes all expected files', async () => {
|
|
249
|
+
await withTmpDir(async (dir) => {
|
|
250
|
+
const targetDir = join(dir, 'my-docs');
|
|
251
|
+
await scaffold('starlight', { projectName: 'my-docs', mode: 'ssg' }, targetDir);
|
|
252
|
+
|
|
253
|
+
const { existsSync } = await import('node:fs');
|
|
254
|
+
// Core config
|
|
255
|
+
expect(existsSync(join(targetDir, 'package.json'))).toBe(true);
|
|
256
|
+
expect(existsSync(join(targetDir, 'tsconfig.json'))).toBe(true);
|
|
257
|
+
expect(existsSync(join(targetDir, 'nitro.config.ts'))).toBe(true);
|
|
258
|
+
expect(existsSync(join(targetDir, 'vite.config.ts'))).toBe(true);
|
|
259
|
+
expect(existsSync(join(targetDir, 'app.ts'))).toBe(true);
|
|
260
|
+
expect(existsSync(join(targetDir, '.gitignore'))).toBe(true);
|
|
261
|
+
expect(existsSync(join(targetDir, 'litro.recipe.json'))).toBe(true);
|
|
262
|
+
// Server
|
|
263
|
+
expect(existsSync(join(targetDir, 'server/starlight.config.js'))).toBe(true);
|
|
264
|
+
expect(existsSync(join(targetDir, 'server/routes/[...].ts'))).toBe(true);
|
|
265
|
+
expect(existsSync(join(targetDir, 'server/middleware/vite-dev.ts'))).toBe(true);
|
|
266
|
+
expect(existsSync(join(targetDir, 'server/stubs/page-manifest.ts'))).toBe(true);
|
|
267
|
+
// Pages
|
|
268
|
+
expect(existsSync(join(targetDir, 'pages/index.ts'))).toBe(true);
|
|
269
|
+
expect(existsSync(join(targetDir, 'pages/docs/[slug].ts'))).toBe(true);
|
|
270
|
+
expect(existsSync(join(targetDir, 'pages/blog/index.ts'))).toBe(true);
|
|
271
|
+
expect(existsSync(join(targetDir, 'pages/blog/[slug].ts'))).toBe(true);
|
|
272
|
+
expect(existsSync(join(targetDir, 'pages/blog/tags/[tag].ts'))).toBe(true);
|
|
273
|
+
// Utilities
|
|
274
|
+
expect(existsSync(join(targetDir, 'src/extract-headings.ts'))).toBe(true);
|
|
275
|
+
expect(existsSync(join(targetDir, 'src/date-utils.ts'))).toBe(true);
|
|
276
|
+
expect(existsSync(join(targetDir, 'src/route-meta.ts'))).toBe(true);
|
|
277
|
+
// Components
|
|
278
|
+
expect(existsSync(join(targetDir, 'src/components/starlight-page.ts'))).toBe(true);
|
|
279
|
+
expect(existsSync(join(targetDir, 'src/components/starlight-header.ts'))).toBe(true);
|
|
280
|
+
expect(existsSync(join(targetDir, 'src/components/starlight-sidebar.ts'))).toBe(true);
|
|
281
|
+
expect(existsSync(join(targetDir, 'src/components/starlight-toc.ts'))).toBe(true);
|
|
282
|
+
expect(existsSync(join(targetDir, 'src/components/sl-card.ts'))).toBe(true);
|
|
283
|
+
expect(existsSync(join(targetDir, 'src/components/sl-card-grid.ts'))).toBe(true);
|
|
284
|
+
expect(existsSync(join(targetDir, 'src/components/sl-badge.ts'))).toBe(true);
|
|
285
|
+
expect(existsSync(join(targetDir, 'src/components/sl-aside.ts'))).toBe(true);
|
|
286
|
+
expect(existsSync(join(targetDir, 'src/components/sl-tabs.ts'))).toBe(true);
|
|
287
|
+
expect(existsSync(join(targetDir, 'src/components/sl-tab-item.ts'))).toBe(true);
|
|
288
|
+
// Content
|
|
289
|
+
expect(existsSync(join(targetDir, 'content/docs/.11tydata.json'))).toBe(true);
|
|
290
|
+
expect(existsSync(join(targetDir, 'content/docs/getting-started.md'))).toBe(true);
|
|
291
|
+
expect(existsSync(join(targetDir, 'content/blog/.11tydata.json'))).toBe(true);
|
|
292
|
+
expect(existsSync(join(targetDir, 'content/blog/welcome.md'))).toBe(true);
|
|
293
|
+
// Public
|
|
294
|
+
expect(existsSync(join(targetDir, 'public/styles/starlight.css'))).toBe(true);
|
|
295
|
+
// Data
|
|
296
|
+
expect(existsSync(join(targetDir, '_data/metadata.js'))).toBe(true);
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('starlight recipe interpolates {{projectName}} in package.json', async () => {
|
|
301
|
+
await withTmpDir(async (dir) => {
|
|
302
|
+
const targetDir = join(dir, 'awesome-docs');
|
|
303
|
+
await scaffold('starlight', { projectName: 'awesome-docs', mode: 'ssg' }, targetDir);
|
|
304
|
+
|
|
305
|
+
const pkg = JSON.parse(await readFile(join(targetDir, 'package.json'), 'utf-8')) as Record<string, unknown>;
|
|
306
|
+
expect(pkg.name).toBe('awesome-docs');
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('starlight recipe interpolates {{projectName}} in server/starlight.config.js', async () => {
|
|
311
|
+
await withTmpDir(async (dir) => {
|
|
312
|
+
const targetDir = join(dir, 'my-docs-site');
|
|
313
|
+
await scaffold('starlight', { projectName: 'my-docs-site', mode: 'ssg' }, targetDir);
|
|
314
|
+
|
|
315
|
+
const config = await readFile(join(targetDir, 'server/starlight.config.js'), 'utf-8');
|
|
316
|
+
expect(config).toContain('my-docs-site');
|
|
317
|
+
expect(config).not.toContain('{{projectName}}');
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('starlight recipe has no un-interpolated {{ }} in any output file', async () => {
|
|
322
|
+
await withTmpDir(async (dir) => {
|
|
323
|
+
const targetDir = join(dir, 'my-docs');
|
|
324
|
+
await scaffold('starlight', { projectName: 'my-docs', mode: 'ssg' }, targetDir);
|
|
325
|
+
|
|
326
|
+
const binaryExts = new Set(['.png', '.jpg', '.jpeg', '.gif', '.ico', '.webp', '.svg',
|
|
327
|
+
'.woff', '.woff2', '.ttf', '.eot', '.otf', '.pdf', '.zip', '.gz', '.tar', '.gitkeep']);
|
|
328
|
+
|
|
329
|
+
async function collectFiles(d: string): Promise<string[]> {
|
|
330
|
+
const { readdir: rd } = await import('node:fs/promises');
|
|
331
|
+
const entries = await rd(d, { withFileTypes: true });
|
|
332
|
+
const results: string[] = [];
|
|
333
|
+
for (const e of entries) {
|
|
334
|
+
const p = join(d, e.name);
|
|
335
|
+
if (e.isDirectory()) results.push(...(await collectFiles(p)));
|
|
336
|
+
else results.push(p);
|
|
337
|
+
}
|
|
338
|
+
return results;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const files = await collectFiles(targetDir);
|
|
342
|
+
for (const file of files) {
|
|
343
|
+
const extPart = file.includes('.') ? `.${file.split('.').pop()}` : '';
|
|
344
|
+
if (binaryExts.has(extPart.toLowerCase())) continue;
|
|
345
|
+
const content = await readFile(file, 'utf-8');
|
|
346
|
+
const matches = content.match(/\{\{[^}]+\}\}/g);
|
|
347
|
+
if (matches) {
|
|
348
|
+
throw new Error(`Un-interpolated placeholder in ${file}: ${matches.join(', ')}`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it('starlight recipe page files use plain <a> tags, not <litro-link>', async () => {
|
|
355
|
+
// SSG pages must use <a> for full-page-reload navigation so each pre-rendered
|
|
356
|
+
// page loads its own __litro_data__. <litro-link> triggers SPA navigation which
|
|
357
|
+
// leaves serverData=null on the navigated page (no __litro_data__ in DOM).
|
|
358
|
+
await withTmpDir(async (dir) => {
|
|
359
|
+
const targetDir = join(dir, 'my-docs');
|
|
360
|
+
await scaffold('starlight', { projectName: 'my-docs', mode: 'ssg' }, targetDir);
|
|
361
|
+
|
|
362
|
+
const pageFiles = [
|
|
363
|
+
join(targetDir, 'pages/index.ts'),
|
|
364
|
+
join(targetDir, 'pages/docs/[slug].ts'),
|
|
365
|
+
join(targetDir, 'pages/blog/index.ts'),
|
|
366
|
+
join(targetDir, 'pages/blog/[slug].ts'),
|
|
367
|
+
join(targetDir, 'pages/blog/tags/[tag].ts'),
|
|
368
|
+
];
|
|
369
|
+
|
|
370
|
+
for (const file of pageFiles) {
|
|
371
|
+
const content = await readFile(file, 'utf-8');
|
|
372
|
+
expect(content, `${file} should not contain <litro-link>`).not.toContain('litro-link');
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
});
|
|
228
376
|
});
|