@vue-skuilder/cli 0.1.7 → 0.1.8-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.
- package/README.md +122 -8
- package/dist/cli.d.ts +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +2 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +34 -3
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/pack.d.ts +10 -0
- package/dist/commands/pack.d.ts.map +1 -1
- package/dist/commands/pack.js +1 -1
- package/dist/commands/pack.js.map +1 -1
- package/dist/commands/studio.d.ts.map +1 -1
- package/dist/commands/studio.js +640 -105
- package/dist/commands/studio.js.map +1 -1
- package/dist/commands/unpack.d.ts +12 -0
- package/dist/commands/unpack.d.ts.map +1 -1
- package/dist/commands/unpack.js +1 -1
- package/dist/commands/unpack.js.map +1 -1
- package/dist/express-assets/app.d.ts +6 -0
- package/dist/express-assets/app.d.ts.map +1 -0
- package/dist/express-assets/app.js +209 -0
- package/dist/express-assets/app.js.map +1 -0
- package/dist/express-assets/assets/classroomDesignDoc.js +24 -0
- package/dist/express-assets/assets/courseValidateDocUpdate.js +56 -0
- package/dist/express-assets/assets/get-tagsDesignDoc.json +9 -0
- package/dist/express-assets/attachment-preprocessing/index.d.ts +11 -0
- package/dist/express-assets/attachment-preprocessing/index.d.ts.map +1 -0
- package/dist/express-assets/attachment-preprocessing/index.js +204 -0
- package/dist/express-assets/attachment-preprocessing/index.js.map +1 -0
- package/dist/express-assets/attachment-preprocessing/normalize.d.ts +7 -0
- package/dist/express-assets/attachment-preprocessing/normalize.d.ts.map +1 -0
- package/dist/express-assets/attachment-preprocessing/normalize.js +90 -0
- package/dist/express-assets/attachment-preprocessing/normalize.js.map +1 -0
- package/dist/express-assets/client-requests/classroom-requests.d.ts +26 -0
- package/dist/express-assets/client-requests/classroom-requests.d.ts.map +1 -0
- package/dist/express-assets/client-requests/classroom-requests.js +171 -0
- package/dist/express-assets/client-requests/classroom-requests.js.map +1 -0
- package/dist/express-assets/client-requests/course-requests.d.ts +10 -0
- package/dist/express-assets/client-requests/course-requests.d.ts.map +1 -0
- package/dist/express-assets/client-requests/course-requests.js +135 -0
- package/dist/express-assets/client-requests/course-requests.js.map +1 -0
- package/dist/express-assets/client-requests/pack-requests.d.ts +19 -0
- package/dist/express-assets/client-requests/pack-requests.d.ts.map +1 -0
- package/dist/express-assets/client-requests/pack-requests.js +130 -0
- package/dist/express-assets/client-requests/pack-requests.js.map +1 -0
- package/dist/express-assets/client.d.ts +31 -0
- package/dist/express-assets/client.d.ts.map +1 -0
- package/dist/express-assets/client.js +70 -0
- package/dist/express-assets/client.js.map +1 -0
- package/dist/express-assets/couchdb/authentication.d.ts +4 -0
- package/dist/express-assets/couchdb/authentication.d.ts.map +1 -0
- package/dist/express-assets/couchdb/authentication.js +69 -0
- package/dist/express-assets/couchdb/authentication.js.map +1 -0
- package/dist/express-assets/couchdb/index.d.ts +18 -0
- package/dist/express-assets/couchdb/index.d.ts.map +1 -0
- package/dist/express-assets/couchdb/index.js +52 -0
- package/dist/express-assets/couchdb/index.js.map +1 -0
- package/dist/express-assets/design-docs.d.ts +63 -0
- package/dist/express-assets/design-docs.d.ts.map +1 -0
- package/dist/express-assets/design-docs.js +90 -0
- package/dist/express-assets/design-docs.js.map +1 -0
- package/dist/express-assets/logger.d.ts +3 -0
- package/dist/express-assets/logger.d.ts.map +1 -0
- package/dist/express-assets/logger.js +62 -0
- package/dist/express-assets/logger.js.map +1 -0
- package/dist/express-assets/routes/logs.d.ts +3 -0
- package/dist/express-assets/routes/logs.d.ts.map +1 -0
- package/dist/express-assets/routes/logs.js +274 -0
- package/dist/express-assets/routes/logs.js.map +1 -0
- package/dist/express-assets/utils/env.d.ts +11 -0
- package/dist/express-assets/utils/env.d.ts.map +1 -0
- package/dist/express-assets/utils/env.js +39 -0
- package/dist/express-assets/utils/env.js.map +1 -0
- package/dist/express-assets/utils/processQueue.d.ts +39 -0
- package/dist/express-assets/utils/processQueue.d.ts.map +1 -0
- package/dist/express-assets/utils/processQueue.js +175 -0
- package/dist/express-assets/utils/processQueue.js.map +1 -0
- package/dist/studio-ui-src/App.vue +132 -0
- package/dist/studio-ui-src/api/index.ts +30 -0
- package/dist/studio-ui-src/components/StudioFlush.vue +108 -0
- package/dist/studio-ui-src/config/development.ts +98 -0
- package/dist/studio-ui-src/index.html +13 -0
- package/dist/studio-ui-src/main.ts +148 -0
- package/dist/studio-ui-src/package.json +35 -0
- package/dist/studio-ui-src/router/index.ts +32 -0
- package/dist/studio-ui-src/stores/useAuthStore.ts +3 -0
- package/dist/studio-ui-src/tsconfig.json +28 -0
- package/dist/studio-ui-src/views/BrowseView.vue +82 -0
- package/dist/studio-ui-src/views/BulkImportView.vue +89 -0
- package/dist/studio-ui-src/views/CourseEditorView.vue +62 -0
- package/dist/studio-ui-src/views/CreateCardView.vue +140 -0
- package/dist/studio-ui-src/vite.config.base.js +100 -0
- package/dist/studio-ui-src/vite.config.ts +26 -0
- package/dist/templates/.skuilder/README.md +29 -0
- package/dist/types.d.ts +5 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/ExpressManager.d.ts +28 -0
- package/dist/utils/ExpressManager.d.ts.map +1 -0
- package/dist/utils/ExpressManager.js +166 -0
- package/dist/utils/ExpressManager.js.map +1 -0
- package/dist/utils/NodeFileSystemAdapter.d.ts +6 -0
- package/dist/utils/NodeFileSystemAdapter.d.ts.map +1 -1
- package/dist/utils/NodeFileSystemAdapter.js +29 -1
- package/dist/utils/NodeFileSystemAdapter.js.map +1 -1
- package/dist/utils/error-reporting.d.ts +54 -0
- package/dist/utils/error-reporting.d.ts.map +1 -0
- package/dist/utils/error-reporting.js +143 -0
- package/dist/utils/error-reporting.js.map +1 -0
- package/dist/utils/pack-courses.d.ts.map +1 -1
- package/dist/utils/pack-courses.js +10 -27
- package/dist/utils/pack-courses.js.map +1 -1
- package/dist/utils/prompts.d.ts.map +1 -1
- package/dist/utils/prompts.js +24 -0
- package/dist/utils/prompts.js.map +1 -1
- package/dist/utils/questions-hash.d.ts +22 -0
- package/dist/utils/questions-hash.d.ts.map +1 -0
- package/dist/utils/questions-hash.js +96 -0
- package/dist/utils/questions-hash.js.map +1 -0
- package/dist/utils/template.d.ts +1 -1
- package/dist/utils/template.d.ts.map +1 -1
- package/dist/utils/template.js +202 -25
- package/dist/utils/template.js.map +1 -1
- package/eslint.config.mjs +1 -1
- package/package.json +30 -11
- package/src/cli.ts +3 -1
- package/src/commands/init.ts +48 -3
- package/src/commands/pack.ts +1 -1
- package/src/commands/studio.ts +850 -121
- package/src/commands/unpack.ts +1 -1
- package/src/types.ts +5 -0
- package/src/utils/ExpressManager.ts +210 -0
- package/src/utils/NodeFileSystemAdapter.ts +46 -2
- package/src/utils/error-reporting.ts +192 -0
- package/src/utils/pack-courses.ts +11 -36
- package/src/utils/prompts.ts +34 -0
- package/src/utils/questions-hash.ts +109 -0
- package/src/utils/template.ts +231 -27
- package/templates/.skuilder/README.md +29 -0
- package/dist/studio-ui-assets/assets/BrowseView-BJbixGOU.js +0 -2
- package/dist/studio-ui-assets/assets/BrowseView-BJbixGOU.js.map +0 -1
- package/dist/studio-ui-assets/assets/BrowseView-CM4HBO4j.css +0 -1
- package/dist/studio-ui-assets/assets/BulkImportView-DB6DYDJU.js +0 -2
- package/dist/studio-ui-assets/assets/BulkImportView-DB6DYDJU.js.map +0 -1
- package/dist/studio-ui-assets/assets/BulkImportView-g4wQUfPA.css +0 -1
- package/dist/studio-ui-assets/assets/CourseEditorView-BIlhlhw1.js +0 -2
- package/dist/studio-ui-assets/assets/CourseEditorView-BIlhlhw1.js.map +0 -1
- package/dist/studio-ui-assets/assets/CourseEditorView-WuPNLVKp.css +0 -1
- package/dist/studio-ui-assets/assets/CreateCardView-CyNOKCkm.css +0 -1
- package/dist/studio-ui-assets/assets/CreateCardView-DPjPvzzt.js +0 -2
- package/dist/studio-ui-assets/assets/CreateCardView-DPjPvzzt.js.map +0 -1
- package/dist/studio-ui-assets/assets/edit-ui.es-DiUxqbgF.js +0 -330
- package/dist/studio-ui-assets/assets/edit-ui.es-DiUxqbgF.js.map +0 -1
- package/dist/studio-ui-assets/assets/index--zY88pg6.css +0 -14
- package/dist/studio-ui-assets/assets/index-BnAv1C72.js +0 -287
- package/dist/studio-ui-assets/assets/index-BnAv1C72.js.map +0 -1
- package/dist/studio-ui-assets/assets/index-DHMXQY3-.js +0 -192
- package/dist/studio-ui-assets/assets/index-DHMXQY3-.js.map +0 -1
- package/dist/studio-ui-assets/assets/materialdesignicons-webfont-B7mPwVP_.ttf +0 -0
- package/dist/studio-ui-assets/assets/materialdesignicons-webfont-CSr8KVlo.eot +0 -0
- package/dist/studio-ui-assets/assets/materialdesignicons-webfont-Dp5v-WZN.woff2 +0 -0
- package/dist/studio-ui-assets/assets/materialdesignicons-webfont-PXm3-2wK.woff +0 -0
- package/dist/studio-ui-assets/assets/vue-DZcMATiC.js +0 -28
- package/dist/studio-ui-assets/assets/vue-DZcMATiC.js.map +0 -1
- package/dist/studio-ui-assets/assets/vuetify-qg7mRxy_.js +0 -6
- package/dist/studio-ui-assets/assets/vuetify-qg7mRxy_.js.map +0 -1
- package/dist/studio-ui-assets/index.html +0 -16
package/dist/commands/studio.js
CHANGED
|
@@ -1,26 +1,52 @@
|
|
|
1
|
+
import { CouchDBManager } from '@vue-skuilder/common/docker';
|
|
2
|
+
import chalk from 'chalk';
|
|
1
3
|
import { Command } from 'commander';
|
|
2
|
-
import path from 'path';
|
|
3
4
|
import fs from 'fs';
|
|
4
|
-
import chalk from 'chalk';
|
|
5
|
-
import { spawn } from 'child_process';
|
|
6
|
-
import { fileURLToPath } from 'url';
|
|
7
|
-
import { dirname } from 'path';
|
|
8
5
|
import http from 'http';
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
import path, { dirname } from 'path';
|
|
7
|
+
import serveStatic from 'serve-static';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
import { VERSION } from '../cli.js';
|
|
10
|
+
import { createStudioBuildError, reportStudioBuildError, StudioBuildErrorType, withStudioBuildErrorHandling, } from '../utils/error-reporting.js';
|
|
11
|
+
import { ExpressManager } from '../utils/ExpressManager.js';
|
|
12
|
+
import { ensureBuildDirectory, ensureCacheDirectory, getStudioBuildPath, hashQuestionsDirectory, studioBuildExists, } from '../utils/questions-hash.js';
|
|
12
13
|
const __filename = fileURLToPath(import.meta.url);
|
|
13
14
|
const __dirname = dirname(__filename);
|
|
14
15
|
export function createStudioCommand() {
|
|
15
16
|
return new Command('studio')
|
|
16
|
-
.description('Launch studio mode
|
|
17
|
+
.description('Launch studio mode: a complete course editing environment with CouchDB, Express API, and web editor')
|
|
17
18
|
.argument('[coursePath]', 'Path to static course directory', '.')
|
|
18
19
|
.option('-p, --port <port>', 'CouchDB port for studio session', '5985')
|
|
19
20
|
.option('--no-browser', 'Skip automatic browser launch')
|
|
20
|
-
.action(launchStudio)
|
|
21
|
+
.action(launchStudio)
|
|
22
|
+
.addHelpText('after', `
|
|
23
|
+
Studio Mode creates a full editing environment for static courses:
|
|
24
|
+
|
|
25
|
+
Services Started:
|
|
26
|
+
• CouchDB instance (Docker) on port 5985+ for temporary editing
|
|
27
|
+
• Express API server on port 3001+ for backend operations
|
|
28
|
+
• Studio web interface on port 7174+ for visual editing
|
|
29
|
+
|
|
30
|
+
Workflow:
|
|
31
|
+
1. Loads course data from public/static-courses/ into CouchDB
|
|
32
|
+
2. Opens web editor for visual course content editing
|
|
33
|
+
3. Use "Flush to Static" to save changes back to your course files
|
|
34
|
+
4. Studio mode overwrites source files - backup before major edits
|
|
35
|
+
|
|
36
|
+
Requirements:
|
|
37
|
+
• Docker (for CouchDB instance)
|
|
38
|
+
• Valid static course project (with package.json)
|
|
39
|
+
• Course data in public/static-courses/ directory
|
|
40
|
+
|
|
41
|
+
Example:
|
|
42
|
+
skuilder studio # Launch in current directory
|
|
43
|
+
skuilder studio ./my-course # Launch for specific course
|
|
44
|
+
skuilder studio --port 6000 # Use custom CouchDB port
|
|
45
|
+
skuilder studio --no-browser # Don't auto-open browser`);
|
|
21
46
|
}
|
|
22
47
|
// Global references for cleanup
|
|
23
48
|
let couchDBManager = null;
|
|
49
|
+
let expressManager = null;
|
|
24
50
|
let studioUIServer = null;
|
|
25
51
|
async function launchStudio(coursePath, options) {
|
|
26
52
|
try {
|
|
@@ -36,6 +62,42 @@ async function launchStudio(coursePath, options) {
|
|
|
36
62
|
process.exit(1);
|
|
37
63
|
}
|
|
38
64
|
console.log(chalk.green(`✅ Valid standalone-ui course detected`));
|
|
65
|
+
// Phase 0.5: Hash questions directory to determine studio-ui build needs
|
|
66
|
+
console.log(chalk.cyan(`🔍 Analyzing local question types...`));
|
|
67
|
+
let questionsHash;
|
|
68
|
+
let studioUIPath;
|
|
69
|
+
try {
|
|
70
|
+
questionsHash = await withStudioBuildErrorHandling(() => hashQuestionsDirectory(resolvedPath), StudioBuildErrorType.QUESTIONS_HASH_ERROR, { coursePath: resolvedPath });
|
|
71
|
+
// Ensure cache directory exists
|
|
72
|
+
await ensureCacheDirectory(resolvedPath);
|
|
73
|
+
const buildExists = studioBuildExists(resolvedPath, questionsHash);
|
|
74
|
+
const buildPath = getStudioBuildPath(resolvedPath, questionsHash);
|
|
75
|
+
console.log(chalk.gray(` Questions hash: ${questionsHash}`));
|
|
76
|
+
console.log(chalk.gray(` Cached build exists: ${buildExists ? 'Yes' : 'No'}`));
|
|
77
|
+
// Determine if we need to rebuild studio-ui
|
|
78
|
+
if (buildExists) {
|
|
79
|
+
console.log(chalk.gray(` Using cached build at: ${buildPath}`));
|
|
80
|
+
studioUIPath = buildPath;
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
console.log(chalk.cyan(`🔨 Building studio-ui with local question types...`));
|
|
84
|
+
studioUIPath = await buildStudioUIWithQuestions(resolvedPath, questionsHash);
|
|
85
|
+
console.log(chalk.green(`✅ Studio-UI build complete: ${studioUIPath}`));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
// Handle catastrophic build errors by falling back to embedded source
|
|
90
|
+
console.log(chalk.yellow(`⚠️ Unable to process questions due to ${error},\n⚠️ Using embedded studio-ui`));
|
|
91
|
+
const embeddedPath = path.join(__dirname, '..', 'studio-ui-src');
|
|
92
|
+
if (fs.existsSync(embeddedPath)) {
|
|
93
|
+
studioUIPath = embeddedPath;
|
|
94
|
+
console.log(chalk.gray(` Using embedded studio-ui source directly`));
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
console.error(chalk.red(`❌ No viable studio-ui source available`));
|
|
98
|
+
throw new Error('Critical error: Cannot locate studio-ui source');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
39
101
|
// Phase 1: CouchDB Management
|
|
40
102
|
const studioDatabaseName = generateStudioDatabaseName(resolvedPath);
|
|
41
103
|
console.log(chalk.cyan(`🗄️ Starting studio CouchDB instance: ${studioDatabaseName}`));
|
|
@@ -43,13 +105,17 @@ async function launchStudio(coursePath, options) {
|
|
|
43
105
|
// Phase 4: Populate CouchDB with course data
|
|
44
106
|
console.log(chalk.cyan(`📦 Unpacking course data to studio database...`));
|
|
45
107
|
const unpackResult = await unpackCourseToStudio(resolvedPath, couchDBManager.getConnectionDetails());
|
|
108
|
+
// Phase 9.5: Launch Express backend
|
|
109
|
+
console.log(chalk.cyan(`⚡ Starting Express backend server...`));
|
|
110
|
+
expressManager = await startExpressBackend(couchDBManager.getConnectionDetails(), resolvedPath);
|
|
46
111
|
// Phase 7: Launch studio-ui server
|
|
47
112
|
console.log(chalk.cyan(`🌐 Starting studio-ui server...`));
|
|
48
113
|
console.log(chalk.gray(` Debug: Unpack result - Database: "${unpackResult.databaseName}", Course ID: "${unpackResult.courseId}"`));
|
|
49
|
-
const studioUIPort = await startStudioUIServer(couchDBManager.getConnectionDetails(), unpackResult);
|
|
114
|
+
const studioUIPort = await startStudioUIServer(couchDBManager.getConnectionDetails(), unpackResult, studioUIPath);
|
|
50
115
|
console.log(chalk.green(`✅ Studio session ready!`));
|
|
51
116
|
console.log(chalk.white(`🎨 Studio URL: http://localhost:${studioUIPort}`));
|
|
52
117
|
console.log(chalk.gray(` Database: ${studioDatabaseName} on port ${options.port}`));
|
|
118
|
+
console.log(chalk.gray(` Express API: ${expressManager.getConnectionDetails().url}`));
|
|
53
119
|
if (options.browser) {
|
|
54
120
|
console.log(chalk.cyan(`🌐 Opening browser...`));
|
|
55
121
|
await openBrowser(`http://localhost:${studioUIPort}`);
|
|
@@ -153,7 +219,7 @@ async function startStudioCouchDB(_databaseName, port) {
|
|
|
153
219
|
}
|
|
154
220
|
}
|
|
155
221
|
/**
|
|
156
|
-
* Stop entire studio session (CouchDB + UI server)
|
|
222
|
+
* Stop entire studio session (CouchDB + Express + UI server)
|
|
157
223
|
*/
|
|
158
224
|
async function stopStudioSession() {
|
|
159
225
|
// Stop studio-ui server
|
|
@@ -168,6 +234,18 @@ async function stopStudioSession() {
|
|
|
168
234
|
}
|
|
169
235
|
studioUIServer = null;
|
|
170
236
|
}
|
|
237
|
+
// Stop Express backend
|
|
238
|
+
if (expressManager) {
|
|
239
|
+
try {
|
|
240
|
+
await expressManager.stop();
|
|
241
|
+
console.log(chalk.green(`✅ Express backend stopped`));
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
245
|
+
console.error(chalk.red(`Error stopping Express backend: ${errorMessage}`));
|
|
246
|
+
}
|
|
247
|
+
expressManager = null;
|
|
248
|
+
}
|
|
171
249
|
// Stop CouchDB
|
|
172
250
|
if (couchDBManager) {
|
|
173
251
|
try {
|
|
@@ -181,10 +259,30 @@ async function stopStudioSession() {
|
|
|
181
259
|
couchDBManager = null;
|
|
182
260
|
}
|
|
183
261
|
}
|
|
184
|
-
async function startStudioUIServer(connectionDetails, unpackResult) {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
262
|
+
async function startStudioUIServer(connectionDetails, unpackResult, studioPath) {
|
|
263
|
+
// Serve from built dist directory if it exists, otherwise fallback to source
|
|
264
|
+
const distPath = path.join(studioPath, 'dist');
|
|
265
|
+
const studioSourcePath = fs.existsSync(distPath) ? distPath : studioPath;
|
|
266
|
+
console.log(chalk.gray(` Serving studio-ui from: ${studioSourcePath}`));
|
|
267
|
+
const serve = serveStatic(studioSourcePath, {
|
|
268
|
+
index: ['index.html'],
|
|
269
|
+
setHeaders: (res, path) => {
|
|
270
|
+
if (path.endsWith('.woff2')) {
|
|
271
|
+
res.setHeader('Content-Type', 'font/woff2');
|
|
272
|
+
}
|
|
273
|
+
else if (path.endsWith('.woff')) {
|
|
274
|
+
res.setHeader('Content-Type', 'font/woff');
|
|
275
|
+
}
|
|
276
|
+
else if (path.endsWith('.ttf')) {
|
|
277
|
+
res.setHeader('Content-Type', 'font/ttf');
|
|
278
|
+
}
|
|
279
|
+
else if (path.endsWith('.eot')) {
|
|
280
|
+
res.setHeader('Content-Type', 'application/vnd.ms-fontobject');
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
if (!fs.existsSync(studioSourcePath)) {
|
|
285
|
+
throw new Error('Studio-UI source not found. Please rebuild the CLI package.');
|
|
188
286
|
}
|
|
189
287
|
// Find available port starting from 7174
|
|
190
288
|
let port = 7174;
|
|
@@ -192,34 +290,38 @@ async function startStudioUIServer(connectionDetails, unpackResult) {
|
|
|
192
290
|
try {
|
|
193
291
|
await new Promise((resolve, reject) => {
|
|
194
292
|
const server = http.createServer((req, res) => {
|
|
195
|
-
|
|
196
|
-
//
|
|
197
|
-
if (
|
|
198
|
-
|
|
199
|
-
|
|
293
|
+
const url = new URL(req.url || '/', `http://${req.headers.host}`);
|
|
294
|
+
// Inject config for index.html
|
|
295
|
+
if (url.pathname === '/' || url.pathname === '/index.html') {
|
|
296
|
+
const indexPath = path.join(studioSourcePath, 'index.html');
|
|
297
|
+
let html = fs.readFileSync(indexPath, 'utf8');
|
|
298
|
+
const connectionScript = `
|
|
299
|
+
<script>
|
|
300
|
+
window.STUDIO_CONFIG = {
|
|
301
|
+
couchdb: {
|
|
302
|
+
url: '${connectionDetails.url}',
|
|
303
|
+
username: '${connectionDetails.username}',
|
|
304
|
+
password: '${connectionDetails.password}'
|
|
305
|
+
},
|
|
306
|
+
database: {
|
|
307
|
+
name: '${unpackResult.databaseName}',
|
|
308
|
+
courseId: '${unpackResult.courseId}',
|
|
309
|
+
originalCourseId: '${unpackResult.courseId}'
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
</script>
|
|
313
|
+
`;
|
|
314
|
+
html = html.replace('</head>', connectionScript + '</head>');
|
|
315
|
+
res.setHeader('Content-Type', 'text/html');
|
|
316
|
+
res.end(html);
|
|
200
317
|
return;
|
|
201
318
|
}
|
|
202
|
-
//
|
|
203
|
-
|
|
204
|
-
// If
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
const ext = path.extname(filePath);
|
|
209
|
-
const contentType = {
|
|
210
|
-
'.html': 'text/html',
|
|
211
|
-
'.js': 'text/javascript',
|
|
212
|
-
'.css': 'text/css',
|
|
213
|
-
'.woff2': 'font/woff2',
|
|
214
|
-
'.woff': 'font/woff',
|
|
215
|
-
'.ttf': 'font/ttf',
|
|
216
|
-
'.eot': 'application/vnd.ms-fontobject',
|
|
217
|
-
}[ext] || 'application/octet-stream';
|
|
218
|
-
res.writeHead(200, { 'Content-Type': contentType });
|
|
219
|
-
// For HTML files, inject CouchDB connection details
|
|
220
|
-
if (ext === '.html') {
|
|
221
|
-
let html = fs.readFileSync(filePath, 'utf8');
|
|
222
|
-
// Inject connection details as script tag before </head>
|
|
319
|
+
// Fallback to serve-static for all other assets
|
|
320
|
+
serve(req, res, () => {
|
|
321
|
+
// If serve-static doesn't find the file, it calls next().
|
|
322
|
+
// We can treat this as a 404, but for SPAs, we should serve index.html.
|
|
323
|
+
const indexPath = path.join(studioSourcePath, 'index.html');
|
|
324
|
+
let html = fs.readFileSync(indexPath, 'utf8');
|
|
223
325
|
const connectionScript = `
|
|
224
326
|
<script>
|
|
225
327
|
window.STUDIO_CONFIG = {
|
|
@@ -230,19 +332,16 @@ async function startStudioUIServer(connectionDetails, unpackResult) {
|
|
|
230
332
|
},
|
|
231
333
|
database: {
|
|
232
334
|
name: '${unpackResult.databaseName}',
|
|
233
|
-
courseId: '${unpackResult.courseId}'
|
|
335
|
+
courseId: '${unpackResult.courseId}',
|
|
336
|
+
originalCourseId: '${unpackResult.courseId}'
|
|
234
337
|
}
|
|
235
338
|
};
|
|
236
339
|
</script>
|
|
237
340
|
`;
|
|
238
341
|
html = html.replace('</head>', connectionScript + '</head>');
|
|
342
|
+
res.setHeader('Content-Type', 'text/html');
|
|
239
343
|
res.end(html);
|
|
240
|
-
}
|
|
241
|
-
else {
|
|
242
|
-
// Serve static files
|
|
243
|
-
const stream = fs.createReadStream(filePath);
|
|
244
|
-
stream.pipe(res);
|
|
245
|
-
}
|
|
344
|
+
});
|
|
246
345
|
});
|
|
247
346
|
server.listen(port, '127.0.0.1', () => {
|
|
248
347
|
studioUIServer = server;
|
|
@@ -301,11 +400,39 @@ async function openBrowser(url) {
|
|
|
301
400
|
console.log(chalk.yellow(`⚠️ Could not automatically open browser. Please visit: ${url}`));
|
|
302
401
|
}
|
|
303
402
|
}
|
|
403
|
+
/**
|
|
404
|
+
* Phase 9.5: Start Express backend server
|
|
405
|
+
*/
|
|
406
|
+
async function startExpressBackend(couchDbConnectionDetails, projectPath) {
|
|
407
|
+
const expressManager = new ExpressManager({
|
|
408
|
+
port: 3001, // Start from 3001 to avoid conflicts
|
|
409
|
+
couchdbUrl: couchDbConnectionDetails.url,
|
|
410
|
+
couchdbUsername: couchDbConnectionDetails.username,
|
|
411
|
+
couchdbPassword: couchDbConnectionDetails.password,
|
|
412
|
+
projectPath: projectPath,
|
|
413
|
+
}, {
|
|
414
|
+
onLog: (message) => console.log(chalk.gray(` Express: ${message}`)),
|
|
415
|
+
onError: (error) => console.error(chalk.red(` Express Error: ${error}`)),
|
|
416
|
+
});
|
|
417
|
+
try {
|
|
418
|
+
await expressManager.start();
|
|
419
|
+
const connectionDetails = expressManager.getConnectionDetails();
|
|
420
|
+
console.log(chalk.green(`✅ Express backend ready`));
|
|
421
|
+
console.log(chalk.gray(` URL: ${connectionDetails.url}`));
|
|
422
|
+
console.log(chalk.gray(` Port: ${connectionDetails.port}`));
|
|
423
|
+
return expressManager;
|
|
424
|
+
}
|
|
425
|
+
catch (error) {
|
|
426
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
427
|
+
console.error(chalk.red(`Failed to start Express backend: ${errorMessage}`));
|
|
428
|
+
throw error;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
304
431
|
/**
|
|
305
432
|
* Phase 4: Unpack course data to studio CouchDB
|
|
306
433
|
*/
|
|
307
434
|
async function unpackCourseToStudio(coursePath, connectionDetails) {
|
|
308
|
-
|
|
435
|
+
try {
|
|
309
436
|
// Find the course data directory (static-data OR public/static-courses)
|
|
310
437
|
let courseDataPath = path.join(coursePath, 'static-data');
|
|
311
438
|
if (!fs.existsSync(courseDataPath)) {
|
|
@@ -321,76 +448,484 @@ async function unpackCourseToStudio(coursePath, connectionDetails) {
|
|
|
321
448
|
courseDataPath = path.join(publicStaticPath, courses[0]);
|
|
322
449
|
}
|
|
323
450
|
else {
|
|
324
|
-
|
|
325
|
-
return;
|
|
451
|
+
throw new Error('No course directories found in public/static-courses/');
|
|
326
452
|
}
|
|
327
453
|
}
|
|
328
454
|
else {
|
|
329
|
-
|
|
330
|
-
return;
|
|
455
|
+
throw new Error('No course data found in static-data/ or public/static-courses/');
|
|
331
456
|
}
|
|
332
457
|
}
|
|
333
458
|
console.log(chalk.gray(` Course data path: ${courseDataPath}`));
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
|
|
459
|
+
console.log(chalk.gray(` Running unpack directly...`));
|
|
460
|
+
// Generate database name the same way unpack command does
|
|
461
|
+
const timestamp = new Date().toISOString().slice(0, 10).replace(/-/g, '');
|
|
462
|
+
const random = Math.random().toString(36).substring(2, 8);
|
|
463
|
+
// We need the course ID from the static course data first
|
|
464
|
+
const { validateStaticCourse } = await import('@vue-skuilder/db');
|
|
465
|
+
const { NodeFileSystemAdapter } = await import('../utils/NodeFileSystemAdapter.js');
|
|
466
|
+
const fileSystemAdapter = new NodeFileSystemAdapter();
|
|
467
|
+
const validation = await validateStaticCourse(courseDataPath, fileSystemAdapter);
|
|
468
|
+
if (!validation.valid) {
|
|
469
|
+
throw new Error('Static course validation failed');
|
|
470
|
+
}
|
|
471
|
+
const studioCourseId = `unpacked_${validation.courseId || 'unknown'}_${timestamp}_${random}`;
|
|
472
|
+
const targetDbName = `coursedb-${studioCourseId}`;
|
|
473
|
+
// Import and call the existing unpack command
|
|
474
|
+
const { unpackCourse } = await import('./unpack.js');
|
|
475
|
+
try {
|
|
476
|
+
await unpackCourse(courseDataPath, {
|
|
477
|
+
server: connectionDetails.url,
|
|
478
|
+
username: connectionDetails.username,
|
|
479
|
+
password: connectionDetails.password,
|
|
480
|
+
database: targetDbName,
|
|
481
|
+
chunkSize: '100',
|
|
482
|
+
validate: false,
|
|
483
|
+
cleanupOnError: true,
|
|
484
|
+
});
|
|
485
|
+
console.log(chalk.green(`✅ Course data unpacked successfully`));
|
|
486
|
+
// Return the database name and course ID for studio use
|
|
487
|
+
const databaseName = studioCourseId;
|
|
488
|
+
const courseId = validation.courseId || '';
|
|
489
|
+
console.log(chalk.gray(` Debug: Extracted - Full DB: "${targetDbName}", Course DB ID: "${databaseName}", Course ID: "${courseId}"`));
|
|
490
|
+
return { databaseName, courseId };
|
|
491
|
+
}
|
|
492
|
+
catch (innerError) {
|
|
493
|
+
console.error(chalk.red(`❌ Failed to unpack course: ${innerError instanceof Error ? innerError.message : String(innerError)}`));
|
|
494
|
+
throw innerError;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
catch (error) {
|
|
498
|
+
console.error(chalk.red(`❌ Studio unpack failed: ${error instanceof Error ? error.message : String(error)}`));
|
|
499
|
+
throw error;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Build studio-ui with local question types integrated
|
|
504
|
+
*/
|
|
505
|
+
async function buildStudioUIWithQuestions(coursePath, questionsHash) {
|
|
506
|
+
const buildPath = await ensureBuildDirectory(coursePath, questionsHash);
|
|
507
|
+
try {
|
|
508
|
+
// Handle special cases
|
|
509
|
+
if (questionsHash === 'no-questions') {
|
|
510
|
+
console.log(chalk.gray(` No local questions detected, using default studio-ui`));
|
|
511
|
+
return await buildDefaultStudioUI(buildPath);
|
|
512
|
+
}
|
|
513
|
+
if (questionsHash === 'empty-questions') {
|
|
514
|
+
console.log(chalk.gray(` Empty questions directory, using default studio-ui`));
|
|
515
|
+
return await buildDefaultStudioUI(buildPath);
|
|
516
|
+
}
|
|
517
|
+
if (questionsHash === 'hash-error') {
|
|
518
|
+
const hashError = createStudioBuildError(StudioBuildErrorType.QUESTIONS_HASH_ERROR, 'Questions directory could not be processed', {
|
|
519
|
+
context: { coursePath, questionsHash },
|
|
520
|
+
recoverable: true,
|
|
521
|
+
fallbackAvailable: true,
|
|
522
|
+
});
|
|
523
|
+
reportStudioBuildError(hashError);
|
|
524
|
+
return await buildDefaultStudioUI(buildPath);
|
|
525
|
+
}
|
|
526
|
+
// Phase 4.1 - Build custom questions library and integrate with studio-ui
|
|
527
|
+
console.log(chalk.cyan(` Building custom questions library...`));
|
|
528
|
+
const customQuestionsData = await buildCustomQuestionsLibrary(coursePath, questionsHash);
|
|
529
|
+
if (customQuestionsData) {
|
|
530
|
+
console.log(chalk.cyan(` Integrating custom questions into studio-ui...`));
|
|
531
|
+
return await buildStudioUIWithCustomQuestions(buildPath, customQuestionsData);
|
|
532
|
+
}
|
|
533
|
+
else {
|
|
534
|
+
console.log(chalk.yellow(` Failed to build custom questions, falling back to default studio-ui`));
|
|
535
|
+
return await buildDefaultStudioUI(buildPath);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
catch (error) {
|
|
539
|
+
const buildError = createStudioBuildError(StudioBuildErrorType.BUILD_FAILURE, 'Studio-UI build process failed', {
|
|
540
|
+
cause: error instanceof Error ? error : undefined,
|
|
541
|
+
context: { coursePath, questionsHash, buildPath },
|
|
542
|
+
recoverable: true,
|
|
543
|
+
fallbackAvailable: true,
|
|
350
544
|
});
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
545
|
+
reportStudioBuildError(buildError);
|
|
546
|
+
// Always try fallback to default studio-ui
|
|
547
|
+
try {
|
|
548
|
+
return await buildDefaultStudioUI(buildPath);
|
|
549
|
+
}
|
|
550
|
+
catch (fallbackError) {
|
|
551
|
+
// If even the fallback fails, this is critical
|
|
552
|
+
const criticalError = createStudioBuildError(StudioBuildErrorType.CRITICAL_ERROR, 'Both primary and fallback studio-ui builds failed', {
|
|
553
|
+
cause: fallbackError instanceof Error ? fallbackError : undefined,
|
|
554
|
+
context: { coursePath, questionsHash, buildPath, originalError: error },
|
|
555
|
+
recoverable: false,
|
|
556
|
+
fallbackAvailable: false,
|
|
557
|
+
});
|
|
558
|
+
reportStudioBuildError(criticalError);
|
|
559
|
+
throw criticalError;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Build default studio-ui (without local questions integration)
|
|
565
|
+
*/
|
|
566
|
+
async function buildDefaultStudioUI(buildPath) {
|
|
567
|
+
const studioSourcePath = path.join(__dirname, '..', 'studio-ui-src');
|
|
568
|
+
try {
|
|
569
|
+
// Verify source directory exists
|
|
570
|
+
if (!fs.existsSync(studioSourcePath)) {
|
|
571
|
+
const sourceError = createStudioBuildError(StudioBuildErrorType.MISSING_SOURCE, `Studio-UI source directory not found at ${studioSourcePath}`, {
|
|
572
|
+
context: { studioSourcePath, buildPath },
|
|
573
|
+
recoverable: true,
|
|
574
|
+
fallbackAvailable: true,
|
|
575
|
+
});
|
|
576
|
+
reportStudioBuildError(sourceError);
|
|
577
|
+
throw sourceError;
|
|
578
|
+
}
|
|
579
|
+
// Copy studio-ui source files to build directory
|
|
580
|
+
console.log(chalk.gray(` Copying studio-ui source to build directory...`));
|
|
581
|
+
const { copyDirectory } = await import('../utils/template.js');
|
|
582
|
+
await withStudioBuildErrorHandling(() => copyDirectory(studioSourcePath, buildPath), StudioBuildErrorType.COPY_FAILURE, { studioSourcePath, buildPath });
|
|
583
|
+
// Transform workspace dependencies to published versions
|
|
584
|
+
console.log(chalk.gray(` Transforming workspace dependencies...`));
|
|
585
|
+
const studioPackageJsonPath = path.join(buildPath, 'package.json');
|
|
586
|
+
await transformPackageJsonForStudioBuild(studioPackageJsonPath);
|
|
587
|
+
// Fix Vite config to use npm packages instead of monorepo paths
|
|
588
|
+
console.log(chalk.gray(` Updating Vite configuration for standalone build...`));
|
|
589
|
+
await fixViteConfigForStandaloneBuild(buildPath);
|
|
590
|
+
// Run Vite build process
|
|
591
|
+
console.log(chalk.gray(` Running Vite build process...`));
|
|
592
|
+
await runViteBuild(buildPath);
|
|
593
|
+
// Verify build output exists
|
|
594
|
+
const distPath = path.join(buildPath, 'dist');
|
|
595
|
+
const indexPath = path.join(distPath, 'index.html');
|
|
596
|
+
if (!fs.existsSync(indexPath)) {
|
|
597
|
+
const buildError = createStudioBuildError(StudioBuildErrorType.BUILD_FAILURE, `Build output missing: ${indexPath}`, {
|
|
598
|
+
context: { buildPath, distPath, indexPath },
|
|
599
|
+
recoverable: true,
|
|
600
|
+
fallbackAvailable: true,
|
|
601
|
+
});
|
|
602
|
+
reportStudioBuildError(buildError);
|
|
603
|
+
throw buildError;
|
|
604
|
+
}
|
|
605
|
+
console.log(chalk.gray(` Default studio-ui built successfully`));
|
|
606
|
+
return buildPath;
|
|
607
|
+
}
|
|
608
|
+
catch (error) {
|
|
609
|
+
// Ultimate fallback: serve directly from embedded source
|
|
610
|
+
if (fs.existsSync(studioSourcePath)) {
|
|
611
|
+
console.log(chalk.yellow(` Using embedded studio-ui source as final fallback`));
|
|
612
|
+
return studioSourcePath;
|
|
613
|
+
}
|
|
614
|
+
// This should never happen, but provides a last resort
|
|
615
|
+
const criticalError = createStudioBuildError(StudioBuildErrorType.CRITICAL_ERROR, 'No viable studio-ui source available', {
|
|
616
|
+
cause: error instanceof Error ? error : undefined,
|
|
617
|
+
context: { studioSourcePath, buildPath },
|
|
618
|
+
recoverable: false,
|
|
619
|
+
fallbackAvailable: false,
|
|
358
620
|
});
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
621
|
+
reportStudioBuildError(criticalError);
|
|
622
|
+
throw criticalError;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Build custom questions library from scaffolded course
|
|
627
|
+
*/
|
|
628
|
+
async function buildCustomQuestionsLibrary(coursePath, questionsHash) {
|
|
629
|
+
try {
|
|
630
|
+
// Check if this is a scaffolded course with dual build system
|
|
631
|
+
const packageJsonPath = path.join(coursePath, 'package.json');
|
|
632
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
633
|
+
console.log(chalk.gray(` No package.json found, skipping custom questions build`));
|
|
634
|
+
return null;
|
|
635
|
+
}
|
|
636
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
637
|
+
// Check if course has the dual build system (build:lib script)
|
|
638
|
+
if (!packageJson.scripts || !packageJson.scripts['build:lib']) {
|
|
639
|
+
console.log(chalk.gray(` Course does not support custom questions library build`));
|
|
640
|
+
return null;
|
|
641
|
+
}
|
|
642
|
+
// Check if course has questions directory with our expected structure
|
|
643
|
+
const questionsIndexPath = path.join(coursePath, 'src', 'questions', 'index.ts');
|
|
644
|
+
if (!fs.existsSync(questionsIndexPath)) {
|
|
645
|
+
console.log(chalk.gray(` No src/questions/index.ts found, skipping custom questions build`));
|
|
646
|
+
return null;
|
|
647
|
+
}
|
|
648
|
+
console.log(chalk.cyan(` Found scaffolded course with custom questions support`));
|
|
649
|
+
console.log(chalk.gray(` Building questions library...`));
|
|
650
|
+
// Build the questions library
|
|
651
|
+
const { spawn } = await import('child_process');
|
|
652
|
+
const buildProcess = spawn('npm', ['run', 'build:lib'], {
|
|
653
|
+
cwd: coursePath,
|
|
654
|
+
stdio: 'pipe',
|
|
655
|
+
env: { ...process.env, BUILD_MODE: 'library' },
|
|
364
656
|
});
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
657
|
+
let buildOutput = '';
|
|
658
|
+
let buildError = '';
|
|
659
|
+
buildProcess.stdout?.on('data', (data) => {
|
|
660
|
+
buildOutput += data.toString();
|
|
661
|
+
});
|
|
662
|
+
buildProcess.stderr?.on('data', (data) => {
|
|
663
|
+
buildError += data.toString();
|
|
664
|
+
});
|
|
665
|
+
const buildExitCode = await new Promise((resolve) => {
|
|
666
|
+
buildProcess.on('close', resolve);
|
|
667
|
+
});
|
|
668
|
+
if (buildExitCode !== 0) {
|
|
669
|
+
const buildFailError = createStudioBuildError(StudioBuildErrorType.BUILD_FAILURE, 'Custom questions library build failed', {
|
|
670
|
+
context: {
|
|
671
|
+
coursePath,
|
|
672
|
+
questionsHash,
|
|
673
|
+
exitCode: buildExitCode,
|
|
674
|
+
output: buildOutput,
|
|
675
|
+
error: buildError,
|
|
676
|
+
},
|
|
677
|
+
recoverable: true,
|
|
678
|
+
fallbackAvailable: true,
|
|
679
|
+
});
|
|
680
|
+
reportStudioBuildError(buildFailError);
|
|
681
|
+
return null;
|
|
682
|
+
}
|
|
683
|
+
console.log(chalk.green(` ✅ Questions library built successfully`));
|
|
684
|
+
// Check that the library build outputs exist
|
|
685
|
+
const libraryPath = path.join(coursePath, 'dist-lib');
|
|
686
|
+
const questionsLibPath = path.join(libraryPath, 'questions.mjs');
|
|
687
|
+
if (!fs.existsSync(questionsLibPath)) {
|
|
688
|
+
console.log(chalk.yellow(` Warning: Expected library output not found at ${questionsLibPath}`));
|
|
689
|
+
return null;
|
|
690
|
+
}
|
|
691
|
+
// Validate that the questions library was built successfully
|
|
692
|
+
console.log(chalk.green(` ✅ Questions library built and available for studio-ui`));
|
|
693
|
+
console.log(chalk.gray(` Library path: ${questionsLibPath}`));
|
|
694
|
+
console.log(chalk.gray(` Package name: ${packageJson.name}`));
|
|
695
|
+
return {
|
|
696
|
+
coursePath,
|
|
697
|
+
questionsHash,
|
|
698
|
+
libraryPath,
|
|
699
|
+
packageName: packageJson.name,
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
catch (error) {
|
|
703
|
+
const generalError = createStudioBuildError(StudioBuildErrorType.BUILD_FAILURE, 'Custom questions library build process failed', {
|
|
704
|
+
cause: error instanceof Error ? error : undefined,
|
|
705
|
+
context: { coursePath, questionsHash },
|
|
706
|
+
recoverable: true,
|
|
707
|
+
fallbackAvailable: true,
|
|
708
|
+
});
|
|
709
|
+
reportStudioBuildError(generalError);
|
|
710
|
+
return null;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* Build studio-ui with custom questions integrated via npm install
|
|
715
|
+
*/
|
|
716
|
+
async function buildStudioUIWithCustomQuestions(buildPath, customQuestionsData) {
|
|
717
|
+
try {
|
|
718
|
+
console.log(chalk.gray(` Setting up studio-ui with custom questions...`));
|
|
719
|
+
// Step 1: Copy studio-ui source files
|
|
720
|
+
const studioSourcePath = path.join(__dirname, '..', 'studio-ui-src');
|
|
721
|
+
const { copyDirectory } = await import('../utils/template.js');
|
|
722
|
+
await copyDirectory(studioSourcePath, buildPath);
|
|
723
|
+
// Step 2: Transform workspace dependencies to published versions
|
|
724
|
+
console.log(chalk.gray(` Transforming workspace dependencies...`));
|
|
725
|
+
const studioPackageJsonPath = path.join(buildPath, 'package.json');
|
|
726
|
+
await transformPackageJsonForStudioBuild(studioPackageJsonPath);
|
|
727
|
+
// Step 2.5: Fix Vite config to use npm packages instead of monorepo paths
|
|
728
|
+
console.log(chalk.gray(` Updating Vite configuration for standalone build...`));
|
|
729
|
+
await fixViteConfigForStandaloneBuild(buildPath);
|
|
730
|
+
// Step 3: Install custom questions package
|
|
731
|
+
console.log(chalk.cyan(` Installing bundled course package: ${customQuestionsData.packageName} from ${customQuestionsData.coursePath}`));
|
|
732
|
+
const distLibPath = path.join(customQuestionsData.coursePath, 'dist-lib');
|
|
733
|
+
if (!fs.existsSync(distLibPath)) {
|
|
734
|
+
throw new Error(`dist-lib directory not found at: ${distLibPath}. Run 'npm run build:lib' first.`);
|
|
735
|
+
}
|
|
736
|
+
// Ensure node_modules directory exists in studio-ui
|
|
737
|
+
const nodeModulesPath = path.join(buildPath, 'node_modules');
|
|
738
|
+
const packageInstallPath = path.join(nodeModulesPath, customQuestionsData.packageName);
|
|
739
|
+
if (!fs.existsSync(nodeModulesPath)) {
|
|
740
|
+
fs.mkdirSync(nodeModulesPath, { recursive: true });
|
|
741
|
+
}
|
|
742
|
+
if (!fs.existsSync(packageInstallPath)) {
|
|
743
|
+
fs.mkdirSync(packageInstallPath, { recursive: true });
|
|
744
|
+
}
|
|
745
|
+
// Copy dist-lib contents to node_modules/{packageName}
|
|
746
|
+
console.log(chalk.gray(` Copying dist-lib to node_modules/${customQuestionsData.packageName}...`));
|
|
747
|
+
await copyDirectory(distLibPath, packageInstallPath);
|
|
748
|
+
// Copy package.json for proper npm module structure
|
|
749
|
+
const originalPackageJsonPath = path.join(customQuestionsData.coursePath, 'package.json');
|
|
750
|
+
const targetPackageJsonPath = path.join(packageInstallPath, 'package.json');
|
|
751
|
+
if (fs.existsSync(originalPackageJsonPath)) {
|
|
752
|
+
fs.copyFileSync(originalPackageJsonPath, targetPackageJsonPath);
|
|
753
|
+
}
|
|
754
|
+
console.log(chalk.green(` ✅ Bundled package installed successfully`));
|
|
755
|
+
// Step 4: Create runtime configuration for custom questions
|
|
756
|
+
const runtimeConfigPath = path.join(buildPath, 'custom-questions-config.json');
|
|
757
|
+
const runtimeConfig = {
|
|
758
|
+
hasCustomQuestions: true,
|
|
759
|
+
questionsHash: customQuestionsData.questionsHash,
|
|
760
|
+
packageName: customQuestionsData.packageName,
|
|
761
|
+
importPath: './questions.mjs',
|
|
762
|
+
};
|
|
763
|
+
fs.writeFileSync(runtimeConfigPath, JSON.stringify(runtimeConfig, null, 2));
|
|
764
|
+
console.log(chalk.gray(` ✅ Custom questions configuration written for package: ${customQuestionsData.packageName}`));
|
|
765
|
+
// Step 5: Run Vite build process
|
|
766
|
+
console.log(chalk.gray(` Running Vite build process...`));
|
|
767
|
+
await runViteBuild(buildPath);
|
|
768
|
+
// Step 6: Copy config file and questions module to built dist directory
|
|
769
|
+
const distPath = path.join(buildPath, 'dist');
|
|
770
|
+
const sourceConfigPath = path.join(buildPath, 'custom-questions-config.json');
|
|
771
|
+
const distConfigPath = path.join(distPath, 'custom-questions-config.json');
|
|
772
|
+
if (fs.existsSync(sourceConfigPath)) {
|
|
773
|
+
fs.copyFileSync(sourceConfigPath, distConfigPath);
|
|
774
|
+
console.log(chalk.gray(` Custom questions config copied to dist directory`));
|
|
775
|
+
}
|
|
776
|
+
// Copy the built questions.mjs file to dist/assets for proper serving
|
|
777
|
+
const nodeModulesQuestionsPath = path.join(buildPath, 'node_modules', customQuestionsData.packageName, 'questions.mjs');
|
|
778
|
+
const distAssetsPath = path.join(distPath, 'assets');
|
|
779
|
+
const distQuestionsPath = path.join(distAssetsPath, 'questions.mjs');
|
|
780
|
+
if (fs.existsSync(nodeModulesQuestionsPath)) {
|
|
781
|
+
// Ensure assets directory exists
|
|
782
|
+
if (!fs.existsSync(distAssetsPath)) {
|
|
783
|
+
fs.mkdirSync(distAssetsPath, { recursive: true });
|
|
784
|
+
}
|
|
785
|
+
fs.copyFileSync(nodeModulesQuestionsPath, distQuestionsPath);
|
|
786
|
+
console.log(chalk.gray(` Built questions.mjs copied to dist/assets directory`));
|
|
787
|
+
}
|
|
788
|
+
else {
|
|
789
|
+
console.log(chalk.yellow(` Warning: questions.mjs not found at ${nodeModulesQuestionsPath}`));
|
|
790
|
+
}
|
|
791
|
+
// Step 7: Verify build output exists
|
|
792
|
+
const indexPath = path.join(distPath, 'index.html');
|
|
793
|
+
if (!fs.existsSync(indexPath)) {
|
|
794
|
+
throw new Error(`Build output missing: ${indexPath}`);
|
|
795
|
+
}
|
|
796
|
+
console.log(chalk.gray(` Studio-ui with custom questions built successfully`));
|
|
797
|
+
return buildPath;
|
|
798
|
+
}
|
|
799
|
+
catch (error) {
|
|
800
|
+
const integrationError = createStudioBuildError(StudioBuildErrorType.BUILD_FAILURE, 'Failed to integrate custom questions into studio-ui via npm install', {
|
|
801
|
+
cause: error instanceof Error ? error : undefined,
|
|
802
|
+
context: { buildPath, customQuestionsData },
|
|
803
|
+
recoverable: true,
|
|
804
|
+
fallbackAvailable: true,
|
|
805
|
+
});
|
|
806
|
+
reportStudioBuildError(integrationError);
|
|
807
|
+
// Fallback to default studio-ui
|
|
808
|
+
console.log(chalk.red(` Exiting`));
|
|
809
|
+
process.exit(1);
|
|
810
|
+
return await buildDefaultStudioUI(buildPath);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* Transform package.json to replace workspace and file dependencies with published versions
|
|
815
|
+
*/
|
|
816
|
+
async function transformPackageJsonForStudioBuild(packageJsonPath) {
|
|
817
|
+
const content = fs.readFileSync(packageJsonPath, 'utf-8');
|
|
818
|
+
const packageJson = JSON.parse(content);
|
|
819
|
+
// Version mappings for vue-skuilder packages
|
|
820
|
+
const vueSkuilderPackageVersions = {
|
|
821
|
+
'@vue-skuilder/common': VERSION,
|
|
822
|
+
'@vue-skuilder/common-ui': VERSION,
|
|
823
|
+
'@vue-skuilder/courses': VERSION,
|
|
824
|
+
'@vue-skuilder/db': VERSION,
|
|
825
|
+
'@vue-skuilder/edit-ui': VERSION,
|
|
826
|
+
'@vue-skuilder/express': VERSION,
|
|
827
|
+
'@vue-skuilder/cli': VERSION,
|
|
828
|
+
};
|
|
829
|
+
// Transform dependencies
|
|
830
|
+
if (packageJson.dependencies) {
|
|
831
|
+
for (const [depName, version] of Object.entries(packageJson.dependencies)) {
|
|
832
|
+
if (typeof version === 'string' &&
|
|
833
|
+
(version.startsWith('workspace:') || version.startsWith('file:'))) {
|
|
834
|
+
const publishedVersion = vueSkuilderPackageVersions[depName];
|
|
835
|
+
if (publishedVersion) {
|
|
836
|
+
packageJson.dependencies[depName] = `^${publishedVersion}`;
|
|
837
|
+
console.log(chalk.gray(` Transformed ${depName}: ${version} → ^${publishedVersion}`));
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
// Transform devDependencies
|
|
843
|
+
if (packageJson.devDependencies) {
|
|
844
|
+
for (const [depName, version] of Object.entries(packageJson.devDependencies)) {
|
|
845
|
+
if (typeof version === 'string' &&
|
|
846
|
+
(version.startsWith('workspace:') || version.startsWith('file:'))) {
|
|
847
|
+
const publishedVersion = vueSkuilderPackageVersions[depName];
|
|
848
|
+
if (publishedVersion) {
|
|
849
|
+
packageJson.devDependencies[depName] = `^${publishedVersion}`;
|
|
850
|
+
console.log(chalk.gray(` Transformed ${depName}: ${version} → ^${publishedVersion}`));
|
|
382
851
|
}
|
|
383
|
-
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
|
|
856
|
+
}
|
|
857
|
+
/**
|
|
858
|
+
* Run Vite build process in the specified directory
|
|
859
|
+
*/
|
|
860
|
+
async function runViteBuild(buildPath) {
|
|
861
|
+
const { spawn } = await import('child_process');
|
|
862
|
+
return new Promise((resolve, reject) => {
|
|
863
|
+
const buildProcess = spawn('npm', ['run', 'build'], {
|
|
864
|
+
cwd: buildPath,
|
|
865
|
+
stdio: 'pipe',
|
|
866
|
+
});
|
|
867
|
+
let buildOutput = '';
|
|
868
|
+
let buildError = '';
|
|
869
|
+
buildProcess.stdout?.on('data', (data) => {
|
|
870
|
+
buildOutput += data.toString();
|
|
871
|
+
});
|
|
872
|
+
buildProcess.stderr?.on('data', (data) => {
|
|
873
|
+
buildError += data.toString();
|
|
874
|
+
});
|
|
875
|
+
buildProcess.on('close', (code) => {
|
|
876
|
+
if (code === 0) {
|
|
877
|
+
console.log(chalk.gray(` Vite build completed successfully`));
|
|
878
|
+
resolve();
|
|
384
879
|
}
|
|
385
880
|
else {
|
|
386
|
-
console.
|
|
387
|
-
|
|
881
|
+
console.log(chalk.yellow(` Vite build failed with exit code ${code}`));
|
|
882
|
+
console.log(chalk.yellow(` Build stdout: ${buildOutput}`));
|
|
883
|
+
console.log(chalk.yellow(` Build stderr: ${buildError}`));
|
|
884
|
+
reject(new Error(`Vite build failed with exit code ${code}: ${buildError}`));
|
|
388
885
|
}
|
|
389
886
|
});
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
reject(error);
|
|
887
|
+
buildProcess.on('error', (error) => {
|
|
888
|
+
reject(new Error(`Failed to start Vite build process: ${error.message}`));
|
|
393
889
|
});
|
|
394
890
|
});
|
|
395
891
|
}
|
|
892
|
+
/**
|
|
893
|
+
* Fix Vite configuration to work in standalone build environment
|
|
894
|
+
*/
|
|
895
|
+
async function fixViteConfigForStandaloneBuild(buildPath) {
|
|
896
|
+
const viteConfigPath = path.join(buildPath, 'vite.config.ts');
|
|
897
|
+
if (!fs.existsSync(viteConfigPath)) {
|
|
898
|
+
console.log(chalk.yellow(` Warning: vite.config.ts not found at ${viteConfigPath}`));
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
// Create a simplified vite config that uses standard npm resolution
|
|
902
|
+
// For custom questions builds, we need Vue bundled in the questions.mjs
|
|
903
|
+
const standaloneViteConfig = `import { defineConfig } from 'vite';
|
|
904
|
+
import vue from '@vitejs/plugin-vue';
|
|
905
|
+
|
|
906
|
+
export default defineConfig({
|
|
907
|
+
plugins: [vue()],
|
|
908
|
+
server: {
|
|
909
|
+
port: 7173,
|
|
910
|
+
host: '0.0.0.0'
|
|
911
|
+
},
|
|
912
|
+
build: {
|
|
913
|
+
target: 'es2020',
|
|
914
|
+
outDir: 'dist',
|
|
915
|
+
sourcemap: true,
|
|
916
|
+
rollupOptions: {
|
|
917
|
+
// Don't externalize Vue for custom questions - bundle it in
|
|
918
|
+
external: [],
|
|
919
|
+
output: {
|
|
920
|
+
manualChunks: {
|
|
921
|
+
vue: ['vue', 'vue-router', 'pinia'],
|
|
922
|
+
vuetify: ['vuetify']
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
});`;
|
|
928
|
+
fs.writeFileSync(viteConfigPath, standaloneViteConfig);
|
|
929
|
+
console.log(chalk.gray(` Vite config replaced with standalone version`));
|
|
930
|
+
}
|
|
396
931
|
//# sourceMappingURL=studio.js.map
|