@grove-dev/starlight 0.2.16 → 0.2.18
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/THIRD_PARTY_LICENSES.md +68 -0
- package/package.json +4 -1
- package/tests/plugin.test.ts +191 -0
- package/tsconfig.json +9 -0
- package/LICENSE +0 -21
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Third-party attributions — `@grove-dev/starlight`
|
|
2
|
+
|
|
3
|
+
This package incorporates design and code from upstream projects. The
|
|
4
|
+
contributions are credited below; reproduction of the upstream license
|
|
5
|
+
texts follows the attribution block. The Grove project ships
|
|
6
|
+
`@grove-dev/starlight` under the same MIT license that the rest of the
|
|
7
|
+
monorepo uses; the third-party license texts below are reproduced here
|
|
8
|
+
to satisfy the upstream attribution requirements.
|
|
9
|
+
|
|
10
|
+
> **⚠️ TODO(decision): license compatibility check.**
|
|
11
|
+
> The audit and the project's own README credit the upstream work as
|
|
12
|
+
> "lucode / lucas-labs" and reference
|
|
13
|
+
> `https://github.com/lucas-labs/@grove-dev/starlight-theme`. Before the
|
|
14
|
+
> first publish of `@grove-dev/starlight` to npm, the project owner
|
|
15
|
+
> must:
|
|
16
|
+
>
|
|
17
|
+
> 1. Confirm the exact upstream repo URL (the `lucas-labs/...` path
|
|
18
|
+
> in the README is a placeholder; the real upstream is
|
|
19
|
+
> `lucode-labs/lucode` or similar — verify on GitHub).
|
|
20
|
+
> 2. Verify the upstream license (MIT? Apache-2.0? something else?)
|
|
21
|
+
> and confirm it is compatible with the Grove monorepo's MIT
|
|
22
|
+
> distribution.
|
|
23
|
+
> 3. If compatible, replace the `<!-- TODO LICENSE TEXT -->` block
|
|
24
|
+
> below with the verbatim upstream license text (most MIT projects
|
|
25
|
+
> use the standard MIT text — the canonical version is at
|
|
26
|
+
> <https://opensource.org/licenses/MIT>).
|
|
27
|
+
> 4. If not compatible, either obtain a written license grant from
|
|
28
|
+
> the upstream maintainer OR replace the derivative design with
|
|
29
|
+
> original work before publishing.
|
|
30
|
+
>
|
|
31
|
+
> Tracking issue:
|
|
32
|
+
> <https://github.com/tortuvshin/grove/issues/new?title=starlight%3A+verify+third-party+license+compatibility>
|
|
33
|
+
|
|
34
|
+
## Upstream credits
|
|
35
|
+
|
|
36
|
+
The design and component overrides in this package derive from:
|
|
37
|
+
|
|
38
|
+
- **[lucode](https://github.com/lucas-labs/@grove-dev/starlight-theme)** by the
|
|
39
|
+
**lucas-labs** organization (also referenced as "Lucode" in the
|
|
40
|
+
package's `user-components.ts` and component overrides). The theme
|
|
41
|
+
recreates the design of [shadcn/ui](https://ui.shadcn.com/) (MIT)
|
|
42
|
+
for use inside Astro Starlight, with custom overrides for the
|
|
43
|
+
header, sidebar, page frame, hero, footer, search, table of
|
|
44
|
+
contents, pagination, and Markdown content. See the package
|
|
45
|
+
`README.md` (the "Attribution" and "Usage" sections) for the
|
|
46
|
+
in-code pointers.
|
|
47
|
+
- **[adrian-ub/starlight-theme-black](https://github.com/adrian-ub/starlight-theme-black)**
|
|
48
|
+
— the earlier shadcn/ui-inspired Starlight theme that
|
|
49
|
+
`lucas-labs/@grove-dev/starlight-theme` was based on (per the
|
|
50
|
+
upstream README).
|
|
51
|
+
- **[shadcn/ui](https://ui.shadcn.com/)** — the original design
|
|
52
|
+
language that the upstream work and this package both target.
|
|
53
|
+
|
|
54
|
+
## Upstream license
|
|
55
|
+
|
|
56
|
+
<!-- TODO LICENSE TEXT: paste the verbatim upstream license (typically
|
|
57
|
+
the standard MIT text with copyright line) here once the license
|
|
58
|
+
compatibility check above is complete. Until then this section is
|
|
59
|
+
intentionally empty so the file can ship without misrepresenting the
|
|
60
|
+
upstream license. -->
|
|
61
|
+
|
|
62
|
+
## License for this package
|
|
63
|
+
|
|
64
|
+
`@grove-dev/starlight` is distributed under the **MIT License** — see
|
|
65
|
+
[`../core/LICENSE`](../../LICENSE) at the monorepo root for the
|
|
66
|
+
canonical text. The third-party attributions above are reproduced for
|
|
67
|
+
compliance with the upstream license terms; they do not change the
|
|
68
|
+
license of the rest of the package.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@grove-dev/starlight",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.18",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Grove's theme for Starlight (the Astro native documentation site generator)",
|
|
6
6
|
"author": "grove-dev",
|
|
@@ -50,5 +50,8 @@
|
|
|
50
50
|
"dependencies": {
|
|
51
51
|
"@pagefind/default-ui": "^1.5.2",
|
|
52
52
|
"marked": "^18.0.2"
|
|
53
|
+
},
|
|
54
|
+
"scripts": {
|
|
55
|
+
"test": "cd ../.. && pnpm exec vitest run --project starlight"
|
|
53
56
|
}
|
|
54
57
|
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @grove-dev/starlight — config / override / virtual module tests.
|
|
3
|
+
*
|
|
4
|
+
* Coverage (per the brief):
|
|
5
|
+
* - plugin.ts:parseConfig: accepts empty config (all defaults),
|
|
6
|
+
* honours user-supplied fields, throws on invalid input
|
|
7
|
+
* - virtual module resolution: the vite plugin's `load` and
|
|
8
|
+
* `resolveId` hooks return the canonical module id when
|
|
9
|
+
* called with the expected arguments, and pass through for
|
|
10
|
+
* unrelated ids
|
|
11
|
+
* - override composition: the override() function fills in
|
|
12
|
+
* every component override slot, warns the user on a clash,
|
|
13
|
+
* and preserves a pre-set user override (does NOT clobber it
|
|
14
|
+
* with the default)
|
|
15
|
+
*/
|
|
16
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
17
|
+
import { plugin } from "../core/plugin.js";
|
|
18
|
+
import { override, COMPONENT_OVERRIDES } from "../core/config/override.js";
|
|
19
|
+
import { vitePlugin } from "../core/config/vite.js";
|
|
20
|
+
import { LucodeStarlightConfigSchema } from "../core/config/schemas.js";
|
|
21
|
+
|
|
22
|
+
describe("parseConfig (exercised through plugin())", () => {
|
|
23
|
+
it("accepts an empty config and fills in all defaults", () => {
|
|
24
|
+
// We can't import parseConfig directly (it's local to
|
|
25
|
+
// plugin.ts), so we exercise it through `plugin(undefined)`,
|
|
26
|
+
// which goes through the same Zod parse path. The hook
|
|
27
|
+
// callback is called with the parsed config; we read it
|
|
28
|
+
// back via the test.
|
|
29
|
+
let captured: unknown;
|
|
30
|
+
const p = plugin();
|
|
31
|
+
// The plugin's `config:setup` hook is the only place
|
|
32
|
+
// parseConfig's output is consumed. Invoke it with a
|
|
33
|
+
// minimal stub of the hook parameters and capture the
|
|
34
|
+
// addIntegration's vite config.
|
|
35
|
+
const addIntegration = vi.fn();
|
|
36
|
+
const updateConfig = vi.fn();
|
|
37
|
+
const calls: Array<{ vite?: { plugins?: Array<{ load?: unknown; resolveId?: unknown }> } }> = [];
|
|
38
|
+
addIntegration.mockImplementation((integration: { hooks: { "astro:config:setup": (args: { updateConfig: (cfg: { vite?: { plugins?: Array<{ load?: unknown; resolveId?: unknown }> } }) => void }) => void } }) => {
|
|
39
|
+
integration.hooks["astro:config:setup"]({
|
|
40
|
+
updateConfig: (cfg) => calls.push(cfg),
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
p.hooks["config:setup"]({
|
|
44
|
+
config: { customCss: [] },
|
|
45
|
+
logger: { warn: () => {}, info: () => {}, error: () => {}, debug: () => {} } as never,
|
|
46
|
+
updateConfig: updateConfig as never,
|
|
47
|
+
addIntegration: addIntegration as never,
|
|
48
|
+
command: "build" as never,
|
|
49
|
+
isRestart: false,
|
|
50
|
+
});
|
|
51
|
+
expect(calls).toHaveLength(1);
|
|
52
|
+
const call = calls[0];
|
|
53
|
+
const vite = call?.vite;
|
|
54
|
+
expect(vite?.plugins).toBeDefined();
|
|
55
|
+
captured = vite;
|
|
56
|
+
// Sanity: parseConfig didn't throw on the empty input.
|
|
57
|
+
expect(captured).toBeDefined();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("honours user-supplied docs.includeAiUtilities", () => {
|
|
61
|
+
// Capture the vite plugin's resolved config to assert
|
|
62
|
+
// that the user's boolean is preserved.
|
|
63
|
+
let capturedPlugin: ReturnType<typeof vitePlugin> | undefined;
|
|
64
|
+
const addIntegration = vi.fn((integration: { hooks: { "astro:config:setup": (args: { updateConfig: (cfg: { vite: { plugins: Array<ReturnType<typeof vitePlugin>> } }) => void }) => void } }) => {
|
|
65
|
+
integration.hooks["astro:config:setup"]({
|
|
66
|
+
updateConfig: (cfg) => {
|
|
67
|
+
capturedPlugin = cfg.vite.plugins[0];
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
const p = plugin({ docs: { includeAiUtilities: true } });
|
|
72
|
+
p.hooks["config:setup"]({
|
|
73
|
+
config: { customCss: [] },
|
|
74
|
+
logger: { warn: () => {}, info: () => {}, error: () => {}, debug: () => {} } as never,
|
|
75
|
+
updateConfig: vi.fn() as never,
|
|
76
|
+
addIntegration: addIntegration as never,
|
|
77
|
+
command: "build" as never,
|
|
78
|
+
isRestart: false,
|
|
79
|
+
});
|
|
80
|
+
expect(capturedPlugin).toBeDefined();
|
|
81
|
+
// The vite plugin's moduleContent embeds the JSON-serialized
|
|
82
|
+
// config. Load it to confirm `includeAiUtilities: true`
|
|
83
|
+
// survived the parse.
|
|
84
|
+
const loaded = capturedPlugin!.load("\0virtual:lucode-starlight-config") as string;
|
|
85
|
+
expect(loaded).toContain('"includeAiUtilities":true');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("falls back to the schema default for an empty docs object", () => {
|
|
89
|
+
// The schema has `.default({ includeAiUtilities: false })`
|
|
90
|
+
// for the docs sub-object. Pin it.
|
|
91
|
+
const parsed = LucodeStarlightConfigSchema.parse({});
|
|
92
|
+
expect(parsed.docs?.includeAiUtilities).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("throws on invalid input (zod validation failure)", () => {
|
|
96
|
+
// A non-string label fails the linkSchema validation.
|
|
97
|
+
expect(() => LucodeStarlightConfigSchema.parse({ navLinks: "not-an-array" })).toThrow();
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe("vitePlugin — virtual module resolution", () => {
|
|
102
|
+
it("resolveId returns the resolved id for the canonical module id", () => {
|
|
103
|
+
const p = vitePlugin(LucodeStarlightConfigSchema.parse({}));
|
|
104
|
+
// The virtual module prefix is 'virtual:'. The plugin
|
|
105
|
+
// matches on the un-prefixed name and returns the
|
|
106
|
+
// '\0'-prefixed resolved id (Vite convention for virtual
|
|
107
|
+
// modules — the null byte prevents the resolved id from
|
|
108
|
+
// being treated as a real file path).
|
|
109
|
+
const result = p.resolveId?.("virtual:lucode-starlight-config");
|
|
110
|
+
expect(result).toBe("\0virtual:lucode-starlight-config");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("resolveId returns undefined for unrelated ids (passthrough)", () => {
|
|
114
|
+
const p = vitePlugin(LucodeStarlightConfigSchema.parse({}));
|
|
115
|
+
expect(p.resolveId?.("virtual:some-other-module")).toBeUndefined();
|
|
116
|
+
expect(p.resolveId?.("./relative-import")).toBeUndefined();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("load returns the JSON-serialized config when given the resolved id", () => {
|
|
120
|
+
const config = LucodeStarlightConfigSchema.parse({ docs: { includeAiUtilities: true } });
|
|
121
|
+
const p = vitePlugin(config);
|
|
122
|
+
const module = p.load?.("\0virtual:lucode-starlight-config") as string;
|
|
123
|
+
expect(module).toContain("export default");
|
|
124
|
+
expect(module).toContain('"includeAiUtilities":true');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("load returns undefined for unrelated ids", () => {
|
|
128
|
+
const p = vitePlugin(LucodeStarlightConfigSchema.parse({}));
|
|
129
|
+
expect(p.load?.("not-the-virtual-id")).toBeUndefined();
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe("override — component override composition", () => {
|
|
134
|
+
let warnSpy: ReturnType<typeof vi.spyOn>;
|
|
135
|
+
|
|
136
|
+
beforeEach(() => {
|
|
137
|
+
warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
afterEach(() => {
|
|
141
|
+
warnSpy.mockRestore();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("fills every component override slot when the user has none", () => {
|
|
145
|
+
// Starlight starts with no components overridden (empty
|
|
146
|
+
// config). The override() function should set every key in
|
|
147
|
+
// COMPONENT_OVERRIDES to point at the @grove-dev/starlight
|
|
148
|
+
// path.
|
|
149
|
+
const components = override(
|
|
150
|
+
{ components: {} } as never,
|
|
151
|
+
COMPONENT_OVERRIDES,
|
|
152
|
+
{ warn: warnSpy, info: () => {}, error: () => {}, debug: () => {} } as never,
|
|
153
|
+
);
|
|
154
|
+
for (const key of COMPONENT_OVERRIDES) {
|
|
155
|
+
expect(components?.[key]).toBe(`@grove-dev/starlight/components/overrides/${key}.astro`);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("preserves a user-supplied component override (does not clobber)", () => {
|
|
160
|
+
// If the user already set components.Footer, we should
|
|
161
|
+
// NOT overwrite it with the default — we warn and skip.
|
|
162
|
+
// Pin: a previous "always overwrite" implementation would
|
|
163
|
+
// silently clobber the user's custom footer.
|
|
164
|
+
const userFooter = "./my-custom/Footer.astro";
|
|
165
|
+
const components = override(
|
|
166
|
+
{ components: { Footer: userFooter } } as never,
|
|
167
|
+
COMPONENT_OVERRIDES,
|
|
168
|
+
{ warn: warnSpy, info: () => {}, error: () => {}, debug: () => {} } as never,
|
|
169
|
+
);
|
|
170
|
+
expect(components?.Footer).toBe(userFooter);
|
|
171
|
+
// The warn call names the slot and points at the override path.
|
|
172
|
+
const warnings = warnSpy.mock.calls.map((c: unknown[]) => String(c[0]));
|
|
173
|
+
expect(warnings.some((w: string) => w.includes("Footer"))).toBe(true);
|
|
174
|
+
// All OTHER slots are still filled in.
|
|
175
|
+
expect(components?.Header).toBe(`@grove-dev/starlight/components/overrides/Header.astro`);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("handles a config with no `components` field at all (undefined)", () => {
|
|
179
|
+
// Defensive: Starlight's config can omit `components`
|
|
180
|
+
// entirely. override() should still produce a full set
|
|
181
|
+
// without throwing.
|
|
182
|
+
const components = override(
|
|
183
|
+
{} as never,
|
|
184
|
+
COMPONENT_OVERRIDES,
|
|
185
|
+
{ warn: warnSpy, info: () => {}, error: () => {}, debug: () => {} } as never,
|
|
186
|
+
);
|
|
187
|
+
for (const key of COMPONENT_OVERRIDES) {
|
|
188
|
+
expect(components?.[key]).toBeDefined();
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"rootDir": ".",
|
|
5
|
+
"noEmit": true,
|
|
6
|
+
"types": ["node", "vitest/globals"]
|
|
7
|
+
},
|
|
8
|
+
"include": ["core/**/*.ts", "tests/**/*.ts", "global.d.ts", "virtual.d.ts", "index.ts", "schema.ts", "user-components.ts"]
|
|
9
|
+
}
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Turtuvshin Byambaa
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|