slidev-workspace 0.2.0 → 0.2.1

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/dist/index.d.ts CHANGED
@@ -45,6 +45,7 @@ declare function useSlides(): {
45
45
  slides: vue0.ComputedRef<SlideData[]>;
46
46
  slidesCount: vue0.ComputedRef<number>;
47
47
  loadSlidesData: () => Promise<void>;
48
+ isLoading: vue0.Ref<boolean, boolean>;
48
49
  };
49
50
  //#endregion
50
51
  //#region src/types/config.d.ts
package/dist/index.js CHANGED
@@ -1,8 +1,44 @@
1
1
  import { computed, ref } from "vue";
2
2
 
3
+ //#region src/env.ts
4
+ const IS_DEVELOPMENT = import.meta.env.MODE === "development";
5
+
6
+ //#endregion
3
7
  //#region src/preview/composables/useSlides.ts
8
+ /**
9
+ * Check if a string is a valid URL
10
+ */
11
+ function isUrl(str) {
12
+ if (!str) return false;
13
+ try {
14
+ new URL(str);
15
+ return true;
16
+ } catch {
17
+ return false;
18
+ }
19
+ }
20
+ /**
21
+ * Resolve background image path
22
+ * If the background is not a URL, construct the full path using slide.path as base
23
+ */
24
+ function resolveBackgroundPath(params) {
25
+ const { background, slidePath, devServerUrl } = params;
26
+ if (!background) return "";
27
+ if (isUrl(background)) return background;
28
+ try {
29
+ const domain = IS_DEVELOPMENT ? devServerUrl : window.location.origin;
30
+ const basePath = slidePath.endsWith("/") ? slidePath : slidePath + "/";
31
+ const relativeBg = background.startsWith("/") ? background.slice(1) : background;
32
+ const resolvedUrl = new URL(basePath + relativeBg, domain);
33
+ return resolvedUrl.href;
34
+ } catch (error) {
35
+ console.error("Failed to resolve background path:", error);
36
+ return background;
37
+ }
38
+ }
4
39
  function useSlides() {
5
40
  const slidesData = ref([]);
41
+ const isLoading = ref(true);
6
42
  const loadSlidesData = async () => {
7
43
  try {
8
44
  const module = await import("slidev:content");
@@ -10,6 +46,8 @@ function useSlides() {
10
46
  } catch (error) {
11
47
  console.warn("Failed to load slides data:", error);
12
48
  slidesData.value = [];
49
+ } finally {
50
+ isLoading.value = false;
13
51
  }
14
52
  };
15
53
  loadSlidesData();
@@ -18,11 +56,17 @@ function useSlides() {
18
56
  return slidesData.value.map((slide, index) => {
19
57
  const port = 3001 + index;
20
58
  const devServerUrl = `http://localhost:${port}`;
59
+ const background = slide.frontmatter.seoMeta?.ogImage || slide.frontmatter.background;
60
+ const imageUrl = background ? resolveBackgroundPath({
61
+ background: slide.frontmatter.background,
62
+ slidePath: slide.path,
63
+ devServerUrl
64
+ }) : "https://cover.sli.dev";
21
65
  return {
22
66
  title: slide.frontmatter.title || slide.path,
23
- url: process.env.NODE_ENV === "development" ? devServerUrl : slide.path,
67
+ url: IS_DEVELOPMENT ? devServerUrl : slide.path,
24
68
  description: slide.frontmatter.info || slide.frontmatter.seoMeta?.ogDescription || "No description available",
25
- image: slide.frontmatter.background || slide.frontmatter.seoMeta?.ogImage || "https://cover.sli.dev",
69
+ image: imageUrl,
26
70
  author: slide.frontmatter.author || "Unknown Author",
27
71
  date: slide.frontmatter.date || new Date().toISOString().split("T")[0],
28
72
  theme: slide.frontmatter.theme,
@@ -35,7 +79,8 @@ function useSlides() {
35
79
  return {
36
80
  slides,
37
81
  slidesCount,
38
- loadSlidesData
82
+ loadSlidesData,
83
+ isLoading
39
84
  };
40
85
  }
41
86
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slidev-workspace",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "A workspace tool for managing multiple Slidev presentations with API-based content management",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,46 @@
1
+ import type { SlideInfo } from "../../../types/slide";
2
+
3
+ const mockSlidesData: SlideInfo[] = [
4
+ {
5
+ id: "slide-1",
6
+ path: "/slides-presentation-1/",
7
+ fullPath: "/path/to/slides-presentation-1",
8
+ sourceDir: "/path/to/slides",
9
+ frontmatter: {
10
+ title: "My First Presentation",
11
+ background: "/bg1.jpg",
12
+ info: "This is my first presentation",
13
+ author: "John Doe",
14
+ date: "2024-01-15",
15
+ theme: "default",
16
+ transition: "fade",
17
+ class: "text-center",
18
+ },
19
+ content: "# Slide content",
20
+ },
21
+ {
22
+ id: "slide-2",
23
+ path: "/slides-presentation-2/",
24
+ fullPath: "/path/to/slides-presentation-2",
25
+ sourceDir: "/path/to/slides",
26
+ frontmatter: {
27
+ title: "Second Presentation",
28
+ background: "https://example.com/bg.jpg",
29
+ seoMeta: {
30
+ ogImage: "https://example.com/og-image.jpg",
31
+ ogDescription: "SEO description",
32
+ },
33
+ },
34
+ content: "# Another slide",
35
+ },
36
+ {
37
+ id: "slide-3",
38
+ path: "/slides-presentation-3/",
39
+ fullPath: "/path/to/slides-presentation-3",
40
+ sourceDir: "/path/to/slides",
41
+ frontmatter: {},
42
+ content: "# Minimal slide",
43
+ },
44
+ ];
45
+
46
+ export default mockSlidesData;
@@ -0,0 +1,222 @@
1
+ import {
2
+ describe,
3
+ it,
4
+ expect,
5
+ beforeEach,
6
+ vi,
7
+ afterEach,
8
+ beforeAll,
9
+ } from "vitest";
10
+
11
+ describe("useSlides (Development Mode)", () => {
12
+ beforeAll(async () => {
13
+ vi.resetModules();
14
+ vi.doMock("../../env", () => ({
15
+ IS_DEVELOPMENT: true,
16
+ }));
17
+ });
18
+
19
+ beforeEach(() => {
20
+ // Reset window.location
21
+ delete (window as unknown as { location: unknown }).location;
22
+ (window as unknown as { location: { origin: string } }).location = {
23
+ origin: "http://localhost:3000",
24
+ };
25
+
26
+ // Clear all mocks
27
+ vi.clearAllMocks();
28
+ });
29
+
30
+ afterEach(() => {
31
+ vi.restoreAllMocks();
32
+ });
33
+
34
+ /**
35
+ * Helper function to setup useSlides in development mode and wait for data to load
36
+ */
37
+ async function setupUseSlides() {
38
+ const { useSlides } = await import("./useSlides");
39
+ const result = useSlides();
40
+
41
+ // Wait for data to finish loading using vitest's waitFor
42
+ await vi.waitFor(
43
+ () => {
44
+ expect(result.isLoading.value).toBe(false);
45
+ },
46
+ { timeout: 1000, interval: 10 },
47
+ );
48
+
49
+ return result;
50
+ }
51
+
52
+ describe("slide data transformation", () => {
53
+ it("should transform slide data correctly with all frontmatter fields", async () => {
54
+ const { slides } = await setupUseSlides();
55
+
56
+ const firstSlide = slides.value[0];
57
+
58
+ expect(firstSlide.title).toBe("My First Presentation");
59
+ expect(firstSlide.description).toBe("This is my first presentation");
60
+ expect(firstSlide.author).toBe("John Doe");
61
+ expect(firstSlide.date).toBe("2024-01-15");
62
+ expect(firstSlide.theme).toBe("default");
63
+ expect(firstSlide.transition).toBe("fade");
64
+ expect(firstSlide.class).toBe("text-center");
65
+ });
66
+
67
+ it("should use path as title fallback", async () => {
68
+ const { slides } = await setupUseSlides();
69
+
70
+ const thirdSlide = slides.value[2];
71
+
72
+ expect(thirdSlide.title).toBe("/slides-presentation-3/");
73
+ });
74
+
75
+ it("should use default description when no info available", async () => {
76
+ const { slides } = await setupUseSlides();
77
+
78
+ const thirdSlide = slides.value[2];
79
+
80
+ expect(thirdSlide.description).toBe("No description available");
81
+ });
82
+
83
+ it("should use seoMeta.ogDescription as description fallback", async () => {
84
+ const { slides } = await setupUseSlides();
85
+
86
+ const secondSlide = slides.value[1];
87
+
88
+ expect(secondSlide.description).toBe("SEO description");
89
+ });
90
+
91
+ it("should use 'Unknown Author' as author fallback", async () => {
92
+ const { slides } = await setupUseSlides();
93
+
94
+ const thirdSlide = slides.value[2];
95
+
96
+ expect(thirdSlide.author).toBe("Unknown Author");
97
+ });
98
+
99
+ it("should use current date as date fallback", async () => {
100
+ const { slides } = await setupUseSlides();
101
+
102
+ const thirdSlide = slides.value[2];
103
+ const expectedDate = new Date().toISOString().split("T")[0];
104
+
105
+ expect(thirdSlide.date).toBe(expectedDate);
106
+ });
107
+ });
108
+
109
+ describe("URL generation", () => {
110
+ it("should generate correct dev server URLs with incremental ports", async () => {
111
+ const { slides } = await setupUseSlides();
112
+
113
+ expect(slides.value[0].url).toBe("http://localhost:3001");
114
+ expect(slides.value[1].url).toBe("http://localhost:3002");
115
+ expect(slides.value[2].url).toBe("http://localhost:3003");
116
+ });
117
+ });
118
+
119
+ describe("background image resolution", () => {
120
+ it("should keep absolute URL backgrounds unchanged", async () => {
121
+ const { slides } = await setupUseSlides();
122
+
123
+ const secondSlide = slides.value[1];
124
+
125
+ expect(secondSlide.image).toBe("https://example.com/bg.jpg");
126
+ });
127
+
128
+ it("should resolve relative background paths in development mode", async () => {
129
+ const { slides } = await setupUseSlides();
130
+
131
+ const firstSlide = slides.value[0];
132
+
133
+ expect(firstSlide.image).toBe(
134
+ "http://localhost:3001/slides-presentation-1/bg1.jpg",
135
+ );
136
+ });
137
+
138
+ it("should use default cover when no background is provided", async () => {
139
+ const { slides } = await setupUseSlides();
140
+
141
+ const thirdSlide = slides.value[2];
142
+
143
+ expect(thirdSlide.image).toBe("https://cover.sli.dev");
144
+ });
145
+ });
146
+ });
147
+
148
+ describe("useSlides (Production Mode)", () => {
149
+ beforeAll(async () => {
150
+ vi.resetModules();
151
+ vi.doMock("../../env", () => ({
152
+ IS_DEVELOPMENT: false,
153
+ }));
154
+ });
155
+
156
+ beforeEach(() => {
157
+ // Reset window.location
158
+ delete (window as unknown as { location: unknown }).location;
159
+ (window as unknown as { location: { origin: string } }).location = {
160
+ origin: "https://my-slides.com",
161
+ };
162
+
163
+ // Clear all mocks
164
+ vi.clearAllMocks();
165
+ });
166
+
167
+ afterEach(() => {
168
+ vi.restoreAllMocks();
169
+ });
170
+
171
+ /**
172
+ * Helper to setup production mode useSlides
173
+ */
174
+ async function setupUseSlidesProduction() {
175
+ const { useSlides: useSlidesProduction } = await import("./useSlides");
176
+ const result = useSlidesProduction();
177
+
178
+ await vi.waitFor(
179
+ () => {
180
+ expect(result.isLoading.value).toBe(false);
181
+ },
182
+ { timeout: 1000, interval: 10 },
183
+ );
184
+
185
+ return result;
186
+ }
187
+
188
+ describe("URL generation in production", () => {
189
+ it("should use slide path as URL in production mode", async () => {
190
+ const result = await setupUseSlidesProduction();
191
+
192
+ expect(result.slides.value[0].url).toBe("/slides-presentation-1/");
193
+ expect(result.slides.value[1].url).toBe("/slides-presentation-2/");
194
+ expect(result.slides.value[2].url).toBe("/slides-presentation-3/");
195
+ });
196
+ });
197
+
198
+ describe("background image resolution in production", () => {
199
+ it("should resolve relative background paths with window.location.origin", async () => {
200
+ const result = await setupUseSlidesProduction();
201
+ const firstSlide = result.slides.value[0];
202
+
203
+ expect(firstSlide.image).toBe(
204
+ "https://my-slides.com/slides-presentation-1/bg1.jpg",
205
+ );
206
+ });
207
+
208
+ it("should keep absolute URL backgrounds unchanged in production", async () => {
209
+ const result = await setupUseSlidesProduction();
210
+ const secondSlide = result.slides.value[1];
211
+
212
+ expect(secondSlide.image).toBe("https://example.com/bg.jpg");
213
+ });
214
+
215
+ it("should use default cover when no background in production", async () => {
216
+ const result = await setupUseSlidesProduction();
217
+ const thirdSlide = result.slides.value[2];
218
+
219
+ expect(thirdSlide.image).toBe("https://cover.sli.dev");
220
+ });
221
+ });
222
+ });
@@ -1,8 +1,58 @@
1
1
  import { computed, ref } from "vue";
2
- import type { SlideData, SlideInfo } from "../../types/slide.js";
2
+ import type { SlideData, SlideInfo } from "../../types/slide";
3
+ import { IS_DEVELOPMENT } from "../../env";
4
+
5
+ /**
6
+ * Check if a string is a valid URL
7
+ */
8
+ function isUrl(str: string | undefined): boolean {
9
+ if (!str) return false;
10
+
11
+ try {
12
+ new URL(str);
13
+ return true;
14
+ } catch {
15
+ return false;
16
+ }
17
+ }
18
+
19
+ /**
20
+ * Resolve background image path
21
+ * If the background is not a URL, construct the full path using slide.path as base
22
+ */
23
+ function resolveBackgroundPath(params: {
24
+ background: string | undefined;
25
+ slidePath: string;
26
+ devServerUrl: string;
27
+ }): string {
28
+ const { background, slidePath, devServerUrl } = params;
29
+
30
+ if (!background) return "";
31
+
32
+ if (isUrl(background)) {
33
+ return background;
34
+ }
35
+
36
+ try {
37
+ const domain = IS_DEVELOPMENT ? devServerUrl : window.location.origin;
38
+ // Ensure slidePath ends with / for proper path resolution
39
+ const basePath = slidePath.endsWith("/") ? slidePath : slidePath + "/";
40
+ // Remove leading / from background to treat it as relative
41
+ const relativeBg = background.startsWith("/")
42
+ ? background.slice(1)
43
+ : background;
44
+ const resolvedUrl = new URL(basePath + relativeBg, domain);
45
+
46
+ return resolvedUrl.href;
47
+ } catch (error) {
48
+ console.error("Failed to resolve background path:", error);
49
+ return background;
50
+ }
51
+ }
3
52
 
4
53
  export function useSlides() {
5
54
  const slidesData = ref<SlideInfo[]>([]);
55
+ const isLoading = ref(true);
6
56
 
7
57
  // Dynamically import slidev:content to avoid build-time issues
8
58
  const loadSlidesData = async () => {
@@ -12,6 +62,8 @@ export function useSlides() {
12
62
  } catch (error) {
13
63
  console.warn("Failed to load slides data:", error);
14
64
  slidesData.value = [];
65
+ } finally {
66
+ isLoading.value = false;
15
67
  }
16
68
  };
17
69
 
@@ -27,17 +79,25 @@ export function useSlides() {
27
79
  // Create dev server URL
28
80
  const devServerUrl = `http://localhost:${port}`;
29
81
 
82
+ // Resolve background image path
83
+ const background =
84
+ slide.frontmatter.seoMeta?.ogImage || slide.frontmatter.background;
85
+ const imageUrl = background
86
+ ? resolveBackgroundPath({
87
+ background: slide.frontmatter.background,
88
+ slidePath: slide.path,
89
+ devServerUrl,
90
+ })
91
+ : "https://cover.sli.dev";
92
+
30
93
  return {
31
94
  title: slide.frontmatter.title || slide.path,
32
- url: process.env.NODE_ENV === "development" ? devServerUrl : slide.path,
95
+ url: IS_DEVELOPMENT ? devServerUrl : slide.path,
33
96
  description:
34
97
  slide.frontmatter.info ||
35
98
  slide.frontmatter.seoMeta?.ogDescription ||
36
99
  "No description available",
37
- image:
38
- slide.frontmatter.background ||
39
- slide.frontmatter.seoMeta?.ogImage ||
40
- "https://cover.sli.dev",
100
+ image: imageUrl,
41
101
  author: slide.frontmatter.author || "Unknown Author",
42
102
  date: slide.frontmatter.date || new Date().toISOString().split("T")[0],
43
103
  theme: slide.frontmatter.theme,
@@ -53,5 +113,6 @@ export function useSlides() {
53
113
  slides,
54
114
  slidesCount,
55
115
  loadSlidesData,
116
+ isLoading,
56
117
  };
57
118
  }