coursecode 0.1.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/LICENSE +21 -0
- package/README.md +322 -0
- package/THIRD_PARTY_NOTICES.md +22 -0
- package/bin/cli.js +331 -0
- package/framework/assets/logo-coursecode-black.svg +14 -0
- package/framework/assets/logo-coursecode-white.svg +14 -0
- package/framework/assets/logo-coursecode.svg +14 -0
- package/framework/css/01-base.css +160 -0
- package/framework/css/02-layout.css +499 -0
- package/framework/css/accessibility.css +834 -0
- package/framework/css/components/accordions.css +710 -0
- package/framework/css/components/assessments.css +520 -0
- package/framework/css/components/audio-player.css +570 -0
- package/framework/css/components/badges.css +80 -0
- package/framework/css/components/breadcrumbs.css +87 -0
- package/framework/css/components/buttons.css +707 -0
- package/framework/css/components/callouts.css +1280 -0
- package/framework/css/components/cards.css +475 -0
- package/framework/css/components/carousel.css +193 -0
- package/framework/css/components/checkbox-group.css +123 -0
- package/framework/css/components/checklist.css +203 -0
- package/framework/css/components/collapse.css +96 -0
- package/framework/css/components/comparison.css +33 -0
- package/framework/css/components/content-image.css +36 -0
- package/framework/css/components/document-gallery.css +425 -0
- package/framework/css/components/dropdown.css +115 -0
- package/framework/css/components/embed-frame.css +142 -0
- package/framework/css/components/engagement.css +412 -0
- package/framework/css/components/features.css +35 -0
- package/framework/css/components/flip-cards.css +253 -0
- package/framework/css/components/footer.css +353 -0
- package/framework/css/components/forms.css +294 -0
- package/framework/css/components/hero.css +216 -0
- package/framework/css/components/images.css +528 -0
- package/framework/css/components/interactive-timeline.css +274 -0
- package/framework/css/components/intro-cards.css +30 -0
- package/framework/css/components/lightbox.css +666 -0
- package/framework/css/components/loading.css +65 -0
- package/framework/css/components/modals.css +235 -0
- package/framework/css/components/notifications.css +107 -0
- package/framework/css/components/quote.css +150 -0
- package/framework/css/components/sidebar.css +684 -0
- package/framework/css/components/slide-header.css +52 -0
- package/framework/css/components/spinner.css +62 -0
- package/framework/css/components/stats.css +44 -0
- package/framework/css/components/steps.css +232 -0
- package/framework/css/components/tables.css +90 -0
- package/framework/css/components/tabs.css +347 -0
- package/framework/css/components/timeline.css +154 -0
- package/framework/css/components/toggle.css +95 -0
- package/framework/css/components/tooltip.css +226 -0
- package/framework/css/components/video-player.css +438 -0
- package/framework/css/design-tokens.css +707 -0
- package/framework/css/framework.css +86 -0
- package/framework/css/interactions/accessibility.css +75 -0
- package/framework/css/interactions/base.css +92 -0
- package/framework/css/interactions/drag-drop.css +295 -0
- package/framework/css/interactions/fill-in-the-blank.css +236 -0
- package/framework/css/interactions/hotspots.css +69 -0
- package/framework/css/interactions/index.css +45 -0
- package/framework/css/interactions/interactive-image.css +359 -0
- package/framework/css/interactions/likert.css +126 -0
- package/framework/css/interactions/matching.css +354 -0
- package/framework/css/interactions/numeric-input.css +78 -0
- package/framework/css/interactions/sequencing.css +378 -0
- package/framework/css/interactions/true-false.css +177 -0
- package/framework/css/layouts/article.css +258 -0
- package/framework/css/layouts/base.css +30 -0
- package/framework/css/layouts/canvas.css +38 -0
- package/framework/css/layouts/focused.css +236 -0
- package/framework/css/layouts/index.css +29 -0
- package/framework/css/layouts/presentation.css +191 -0
- package/framework/css/layouts/traditional.css +52 -0
- package/framework/css/responsive.css +439 -0
- package/framework/css/utilities/accessibility-utils.css +59 -0
- package/framework/css/utilities/animations.css +419 -0
- package/framework/css/utilities/borders.css +72 -0
- package/framework/css/utilities/colors.css +76 -0
- package/framework/css/utilities/container.css +46 -0
- package/framework/css/utilities/decorative.css +442 -0
- package/framework/css/utilities/display.css +257 -0
- package/framework/css/utilities/flexbox.css +80 -0
- package/framework/css/utilities/grid.css +69 -0
- package/framework/css/utilities/icons.css +534 -0
- package/framework/css/utilities/lists.css +190 -0
- package/framework/css/utilities/spacing.css +167 -0
- package/framework/css/utilities/tables.css +81 -0
- package/framework/css/utilities/typography.css +159 -0
- package/framework/css/utilities/visibility.css +117 -0
- package/framework/docs/COURSE_AUTHORING_GUIDE.md +1773 -0
- package/framework/docs/COURSE_OUTLINE_GUIDE.md +725 -0
- package/framework/docs/COURSE_OUTLINE_TEMPLATE.md +161 -0
- package/framework/docs/DATA_MODEL.md +409 -0
- package/framework/docs/FRAMEWORK_GUIDE.md +1088 -0
- package/framework/docs/USER_GUIDE.md +583 -0
- package/framework/docs/examples/cloudflare-channel-relay.js +169 -0
- package/framework/docs/examples/cloudflare-data-worker.js +102 -0
- package/framework/docs/examples/cloudflare-error-worker.js +228 -0
- package/framework/index.html +175 -0
- package/framework/js/app/AppActions.js +410 -0
- package/framework/js/app/AppState.js +225 -0
- package/framework/js/app/AppUI.js +616 -0
- package/framework/js/assessment/AssessmentActions.js +615 -0
- package/framework/js/assessment/AssessmentFactory.js +471 -0
- package/framework/js/assessment/AssessmentState.js +322 -0
- package/framework/js/assessment/AssessmentUI.js +451 -0
- package/framework/js/automation/api-engagement.js +196 -0
- package/framework/js/automation/api-interactions.js +167 -0
- package/framework/js/automation/api.js +242 -0
- package/framework/js/automation/index.js +41 -0
- package/framework/js/components/interactions/drag-drop.js +884 -0
- package/framework/js/components/interactions/fill-in.js +535 -0
- package/framework/js/components/interactions/hotspot.js +702 -0
- package/framework/js/components/interactions/interaction-base.js +511 -0
- package/framework/js/components/interactions/likert.js +301 -0
- package/framework/js/components/interactions/matching.js +699 -0
- package/framework/js/components/interactions/multiple-choice.js +377 -0
- package/framework/js/components/interactions/numeric.js +271 -0
- package/framework/js/components/interactions/sequencing.js +423 -0
- package/framework/js/components/interactions/true-false.js +241 -0
- package/framework/js/components/ui-components/accordion.js +442 -0
- package/framework/js/components/ui-components/alert.js +88 -0
- package/framework/js/components/ui-components/audio-player.js +1193 -0
- package/framework/js/components/ui-components/callout.js +121 -0
- package/framework/js/components/ui-components/carousel.js +145 -0
- package/framework/js/components/ui-components/checkbox-group.js +87 -0
- package/framework/js/components/ui-components/checklist.js +40 -0
- package/framework/js/components/ui-components/collapse.js +114 -0
- package/framework/js/components/ui-components/comparison.js +30 -0
- package/framework/js/components/ui-components/conditional-display.js +150 -0
- package/framework/js/components/ui-components/content-image.js +41 -0
- package/framework/js/components/ui-components/dropdown.js +262 -0
- package/framework/js/components/ui-components/embed-frame.js +274 -0
- package/framework/js/components/ui-components/features.js +33 -0
- package/framework/js/components/ui-components/flip-card.js +230 -0
- package/framework/js/components/ui-components/form-validator.js +76 -0
- package/framework/js/components/ui-components/hero.js +49 -0
- package/framework/js/components/ui-components/index.js +12 -0
- package/framework/js/components/ui-components/interactive-image.js +235 -0
- package/framework/js/components/ui-components/interactive-timeline.js +285 -0
- package/framework/js/components/ui-components/intro-cards.js +35 -0
- package/framework/js/components/ui-components/lightbox.js +652 -0
- package/framework/js/components/ui-components/modal.js +386 -0
- package/framework/js/components/ui-components/notifications.js +145 -0
- package/framework/js/components/ui-components/progress.js +88 -0
- package/framework/js/components/ui-components/quote.js +41 -0
- package/framework/js/components/ui-components/stats.js +33 -0
- package/framework/js/components/ui-components/steps.js +41 -0
- package/framework/js/components/ui-components/tabs.js +255 -0
- package/framework/js/components/ui-components/timeline.js +42 -0
- package/framework/js/components/ui-components/toggle-group.js +73 -0
- package/framework/js/components/ui-components/tooltip.js +458 -0
- package/framework/js/components/ui-components/value-display.js +133 -0
- package/framework/js/components/ui-components/video-player.js +686 -0
- package/framework/js/core/component-catalog.js +121 -0
- package/framework/js/core/event-bus.js +178 -0
- package/framework/js/core/interaction-catalog.js +149 -0
- package/framework/js/dev/runtime-linter.js +1725 -0
- package/framework/js/drivers/cmi5-driver.js +768 -0
- package/framework/js/drivers/driver-factory.js +77 -0
- package/framework/js/drivers/driver-interface.js +110 -0
- package/framework/js/drivers/http-driver-base.js +241 -0
- package/framework/js/drivers/lti-driver.js +508 -0
- package/framework/js/drivers/proxy-driver.js +444 -0
- package/framework/js/drivers/scorm-12-driver.js +560 -0
- package/framework/js/drivers/scorm-2004-driver.js +775 -0
- package/framework/js/drivers/scorm-driver-base.js +112 -0
- package/framework/js/engagement/engagement-manager.js +404 -0
- package/framework/js/engagement/engagement-progress.js +191 -0
- package/framework/js/engagement/engagement-trackers.js +215 -0
- package/framework/js/engagement/requirement-strategies.js +268 -0
- package/framework/js/main.js +727 -0
- package/framework/js/managers/accessibility-manager.js +499 -0
- package/framework/js/managers/assessment-manager.js +230 -0
- package/framework/js/managers/audio-manager.js +944 -0
- package/framework/js/managers/comment-manager.js +88 -0
- package/framework/js/managers/flag-manager.js +86 -0
- package/framework/js/managers/interaction-manager.js +254 -0
- package/framework/js/managers/interaction-registry.js +96 -0
- package/framework/js/managers/objective-manager.js +423 -0
- package/framework/js/managers/score-manager.js +441 -0
- package/framework/js/managers/video-manager.js +536 -0
- package/framework/js/navigation/Breadcrumbs.js +234 -0
- package/framework/js/navigation/NavigationActions.js +1132 -0
- package/framework/js/navigation/NavigationState.js +276 -0
- package/framework/js/navigation/NavigationUI.js +574 -0
- package/framework/js/navigation/document-gallery.js +357 -0
- package/framework/js/navigation/navigation-helpers.js +175 -0
- package/framework/js/navigation/navigation-validators.js +174 -0
- package/framework/js/state/index.js +8 -0
- package/framework/js/state/lms-connection.js +482 -0
- package/framework/js/state/lms-error-utils.js +58 -0
- package/framework/js/state/state-commits.js +200 -0
- package/framework/js/state/state-domains.js +86 -0
- package/framework/js/state/state-manager.js +502 -0
- package/framework/js/state/state-validation.js +311 -0
- package/framework/js/state/transaction-log.js +41 -0
- package/framework/js/state/xapi-statement-service.js +325 -0
- package/framework/js/utilities/access-control.js +99 -0
- package/framework/js/utilities/breakpoint-manager.js +315 -0
- package/framework/js/utilities/canvas-slide.js +35 -0
- package/framework/js/utilities/conditional-display.js +388 -0
- package/framework/js/utilities/course-channel.js +214 -0
- package/framework/js/utilities/course-helpers.js +420 -0
- package/framework/js/utilities/data-reporter.js +273 -0
- package/framework/js/utilities/error-reporter.js +313 -0
- package/framework/js/utilities/hotspot-helper.js +341 -0
- package/framework/js/utilities/icons.js +348 -0
- package/framework/js/utilities/logger.js +92 -0
- package/framework/js/utilities/markdown-renderer.js +45 -0
- package/framework/js/utilities/scroll-tracker.js +68 -0
- package/framework/js/utilities/ui-initializer.js +146 -0
- package/framework/js/utilities/utilities.js +293 -0
- package/framework/js/utilities/view-manager.js +227 -0
- package/framework/js/validation/html-validators.js +422 -0
- package/framework/js/validation/scorm-validators.js +438 -0
- package/framework/js/vendor/pipwerks.js +931 -0
- package/framework/scripts/generate-narration.js +629 -0
- package/framework/scripts/tts-providers/azure-provider.js +178 -0
- package/framework/scripts/tts-providers/base-provider.js +81 -0
- package/framework/scripts/tts-providers/deepgram-provider.js +135 -0
- package/framework/scripts/tts-providers/elevenlabs-provider.js +148 -0
- package/framework/scripts/tts-providers/google-provider.js +272 -0
- package/framework/scripts/tts-providers/index.js +158 -0
- package/framework/scripts/tts-providers/openai-provider.js +143 -0
- package/framework/version.json +63 -0
- package/lib/authoring-api.js +919 -0
- package/lib/build-linter.js +450 -0
- package/lib/build-packaging.js +186 -0
- package/lib/build.js +88 -0
- package/lib/cloud.js +691 -0
- package/lib/convert.js +341 -0
- package/lib/course-parser.js +936 -0
- package/lib/course-writer.js +258 -0
- package/lib/create.js +248 -0
- package/lib/css-index.js +237 -0
- package/lib/dev.js +51 -0
- package/lib/export-content.js +1246 -0
- package/lib/headless-browser.js +413 -0
- package/lib/import.js +377 -0
- package/lib/index.js +80 -0
- package/lib/info.js +79 -0
- package/lib/interaction-formatters.js +568 -0
- package/lib/manifest/cmi5-manifest.js +63 -0
- package/lib/manifest/lti-tool-config.js +53 -0
- package/lib/manifest/manifest-factory.js +99 -0
- package/lib/manifest/scorm-12-manifest.js +61 -0
- package/lib/manifest/scorm-2004-manifest.js +94 -0
- package/lib/manifest/scorm-proxy-manifest.js +104 -0
- package/lib/manifest-parser.js +96 -0
- package/lib/mcp-prompts.js +753 -0
- package/lib/mcp-server.js +316 -0
- package/lib/narration.js +53 -0
- package/lib/pdf-structure.js +142 -0
- package/lib/preview-export.js +231 -0
- package/lib/preview-routes-api.js +662 -0
- package/lib/preview-routes-editing.js +159 -0
- package/lib/preview-routes-lms.js +230 -0
- package/lib/preview-server.js +564 -0
- package/lib/project-utils.js +269 -0
- package/lib/proxy-templates/proxy.html +68 -0
- package/lib/proxy-templates/scorm-bridge.js +112 -0
- package/lib/scaffold.js +193 -0
- package/lib/schema-extractor.js +361 -0
- package/lib/slide-source-editor.js +586 -0
- package/lib/stub-player/app-viewer.js +195 -0
- package/lib/stub-player/app.js +370 -0
- package/lib/stub-player/catalog-panel.js +312 -0
- package/lib/stub-player/config-panel.js +1303 -0
- package/lib/stub-player/content-generator.js +586 -0
- package/lib/stub-player/content-viewer.js +173 -0
- package/lib/stub-player/debug-panel.js +420 -0
- package/lib/stub-player/edit-mode.js +922 -0
- package/lib/stub-player/edit-utils.js +400 -0
- package/lib/stub-player/header-bar.js +354 -0
- package/lib/stub-player/interaction-editor.js +210 -0
- package/lib/stub-player/interactions-panel.js +565 -0
- package/lib/stub-player/lms-api.js +1094 -0
- package/lib/stub-player/login-screen.js +74 -0
- package/lib/stub-player/outline-mode.js +689 -0
- package/lib/stub-player/styles/_assessments-panel.css +245 -0
- package/lib/stub-player/styles/_base.css +89 -0
- package/lib/stub-player/styles/_catalog-icons.css +96 -0
- package/lib/stub-player/styles/_catalog-panel.css +291 -0
- package/lib/stub-player/styles/_config-panel.css +636 -0
- package/lib/stub-player/styles/_content-viewer.css +834 -0
- package/lib/stub-player/styles/_debug-panel.css +576 -0
- package/lib/stub-player/styles/_edit-mode.css +128 -0
- package/lib/stub-player/styles/_header-bar.css +343 -0
- package/lib/stub-player/styles/_interaction-editor.css +140 -0
- package/lib/stub-player/styles/_interactions-panel.css +1038 -0
- package/lib/stub-player/styles/_login-screen.css +102 -0
- package/lib/stub-player/styles/_outline-mode.css +752 -0
- package/lib/stub-player/styles.css +15 -0
- package/lib/stub-player.js +160 -0
- package/lib/test-data-reporting.js +176 -0
- package/lib/test-error-reporting.js +146 -0
- package/lib/token.js +86 -0
- package/lib/upgrade.js +257 -0
- package/lib/validation-rules.js +517 -0
- package/lib/vite-plugin-content-discovery.js +296 -0
- package/package.json +108 -0
- package/schemas/XMLSchema.dtd +402 -0
- package/schemas/adlcp_v1p3.xsd +111 -0
- package/schemas/adlnav_v1p3.xsd +61 -0
- package/schemas/adlseq_v1p3.xsd +93 -0
- package/schemas/common/anyElement.xsd +27 -0
- package/schemas/common/dataTypes.xsd +138 -0
- package/schemas/common/elementNames.xsd +767 -0
- package/schemas/common/elementTypes.xsd +786 -0
- package/schemas/common/rootElement.xsd +31 -0
- package/schemas/common/vocabTypes.xsd +345 -0
- package/schemas/common/vocabValues.xsd +257 -0
- package/schemas/datatypes.dtd +203 -0
- package/schemas/ims_xml.xsd +35 -0
- package/schemas/imscp_v1p1.xsd +368 -0
- package/schemas/imsss_v1p0.xsd +67 -0
- package/schemas/imsss_v1p0auxresource.xsd +19 -0
- package/schemas/imsss_v1p0control.xsd +20 -0
- package/schemas/imsss_v1p0delivery.xsd +17 -0
- package/schemas/imsss_v1p0limit.xsd +47 -0
- package/schemas/imsss_v1p0objective.xsd +67 -0
- package/schemas/imsss_v1p0random.xsd +16 -0
- package/schemas/imsss_v1p0rollup.xsd +46 -0
- package/schemas/imsss_v1p0seqrule.xsd +108 -0
- package/schemas/imsss_v1p0util.xsd +94 -0
- package/schemas/license.txt +17 -0
- package/schemas/lom.xsd +102 -0
- package/schemas/lomCustom.xsd +62 -0
- package/schemas/lomLoose.xsd +62 -0
- package/schemas/lomStrict.xsd +62 -0
- package/schemas/xml.xsd +81 -0
- package/template/.env.example +92 -0
- package/template/course/assets/audio/example-intro.mp3 +0 -0
- package/template/course/assets/audio/example-ui-demo--compact-player.mp3 +0 -0
- package/template/course/assets/audio/example-ui-demo--demo-modal.mp3 +0 -0
- package/template/course/assets/audio/example-ui-demo--full-player.mp3 +0 -0
- package/template/course/assets/docs/example_md_1.md +39 -0
- package/template/course/assets/docs/example_md_2.md +41 -0
- package/template/course/assets/docs/example_pdf_1_thumbnail.png +0 -0
- package/template/course/assets/docs/example_pdf_2.pdf +0 -0
- package/template/course/assets/images/course-architecture.svg +36 -0
- package/template/course/assets/images/logo.svg +14 -0
- package/template/course/assets/widgets/counter-demo.html +190 -0
- package/template/course/assets/widgets/gravity-painter.html +384 -0
- package/template/course/course-config.js +539 -0
- package/template/course/icons.js +19 -0
- package/template/course/interactions/PLUGIN_GUIDE.md +97 -0
- package/template/course/slides/example-course-structure.js +138 -0
- package/template/course/slides/example-final-exam.js +144 -0
- package/template/course/slides/example-finishing.js +127 -0
- package/template/course/slides/example-interactions-showcase.js +615 -0
- package/template/course/slides/example-preview-tour.js +129 -0
- package/template/course/slides/example-remedial.js +143 -0
- package/template/course/slides/example-summary.js +103 -0
- package/template/course/slides/example-ui-showcase.js +1805 -0
- package/template/course/slides/example-welcome.js +123 -0
- package/template/course/slides/example-workflow.js +140 -0
- package/template/course/theme.css +165 -0
- package/template/eslint.config.js +47 -0
- package/template/package.json +28 -0
- package/template/vite.config.js +339 -0
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* preview-server.js - Live preview with stub LMS + Vite build watch
|
|
3
|
+
*
|
|
4
|
+
* Runs Vite in build watch mode to output to dist/, then serves dist/ with
|
|
5
|
+
* a stub SCORM API wrapper. Includes live reload via Server-Sent Events.
|
|
6
|
+
*
|
|
7
|
+
* Supports two modes:
|
|
8
|
+
* - Course project mode: expects course/ and framework/ at cwd
|
|
9
|
+
* - Framework dev mode: expects template/course/ and framework/ at cwd (use --framework-dev)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import fs from 'fs';
|
|
13
|
+
import path from 'path';
|
|
14
|
+
import http from 'http';
|
|
15
|
+
import { spawn, exec } from 'child_process';
|
|
16
|
+
import { fileURLToPath } from 'url';
|
|
17
|
+
|
|
18
|
+
import { generateStubPlayer } from './stub-player.js';
|
|
19
|
+
import { generateContentHtml } from './stub-player/content-generator.js';
|
|
20
|
+
import { parseElements, resolveElementByPath } from './course-parser.js';
|
|
21
|
+
import { getComponentCatalog, getInteractionCatalog } from './authoring-api.js';
|
|
22
|
+
import { handleApiRoutes } from './preview-routes-api.js';
|
|
23
|
+
import { handleEditingRoutes } from './preview-routes-editing.js';
|
|
24
|
+
import { handleLmsRoutes, createLmsStore } from './preview-routes-lms.js';
|
|
25
|
+
import {
|
|
26
|
+
validateProject, escapeHtml, getMimeType, serveFile,
|
|
27
|
+
countSlides, findSlideById, collectSlideIds
|
|
28
|
+
} from './project-utils.js';
|
|
29
|
+
|
|
30
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
31
|
+
|
|
32
|
+
// ============================================================================
|
|
33
|
+
// Utility Functions
|
|
34
|
+
// ============================================================================
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Find an element by its structural path in HTML.
|
|
38
|
+
* Uses course-parser's universal element parsing for consistency across all tools.
|
|
39
|
+
*/
|
|
40
|
+
function findElementByPath(html, targetPath) {
|
|
41
|
+
const elements = parseElements(html);
|
|
42
|
+
return resolveElementByPath(elements, targetPath);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Simple markdown-to-HTML conversion for outline display.
|
|
49
|
+
*/
|
|
50
|
+
function simpleMarkdownToHtml(md) {
|
|
51
|
+
const lines = md.split('\n');
|
|
52
|
+
const html = [];
|
|
53
|
+
let inCodeBlock = false;
|
|
54
|
+
let inList = false;
|
|
55
|
+
|
|
56
|
+
for (const line of lines) {
|
|
57
|
+
if (line.trim().startsWith('```')) {
|
|
58
|
+
if (inCodeBlock) {
|
|
59
|
+
html.push('</code></pre>');
|
|
60
|
+
inCodeBlock = false;
|
|
61
|
+
} else {
|
|
62
|
+
if (inList) { html.push('</ul>'); inList = false; }
|
|
63
|
+
html.push('<pre><code>');
|
|
64
|
+
inCodeBlock = true;
|
|
65
|
+
}
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (inCodeBlock) {
|
|
69
|
+
html.push(escapeHtml(line));
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const trimmed = line.trim();
|
|
74
|
+
if (!trimmed) {
|
|
75
|
+
if (inList) { html.push('</ul>'); inList = false; }
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const headerMatch = trimmed.match(/^(#{1,6})\s+(.+)/);
|
|
80
|
+
if (headerMatch) {
|
|
81
|
+
if (inList) { html.push('</ul>'); inList = false; }
|
|
82
|
+
const level = headerMatch[1].length;
|
|
83
|
+
html.push(`<h${level}>${inlineFormat(headerMatch[2])}</h${level}>`);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (trimmed.match(/^[-*]\s+/)) {
|
|
88
|
+
if (!inList) { html.push('<ul>'); inList = true; }
|
|
89
|
+
html.push(`<li>${inlineFormat(trimmed.replace(/^[-*]\s+/, ''))}</li>`);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (trimmed.match(/^\d+\.\s+/)) {
|
|
94
|
+
if (!inList) { html.push('<ul>'); inList = true; }
|
|
95
|
+
html.push(`<li>${inlineFormat(trimmed.replace(/^\d+\.\s+/, ''))}</li>`);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (trimmed.match(/^---+$/)) {
|
|
100
|
+
if (inList) { html.push('</ul>'); inList = false; }
|
|
101
|
+
html.push('<hr>');
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (inList) { html.push('</ul>'); inList = false; }
|
|
106
|
+
html.push(`<p>${inlineFormat(trimmed)}</p>`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (inList) html.push('</ul>');
|
|
110
|
+
if (inCodeBlock) html.push('</code></pre>');
|
|
111
|
+
return html.join('\n');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function inlineFormat(text) {
|
|
115
|
+
return escapeHtml(text)
|
|
116
|
+
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
|
117
|
+
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
|
118
|
+
.replace(/`(.+?)`/g, '<code>$1</code>')
|
|
119
|
+
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Returns example HTML for a given component or interaction type.
|
|
124
|
+
* Dynamically pulls from schema.example via catalog APIs.
|
|
125
|
+
*/
|
|
126
|
+
function getExampleHtml(type, category) {
|
|
127
|
+
try {
|
|
128
|
+
if (category === 'interaction') {
|
|
129
|
+
const catalog = getInteractionCatalog(type);
|
|
130
|
+
if (catalog.example) return catalog.example;
|
|
131
|
+
} else {
|
|
132
|
+
const catalog = getComponentCatalog(type);
|
|
133
|
+
if (catalog.example) return catalog.example;
|
|
134
|
+
if (catalog.usage) return catalog.usage;
|
|
135
|
+
}
|
|
136
|
+
} catch { /* ignore catalog errors */ }
|
|
137
|
+
|
|
138
|
+
return `<div class="callout callout-info"><p>No preview available for <strong>${escapeHtml(type)}</strong>.</p></div>`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Parse multipart form-data and save uploaded files to the correct assets subdirectory.
|
|
143
|
+
*/
|
|
144
|
+
function parseAndSaveFiles(buffer, boundary, assetsDir) {
|
|
145
|
+
const boundaryBuf = Buffer.from('--' + boundary);
|
|
146
|
+
const uploaded = [];
|
|
147
|
+
let pos = 0;
|
|
148
|
+
|
|
149
|
+
while (pos < buffer.length) {
|
|
150
|
+
const start = buffer.indexOf(boundaryBuf, pos);
|
|
151
|
+
if (start === -1) break;
|
|
152
|
+
const end = buffer.indexOf(boundaryBuf, start + boundaryBuf.length);
|
|
153
|
+
if (end === -1) break;
|
|
154
|
+
|
|
155
|
+
const part = buffer.slice(start + boundaryBuf.length, end);
|
|
156
|
+
const headerEnd = part.indexOf('\r\n\r\n');
|
|
157
|
+
if (headerEnd === -1) { pos = end; continue; }
|
|
158
|
+
|
|
159
|
+
const headers = part.slice(0, headerEnd).toString('utf-8');
|
|
160
|
+
const filenameMatch = headers.match(/filename="([^"]+)"/);
|
|
161
|
+
if (!filenameMatch) { pos = end; continue; }
|
|
162
|
+
|
|
163
|
+
const filename = path.basename(filenameMatch[1]);
|
|
164
|
+
const fileData = part.slice(headerEnd + 4, part.length - 2);
|
|
165
|
+
|
|
166
|
+
const ext = path.extname(filename).toLowerCase();
|
|
167
|
+
let subdir;
|
|
168
|
+
if (['.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.avif'].includes(ext)) {
|
|
169
|
+
subdir = 'images';
|
|
170
|
+
} else if (['.mp3', '.wav', '.ogg', '.m4a', '.aac'].includes(ext)) {
|
|
171
|
+
subdir = 'audio';
|
|
172
|
+
} else {
|
|
173
|
+
subdir = 'docs';
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const targetDir = path.join(assetsDir, subdir);
|
|
177
|
+
if (!fs.existsSync(targetDir)) fs.mkdirSync(targetDir, { recursive: true });
|
|
178
|
+
|
|
179
|
+
fs.writeFileSync(path.join(targetDir, filename), fileData);
|
|
180
|
+
uploaded.push({ filename, subdir });
|
|
181
|
+
console.log(` š¦ Asset uploaded: ${subdir}/${filename}`);
|
|
182
|
+
pos = end;
|
|
183
|
+
}
|
|
184
|
+
return uploaded;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
function getCourseTitle(coursePath) {
|
|
189
|
+
const configPath = path.join(coursePath, 'course-config.js');
|
|
190
|
+
try {
|
|
191
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
192
|
+
const match = content.match(/title:\s*['"`]([^'"`]+)['"`]/);
|
|
193
|
+
return match ? match[1] : 'SCORM Course';
|
|
194
|
+
} catch {
|
|
195
|
+
return 'SCORM Course';
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
// ============================================================================
|
|
202
|
+
// Main Server
|
|
203
|
+
// ============================================================================
|
|
204
|
+
|
|
205
|
+
export async function previewServer(options = {}) {
|
|
206
|
+
const frameworkDev = options.frameworkDev || false;
|
|
207
|
+
const paths = validateProject({ frameworkDev });
|
|
208
|
+
const title = options.title || getCourseTitle(paths.coursePath);
|
|
209
|
+
const previewPort = parseInt(options.port || '4173', 10);
|
|
210
|
+
const distDir = path.join(process.cwd(), 'dist');
|
|
211
|
+
|
|
212
|
+
console.log('\nš Starting preview server...');
|
|
213
|
+
console.log(` š Course: ${paths.coursePath}`);
|
|
214
|
+
console.log(` šØ Build output: ${distDir}`);
|
|
215
|
+
|
|
216
|
+
// Build tracking state
|
|
217
|
+
const buildState = {
|
|
218
|
+
errors: [],
|
|
219
|
+
warnings: [],
|
|
220
|
+
lastBuildTime: null,
|
|
221
|
+
lastBuildSuccess: false
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const sseClients = new Set();
|
|
225
|
+
const broadcastReload = () => {
|
|
226
|
+
for (const client of sseClients) {
|
|
227
|
+
client.write('data: reload\n\n');
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
// Resolve Vite binary ā on Unix, spawn directly to avoid zombie /bin/sh
|
|
232
|
+
// processes that survive when we kill the parent PID. On Windows, .cmd files
|
|
233
|
+
// require shell: true (no zombie risk since Windows doesn't fork /bin/sh).
|
|
234
|
+
const isWindows = process.platform === 'win32';
|
|
235
|
+
const viteBin = path.join(process.cwd(), 'node_modules', '.bin', 'vite');
|
|
236
|
+
const viteArgs = ['build', '--watch', '--mode', 'development', '--logLevel', 'warn'];
|
|
237
|
+
if (paths.viteConfig) {
|
|
238
|
+
viteArgs.push('--config', paths.viteConfig);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const env = { ...process.env };
|
|
242
|
+
// Expose lib dir so vite.config.js can resolve coursecode utilities
|
|
243
|
+
// even when npm link + Vite's .vite-temp copy breaks normal resolution
|
|
244
|
+
env.COURSECODE_LIB_DIR = __dirname;
|
|
245
|
+
// Signal to framework code that this is a local dev build.
|
|
246
|
+
// Vite auto-exposes VITE_* env vars to client code via import.meta.env.
|
|
247
|
+
// Reporters check this to suppress external reporting locally.
|
|
248
|
+
env.VITE_COURSECODE_LOCAL = 'true';
|
|
249
|
+
if (options.format) {
|
|
250
|
+
const previewFormat = options.format.replace(/-proxy$|-remote$/, '');
|
|
251
|
+
env.LMS_FORMAT = previewFormat;
|
|
252
|
+
if (previewFormat !== options.format) {
|
|
253
|
+
console.log(` š¦ Format: ${options.format} ā ${previewFormat} (preview mode)\n`);
|
|
254
|
+
} else {
|
|
255
|
+
console.log(` š¦ Format override: ${options.format}\n`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Start Vite build in watch mode
|
|
260
|
+
const viteProcess = spawn(viteBin, viteArgs, {
|
|
261
|
+
cwd: process.cwd(),
|
|
262
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
263
|
+
env,
|
|
264
|
+
shell: isWindows
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
let initialBuildDone = false;
|
|
268
|
+
|
|
269
|
+
viteProcess.stdout.on('data', (data) => {
|
|
270
|
+
const output = data.toString();
|
|
271
|
+
process.stdout.write(output);
|
|
272
|
+
if (output.includes('Build complete')) {
|
|
273
|
+
buildState.lastBuildTime = new Date().toISOString();
|
|
274
|
+
buildState.lastBuildSuccess = true;
|
|
275
|
+
buildState.errors = [];
|
|
276
|
+
if (initialBuildDone) {
|
|
277
|
+
broadcastReload();
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (output.includes('warning') || output.includes('Warning')) {
|
|
281
|
+
const lines = output.split('\n').filter(l => l.includes('warning') || l.includes('Warning'));
|
|
282
|
+
for (const line of lines) {
|
|
283
|
+
if (!buildState.warnings.some(w => w.message === line.trim())) {
|
|
284
|
+
buildState.warnings.push({ type: 'warning', message: line.trim(), time: new Date().toISOString() });
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if (buildState.warnings.length > 20) buildState.warnings = buildState.warnings.slice(-20);
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
viteProcess.stderr.on('data', (data) => {
|
|
292
|
+
const output = data.toString();
|
|
293
|
+
process.stderr.write(output);
|
|
294
|
+
if (output.includes('error') || output.includes('Error') || output.includes('ERROR')) {
|
|
295
|
+
buildState.lastBuildSuccess = false;
|
|
296
|
+
buildState.errors.push({
|
|
297
|
+
type: 'build',
|
|
298
|
+
message: output.trim(),
|
|
299
|
+
time: new Date().toISOString()
|
|
300
|
+
});
|
|
301
|
+
if (buildState.errors.length > 10) buildState.errors = buildState.errors.slice(-10);
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// Wait for initial build
|
|
306
|
+
await new Promise((resolve) => {
|
|
307
|
+
const indexPath = path.join(distDir, 'index.html');
|
|
308
|
+
let attempts = 0;
|
|
309
|
+
const maxAttempts = 120;
|
|
310
|
+
|
|
311
|
+
const checkReady = setInterval(() => {
|
|
312
|
+
attempts++;
|
|
313
|
+
if (fs.existsSync(indexPath)) {
|
|
314
|
+
clearInterval(checkReady);
|
|
315
|
+
initialBuildDone = true;
|
|
316
|
+
resolve();
|
|
317
|
+
} else if (attempts >= maxAttempts) {
|
|
318
|
+
clearInterval(checkReady);
|
|
319
|
+
console.error('ā ļø Build timeout - index.html not found');
|
|
320
|
+
initialBuildDone = true;
|
|
321
|
+
resolve();
|
|
322
|
+
}
|
|
323
|
+
}, 500);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// Generate course content HTML
|
|
327
|
+
let courseContent = null;
|
|
328
|
+
if (options.content !== false) {
|
|
329
|
+
console.log(' Generating course content for viewer...');
|
|
330
|
+
courseContent = await generateContentHtml({
|
|
331
|
+
coursePath: paths.coursePath,
|
|
332
|
+
includeNarration: true
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Generate stub player HTML
|
|
337
|
+
const storageKey = frameworkDev ? 'scorm_framework_dev' : 'scorm_preview_live';
|
|
338
|
+
const stubHtml = generateStubPlayer({
|
|
339
|
+
title,
|
|
340
|
+
launchUrl: '/course/index.html',
|
|
341
|
+
storageKey,
|
|
342
|
+
password: null,
|
|
343
|
+
isLive: true,
|
|
344
|
+
liveReload: true,
|
|
345
|
+
courseContent,
|
|
346
|
+
isDesktop: options.desktop || false
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
// Shared context object passed to route modules
|
|
350
|
+
const ctx = {
|
|
351
|
+
paths,
|
|
352
|
+
distDir,
|
|
353
|
+
buildState,
|
|
354
|
+
sseClients,
|
|
355
|
+
broadcastReload,
|
|
356
|
+
lmsStore: createLmsStore(),
|
|
357
|
+
getMimeType,
|
|
358
|
+
findSlideById,
|
|
359
|
+
countSlides,
|
|
360
|
+
collectSlideIds,
|
|
361
|
+
simpleMarkdownToHtml,
|
|
362
|
+
getExampleHtml,
|
|
363
|
+
parseAndSaveFiles,
|
|
364
|
+
serveFile,
|
|
365
|
+
findElementByPath
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
// Create HTTP server and dispatch routes
|
|
369
|
+
const server = http.createServer(async (req, res) => {
|
|
370
|
+
const url = req.url.split('?')[0];
|
|
371
|
+
|
|
372
|
+
// LMS routes (state sync + testing API)
|
|
373
|
+
if (handleLmsRoutes(ctx, req, res, url)) return;
|
|
374
|
+
|
|
375
|
+
// API routes (read-only)
|
|
376
|
+
if (await handleApiRoutes(ctx, req, res, url)) return;
|
|
377
|
+
|
|
378
|
+
// Editing routes (mutations)
|
|
379
|
+
if (handleEditingRoutes(ctx, req, res, url)) return;
|
|
380
|
+
|
|
381
|
+
// Serve stub player for root
|
|
382
|
+
if (url === '/' || url === '/index.html') {
|
|
383
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
384
|
+
res.end(stubHtml);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Serve files from dist/ for /course/* requests
|
|
389
|
+
if (url.startsWith('/course/')) {
|
|
390
|
+
const relativePath = url.slice('/course/'.length) || 'index.html';
|
|
391
|
+
const filePath = path.join(distDir, relativePath);
|
|
392
|
+
serveFile(filePath, res);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (url === '/course') {
|
|
397
|
+
const filePath = path.join(distDir, 'index.html');
|
|
398
|
+
serveFile(filePath, res);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Serve _content-manifest.json from dist/
|
|
403
|
+
if (url === '/_content-manifest.json') {
|
|
404
|
+
const filePath = path.join(distDir, '_content-manifest.json');
|
|
405
|
+
serveFile(filePath, res);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Serve _gallery-manifest.json (generated on-the-fly in dev mode)
|
|
410
|
+
if (url === '/_gallery-manifest.json' || url === '/course/_gallery-manifest.json') {
|
|
411
|
+
const staticPath = path.join(distDir, '_gallery-manifest.json');
|
|
412
|
+
if (fs.existsSync(staticPath)) {
|
|
413
|
+
serveFile(staticPath, res);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
try {
|
|
418
|
+
const docsDir = path.join(paths.coursePath, 'assets', 'docs');
|
|
419
|
+
if (!fs.existsSync(docsDir)) {
|
|
420
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
421
|
+
res.end(JSON.stringify({ items: [] }));
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const files = fs.readdirSync(docsDir);
|
|
426
|
+
const thumbnailFiles = new Set(
|
|
427
|
+
files.filter(f => f.match(/_thumbnail\.(png|jpg|jpeg|webp)$/i))
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
const allowedTypes = new Set(['pdf', 'md', 'jpg', 'png']);
|
|
431
|
+
const items = [];
|
|
432
|
+
|
|
433
|
+
for (const file of files) {
|
|
434
|
+
if (file.startsWith('.')) continue;
|
|
435
|
+
if (file.match(/_thumbnail\.(png|jpg|jpeg|webp)$/i)) continue;
|
|
436
|
+
|
|
437
|
+
const ext = path.extname(file).slice(1).toLowerCase();
|
|
438
|
+
if (!allowedTypes.has(ext)) continue;
|
|
439
|
+
|
|
440
|
+
let type;
|
|
441
|
+
if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'].includes(ext)) {
|
|
442
|
+
type = 'image';
|
|
443
|
+
} else if (ext === 'pdf') {
|
|
444
|
+
type = 'pdf';
|
|
445
|
+
} else if (ext === 'md') {
|
|
446
|
+
type = 'markdown';
|
|
447
|
+
} else {
|
|
448
|
+
type = 'file';
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const src = `course/assets/docs/${file}`;
|
|
452
|
+
const baseName = path.basename(file, path.extname(file));
|
|
453
|
+
|
|
454
|
+
let thumbnail = null;
|
|
455
|
+
for (const thumbExt of ['png', 'jpg', 'jpeg', 'webp']) {
|
|
456
|
+
const thumbFile = `${baseName}_thumbnail.${thumbExt}`;
|
|
457
|
+
if (thumbnailFiles.has(thumbFile)) {
|
|
458
|
+
thumbnail = `course/assets/docs/${thumbFile}`;
|
|
459
|
+
break;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const label = baseName
|
|
464
|
+
.replace(/[_-]/g, ' ')
|
|
465
|
+
.replace(/\b\w/g, c => c.toUpperCase());
|
|
466
|
+
|
|
467
|
+
items.push({ src, type, label, ...(thumbnail ? { thumbnail } : {}) });
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
items.sort((a, b) => a.label.localeCompare(b.label));
|
|
471
|
+
|
|
472
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
473
|
+
res.end(JSON.stringify({ items }));
|
|
474
|
+
} catch (err) {
|
|
475
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
476
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
477
|
+
}
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// 404 for everything else
|
|
482
|
+
res.writeHead(404);
|
|
483
|
+
res.end('Not found');
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
const contentNote = options.content !== false ? ' ⢠Content viewer (š button in toolbar)\n' : '';
|
|
487
|
+
|
|
488
|
+
const startListening = (retried = false) => {
|
|
489
|
+
server.listen(previewPort, () => {
|
|
490
|
+
console.log(`
|
|
491
|
+
ā
Preview server running!
|
|
492
|
+
|
|
493
|
+
šÆ Open: http://localhost:${previewPort}
|
|
494
|
+
|
|
495
|
+
Features:
|
|
496
|
+
⢠Live reload - browser updates automatically on rebuild
|
|
497
|
+
⢠Auto-rebuild on file changes (watch mode)
|
|
498
|
+
⢠Stub SCORM API with localStorage persistence
|
|
499
|
+
⢠Debug panel with API log and validation
|
|
500
|
+
⢠MCP automation bridge (coursecode mcp)
|
|
501
|
+
${contentNote}
|
|
502
|
+
URL Parameters:
|
|
503
|
+
⢠?skipGating=true - Bypass navigation locks
|
|
504
|
+
⢠?debug=true - Open debug panel on load
|
|
505
|
+
|
|
506
|
+
Press Ctrl+C to stop
|
|
507
|
+
`);
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
server.on('error', (err) => {
|
|
511
|
+
if (err.code === 'EADDRINUSE' && !retried) {
|
|
512
|
+
console.warn(`\nā ļø STALE PROCESS DETECTED ā port ${previewPort} is already in use.`);
|
|
513
|
+
console.warn(' Killing stale process and retrying...');
|
|
514
|
+
exec(`lsof -ti :${previewPort}`, (_, stdout) => {
|
|
515
|
+
const pids = (stdout || '').trim();
|
|
516
|
+
if (pids) console.warn(` Killed PID(s): ${pids.split('\n').join(', ')}`);
|
|
517
|
+
exec(`lsof -ti :${previewPort} | xargs kill -9 2>/dev/null`, () => {
|
|
518
|
+
setTimeout(() => {
|
|
519
|
+
server.close();
|
|
520
|
+
startListening(true);
|
|
521
|
+
}, 500);
|
|
522
|
+
});
|
|
523
|
+
});
|
|
524
|
+
} else if (err.code === 'EADDRINUSE') {
|
|
525
|
+
console.error(`\nā Port ${previewPort} is still in use after retry. Kill it manually:\n lsof -ti :${previewPort} | xargs kill -9`);
|
|
526
|
+
process.exit(1);
|
|
527
|
+
} else {
|
|
528
|
+
throw err;
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
startListening();
|
|
534
|
+
|
|
535
|
+
// Handle cleanup
|
|
536
|
+
const cleanup = () => {
|
|
537
|
+
console.log('\n\nShutting down...');
|
|
538
|
+
viteProcess.kill();
|
|
539
|
+
server.close();
|
|
540
|
+
process.exit(0);
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
process.on('SIGINT', cleanup);
|
|
544
|
+
process.on('SIGTERM', cleanup);
|
|
545
|
+
|
|
546
|
+
viteProcess.on('close', (code) => {
|
|
547
|
+
if (code !== 0 && code !== null) {
|
|
548
|
+
console.error(`Vite process exited with code ${code}`);
|
|
549
|
+
server.close();
|
|
550
|
+
process.exit(1);
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// CLI entry point - allows running directly: node lib/preview-server.js [--framework-dev]
|
|
556
|
+
if (process.argv[1] && process.argv[1].endsWith('preview-server.js')) {
|
|
557
|
+
const args = process.argv.slice(2);
|
|
558
|
+
const options = {
|
|
559
|
+
frameworkDev: args.includes('--framework-dev'),
|
|
560
|
+
port: args.find(a => a.startsWith('--port='))?.split('=')[1] || '4173',
|
|
561
|
+
format: process.env.LMS_FORMAT || null
|
|
562
|
+
};
|
|
563
|
+
previewServer(options);
|
|
564
|
+
}
|