@chassis-ui/docs 0.5.0 → 0.6.0-next.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,211 @@
1
+ import { z } from 'astro/zod'
2
+
3
+ // Sites extend the schemas below. They must use this `z`, so that the site and the package
4
+ // share one copy of Zod.
5
+ export { z }
6
+
7
+ export const zVersionMajorMinor = z.string().regex(/^\d+\.\d+$/)
8
+
9
+ // https://ihateregex.io/expr/semver/
10
+ const unboundSemverRegex =
11
+ /(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?/
12
+
13
+ export const zVersionSemver = z.string().regex(new RegExp(`^${unboundSemverRegex.source}$`))
14
+ export const zPrefixedVersionSemver = z
15
+ .string()
16
+ .regex(new RegExp(`^v${unboundSemverRegex.source}$`))
17
+
18
+ export const zLanguageCode = z.string().regex(/^[a-z]{2}(?:-[a-zA-Z]{2})?$/)
19
+
20
+ const zHeadingRange = z.object({
21
+ min: z.number().int().min(1).max(6),
22
+ max: z.number().int().min(1).max(6)
23
+ })
24
+
25
+ /**
26
+ * The keys of `config.yml` that the package reads.
27
+ *
28
+ * The schema is strict: a key that is not in it fails the build. A site with keys of its own
29
+ * extends the schema and passes the result to the integration.
30
+ *
31
+ * @example
32
+ * ```ts
33
+ * import { configSchema, z } from '@chassis-ui/docs/schema'
34
+ *
35
+ * export const siteConfigSchema = configSchema.extend({
36
+ * blog: z.object({ pageSize: z.number() })
37
+ * })
38
+ * ```
39
+ */
40
+ export const configSchema = z.strictObject({
41
+ /** Google Analytics is loaded in production builds when `googleId` is set. */
42
+ analytics: z.strictObject({ googleId: z.string().optional() }).default({}),
43
+ /** Heading levels that get an anchor link. */
44
+ anchors: zHeadingRange.default({ min: 2, max: 5 }),
45
+ authors: z.string(),
46
+ /** Canonical URL of the site, e.g. `"https://chassis-ui.com/css"`. */
47
+ baseURL: z.url(),
48
+ /** Version of the release that the site documents. Links to source files use its tag. */
49
+ currentVersion: zVersionSemver,
50
+ description: z.string(),
51
+ /** URL path of the docs pages, e.g. `"/css/docs"`. */
52
+ docsPath: z.string().startsWith('/'),
53
+ /** Figma Community handle, without the `@`. The header links to it when set. */
54
+ figmaHandle: z.string().optional(),
55
+ /** GitHub organisation or user name, not a URL. The header links to it when set. */
56
+ githubOrg: z
57
+ .string()
58
+ .regex(/^[\w.-]+$/, 'Expected a GitHub organisation name, not a URL')
59
+ .optional(),
60
+ /** Repository URL, e.g. `"https://github.com/chassis-ui/css"`. */
61
+ repo: z.url(),
62
+ /** The branch that "View on GitHub" links to. */
63
+ siteBranch: z.string().default('main'),
64
+ /** The site's root from the root of the repository, e.g. `"packages/site"`. */
65
+ sitePath: z.string().optional(),
66
+ /**
67
+ * Directory that the `file` props of `<ScssDocs>`, `<ScssDocsSimple>`, `<JsDocs>` and
68
+ * `<Code>` are relative to, itself relative to the site's root, e.g. `"../css"`.
69
+ */
70
+ sourceDir: z.string().default('.'),
71
+ /** The same directory from the root of the repository, e.g. `"packages/css"`. */
72
+ sourcePath: z.string().optional(),
73
+ subtitle: z.string(),
74
+ title: z.string(),
75
+ /** Heading levels that the table of contents lists. */
76
+ toc: zHeadingRange.default({ min: 2, max: 6 }),
77
+ /** X handle, without the `@`. Used in the social meta tags when set. */
78
+ xUsername: z.string().optional()
79
+ })
80
+
81
+ /** The parsed `config.yml`, limited to the keys that the package reads. */
82
+ export type ChassisConfig = z.infer<typeof configSchema>
83
+
84
+ /** A schema that a site passes to the integration: `configSchema` or an extension of it. */
85
+ export type ChassisConfigSchema = z.ZodType<ChassisConfig>
86
+
87
+ /**
88
+ * Keys that were renamed in 0.6.0. Loading a `config.yml` that still has one of them fails
89
+ * with a message that names the new key.
90
+ */
91
+ export const renamedConfigKeys: Record<string, string> = {
92
+ 'analytics.google_id': 'analytics.googleId',
93
+ current_version: 'currentVersion',
94
+ figma_handle: 'figmaHandle',
95
+ github_org: 'githubOrg',
96
+ x_username: 'xUsername'
97
+ }
98
+
99
+ /** Keys that were removed in 0.6.0, with the reason. */
100
+ export const removedConfigKeys: Record<string, string> = {
101
+ docsDir: 'The site root is the Astro root. Delete the key.'
102
+ }
103
+
104
+ // A key that was renamed in 0.6.0. The schemas below are not strict, so the old key would
105
+ // be dropped silently. This makes it fail, with a message that names the new key.
106
+ function renamedTo(newKey: string) {
107
+ return z.never({ error: `This key was renamed to \`${newKey}\`.` }).optional()
108
+ }
109
+
110
+ const sidebarMetaSchema = z.object({ added: z.string() }).array().optional()
111
+
112
+ /** The entries of `data/sidebar.yml`. */
113
+ export const sidebarSchema = z
114
+ .object({
115
+ title: z.string(),
116
+ section: z.string().optional(),
117
+ icon: z.string().optional(),
118
+ iconColor: z.string().optional(),
119
+ icon_color: renamedTo('iconColor'),
120
+ pages: z
121
+ .object({
122
+ title: z.string().optional(),
123
+ href: z.string().optional(),
124
+ group: z.string().optional(),
125
+ meta: sidebarMetaSchema,
126
+ pages: z
127
+ .object({
128
+ title: z.string(),
129
+ meta: sidebarMetaSchema
130
+ })
131
+ .array()
132
+ .optional()
133
+ })
134
+ .array()
135
+ .optional()
136
+ })
137
+ .array()
138
+
139
+ export type Sidebar = z.infer<typeof sidebarSchema>
140
+ export type SidebarGroup = Sidebar[number]
141
+ export type SidebarItem = NonNullable<SidebarGroup['pages']>[number]
142
+ export type SidebarSubItem = NonNullable<SidebarItem['pages']>[number]
143
+
144
+ /**
145
+ * Frontmatter of a page in the `docs` collection.
146
+ *
147
+ * @example
148
+ * ```ts
149
+ * // src/content.config.ts
150
+ * import { defineCollection } from 'astro:content'
151
+ * import { glob } from 'astro/loaders'
152
+ * import { calloutsSchema, docsSchema } from '@chassis-ui/docs/schema'
153
+ *
154
+ * export const collections = {
155
+ * docs: defineCollection({
156
+ * loader: glob({ pattern: '**\/*.{md,mdx}', base: './content/docs' }),
157
+ * schema: docsSchema
158
+ * }),
159
+ * callouts: defineCollection({
160
+ * loader: glob({ pattern: '**\/*.md', base: './content/callouts' }),
161
+ * schema: calloutsSchema
162
+ * })
163
+ * }
164
+ * ```
165
+ */
166
+ export const docsSchema = z
167
+ .object({
168
+ added: z
169
+ .object({
170
+ showBadge: z.boolean().optional(),
171
+ show_badge: renamedTo('showBadge'),
172
+ version: z.string()
173
+ })
174
+ .optional(),
175
+ aliases: z.string().or(z.string().array()).optional(),
176
+ description: z.string(),
177
+ direction: z.literal('rtl').optional(),
178
+ extraJs: z
179
+ .object({
180
+ async: z.boolean().optional(),
181
+ src: z.string()
182
+ })
183
+ .array()
184
+ .optional(),
185
+ extra_js: renamedTo('extraJs'),
186
+ sections: z
187
+ .object({
188
+ description: z.string(),
189
+ title: z.string(),
190
+ slug: z.string().optional()
191
+ })
192
+ .array()
193
+ .optional(),
194
+ thumbnail: z.string().optional(),
195
+ title: z.string(),
196
+ toc: z.boolean().optional()
197
+ })
198
+ .partial()
199
+
200
+ export type DocsFrontmatter = z.infer<typeof docsSchema>
201
+
202
+ /** Frontmatter of an entry in the `callouts` collection. */
203
+ export const calloutsSchema = z.object({})
204
+
205
+ /** An entry of the `docs` collection, as far as the package reads it. */
206
+ export interface DocsPage {
207
+ id: string
208
+ data: DocsFrontmatter
209
+ /** Path of the source file from the site's root. */
210
+ filePath?: string
211
+ }
@@ -1,6 +1,6 @@
1
1
  import fs from 'node:fs'
2
2
  import path from 'node:path'
3
- import type { AstroIntegration } from 'astro'
3
+ import { resolvePackageFilePath } from './paths'
4
4
 
5
5
  interface MdxjsEsmNode {
6
6
  type: 'mdxjsEsm'
@@ -8,9 +8,13 @@ interface MdxjsEsmNode {
8
8
  data: { estree: object }
9
9
  }
10
10
 
11
+ function getComponentName(filePath: string): string {
12
+ return path.parse(filePath).name.replaceAll(/[^\w\d]/g, '')
13
+ }
14
+
11
15
  function buildImportsNode(imports: string[]): MdxjsEsmNode {
12
16
  const body = imports.map((filePath) => {
13
- const name = path.parse(filePath).name.replaceAll(/[^\w\d]/g, '')
17
+ const name = getComponentName(filePath)
14
18
  return {
15
19
  type: 'ImportDeclaration',
16
20
  specifiers: [{ type: 'ImportDefaultSpecifier', local: { type: 'Identifier', name } }],
@@ -26,89 +30,100 @@ function buildImportsNode(imports: string[]): MdxjsEsmNode {
26
30
  }
27
31
 
28
32
  export interface ChassisAutoImportOptions {
29
- /** Absolute path to the docs site root (e.g. `getDocsFsPath()`). Used for the
30
- * site-local shortcodes directory and the type definitions output path. */
31
- docsPath: string
32
- /** Absolute path to the directory containing `node_modules`. Defaults to
33
- * `docsPath` when omitted — override when `node_modules` lives at the repo root. */
34
- modulesPath?: string
33
+ /**
34
+ * The site's root.
35
+ * @default process.cwd()
36
+ */
37
+ root?: string
38
+ /**
39
+ * The site's own shortcodes, relative to `root`. A component here replaces the component
40
+ * of the package that has the same name.
41
+ * @default 'src/components/shortcodes'
42
+ */
43
+ dir?: string
44
+ /** Names of the only shortcodes of the package to import, e.g. `['Code', 'Callout']`. */
45
+ include?: string[]
46
+ /** Names of shortcodes of the package to leave out, e.g. `['Icon']`. */
47
+ exclude?: string[]
48
+ /**
49
+ * Absolute path of the package's shortcodes. Found from the package's own location when
50
+ * omitted.
51
+ */
52
+ packageDir?: string
35
53
  }
36
54
 
37
- function scanComponents({ docsPath, modulesPath = docsPath }: ChassisAutoImportOptions) {
38
- const dirs = [
39
- path.join(modulesPath, 'node_modules/@chassis-ui/docs/src/components/shortcodes'),
40
- path.join(docsPath, 'src/components/shortcodes')
41
- ]
55
+ export interface ChassisAutoImport {
56
+ /** Import specifiers of the components, in import order. */
57
+ imports: string[]
58
+ /** Source of a declaration file that makes the components global in MDX files. */
59
+ typeDefinitions: string
60
+ /** Factory for the remark plugin that injects the imports into every `.mdx` file. */
61
+ plugin: () => ReturnType<typeof createAutoImportPlugin>
62
+ }
42
63
 
43
- const components: string[] = []
44
- const definitions: string[] = []
45
-
46
- for (const dir of dirs) {
47
- if (!fs.existsSync(dir)) continue
48
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
49
- if (!entry.isFile()) continue
50
- components.push(`${dir}/${entry.name}`)
51
- if (entry.name.endsWith('.astro')) {
52
- const importPath = dir.includes('@chassis-ui/docs')
53
- ? `@chassis-ui/docs/shortcodes`
54
- : `@shortcodes`
55
- definitions.push(
56
- `export const ${entry.name.replace('.astro', '')}: typeof import('${importPath}/${entry.name}').default`
57
- )
58
- }
59
- }
60
- }
64
+ function listFiles(directory: string): string[] {
65
+ if (!fs.existsSync(directory)) return []
61
66
 
62
- return { components, definitions }
67
+ return fs
68
+ .readdirSync(directory, { withFileTypes: true })
69
+ .filter((entry) => entry.isFile() && !entry.name.startsWith('.'))
70
+ .map((entry) => entry.name)
71
+ .sort()
63
72
  }
64
73
 
65
74
  /**
66
- * Astro integration that writes the auto-import type definitions file, and a
67
- * remark plugin factory for injecting shortcode imports into every MDX file.
68
- *
69
- * Replaces `astro-auto-import`, which triggers a deprecation warning in Astro 7.
70
- *
71
- * @example
72
- * ```ts
73
- * // astro.ts
74
- * const { integration, plugin } = chassisAutoImport({ docsPath: getDocsFsPath(), modulesPath: process.cwd() })
75
- *
76
- * // astro.config.ts
77
- * markdown: getDocsMarkdownConfig({ remarkPlugins: [plugin()] })
78
- * integrations: [integration()]
79
- * ```
75
+ * Finds the shortcodes of the package and of the site, and returns what is needed to import
76
+ * them into every MDX file. The `chassisDocs()` integration calls it. A site calls it only
77
+ * to build its own markdown configuration.
80
78
  */
81
- export function chassisAutoImport(options: ChassisAutoImportOptions): {
82
- /** Astro integration that writes `src/types/auto-import.d.ts`. */
83
- integration: () => AstroIntegration
84
- /** Factory for the remark plugin that injects imports into every `.mdx` file. */
85
- plugin: () => ReturnType<typeof createAutoImportPlugin>
86
- } {
87
- const { components, definitions } = scanComponents(options)
88
- const typeDefinitionsPath = path.join(options.docsPath, 'src/types/auto-import.d.ts')
79
+ export function chassisAutoImport(options: ChassisAutoImportOptions = {}): ChassisAutoImport {
80
+ const {
81
+ root = process.cwd(),
82
+ dir = 'src/components/shortcodes',
83
+ include,
84
+ exclude = [],
85
+ packageDir = resolvePackageFilePath('shortcodes')
86
+ } = options
87
+
88
+ const siteDir = path.resolve(root, dir)
89
+ const siteFiles = listFiles(siteDir)
90
+ const siteNames = new Set(siteFiles.map(getComponentName))
91
+
92
+ const packageFiles = listFiles(packageDir).filter((file) => {
93
+ const name = getComponentName(file)
94
+
95
+ if (siteNames.has(name) || exclude.includes(name)) return false
96
+
97
+ return include ? include.includes(name) : true
98
+ })
99
+
100
+ // The package's components are imported by specifier, not by path. The path of an
101
+ // installed package is outside the site, where the dev server refuses to serve files.
102
+ const components = [
103
+ ...packageFiles.map((file) => ({ file, source: `@chassis-ui/docs/shortcodes/${file}` })),
104
+ ...siteFiles.map((file) => ({ file, source: path.join(siteDir, file) }))
105
+ ]
106
+
107
+ const definitions = components
108
+ .filter(({ file }) => file.endsWith('.astro'))
109
+ .map(
110
+ ({ file, source }) =>
111
+ `export const ${getComponentName(file)}: typeof import('${source}').default`
112
+ )
113
+
114
+ const imports = components.map(({ source }) => source)
89
115
 
90
116
  return {
91
- integration: () => ({
92
- name: 'chassis-auto-import',
93
- hooks: {
94
- 'astro:config:setup': () => {
95
- fs.writeFileSync(
96
- typeDefinitionsPath,
97
- `/**
98
- * DO NOT EDIT THIS FILE MANUALLY.
99
- *
100
- * This file is automatically generated by the Chassis Astro Integration.
101
- * It contains the type definitions for the components that are auto imported in all pages.
117
+ imports,
118
+ typeDefinitions: `/**
119
+ * Generated by the integration of @chassis-ui/docs.
120
+ * The components that are imported into every MDX file.
102
121
  */
103
122
  export declare global {
104
123
  ${definitions.join('\n ')}
105
124
  }
106
- `
107
- )
108
- }
109
- }
110
- }),
111
- plugin: () => createAutoImportPlugin(components)
125
+ `,
126
+ plugin: () => createAutoImportPlugin(imports)
112
127
  }
113
128
  }
114
129
 
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Returns the text between `// <kind>-docs-start <name>` and the first
3
+ * `// <kind>-docs-end <name>` after it, or `undefined` when the file has no such part.
4
+ *
5
+ * The end marker has to name the part exactly: the part `make-col` ends at
6
+ * `// scss-docs-end make-col`, not at `// scss-docs-end make-col-auto`.
7
+ */
8
+ export function extractDocsSnippet(
9
+ content: string,
10
+ kind: 'js' | 'scss',
11
+ name: string
12
+ ): string | undefined {
13
+ const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
14
+ const matches = content.match(
15
+ new RegExp(
16
+ `// ${kind}-docs-start ${escapedName}\\n([\\s\\S]*?)// ${kind}-docs-end ${escapedName}(?![\\w-])`
17
+ )
18
+ )
19
+
20
+ return matches?.[1] || undefined
21
+ }
@@ -0,0 +1,39 @@
1
+ import path from 'node:path'
2
+ import type { ChassisConfig } from './schema'
3
+
4
+ /** The keys of `config.yml` that say where files live, on disk and on GitHub. */
5
+ export type SourceConfig = Pick<
6
+ ChassisConfig,
7
+ 'repo' | 'currentVersion' | 'siteBranch' | 'sitePath' | 'sourcePath'
8
+ >
9
+
10
+ /** Joins URL segments, skipping empty ones, and resolves any `..` in them. */
11
+ function toUrl(segments: (string | undefined)[]): string {
12
+ const url = segments.filter(Boolean).join('/').replaceAll('\\', '/')
13
+
14
+ return new URL(url).href
15
+ }
16
+
17
+ /**
18
+ * Absolute path of a file or directory that a `file` prop names.
19
+ *
20
+ * @param sourceDir Absolute path of the directory that `file` props are relative to.
21
+ */
22
+ export function resolveSourceFsPath(sourceDir: string, file = ''): string {
23
+ return path.resolve(sourceDir, file)
24
+ }
25
+
26
+ /** URL of a source file on GitHub, in the tag of the current version. */
27
+ export function resolveSourceUrl(config: SourceConfig, file: string): string {
28
+ return toUrl([config.repo, 'blob', `v${config.currentVersion}`, config.sourcePath, file])
29
+ }
30
+
31
+ /**
32
+ * URL of a file of the site on GitHub, on the site's branch.
33
+ *
34
+ * @param filePath Path of the file from the site's root, as Astro stores it in a content
35
+ * entry's `filePath` (e.g. `"content/docs/getting-started/introduction.mdx"`).
36
+ */
37
+ export function resolveSiteFileUrl(config: SourceConfig, filePath: string): string {
38
+ return toUrl([config.repo, 'blob', config.siteBranch, config.sitePath, filePath])
39
+ }
package/src/libs/toc.ts CHANGED
@@ -37,7 +37,9 @@ export function generateToc(allHeadings: MarkdownHeading[], config: TocConfig):
37
37
 
38
38
  const previousEntry = toc[toc.length - 1]
39
39
 
40
- if (heading.depth === previousEntry.depth) {
40
+ // A heading at the depth of the previous top-level entry, or shallower, starts a new
41
+ // top-level entry. A page whose first heading is an `<h3>` has later `<h2>`s here.
42
+ if (heading.depth <= previousEntry.depth) {
41
43
  toc.push({ ...heading, children: [] })
42
44
  continue
43
45
  }
@@ -1,4 +1,7 @@
1
- // Local docs variables
1
+ // The Sass variables and custom properties of the docs.
2
+ //
3
+ // This partial is public: a site may `@use "@chassis-ui/docs/scss/vars"` in its own
4
+ // stylesheets. Removing or renaming a variable is a breaking change.
2
5
  @use "@chassis-ui/css/scss/functions" as *;
3
6
  @use "@chassis-ui/css/scss/mixins" as *;
4
7
 
package/src/site.ts ADDED
@@ -0,0 +1,150 @@
1
+ // What pages and components read from the site. It is backed by the modules that the
2
+ // integration provides, so it works only in code that Astro renders: not in
3
+ // `astro.config.ts`, and not in the browser.
4
+
5
+ import path from 'node:path'
6
+ import type { z } from 'astro/zod'
7
+ import config from 'virtual:chassis-docs/config'
8
+ import sidebar from 'virtual:chassis-docs/sidebar'
9
+ import * as paths from 'virtual:chassis-docs/paths'
10
+ import { getCollection, getEntry, type render } from 'astro:content'
11
+ import { loadData } from './libs/config'
12
+ import { joinDocsPath } from './libs/docs-path'
13
+ import { getNodeModulesFsPaths, resolvePackageFilePath } from './libs/paths'
14
+ import { replaceConfigInText, replaceDocsrefInText } from './libs/remark'
15
+ import type { ChassisConfig, DocsPage, Sidebar } from './libs/schema'
16
+ import { resolveSiteFileUrl, resolveSourceFsPath, resolveSourceUrl } from './libs/source'
17
+
18
+ /**
19
+ * The parsed `config.yml`.
20
+ *
21
+ * A site with keys of its own names its config type: `getConfig<SiteConfig>()`, where
22
+ * `SiteConfig` is inferred from the schema that the site passes to the integration.
23
+ */
24
+ export function getConfig<TConfig extends ChassisConfig = ChassisConfig>(): TConfig {
25
+ return config as TConfig
26
+ }
27
+
28
+ /** The parsed `data/sidebar.yml`. Empty when the site has no sidebar file. */
29
+ export function getSidebar(): Sidebar {
30
+ return sidebar
31
+ }
32
+
33
+ /** URL path of a docs page, e.g. `getDocsPath('/components/button/')`. */
34
+ export function getDocsPath(inputPath: string): string {
35
+ return joinDocsPath(config.docsPath, inputPath)
36
+ }
37
+
38
+ /** Absolute path of the site's root: the directory that holds `astro.config.ts`. */
39
+ export function getSiteRoot(): string {
40
+ return paths.root
41
+ }
42
+
43
+ /** Absolute path of a file in the site's root. */
44
+ export function getSiteFsPath(file = ''): string {
45
+ return path.resolve(paths.root, file)
46
+ }
47
+
48
+ /** Absolute path of a file in the site's `public` directory. */
49
+ export function getPublicFsPath(file = ''): string {
50
+ return path.join(paths.publicDir, file)
51
+ }
52
+
53
+ /** Absolute path of a file or directory that a `file` prop names. */
54
+ export function getSourceFsPath(file = ''): string {
55
+ return resolveSourceFsPath(paths.sourceDir, file)
56
+ }
57
+
58
+ /** URL of a source file on GitHub, in the tag of the current version. */
59
+ export function getSourceUrl(file: string): string {
60
+ return resolveSourceUrl(config, file)
61
+ }
62
+
63
+ /** URL of a file of the site on GitHub, on the site's branch. */
64
+ export function getSiteFileUrl(filePath: string): string {
65
+ return resolveSiteFileUrl(config, filePath)
66
+ }
67
+
68
+ /**
69
+ * Absolute path of a file of the package, named by its public path, e.g.
70
+ * `getPackageFilePath('js/color-modes.js')`.
71
+ */
72
+ export function getPackageFilePath(file: string): string {
73
+ return resolvePackageFilePath(file, paths.packageRoot)
74
+ }
75
+
76
+ /** The `node_modules` directories that the site resolves packages from, nearest first. */
77
+ export function getNodeModulesPaths(): string[] {
78
+ return getNodeModulesFsPaths(paths.root)
79
+ }
80
+
81
+ /**
82
+ * Returns a `getData(name)` function for the site's own data files. Each key of
83
+ * `definitions` is the name of a YAML file in `dir`, and its value is the schema of the file.
84
+ *
85
+ * @example
86
+ * ```ts
87
+ * import { createDataLoader } from '@chassis-ui/docs/site'
88
+ * import { z } from '@chassis-ui/docs/schema'
89
+ *
90
+ * export const getData = createDataLoader({
91
+ * breakpoints: z.object({ name: z.string() }).array()
92
+ * })
93
+ * ```
94
+ */
95
+ export function createDataLoader<TDefinitions extends Record<string, z.ZodType>>(
96
+ definitions: TDefinitions,
97
+ options: { dir?: string } = {}
98
+ ) {
99
+ const cache = new Map<keyof TDefinitions, unknown>()
100
+
101
+ return function getData<TName extends keyof TDefinitions & string>(
102
+ name: TName
103
+ ): z.infer<TDefinitions[TName]> {
104
+ if (!cache.has(name)) {
105
+ cache.set(
106
+ name,
107
+ loadData({
108
+ file: path.join(options.dir ?? 'data', `${name}.yml`),
109
+ root: paths.root,
110
+ schema: definitions[name]
111
+ })
112
+ )
113
+ }
114
+
115
+ return cache.get(name) as z.infer<TDefinitions[TName]>
116
+ }
117
+ }
118
+
119
+ type RenderableEntry = Parameters<typeof render>[0]
120
+
121
+ // The package cannot know the collections of the site, so their names are not type-checked.
122
+ const getSiteCollection = getCollection as unknown as (_name: string) => Promise<DocsPage[]>
123
+ const getSiteEntry = getEntry as unknown as (
124
+ _collection: string,
125
+ _id: string
126
+ ) => Promise<RenderableEntry | undefined>
127
+
128
+ let docsPages: Promise<DocsPage[]> | undefined
129
+
130
+ /** The entries of the site's `docs` collection. */
131
+ export function getDocsPages(): Promise<DocsPage[]> {
132
+ docsPages ??= getSiteCollection('docs')
133
+
134
+ return docsPages
135
+ }
136
+
137
+ /** An entry of the site's `callouts` collection, or `undefined` when there is none. */
138
+ export function getCallout(name: string): Promise<RenderableEntry | undefined> {
139
+ return getSiteEntry('callouts', name)
140
+ }
141
+
142
+ /** Replaces each `[[config:key]]` in `text` with the value of that key of `config.yml`. */
143
+ export function resolveConfigRefs(text: string): string {
144
+ return replaceConfigInText(text, config)
145
+ }
146
+
147
+ /** Replaces each `[[docsref:/path]]` in `text` with the URL path of that docs page. */
148
+ export function resolveDocsrefs(text: string): string {
149
+ return replaceDocsrefInText(text, { docsPath: config.docsPath })
150
+ }
package/virtual.d.ts ADDED
@@ -0,0 +1,25 @@
1
+ // Modules that the integration provides. A site gets these declarations from the
2
+ // integration. This file is for type-checking the package on its own.
3
+
4
+ declare module 'virtual:chassis-docs/config' {
5
+ const config: import('./src/libs/schema').ChassisConfig
6
+ export default config
7
+ }
8
+
9
+ declare module 'virtual:chassis-docs/sidebar' {
10
+ const sidebar: import('./src/libs/schema').Sidebar
11
+ export default sidebar
12
+ }
13
+
14
+ declare module 'virtual:chassis-docs/paths' {
15
+ /** The site's root. */
16
+ export const root: string
17
+ /** The site's `public` directory. */
18
+ export const publicDir: string
19
+ /** The directory that `file` props are relative to. */
20
+ export const sourceDir: string
21
+ /** The directory of `@chassis-ui/docs`. */
22
+ export const packageRoot: string
23
+ }
24
+
25
+ declare module 'virtual:chassis-docs/styles' {}