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
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:
|
|
67
|
+
url: IS_DEVELOPMENT ? devServerUrl : slide.path,
|
|
24
68
|
description: slide.frontmatter.info || slide.frontmatter.seoMeta?.ogDescription || "No description available",
|
|
25
|
-
image:
|
|
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
|
@@ -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
|
|
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:
|
|
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
|
}
|