@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.
@@ -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.16",
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.