@chassis-ui/docs 0.5.1 → 0.6.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 +286 -0
- package/README.md +227 -71
- package/UPGRADING.md +243 -0
- package/index.ts +7 -1
- package/package.json +31 -8
- package/src/components/DocsSidebar.astro +9 -9
- package/src/components/shortcodes/Callout.astro +4 -4
- package/src/components/shortcodes/Code.astro +3 -3
- package/src/components/shortcodes/JsDocs.astro +6 -7
- package/src/components/shortcodes/ScssDocs.astro +7 -9
- package/src/components/shortcodes/ScssDocsSimple.astro +6 -7
- package/src/integration.ts +271 -0
- package/src/js/clipboard.ts +65 -0
- package/src/layouts/BaseLayout.astro +7 -9
- package/src/layouts/DocsLayout.astro +11 -13
- package/src/layouts/RedirectLayout.astro +1 -1
- package/src/layouts/head/Analytics.astro +6 -4
- package/src/layouts/head/Head.astro +2 -2
- package/src/layouts/head/Social.astro +5 -6
- package/src/layouts/header/Navigation.astro +28 -19
- package/src/libs/config.ts +138 -0
- package/src/libs/docs-path.ts +12 -0
- package/src/libs/highlight.ts +4 -44
- package/src/libs/paths.ts +160 -0
- package/src/libs/remark.ts +184 -0
- package/src/libs/schema.ts +211 -0
- package/src/libs/shortcodes.ts +86 -71
- package/src/libs/snippet.ts +21 -0
- package/src/libs/source.ts +17 -40
- package/src/libs/toc.ts +3 -1
- package/src/scss/_vars.scss +4 -1
- package/src/site.ts +150 -0
- package/virtual.d.ts +25 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import type { Root } from 'mdast'
|
|
2
|
+
import type { MdxJsxAttribute, MdxJsxExpressionAttribute } from 'mdast-util-mdx-jsx'
|
|
3
|
+
import type { Plugin } from 'unified'
|
|
4
|
+
import { visit } from 'unist-util-visit'
|
|
5
|
+
import { joinDocsPath } from './docs-path'
|
|
6
|
+
|
|
7
|
+
// [[config:foo]]
|
|
8
|
+
// [[config:foo.bar]]
|
|
9
|
+
const configRegExp = /\[\[config:(?<name>[\w.]+)\]\]/g
|
|
10
|
+
// [[docsref:/foo]]
|
|
11
|
+
// [[docsref:/foo/bar#baz]]
|
|
12
|
+
const docsrefRegExp = /\[\[docsref:(?<path>[\w./#-]+)\]\]/g
|
|
13
|
+
|
|
14
|
+
type Replacer = (_text: string) => string
|
|
15
|
+
type MdxAttributes = (MdxJsxAttribute | MdxJsxExpressionAttribute)[]
|
|
16
|
+
|
|
17
|
+
export interface RemarkCxConfigOptions {
|
|
18
|
+
/** The parsed `config.yml`. */
|
|
19
|
+
config: object
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface RemarkCxDocsrefOptions {
|
|
23
|
+
/** The `docsPath` of the site's `config.yml`. */
|
|
24
|
+
docsPath: string
|
|
25
|
+
/** Called with each docs path that a page links to. */
|
|
26
|
+
onDocsPath?: (_path: string) => void
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Replaces each `[[config:key]]` in `text` with the value of that key. */
|
|
30
|
+
export function replaceConfigInText(text: string, config: object): string {
|
|
31
|
+
return text.replace(configRegExp, (_match, keyPath: string) => {
|
|
32
|
+
const value = keyPath.split('.').reduce<unknown>((values, part) => {
|
|
33
|
+
return values && typeof values === 'object'
|
|
34
|
+
? (values as Record<string, unknown>)[part]
|
|
35
|
+
: undefined
|
|
36
|
+
}, config)
|
|
37
|
+
|
|
38
|
+
if (typeof value !== 'string' || !value) {
|
|
39
|
+
throw new Error(`Failed to find a valid configuration value for '${keyPath}'.`)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return value
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Replaces each `[[docsref:/path]]` in `text` with the URL path of that docs page. */
|
|
47
|
+
export function replaceDocsrefInText(text: string, options: RemarkCxDocsrefOptions): string {
|
|
48
|
+
return text.replace(docsrefRegExp, (_match, inputPath: string) => {
|
|
49
|
+
options.onDocsPath?.(inputPath)
|
|
50
|
+
|
|
51
|
+
return joinDocsPath(options.docsPath, inputPath)
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function replaceInAttributes(attributes: MdxAttributes, replacer: Replacer) {
|
|
56
|
+
return attributes.map((attribute) => {
|
|
57
|
+
if (attribute.type === 'mdxJsxAttribute' && typeof attribute.value === 'string') {
|
|
58
|
+
attribute.value = replacer(attribute.value)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return attribute
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function replaceInFrontmatter(record: Record<string, unknown>, replacer: Replacer) {
|
|
66
|
+
for (const [key, value] of Object.entries(record)) {
|
|
67
|
+
if (typeof value === 'string') {
|
|
68
|
+
record[key] = replacer(value)
|
|
69
|
+
} else if (Array.isArray(value)) {
|
|
70
|
+
record[key] = value.map((arrayValue) => {
|
|
71
|
+
return typeof arrayValue === 'string'
|
|
72
|
+
? replacer(arrayValue)
|
|
73
|
+
: typeof arrayValue === 'object'
|
|
74
|
+
? replaceInFrontmatter(arrayValue, replacer)
|
|
75
|
+
: arrayValue
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return record
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function containsFrontmatter(data: unknown): data is { frontmatter: Record<string, unknown> } {
|
|
84
|
+
return data != undefined && typeof data === 'object' && 'frontmatter' in data
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* A remark plugin that replaces config values in Markdown and MDX files. `[[config:foo]]`
|
|
89
|
+
* becomes the value of the `foo` key of `config.yml`, and `[[config:foo.bar]]` reads a nested
|
|
90
|
+
* key. It also works in frontmatter.
|
|
91
|
+
*/
|
|
92
|
+
export const remarkCxConfig: Plugin<[RemarkCxConfigOptions], Root> = function ({ config }) {
|
|
93
|
+
const replacer: Replacer = (text) => replaceConfigInText(text, config)
|
|
94
|
+
|
|
95
|
+
return function remarkCxConfigPlugin(ast, file) {
|
|
96
|
+
if (containsFrontmatter(file.data.astro)) {
|
|
97
|
+
replaceInFrontmatter(file.data.astro.frontmatter, replacer)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// https://github.com/syntax-tree/mdast#nodes
|
|
101
|
+
// https://github.com/syntax-tree/mdast-util-mdx-jsx#nodes
|
|
102
|
+
visit(
|
|
103
|
+
ast,
|
|
104
|
+
['code', 'definition', 'image', 'inlineCode', 'link', 'mdxJsxFlowElement', 'text'],
|
|
105
|
+
(node) => {
|
|
106
|
+
switch (node.type) {
|
|
107
|
+
case 'code':
|
|
108
|
+
case 'inlineCode':
|
|
109
|
+
case 'text': {
|
|
110
|
+
node.value = replacer(node.value)
|
|
111
|
+
break
|
|
112
|
+
}
|
|
113
|
+
case 'image': {
|
|
114
|
+
if (node.alt) {
|
|
115
|
+
node.alt = replacer(node.alt)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
node.url = replacer(node.url)
|
|
119
|
+
break
|
|
120
|
+
}
|
|
121
|
+
case 'definition':
|
|
122
|
+
case 'link': {
|
|
123
|
+
node.url = replacer(node.url)
|
|
124
|
+
break
|
|
125
|
+
}
|
|
126
|
+
case 'mdxJsxFlowElement': {
|
|
127
|
+
node.attributes = replaceInAttributes(node.attributes, replacer)
|
|
128
|
+
break
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* A remark plugin that turns `[[docsref:/foo]]` into a link to the docs page `/foo`, under
|
|
138
|
+
* the `docsPath` of `config.yml`. It also works in frontmatter.
|
|
139
|
+
*/
|
|
140
|
+
export const remarkCxDocsref: Plugin<[RemarkCxDocsrefOptions], Root> = function (options) {
|
|
141
|
+
const replacer: Replacer = (text) => replaceDocsrefInText(text, options)
|
|
142
|
+
|
|
143
|
+
return function remarkCxDocsrefPlugin(ast, file) {
|
|
144
|
+
if (containsFrontmatter(file.data.astro)) {
|
|
145
|
+
replaceInFrontmatter(file.data.astro.frontmatter, replacer)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// https://github.com/syntax-tree/mdast#nodes
|
|
149
|
+
// https://github.com/syntax-tree/mdast-util-mdx-jsx#nodes
|
|
150
|
+
visit(
|
|
151
|
+
ast,
|
|
152
|
+
[
|
|
153
|
+
'code',
|
|
154
|
+
'definition',
|
|
155
|
+
'image',
|
|
156
|
+
'inlineCode',
|
|
157
|
+
'link',
|
|
158
|
+
'mdxJsxFlowElement',
|
|
159
|
+
'mdxJsxTextElement',
|
|
160
|
+
'text'
|
|
161
|
+
],
|
|
162
|
+
(node) => {
|
|
163
|
+
switch (node.type) {
|
|
164
|
+
case 'code':
|
|
165
|
+
case 'inlineCode':
|
|
166
|
+
case 'text': {
|
|
167
|
+
node.value = replacer(node.value)
|
|
168
|
+
break
|
|
169
|
+
}
|
|
170
|
+
case 'definition':
|
|
171
|
+
case 'link': {
|
|
172
|
+
node.url = replacer(node.url)
|
|
173
|
+
break
|
|
174
|
+
}
|
|
175
|
+
case 'mdxJsxFlowElement':
|
|
176
|
+
case 'mdxJsxTextElement': {
|
|
177
|
+
node.attributes = replaceInAttributes(node.attributes, replacer)
|
|
178
|
+
break
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -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
|
+
}
|
package/src/libs/shortcodes.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
|
-
import
|
|
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 =
|
|
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
|
-
/**
|
|
30
|
-
*
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
44
|
-
|
|
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
|
|
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
|
-
*
|
|
67
|
-
*
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
+
}
|
package/src/libs/source.ts
CHANGED
|
@@ -1,34 +1,11 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
|
-
import {
|
|
2
|
+
import type { ChassisConfig } from './schema'
|
|
3
3
|
|
|
4
|
-
/**
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
*/
|
|
10
|
-
interface SourceConfig {
|
|
11
|
-
/** Repository URL (e.g. `"https://github.com/chassis-ui/css"`). */
|
|
12
|
-
repo: string
|
|
13
|
-
/** Version of the release that links to source files name. */
|
|
14
|
-
current_version: string
|
|
15
|
-
/**
|
|
16
|
-
* Directory the `file` props of `<ScssDocs>`, `<ScssDocsSimple>`, `<JsDocs>` and `<Code>` are
|
|
17
|
-
* relative to, itself relative to the working directory of the build (e.g. `"../css"` for a
|
|
18
|
-
* site in `packages/site`).
|
|
19
|
-
*/
|
|
20
|
-
sourceDir?: string
|
|
21
|
-
/** The same directory from the root of the repository (e.g. `"packages/css"`). */
|
|
22
|
-
sourcePath?: string
|
|
23
|
-
/** The site's root from the root of the repository (e.g. `"packages/site"`). */
|
|
24
|
-
sitePath?: string
|
|
25
|
-
/** The branch that "View on GitHub" links to. Defaults to `"main"`. */
|
|
26
|
-
siteBranch?: string
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function getSourceConfig(): SourceConfig {
|
|
30
|
-
return getConfig() as SourceConfig
|
|
31
|
-
}
|
|
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
|
+
>
|
|
32
9
|
|
|
33
10
|
/** Joins URL segments, skipping empty ones, and resolves any `..` in them. */
|
|
34
11
|
function toUrl(segments: (string | undefined)[]): string {
|
|
@@ -37,16 +14,18 @@ function toUrl(segments: (string | undefined)[]): string {
|
|
|
37
14
|
return new URL(url).href
|
|
38
15
|
}
|
|
39
16
|
|
|
40
|
-
/**
|
|
41
|
-
|
|
42
|
-
|
|
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)
|
|
43
24
|
}
|
|
44
25
|
|
|
45
26
|
/** URL of a source file on GitHub, in the tag of the current version. */
|
|
46
|
-
export function
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
return toUrl([repo, 'blob', `v${version}`, sourcePath, file])
|
|
27
|
+
export function resolveSourceUrl(config: SourceConfig, file: string): string {
|
|
28
|
+
return toUrl([config.repo, 'blob', `v${config.currentVersion}`, config.sourcePath, file])
|
|
50
29
|
}
|
|
51
30
|
|
|
52
31
|
/**
|
|
@@ -55,8 +34,6 @@ export function getSourceUrl(file: string): string {
|
|
|
55
34
|
* @param filePath Path of the file from the site's root, as Astro stores it in a content
|
|
56
35
|
* entry's `filePath` (e.g. `"content/docs/getting-started/introduction.mdx"`).
|
|
57
36
|
*/
|
|
58
|
-
export function
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
return toUrl([repo, 'blob', siteBranch, sitePath, filePath])
|
|
37
|
+
export function resolveSiteFileUrl(config: SourceConfig, filePath: string): string {
|
|
38
|
+
return toUrl([config.repo, 'blob', config.siteBranch, config.sitePath, filePath])
|
|
62
39
|
}
|