supaslidev 0.1.4 → 0.2.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.
Files changed (67) hide show
  1. package/app/app.config.ts +9 -0
  2. package/app/assets/css/main.css +90 -0
  3. package/app/components/AppHeader.vue +429 -0
  4. package/app/components/CreatePresentationDialog.vue +236 -0
  5. package/app/components/EmptyState.vue +37 -0
  6. package/app/components/ImportPresentationDialog.vue +865 -0
  7. package/app/components/PresentationCard.vue +343 -0
  8. package/app/components/PresentationListItem.vue +242 -0
  9. package/app/composables/useServers.ts +148 -0
  10. package/app/layouts/default.vue +49 -0
  11. package/app/pages/index.vue +542 -0
  12. package/dist/cli/index.js +183751 -137
  13. package/dist/config.d.ts +8 -0
  14. package/dist/config.js +16 -0
  15. package/dist/index.d.ts +21 -0
  16. package/dist/index.js +3 -0
  17. package/dist/module.d.ts +6 -0
  18. package/dist/module.js +9168 -0
  19. package/dist/prompt.js +847 -0
  20. package/nuxt.config.ts +53 -0
  21. package/package.json +26 -19
  22. package/server/api/export/[id].post.ts +67 -0
  23. package/server/api/open-editor/[id].post.ts +28 -0
  24. package/server/api/presentations/import.post.ts +139 -0
  25. package/server/api/presentations/index.get.ts +18 -0
  26. package/server/api/presentations/index.post.ts +175 -0
  27. package/server/api/presentations/upload.post.ts +174 -0
  28. package/server/api/presentations/validate.post.ts +14 -0
  29. package/server/api/servers/[id].delete.ts +15 -0
  30. package/server/api/servers/[id].post.ts +17 -0
  31. package/server/api/servers/index.delete.ts +5 -0
  32. package/server/api/servers/index.get.ts +5 -0
  33. package/server/api/servers/stop-all.post.ts +5 -0
  34. package/server/plugins/generate.ts +12 -0
  35. package/server/plugins/shutdown.ts +16 -0
  36. package/server/routes/exports/[...path].get.ts +25 -0
  37. package/server/utils/config.ts +13 -0
  38. package/server/utils/process-manager.ts +119 -0
  39. package/src/cli/commands/create.ts +125 -0
  40. package/src/cli/commands/deploy.ts +90 -0
  41. package/src/cli/commands/dev.ts +116 -0
  42. package/src/cli/commands/export.ts +63 -0
  43. package/src/cli/commands/import.ts +178 -0
  44. package/src/cli/commands/present.ts +111 -0
  45. package/src/cli/index.ts +87 -0
  46. package/src/cli/utils.ts +94 -0
  47. package/src/config.ts +21 -0
  48. package/src/index.ts +2 -0
  49. package/src/module.ts +12 -0
  50. package/src/shared/catalog.ts +94 -0
  51. package/src/shared/copy.ts +28 -0
  52. package/src/shared/index.ts +29 -0
  53. package/{scripts/generate-presentations.mjs → src/shared/presentations.ts} +23 -46
  54. package/src/shared/types.ts +29 -0
  55. package/src/shared/validation.ts +111 -0
  56. package/dist/assets/index-BerY9FcI.js +0 -49
  57. package/dist/assets/index-CVzsY-on.css +0 -1
  58. package/dist/index.html +0 -24
  59. package/server/api.js +0 -1225
  60. /package/{dist → public}/apple-touch-icon.png +0 -0
  61. /package/{dist → public}/favicon-96x96.png +0 -0
  62. /package/{dist → public}/favicon.ico +0 -0
  63. /package/{dist → public}/favicon.svg +0 -0
  64. /package/{dist → public}/site.webmanifest +0 -0
  65. /package/{dist → public}/ssl-logo.png +0 -0
  66. /package/{dist → public}/web-app-manifest-192x192.png +0 -0
  67. /package/{dist → public}/web-app-manifest-512x512.png +0 -0
@@ -0,0 +1,174 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { join, dirname, normalize, resolve, sep } from 'node:path';
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
4
+ import {
5
+ SLUG_REGEX,
6
+ shouldIgnore,
7
+ convertToCatalogDependencies,
8
+ hasSharedPackage,
9
+ addSharedDependencyToPackageJson,
10
+ addSharedAddonToSlides,
11
+ regeneratePresentationsJson,
12
+ } from '../../../src/shared/index.js';
13
+ import { getProjectRoot, getPresentationsDir, getPresentationsJsonPath } from '../../utils/config';
14
+
15
+ export default defineEventHandler(async (event) => {
16
+ const body = await readBody(event);
17
+ const { files, name, folderName } = body;
18
+
19
+ if (!Array.isArray(files) || files.length === 0) {
20
+ throw createError({
21
+ statusCode: 400,
22
+ data: { field: 'files', message: 'No files provided' },
23
+ });
24
+ }
25
+
26
+ const hasSlides = files.some((f: { path: string }) => f.path === 'slides.md');
27
+ const hasPackageJson = files.some((f: { path: string }) => f.path === 'package.json');
28
+
29
+ if (!hasSlides) {
30
+ throw createError({
31
+ statusCode: 400,
32
+ data: { field: 'files', message: 'No slides.md found in uploaded files' },
33
+ });
34
+ }
35
+
36
+ if (!hasPackageJson) {
37
+ throw createError({
38
+ statusCode: 400,
39
+ data: { field: 'files', message: 'No package.json found in uploaded files' },
40
+ });
41
+ }
42
+
43
+ const projectRoot = getProjectRoot();
44
+ const presentationsDir = getPresentationsDir();
45
+
46
+ const presentationName =
47
+ name ||
48
+ (folderName || 'presentation')
49
+ .toLowerCase()
50
+ .replace(/[^a-z0-9-]/g, '-')
51
+ .replace(/-{2,}/g, '-')
52
+ .replace(/^-+|-+$/g, '')
53
+ || 'presentation';
54
+
55
+ if (!SLUG_REGEX.test(presentationName)) {
56
+ throw createError({
57
+ statusCode: 400,
58
+ data: {
59
+ field: 'name',
60
+ message: 'Name must be a valid slug (lowercase letters, numbers, hyphens only)',
61
+ },
62
+ });
63
+ }
64
+
65
+ const destinationPath = join(presentationsDir, presentationName);
66
+
67
+ if (existsSync(destinationPath)) {
68
+ throw createError({
69
+ statusCode: 400,
70
+ data: { field: 'name', message: 'A presentation with this name already exists' },
71
+ });
72
+ }
73
+
74
+ mkdirSync(destinationPath, { recursive: true });
75
+
76
+ const resolvedDest = resolve(destinationPath);
77
+ for (const file of files) {
78
+ if (shouldIgnore(file.path.split('/')[0])) {
79
+ continue;
80
+ }
81
+
82
+ const normalizedPath = normalize(file.path).replace(/^(\.\.[\\/])+/, '');
83
+ if (normalizedPath.includes('..') || normalizedPath.startsWith('/') || normalizedPath.includes('\0')) {
84
+ continue;
85
+ }
86
+ const filePath = resolve(destinationPath, normalizedPath);
87
+ if (!filePath.startsWith(resolvedDest + sep) && filePath !== resolvedDest) {
88
+ continue;
89
+ }
90
+ const fileDir = dirname(filePath);
91
+
92
+ if (!existsSync(fileDir)) {
93
+ mkdirSync(fileDir, { recursive: true });
94
+ }
95
+
96
+ if (file.encoding === 'base64') {
97
+ writeFileSync(filePath, Buffer.from(file.content, 'base64'));
98
+ } else {
99
+ writeFileSync(filePath, file.content, 'utf-8');
100
+ }
101
+ }
102
+
103
+ const packageJsonPath = join(destinationPath, 'package.json');
104
+ let packageJson: Record<string, unknown>;
105
+ try {
106
+ const packageJsonContent = readFileSync(packageJsonPath, 'utf-8');
107
+ packageJson = JSON.parse(packageJsonContent);
108
+ } catch {
109
+ throw createError({
110
+ statusCode: 400,
111
+ data: { field: 'files', message: 'Malformed package.json in uploaded files' },
112
+ });
113
+ }
114
+
115
+ packageJson.name = `@supaslidev/${presentationName}`;
116
+ packageJson.private = true;
117
+ packageJson.scripts = {
118
+ dev: 'slidev --open',
119
+ build: 'slidev build',
120
+ export: 'slidev export',
121
+ };
122
+
123
+ if (packageJson.dependencies) {
124
+ packageJson.dependencies = convertToCatalogDependencies(packageJson.dependencies);
125
+ }
126
+ if (packageJson.devDependencies) {
127
+ packageJson.devDependencies = convertToCatalogDependencies(packageJson.devDependencies);
128
+ }
129
+
130
+ const sharedExists = hasSharedPackage(projectRoot);
131
+ if (sharedExists) {
132
+ addSharedDependencyToPackageJson(packageJson);
133
+ }
134
+
135
+ writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n');
136
+
137
+ if (sharedExists) {
138
+ const slidesPath = join(destinationPath, 'slides.md');
139
+ if (existsSync(slidesPath)) {
140
+ addSharedAddonToSlides(slidesPath);
141
+ }
142
+ }
143
+
144
+ regeneratePresentationsJson(presentationsDir, getPresentationsJsonPath());
145
+
146
+ const presentation = {
147
+ id: presentationName,
148
+ title: presentationName,
149
+ description: '',
150
+ theme: 'default',
151
+ background: 'https://cover.sli.dev',
152
+ duration: '',
153
+ };
154
+
155
+ // Run pnpm install in background
156
+ const install = spawn('pnpm', ['install'], {
157
+ cwd: projectRoot,
158
+ stdio: 'inherit',
159
+ shell: true,
160
+ });
161
+
162
+ install.on('close', (code: number | null) => {
163
+ if (code !== 0) {
164
+ console.error(`[upload] pnpm install failed with code ${code}`);
165
+ }
166
+ });
167
+
168
+ install.on('error', (err) => {
169
+ console.error(`[upload] pnpm install spawn error: ${err.message}`);
170
+ });
171
+
172
+ setResponseStatus(event, 201);
173
+ return presentation;
174
+ });
@@ -0,0 +1,14 @@
1
+ import { validatePaths } from '../../../src/shared/index.js';
2
+
3
+ export default defineEventHandler(async (event) => {
4
+ const body = await readBody(event);
5
+
6
+ if (!body || !Array.isArray(body.paths)) {
7
+ throw createError({
8
+ statusCode: 400,
9
+ data: { message: 'paths must be an array' },
10
+ });
11
+ }
12
+
13
+ return validatePaths(body.paths);
14
+ });
@@ -0,0 +1,15 @@
1
+ import { stopPresentationServer } from '../../utils/process-manager';
2
+
3
+ export default defineEventHandler((event) => {
4
+ const id = getRouterParam(event, 'id');
5
+ if (!id) {
6
+ throw createError({ statusCode: 400, data: { message: 'Missing server id' } });
7
+ }
8
+ const result = stopPresentationServer(id);
9
+
10
+ if (!result.success) {
11
+ throw createError({ statusCode: 404, data: result });
12
+ }
13
+
14
+ return result;
15
+ });
@@ -0,0 +1,17 @@
1
+ import { startPresentationServer } from '../../utils/process-manager';
2
+ import { getProjectRoot } from '../../utils/config';
3
+
4
+ export default defineEventHandler((event) => {
5
+ const id = getRouterParam(event, 'id');
6
+ if (!id) {
7
+ throw createError({ statusCode: 400, data: { message: 'Missing server id' } });
8
+ }
9
+ const projectRoot = getProjectRoot();
10
+ const result = startPresentationServer(id, projectRoot);
11
+
12
+ if (!result.success) {
13
+ throw createError({ statusCode: 500, data: result });
14
+ }
15
+
16
+ return result;
17
+ });
@@ -0,0 +1,5 @@
1
+ import { stopAllPresentationServers } from '../../utils/process-manager';
2
+
3
+ export default defineEventHandler(() => {
4
+ return stopAllPresentationServers();
5
+ });
@@ -0,0 +1,5 @@
1
+ import { getServersStatus } from '../../utils/process-manager';
2
+
3
+ export default defineEventHandler(() => {
4
+ return getServersStatus();
5
+ });
@@ -0,0 +1,5 @@
1
+ import { stopAllPresentationServers } from '../../utils/process-manager';
2
+
3
+ export default defineEventHandler(() => {
4
+ return stopAllPresentationServers();
5
+ });
@@ -0,0 +1,12 @@
1
+ import { regeneratePresentationsJson } from '../../src/shared/presentations.js';
2
+ import { getPresentationsDir, getPresentationsJsonPath } from '../utils/config';
3
+
4
+ export default defineNitroPlugin(() => {
5
+ const presentationsDir = getPresentationsDir();
6
+ const presentationsJsonPath = getPresentationsJsonPath();
7
+
8
+ console.log(`[generate] presentationsDir: ${presentationsDir}`);
9
+ console.log(`[generate] presentationsJsonPath: ${presentationsJsonPath}`);
10
+
11
+ regeneratePresentationsJson(presentationsDir, presentationsJsonPath);
12
+ });
@@ -0,0 +1,16 @@
1
+ import { stopAllPresentationServers } from '../utils/process-manager';
2
+
3
+ export default defineNitroPlugin((nitroApp) => {
4
+ const cleanup = () => {
5
+ console.log('\nShutting down servers...');
6
+ stopAllPresentationServers();
7
+ process.exit(0);
8
+ };
9
+
10
+ process.on('SIGINT', cleanup);
11
+ process.on('SIGTERM', cleanup);
12
+
13
+ nitroApp.hooks.hook('close', () => {
14
+ stopAllPresentationServers();
15
+ });
16
+ });
@@ -0,0 +1,25 @@
1
+ import { join, basename, relative } from 'node:path';
2
+ import { existsSync, createReadStream } from 'node:fs';
3
+ import { getProjectRoot } from '../../utils/config';
4
+
5
+ export default defineEventHandler((event) => {
6
+ const path = getRouterParam(event, 'path') || '';
7
+ const projectRoot = getProjectRoot();
8
+ const exportsDir = join(projectRoot, 'exports');
9
+ const filePath = join(exportsDir, path);
10
+
11
+ const rel = relative(exportsDir, filePath);
12
+ if (
13
+ rel.startsWith('..') ||
14
+ !filePath.startsWith(exportsDir) ||
15
+ !existsSync(filePath) ||
16
+ !filePath.endsWith('.pdf')
17
+ ) {
18
+ throw createError({ statusCode: 404, message: 'Not found' });
19
+ }
20
+
21
+ setHeader(event, 'Content-Type', 'application/pdf');
22
+ setHeader(event, 'Content-Disposition', `inline; filename="${basename(filePath)}"`);
23
+
24
+ return sendStream(event, createReadStream(filePath));
25
+ });
@@ -0,0 +1,13 @@
1
+ import { join } from 'node:path';
2
+
3
+ export function getProjectRoot(): string {
4
+ return process.env.SUPASLIDEV_PROJECT_ROOT || join(process.cwd());
5
+ }
6
+
7
+ export function getPresentationsDir(): string {
8
+ return process.env.SUPASLIDEV_PRESENTATIONS_DIR || join(getProjectRoot(), 'presentations');
9
+ }
10
+
11
+ export function getPresentationsJsonPath(): string {
12
+ return join(getProjectRoot(), '.supaslidev', 'presentations.json');
13
+ }
@@ -0,0 +1,119 @@
1
+ import { spawn, execSync, type ChildProcess } from 'node:child_process';
2
+ import { join } from 'node:path';
3
+ import { existsSync } from 'node:fs';
4
+ import { isValidPresentationId } from '../../src/shared/validation.js';
5
+
6
+ const IS_WINDOWS = process.platform === 'win32';
7
+
8
+ interface ServerEntry {
9
+ process: ChildProcess;
10
+ port: number;
11
+ }
12
+
13
+ const runningServers = new Map<string, ServerEntry>();
14
+
15
+ export function getNextPort(): number {
16
+ const usedPorts = new Set([...runningServers.values()].map((s) => s.port));
17
+ let port = 3030;
18
+ while (usedPorts.has(port)) {
19
+ port++;
20
+ }
21
+ return port;
22
+ }
23
+
24
+ export function startPresentationServer(
25
+ presentationId: string,
26
+ projectRoot: string,
27
+ ): { success: boolean; port?: number; error?: string; alreadyRunning?: boolean } {
28
+ if (!isValidPresentationId(presentationId)) {
29
+ return { success: false, error: 'Invalid presentation id' };
30
+ }
31
+
32
+ if (runningServers.has(presentationId)) {
33
+ return { success: true, port: runningServers.get(presentationId)!.port, alreadyRunning: true };
34
+ }
35
+
36
+ const presentationPath = join(projectRoot, 'presentations', presentationId);
37
+ const slidevBin = join(presentationPath, 'node_modules', '.bin', 'slidev');
38
+
39
+ if (!existsSync(presentationPath)) {
40
+ return { success: false, error: 'Presentation not found' };
41
+ }
42
+
43
+ if (!existsSync(slidevBin)) {
44
+ return { success: false, error: 'Dependencies not installed. Run pnpm install first.' };
45
+ }
46
+
47
+ const port = getNextPort();
48
+
49
+ const child = spawn(slidevBin, ['--port', String(port), '--open', 'false'], {
50
+ cwd: presentationPath,
51
+ stdio: ['pipe', 'pipe', 'pipe'],
52
+ detached: !IS_WINDOWS,
53
+ shell: IS_WINDOWS,
54
+ });
55
+
56
+ child.unref();
57
+
58
+ runningServers.set(presentationId, { process: child, port });
59
+
60
+ child.stderr?.on('data', (data: Buffer) => {
61
+ console.error(`[${presentationId}] stderr:`, data.toString());
62
+ });
63
+
64
+ child.on('close', (code: number | null) => {
65
+ console.log(`[${presentationId}] process exited with code ${code}`);
66
+ runningServers.delete(presentationId);
67
+ });
68
+
69
+ child.on('error', (err: Error) => {
70
+ console.error(`Failed to start server for ${presentationId}:`, err);
71
+ runningServers.delete(presentationId);
72
+ });
73
+
74
+ return { success: true, port, alreadyRunning: false };
75
+ }
76
+
77
+ export function stopPresentationServer(presentationId: string): {
78
+ success: boolean;
79
+ error?: string;
80
+ } {
81
+ const server = runningServers.get(presentationId);
82
+ if (!server) {
83
+ return { success: false, error: 'Server not running' };
84
+ }
85
+
86
+ if (IS_WINDOWS) {
87
+ try {
88
+ execSync(`taskkill /pid ${server.process.pid} /T /F`, { stdio: 'ignore' });
89
+ } catch {
90
+ // Process may have already exited
91
+ }
92
+ } else {
93
+ server.process.kill('SIGTERM');
94
+ }
95
+ runningServers.delete(presentationId);
96
+ return { success: true };
97
+ }
98
+
99
+ export function stopAllPresentationServers(): {
100
+ success: boolean;
101
+ stopped: string[];
102
+ } {
103
+ const stopped: string[] = [];
104
+ for (const [id] of runningServers) {
105
+ const result = stopPresentationServer(id);
106
+ if (result.success) {
107
+ stopped.push(id);
108
+ }
109
+ }
110
+ return { success: true, stopped };
111
+ }
112
+
113
+ export function getServersStatus(): Record<string, { port: number }> {
114
+ const servers: Record<string, { port: number }> = {};
115
+ for (const [id, server] of runningServers) {
116
+ servers[id] = { port: server.port };
117
+ }
118
+ return servers;
119
+ }
@@ -0,0 +1,125 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { join } from 'node:path';
3
+ import { existsSync, readFileSync, writeFileSync, readdirSync, statSync } from 'node:fs';
4
+ import { findProjectRoot } from '../utils.js';
5
+ import {
6
+ validateName,
7
+ hasSharedPackage,
8
+ addSharedAddonToSlides,
9
+ addSharedDependencyToPackageJson,
10
+ } from '../../shared/index.js';
11
+
12
+ function checkDuplicateName(presentationsDir: string, name: string): void {
13
+ const presentationPath = join(presentationsDir, name);
14
+ if (existsSync(presentationPath)) {
15
+ throw new Error(`Presentation "${name}" already exists`);
16
+ }
17
+ }
18
+
19
+ function getExistingPresentations(presentationsDir: string): Set<string> {
20
+ if (!existsSync(presentationsDir)) return new Set();
21
+ return new Set(
22
+ readdirSync(presentationsDir).filter((name) => {
23
+ const fullPath = join(presentationsDir, name);
24
+ return statSync(fullPath).isDirectory();
25
+ }),
26
+ );
27
+ }
28
+
29
+ function configureSharedPackage(projectRoot: string, presentationDir: string): void {
30
+ if (!hasSharedPackage(projectRoot)) return;
31
+
32
+ const slidesPath = join(presentationDir, 'slides.md');
33
+ const packageJsonPath = join(presentationDir, 'package.json');
34
+
35
+ if (existsSync(slidesPath)) {
36
+ addSharedAddonToSlides(slidesPath);
37
+ }
38
+
39
+ if (existsSync(packageJsonPath)) {
40
+ const content = readFileSync(packageJsonPath, 'utf-8');
41
+ const packageJson = JSON.parse(content);
42
+ addSharedDependencyToPackageJson(packageJson);
43
+ writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n');
44
+ }
45
+ }
46
+
47
+ export async function create(name?: string): Promise<void> {
48
+ const projectRoot = findProjectRoot();
49
+
50
+ if (!projectRoot) {
51
+ console.error('Error: Could not find a Supaslidev project.');
52
+ console.error('Make sure you are in a directory with a "presentations" folder.');
53
+ process.exit(1);
54
+ }
55
+
56
+ const presentationsDir = join(projectRoot, 'presentations');
57
+
58
+ if (!existsSync(presentationsDir)) {
59
+ console.error(`Error: No "presentations" folder found at ${presentationsDir}`);
60
+ process.exit(1);
61
+ }
62
+
63
+ if (name) {
64
+ try {
65
+ validateName(name);
66
+ checkDuplicateName(presentationsDir, name);
67
+ } catch (err) {
68
+ console.error(`Error: ${err instanceof Error ? err.message : 'Invalid name'}`);
69
+ process.exit(1);
70
+ }
71
+ }
72
+
73
+ const existingPresentations = getExistingPresentations(presentationsDir);
74
+
75
+ const args = ['create', 'slidev'];
76
+ if (name) {
77
+ args.push(name);
78
+ }
79
+
80
+ console.log(`Creating new presentation in: ${presentationsDir}`);
81
+
82
+ return new Promise((resolve, reject) => {
83
+ const child = spawn('pnpm', args, {
84
+ cwd: presentationsDir,
85
+ stdio: 'inherit',
86
+ });
87
+
88
+ child.on('close', (code) => {
89
+ if (code !== 0) {
90
+ console.error(`Failed to create presentation (exit code: ${code})`);
91
+ reject(new Error(`Process exited with code ${code}`));
92
+ return;
93
+ }
94
+
95
+ const presentationName = name ?? findNewPresentation(presentationsDir, existingPresentations);
96
+
97
+ if (presentationName) {
98
+ const presentationDir = join(presentationsDir, presentationName);
99
+ configureSharedPackage(projectRoot, presentationDir);
100
+ }
101
+
102
+ console.log('\nPresentation created successfully!');
103
+ console.log('Run "supaslidev present <name>" to start a dev server for your presentation.');
104
+ resolve();
105
+ });
106
+
107
+ child.on('error', (err) => {
108
+ console.error('Failed to create presentation:', err.message);
109
+ reject(err);
110
+ });
111
+ });
112
+ }
113
+
114
+ function findNewPresentation(
115
+ presentationsDir: string,
116
+ existingPresentations: Set<string>,
117
+ ): string | null {
118
+ const currentPresentations = getExistingPresentations(presentationsDir);
119
+ for (const name of currentPresentations) {
120
+ if (!existingPresentations.has(name)) {
121
+ return name;
122
+ }
123
+ }
124
+ return null;
125
+ }
@@ -0,0 +1,90 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { join } from 'node:path';
3
+ import { existsSync, mkdirSync, cpSync, rmSync, writeFileSync } from 'node:fs';
4
+ import {
5
+ findProjectRoot,
6
+ getPresentations,
7
+ printAvailablePresentations,
8
+ createVercelConfig,
9
+ createNetlifyConfig,
10
+ createDeployPackageJson,
11
+ } from '../utils.js';
12
+
13
+ export interface DeployOptions {
14
+ output?: string;
15
+ }
16
+
17
+ export async function deploy(name: string, options: DeployOptions = {}): Promise<void> {
18
+ const projectRoot = findProjectRoot();
19
+
20
+ if (!projectRoot) {
21
+ console.error('Error: Could not find a Supaslidev project.');
22
+ console.error('Make sure you are in a directory with a "presentations" folder.');
23
+ process.exit(1);
24
+ }
25
+
26
+ const presentationsDir = join(projectRoot, 'presentations');
27
+ const deployDir = join(projectRoot, 'deploy');
28
+ const presentations = getPresentations(presentationsDir);
29
+
30
+ if (!presentations.includes(name)) {
31
+ console.error(`Error: Presentation "${name}" not found`);
32
+ printAvailablePresentations(presentations);
33
+ process.exit(1);
34
+ }
35
+
36
+ const presentationDir = join(presentationsDir, name);
37
+ const presentationDistDir = join(presentationDir, 'dist');
38
+ const outputDir = options.output ?? join(deployDir, name);
39
+ const outputDistDir = join(outputDir, 'dist');
40
+
41
+ console.log('\n' + '='.repeat(50));
42
+ console.log(` Preparing deployment: ${name}`);
43
+ console.log('='.repeat(50) + '\n');
44
+
45
+ console.log('Step 1/3: Building presentation...');
46
+
47
+ const slidev = spawn('npx', ['slidev', 'build'], {
48
+ cwd: presentationDir,
49
+ stdio: 'inherit',
50
+ shell: true,
51
+ });
52
+
53
+ slidev.on('error', (err) => {
54
+ console.error(`Failed to build presentation: ${err.message}`);
55
+ process.exit(1);
56
+ });
57
+
58
+ slidev.on('close', (code) => {
59
+ if (code !== 0) {
60
+ console.error(`\nBuild failed with exit code ${code}`);
61
+ process.exit(code ?? 1);
62
+ }
63
+
64
+ console.log('\nStep 2/3: Creating deploy package...');
65
+
66
+ if (existsSync(outputDir)) {
67
+ rmSync(outputDir, { recursive: true });
68
+ }
69
+ mkdirSync(outputDir, { recursive: true });
70
+
71
+ cpSync(presentationDistDir, outputDistDir, { recursive: true });
72
+
73
+ console.log('Step 3/3: Adding deployment configurations...');
74
+
75
+ writeFileSync(join(outputDir, 'vercel.json'), createVercelConfig());
76
+ writeFileSync(join(outputDir, 'netlify.toml'), createNetlifyConfig());
77
+ writeFileSync(join(outputDir, 'package.json'), createDeployPackageJson(name));
78
+
79
+ console.log('\n' + '='.repeat(50));
80
+ console.log(' Deployment package ready!');
81
+ console.log('='.repeat(50));
82
+ console.log(`\n Output: ${outputDir}/`);
83
+ console.log('\n Deploy with Vercel:');
84
+ console.log(` cd ${outputDir} && vercel`);
85
+ console.log('\n Deploy with Netlify:');
86
+ console.log(` cd ${outputDir} && netlify deploy --prod`);
87
+ console.log('\n Or push to Git and import in Vercel/Netlify dashboard.');
88
+ console.log('');
89
+ });
90
+ }