starlight-versions 0.1.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.
@@ -0,0 +1,88 @@
1
+ import path from 'node:path'
2
+ import url from 'node:url'
3
+
4
+ import type { Props } from '@astrojs/starlight/props'
5
+ import type { docsSchema } from '@astrojs/starlight/schema'
6
+ import type { StarlightPlugin } from '@astrojs/starlight/types'
7
+ import type { z } from 'astro/zod'
8
+ import yaml from 'yaml'
9
+
10
+ import { isAbsoluteLink, slugifyPath } from './path'
11
+
12
+ export function parseFrontmatter(frontmatter: string) {
13
+ return yaml.parse(frontmatter) as StarlightFrontmatter
14
+ }
15
+
16
+ export function getFrontmatterNodeValue(frontmatter: StarlightFrontmatter) {
17
+ return yaml.stringify(frontmatter).trim()
18
+ }
19
+
20
+ export function getDocSlug(docsDir: URL, doc: URL) {
21
+ const docPath = path.relative(url.fileURLToPath(docsDir), url.fileURLToPath(doc))
22
+ const slug = slugifyPath(docPath)
23
+
24
+ return slug === 'index' ? '/' : slug.replace(/\/index$/, '')
25
+ }
26
+
27
+ export function getDocLocale(slug: string, starlightConfig: StarlightUserConfig): string | undefined {
28
+ const locales = Object.keys(starlightConfig.locales ?? {})
29
+
30
+ if (locales.length === 0) return undefined
31
+
32
+ const localeSegment = slug.split('/')[0]
33
+
34
+ if (!localeSegment) return undefined
35
+
36
+ return locales.find((locale) => locale === localeSegment)
37
+ }
38
+
39
+ export function addPrefixToSidebarConfig(
40
+ prefix: string,
41
+ sidebar: NonNullable<StarlightSidebarUserConfig>,
42
+ ): NonNullable<StarlightSidebarUserConfig> {
43
+ return sidebar.map((item) => {
44
+ if ('items' in item) {
45
+ return {
46
+ ...item,
47
+ items: addPrefixToSidebarConfig(prefix, item.items),
48
+ }
49
+ } else if ('autogenerate' in item) {
50
+ return {
51
+ ...item,
52
+ autogenerate: {
53
+ ...item.autogenerate,
54
+ directory: path.posix.join(prefix, item.autogenerate.directory),
55
+ },
56
+ }
57
+ } else if (isAbsoluteLink(item.link)) {
58
+ return item
59
+ }
60
+
61
+ const segments = item.link.split('/')
62
+ segments.splice(1, 0, prefix)
63
+
64
+ return {
65
+ ...item,
66
+ link: segments.join('/'),
67
+ }
68
+ })
69
+ }
70
+
71
+ type StarlightFrontmatter = z.input<ReturnType<ReturnType<typeof docsSchema>>> & {
72
+ slug?: string
73
+ hero?: StarlightFrontmatteHero
74
+ }
75
+
76
+ // Just to simplify type checking when updating the frontmatter,
77
+ interface StarlightFrontmatteHero {
78
+ image?: {
79
+ file?: string
80
+ dark?: string
81
+ light?: string
82
+ }
83
+ }
84
+
85
+ export type StarlightUserConfig = Parameters<StarlightPlugin['hooks']['setup']>['0']['config']
86
+ export type StarlightSidebarUserConfig = StarlightUserConfig['sidebar']
87
+
88
+ export type StarlightSidebar = Props['sidebar']
@@ -0,0 +1,355 @@
1
+ import type { StarlightConfig } from '@astrojs/starlight/types'
2
+ import type { AstroIntegrationLogger } from 'astro'
3
+ import { z } from 'astro/zod'
4
+
5
+ import type { StarlightVersionsConfig } from '..'
6
+ import type { DocsVersionsConfig } from '../schema'
7
+
8
+ import { copyDirectory, copyFile, ensureDirectory, listDirectory, readJSONFile, writeJSONFile } from './fs'
9
+ import { transformMarkdown } from './markdown'
10
+ import { ensureTrailingSlash, getExtension, stripExtension, stripLeadingSlash, stripTrailingSlash } from './path'
11
+ import { throwPluginError } from './plugin'
12
+ import {
13
+ getDocSlug,
14
+ type StarlightSidebarUserConfig,
15
+ type StarlightUserConfig,
16
+ addPrefixToSidebarConfig,
17
+ type StarlightSidebar,
18
+ getDocLocale,
19
+ } from './starlight'
20
+
21
+ const currentVersionSidebarGroupLabel = Symbol('StarlightVersionsCurrentVersionSidebarGroupLabel')
22
+
23
+ export const VersionBaseSchema = z.object({
24
+ /**
25
+ * The version redirect strategy used when navigating to this version:
26
+ *
27
+ * - `same-page`: Redirect to the same page when navigating to this version.
28
+ * - `root`: Redirect to the root page of the documentation when to this version.
29
+ *
30
+ * @default 'same-page'
31
+ */
32
+ redirect: z.union([z.literal('root'), z.literal('same-page')]).default('same-page'),
33
+ })
34
+
35
+ export const VersionSchema = z
36
+ .object({
37
+ /**
38
+ * An optional label used in the UI when displaying the version.
39
+ * If not provided, the version slug is used.
40
+ *
41
+ * @example 'v2.0'
42
+ * @example 'v3.1.2'
43
+ */
44
+ label: z.string().optional(),
45
+ /**
46
+ * The version slug used in URLs to identify the version and its content.
47
+ *
48
+ * @example '2.0'
49
+ * @example '3-1-2'
50
+ */
51
+ slug: z.string().refine((value) => stripLeadingSlash(stripTrailingSlash(value))),
52
+ })
53
+ .merge(VersionBaseSchema)
54
+
55
+ export async function ensureNewVersion(
56
+ config: StarlightVersionsConfig,
57
+ starlightConfig: StarlightUserConfig,
58
+ srcDir: URL,
59
+ logger: AstroIntegrationLogger,
60
+ ) {
61
+ const docsDir = new URL('content/docs/', srcDir)
62
+ const newVersion = await checkForNewVersion(config, docsDir)
63
+ const locales = Object.keys(starlightConfig.locales ?? {})
64
+
65
+ if (!newVersion) return
66
+
67
+ const assets: VersionAsset[] = []
68
+
69
+ await copyDirectory(docsDir, new URL(ensureTrailingSlash(newVersion.slug), docsDir), async (entry) => {
70
+ if (entry.type === 'directory') {
71
+ if (!entry.isRoot) {
72
+ const segments = entry.source.pathname.split('/')
73
+ const lastSegment = segments.at(-2)
74
+ const secondLastSegment = segments.at(-3)
75
+
76
+ if (secondLastSegment && lastSegment === newVersion.slug && locales.includes(secondLastSegment)) {
77
+ // Skip version directories in a locale directory.
78
+ return true
79
+ }
80
+
81
+ // Do not skip other non-root directories.
82
+ return false
83
+ }
84
+
85
+ // Skip root version directories.
86
+ if (entry.name in config.versionsBySlug) return true
87
+
88
+ const localeDir = locales.find((locale) => locale === entry.name)
89
+
90
+ // Copy root directories not matching any locale.
91
+ if (!localeDir) return false
92
+
93
+ // Otherwise, swap the locale and version directories.
94
+ return new URL(`../../${localeDir}/${newVersion.slug}/`, entry.dest)
95
+ }
96
+
97
+ const slug = getDocSlug(docsDir, entry.url)
98
+
99
+ const md = await transformMarkdown(entry.content, {
100
+ assets: [],
101
+ locale: getDocLocale(slug, starlightConfig),
102
+ slug,
103
+ url: entry.url,
104
+ version: newVersion,
105
+ })
106
+
107
+ assets.push(...(md.assets ?? []))
108
+
109
+ return md.content
110
+ })
111
+
112
+ for (const asset of assets) {
113
+ await copyFile(asset.source, asset.dest)
114
+ }
115
+
116
+ await makeVersionConfig(newVersion, starlightConfig, srcDir)
117
+
118
+ logger.info(`Created new version '${newVersion.slug}'.`)
119
+ }
120
+
121
+ export async function getVersionedSidebar(
122
+ config: StarlightVersionsConfig,
123
+ currentSidebar: StarlightSidebarUserConfig,
124
+ srcDir: URL,
125
+ ): Promise<NonNullable<StarlightSidebarUserConfig>> {
126
+ const sidebar: StarlightSidebarUserConfig = [
127
+ {
128
+ label: currentVersionSidebarGroupLabel.toString(),
129
+ items: currentSidebar ?? [],
130
+ },
131
+ ]
132
+
133
+ for (const version of config.versions) {
134
+ const versionSidebar = await getSidebarVersionGroup(version, srcDir)
135
+ sidebar.push(versionSidebar)
136
+ }
137
+
138
+ return sidebar
139
+ }
140
+
141
+ // A version is considered as the current version if it's undefined.
142
+ export function getVersionSidebar(version: Version | undefined, sidebar: StarlightSidebar): StarlightSidebar {
143
+ const sidebarVersionGroup = sidebar.find(
144
+ (item) => item.label === (version?.slug ?? currentVersionSidebarGroupLabel.toString()),
145
+ )
146
+
147
+ if (!sidebarVersionGroup || !('entries' in sidebarVersionGroup)) {
148
+ throwPluginError(
149
+ `Failed to find a sidebar group for the ${version ? `version '${version.slug}'` : 'current version'}.`,
150
+ )
151
+ }
152
+
153
+ return sidebarVersionGroup.entries
154
+ }
155
+
156
+ // An undefined version is valid and represents the current version.
157
+ // https://github.com/withastro/starlight/blob/64288fb0051310f7148afd13f65c578664f04eb2/packages/starlight/utils/localizedUrl.ts
158
+ export function getVersionURL(
159
+ config: StarlightVersionsConfig,
160
+ starlightConfig: StarlightConfig,
161
+ url: URL,
162
+ version: Version | undefined,
163
+ ): URL {
164
+ const versionURL = new URL(url)
165
+ const versionSlug = version?.slug ?? ''
166
+ const versionRedirect = version?.redirect ?? config.current.redirect
167
+
168
+ const base = stripTrailingSlash(import.meta.env.BASE_URL)
169
+ const hasBase = versionURL.pathname.startsWith(base)
170
+
171
+ if (hasBase) {
172
+ versionURL.pathname = versionURL.pathname.replace(base, '')
173
+ }
174
+
175
+ let baseSegment: string | undefined
176
+ let localeSegment: string | undefined
177
+
178
+ const isHTML = getExtension(versionURL.pathname) === '.html'
179
+ const [, firstSegment, secondSegment] = versionURL.pathname.split('/')
180
+
181
+ if (starlightConfig.isMultilingual || starlightConfig.locales) {
182
+ const versionOrLocale = firstSegment?.replace('.html', '')
183
+ const isRootLocale = versionOrLocale && !Object.keys(starlightConfig.locales).includes(versionOrLocale)
184
+ baseSegment = isRootLocale ? firstSegment : secondSegment
185
+
186
+ if (!isRootLocale) {
187
+ localeSegment = versionOrLocale
188
+ versionURL.pathname = versionURL.pathname.replace(`/${firstSegment}`, '')
189
+ }
190
+ } else {
191
+ baseSegment = firstSegment
192
+ }
193
+
194
+ const isRootHTML = baseSegment && getExtension(baseSegment) === '.html'
195
+ const baseSlug = baseSegment && isRootHTML ? stripExtension(baseSegment) : baseSegment
196
+
197
+ if (baseSlug && baseSlug in config.versionsBySlug) {
198
+ if (versionSlug) {
199
+ versionURL.pathname =
200
+ versionRedirect === 'same-page'
201
+ ? versionURL.pathname.replace(baseSlug, versionSlug)
202
+ : `${versionSlug}${isHTML ? '.html' : '/'}`
203
+ } else if (isRootHTML) {
204
+ versionURL.pathname = '/index.html'
205
+ } else {
206
+ versionURL.pathname =
207
+ versionRedirect === 'same-page' ? versionURL.pathname.replace(`/${baseSlug}`, '') : isHTML ? '/index.html' : '/'
208
+ }
209
+ } else if (versionSlug) {
210
+ versionURL.pathname =
211
+ baseSegment === 'index.html'
212
+ ? `/${versionSlug}.html`
213
+ : versionRedirect === 'same-page'
214
+ ? `/${versionSlug}${versionURL.pathname}`
215
+ : isHTML
216
+ ? `${versionSlug}.html`
217
+ : `/${versionSlug}/`
218
+ } else if (versionRedirect === 'root' && !isRootHTML) {
219
+ versionURL.pathname = isHTML ? '/index.html' : versionSlug
220
+ }
221
+
222
+ if (localeSegment) {
223
+ versionURL.pathname = isHTML
224
+ ? versionURL.pathname === '/index.html'
225
+ ? `/${localeSegment}.html`
226
+ : `/${localeSegment}${versionURL.pathname.replace(/\/$/, '.html')}`
227
+ : `/${localeSegment}${versionURL.pathname}`
228
+ }
229
+
230
+ if (hasBase) {
231
+ versionURL.pathname = base + versionURL.pathname
232
+ }
233
+
234
+ return versionURL
235
+ }
236
+
237
+ // An undefined version is valid and represents the current version.
238
+ export function getVersionFromSlug(
239
+ config: StarlightVersionsConfig,
240
+ starlightConfig: StarlightConfig,
241
+ slug: string,
242
+ ): Version | undefined {
243
+ const segments = slug.split('/')
244
+
245
+ const versionOrLocaleSegment = segments[0]
246
+
247
+ if (!versionOrLocaleSegment) return undefined
248
+
249
+ const version = config.versions.find((version) => version.slug === versionOrLocaleSegment)
250
+
251
+ if (version) return version
252
+
253
+ const locales = Object.keys(starlightConfig.locales ?? {})
254
+
255
+ if (!locales.includes(versionOrLocaleSegment)) return undefined
256
+
257
+ const versionSegment = segments[1]
258
+
259
+ return config.versions.find((version) => version.slug === versionSegment)
260
+ }
261
+
262
+ export function getVersionIdentifier(version: Version | undefined): string {
263
+ return version?.slug ?? 'current'
264
+ }
265
+
266
+ // An undefined version is valid and represents the current version.
267
+ export function getVersionFromPaginationLink(
268
+ config: StarlightVersionsConfig,
269
+ link: string,
270
+ locale: string | undefined,
271
+ ): Version | undefined {
272
+ const [, ...segments] = link.split('/')
273
+
274
+ if (import.meta.env.BASE_URL !== '/') {
275
+ // Remove the base segment if configured.
276
+ segments.splice(0, 1)
277
+ }
278
+
279
+ if (locale) {
280
+ // Remove the locale segment if the current locale is not a root locale.
281
+ segments.splice(0, 1)
282
+ }
283
+
284
+ const versionSegment = segments[0]
285
+
286
+ if (!versionSegment) return undefined
287
+
288
+ return config.versions.find((version) => version.slug === versionSegment)
289
+ }
290
+
291
+ async function getSidebarVersionGroup(version: Version, srcDir: URL) {
292
+ const versionConfig = await getVersionConfig(version, srcDir)
293
+
294
+ if (!versionConfig.sidebar) {
295
+ return {
296
+ label: version.slug,
297
+ autogenerate: { directory: version.slug },
298
+ }
299
+ }
300
+
301
+ return {
302
+ label: version.slug,
303
+ items: addPrefixToSidebarConfig(version.slug, versionConfig.sidebar),
304
+ }
305
+ }
306
+
307
+ async function getVersionConfig(version: Version, srcDir: URL) {
308
+ try {
309
+ return await readJSONFile<DocsVersionsConfig>(getVersionConfigURL(version, srcDir))
310
+ } catch (error) {
311
+ throw new Error(`Failed to read the version '${version.slug}' configuration file.`, { cause: error })
312
+ }
313
+ }
314
+
315
+ async function checkForNewVersion(config: StarlightVersionsConfig, docsDir: URL): Promise<Version | undefined> {
316
+ let newVersion: Version | undefined
317
+
318
+ const docsDirEntries = await listDirectory(docsDir)
319
+ const docsDirDirectories = new Set(docsDirEntries.filter((entry) => entry.isDirectory()).map((dirent) => dirent.name))
320
+
321
+ for (const version of config.versions) {
322
+ if (!docsDirDirectories.has(version.slug)) {
323
+ if (newVersion) {
324
+ throw new Error('Only one new version can be configured at a time.')
325
+ }
326
+ newVersion = version
327
+ }
328
+ }
329
+
330
+ return newVersion
331
+ }
332
+
333
+ async function makeVersionConfig(version: Version, starlightConfig: StarlightUserConfig, srcDir: URL) {
334
+ const versionsDir = getVersionContentCollectionURL(srcDir)
335
+
336
+ await ensureDirectory(versionsDir)
337
+ await writeJSONFile(getVersionConfigURL(version, srcDir), {
338
+ sidebar: starlightConfig.sidebar,
339
+ } satisfies DocsVersionsConfig)
340
+ }
341
+
342
+ function getVersionContentCollectionURL(srcDir: URL) {
343
+ return new URL('content/versions/', srcDir)
344
+ }
345
+
346
+ function getVersionConfigURL(version: Version, srcDir: URL) {
347
+ return new URL(`${version.slug}.json`, getVersionContentCollectionURL(srcDir))
348
+ }
349
+
350
+ export type Version = z.output<typeof VersionSchema>
351
+
352
+ export interface VersionAsset {
353
+ source: URL
354
+ dest: URL
355
+ }
package/libs/vite.ts ADDED
@@ -0,0 +1,21 @@
1
+ import type { ViteUserConfig } from 'astro'
2
+
3
+ import type { StarlightVersionsConfig } from '..'
4
+
5
+ export function vitePluginStarlightVersions(config: StarlightVersionsConfig): VitePlugin {
6
+ const moduleId = 'virtual:starlight-versions-config'
7
+ const resolvedModuleId = `\0${moduleId}`
8
+ const moduleContent = `export default ${JSON.stringify(config)}`
9
+
10
+ return {
11
+ name: 'vite-plugin-starlight-versions',
12
+ load(id) {
13
+ return id === resolvedModuleId ? moduleContent : undefined
14
+ },
15
+ resolveId(id) {
16
+ return id === moduleId ? resolvedModuleId : undefined
17
+ },
18
+ }
19
+ }
20
+
21
+ type VitePlugin = NonNullable<ViteUserConfig['plugins']>[number]
@@ -0,0 +1,9 @@
1
+ ---
2
+ import Default from '@astrojs/starlight/components/Banner.astro'
3
+ import type { Props } from '@astrojs/starlight/props'
4
+
5
+ import VersionBanner from '../components/VersionBanner.astro'
6
+ ---
7
+
8
+ <VersionBanner {...Astro.props} />
9
+ <Default {...Astro.props}><slot /></Default>
@@ -0,0 +1,9 @@
1
+ ---
2
+ import Default from '@astrojs/starlight/components/PageTitle.astro'
3
+ import type { Props } from '@astrojs/starlight/props'
4
+
5
+ import VersionNotice from '../components/VersionNotice.astro'
6
+ ---
7
+
8
+ <Default {...Astro.props}><slot /></Default>
9
+ <VersionNotice {...Astro.props} />
@@ -0,0 +1,7 @@
1
+ ---
2
+ import type { Props } from '@astrojs/starlight/props'
3
+
4
+ import VersionPagination from '../components/VersionPagination.astro'
5
+ ---
6
+
7
+ <VersionPagination {...Astro.props} />
@@ -0,0 +1,7 @@
1
+ ---
2
+ import type { Props } from '@astrojs/starlight/props'
3
+
4
+ import VersionSearch from '../components/VersionSearch.astro'
5
+ ---
6
+
7
+ <VersionSearch {...Astro.props} />
@@ -0,0 +1,7 @@
1
+ ---
2
+ import type { Props } from '@astrojs/starlight/props'
3
+
4
+ import VersionSidebar from '../components/VersionSidebar.astro'
5
+ ---
6
+
7
+ <VersionSidebar {...Astro.props} />
@@ -0,0 +1,9 @@
1
+ ---
2
+ import Default from '@astrojs/starlight/components/ThemeSelect.astro'
3
+ import type { Props } from '@astrojs/starlight/props'
4
+
5
+ import VersionSelect from '../components/VersionSelect.astro'
6
+ ---
7
+
8
+ <VersionSelect {...Astro.props} />
9
+ <Default {...Astro.props}><slot /></Default>
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "starlight-versions",
3
+ "version": "0.1.0",
4
+ "license": "MIT",
5
+ "description": "Starlight plugin to version documentation pages.",
6
+ "author": "HiDeoo <github@hideoo.dev> (https://hideoo.dev)",
7
+ "type": "module",
8
+ "exports": {
9
+ ".": "./index.ts",
10
+ "./schema": "./schema.ts",
11
+ "./components/VersionBanner.astro": "./components/VersionBanner.astro",
12
+ "./components/VersionNotice.astro": "./components/VersionNotice.astro",
13
+ "./components/VersionPagination.astro": "./components/VersionPagination.astro",
14
+ "./components/VersionSearch.astro": "./components/VersionSearch.astro",
15
+ "./components/VersionSelect.astro": "./components/VersionSelect.astro",
16
+ "./components/VersionSidebar.astro": "./components/VersionSidebar.astro",
17
+ "./overrides/Banner.astro": "./overrides/Banner.astro",
18
+ "./overrides/PageTitle.astro": "./overrides/PageTitle.astro",
19
+ "./overrides/Pagination.astro": "./overrides/Pagination.astro",
20
+ "./overrides/Search.astro": "./overrides/Search.astro",
21
+ "./overrides/Sidebar.astro": "./overrides/Sidebar.astro",
22
+ "./overrides/ThemeSelect.astro": "./overrides/ThemeSelect.astro",
23
+ "./package.json": "./package.json"
24
+ },
25
+ "dependencies": {
26
+ "@pagefind/default-ui": "^1.0.3",
27
+ "github-slugger": "^2.0.0",
28
+ "mdast-util-mdx-jsx": "^3.1.2",
29
+ "mdast-util-mdxjs-esm": "^2.0.1",
30
+ "remark": "^15.0.1",
31
+ "remark-frontmatter": "^5.0.0",
32
+ "remark-mdx": "^3.0.1",
33
+ "unist-util-visit": "^5.0.0",
34
+ "vfile": "^6.0.1",
35
+ "yaml": "^2.4.2"
36
+ },
37
+ "devDependencies": {
38
+ "@types/mdast": "^4.0.4",
39
+ "fast-glob": "^3.3.2",
40
+ "vitest": "^1.6.0"
41
+ },
42
+ "peerDependencies": {
43
+ "@astrojs/starlight": ">=0.23.0"
44
+ },
45
+ "engines": {
46
+ "node": ">=18"
47
+ },
48
+ "publishConfig": {
49
+ "access": "public"
50
+ },
51
+ "sideEffects": false,
52
+ "keywords": [
53
+ "starlight",
54
+ "plugin",
55
+ "versions",
56
+ "versioning",
57
+ "documentation",
58
+ "astro"
59
+ ],
60
+ "homepage": "https://github.com/HiDeoo/starlight-versions",
61
+ "repository": {
62
+ "type": "git",
63
+ "url": "https://github.com/HiDeoo/starlight-versions.git",
64
+ "directory": "packages/starlight-versions"
65
+ },
66
+ "bugs": "https://github.com/HiDeoo/starlight-versions/issues",
67
+ "scripts": {
68
+ "test": "vitest",
69
+ "lint": "prettier -c --cache . && eslint . --cache --max-warnings=0"
70
+ }
71
+ }
package/schema.ts ADDED
@@ -0,0 +1,13 @@
1
+ import { z } from 'astro/zod'
2
+
3
+ import type { StarlightSidebarUserConfig } from './libs/starlight'
4
+
5
+ export function docsVersionsSchema() {
6
+ return z.object({
7
+ sidebar: z.any().optional(),
8
+ })
9
+ }
10
+
11
+ export interface DocsVersionsConfig {
12
+ sidebar?: StarlightSidebarUserConfig
13
+ }
package/virtual.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ declare module 'virtual:starlight-versions-config' {
2
+ const StarlightVersionsConfig: import('./index').StarlightVersionsConfig
3
+
4
+ export default StarlightVersionsConfig
5
+ }
6
+
7
+ declare module 'virtual:starlight/user-config' {
8
+ const StarlightConfig: import('@astrojs/starlight/types').StarlightConfig
9
+
10
+ export default StarlightConfig
11
+ }
12
+
13
+ declare module 'virtual:starlight/project-context' {
14
+ const ProjectContext: {
15
+ root: string
16
+ srcDir: string
17
+ trailingSlash: import('astro').AstroConfig['trailingSlash']
18
+ build: {
19
+ format: import('astro').AstroConfig['build']['format']
20
+ }
21
+ }
22
+ export default ProjectContext
23
+ }