From 5d4872b75113c7ea412e29968f96dfdaaeccdf39 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 4 Nov 2025 18:51:10 +0000 Subject: [PATCH 1/5] Fix static course manifest structure for scaffolded apps Updates CLI and standalone-ui to use the new hierarchical manifest structure that was introduced for the docs site but not propagated to scaffolded applications. Changes: - CLI now generates course-level skuilder.json for each course - CLI now generates root skuilder.json in public/ with course dependencies - standalone-ui updated to fetch root manifest and use new DataLayerProvider API The new structure has three levels: 1. Root /public/skuilder.json (lists course dependencies) 2. Course /static-courses/{id}/skuilder.json (points to manifest.json) 3. Course /static-courses/{id}/manifest.json (contains courseConfig and data) This matches the implementation in docs/.vitepress/theme/composables/useStaticDataLayer.ts and the updated StaticDataLayerProvider API from commits 4fb97b0, 0e2fb4b, 40aaf39. Fixes runtime failures in newly scaffolded static apps caused by API mismatch. --- packages/cli/src/utils/template.ts | 45 ++++++++++++++++++++++++++++++ packages/standalone-ui/src/main.ts | 29 ++++++++----------- 2 files changed, 56 insertions(+), 18 deletions(-) diff --git a/packages/cli/src/utils/template.ts b/packages/cli/src/utils/template.ts index 4589f8883..beecb3e6d 100644 --- a/packages/cli/src/utils/template.ts +++ b/packages/cli/src/utils/template.ts @@ -289,6 +289,35 @@ export async function generateSkuilderConfig( await fs.writeFile(configPath, JSON.stringify(skuilderConfig, null, 2)); + // For static data layer, create root skuilder.json manifest + if (config.dataLayerType === 'static' && outputPath && skuilderConfig.course) { + const publicPath = path.join(outputPath, 'public'); + await fs.mkdir(publicPath, { recursive: true }); + + // Determine course IDs to include in dependencies + const courseIds = config.importCourseIds && config.importCourseIds.length > 0 + ? config.importCourseIds + : [skuilderConfig.course]; + + // Create root skuilder.json with course dependencies + const rootManifest = { + name: `@skuilder/${config.projectName}`, + version: '1.0.0', + description: config.title, + dependencies: Object.fromEntries( + courseIds.map((courseId) => [ + `@skuilder/course-${courseId}`, + `/static-courses/${courseId}/skuilder.json`, + ]) + ), + }; + + await fs.writeFile( + path.join(publicPath, 'skuilder.json'), + JSON.stringify(rootManifest, null, 2) + ); + } + // For static data layer without imports, create empty course structure if ( config.dataLayerType === 'static' && @@ -393,6 +422,22 @@ async function createEmptyCourseStructure( await fs.writeFile(path.join(coursePath, 'manifest.json'), JSON.stringify(manifest, null, 2)); + // Create course-level skuilder.json (points to manifest.json) + const courseSkuilderJson = { + name: `@skuilder/course-${courseId}`, + version: '1.0.0', + description: title, + content: { + type: 'static', + manifest: './manifest.json', + }, + }; + + await fs.writeFile( + path.join(coursePath, 'skuilder.json'), + JSON.stringify(courseSkuilderJson, null, 2) + ); + // Create empty tags index await fs.writeFile( path.join(coursePath, 'indices', 'tags.json'), diff --git a/packages/standalone-ui/src/main.ts b/packages/standalone-ui/src/main.ts index 3a47aba9b..c4104e6ba 100644 --- a/packages/standalone-ui/src/main.ts +++ b/packages/standalone-ui/src/main.ts @@ -42,32 +42,25 @@ import config from '../skuilder.config.json'; }; if (config.dataLayerType === 'static') { - // Load manifest for static mode - const courseId = config.course; - if (!courseId) { - throw new Error('Course ID required for static data layer'); - } - + // Load root skuilder.json manifest for static mode try { - const manifestResponse = await fetch(`/static-courses/${courseId}/manifest.json`); - if (!manifestResponse.ok) { + const rootManifestUrl = '/skuilder.json'; + const rootManifestResponse = await fetch(rootManifestUrl); + if (!rootManifestResponse.ok) { throw new Error( - `Failed to load manifest: ${manifestResponse.status} ${manifestResponse.statusText}` + `Failed to load root manifest: ${rootManifestResponse.status} ${rootManifestResponse.statusText}` ); } - const manifest = await manifestResponse.json(); - console.log(`Loaded manifest for course ${courseId}`); - console.log(JSON.stringify(manifest)); + const rootManifest = await rootManifestResponse.json(); + console.log('[DEBUG] Loaded root manifest:', rootManifest); dataLayerOptions = { - staticContentPath: '/static-courses', - manifests: { - [courseId]: manifest, - }, + rootManifest, + rootManifestUrl: new URL(rootManifestUrl, window.location.href).href, }; } catch (error) { - console.error('[DEBUG] Failed to load course manifest:', error); - throw new Error(`Could not load course manifest for ${courseId}: ${error}`); + console.error('[DEBUG] Failed to load root manifest:', error); + throw new Error(`Could not load root skuilder.json manifest: ${error}`); } } From 79f544f39da054d69e6b0eafbca0d85b72f8318f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 4 Nov 2025 22:08:28 +0000 Subject: [PATCH 2/5] Generate course-level skuilder.json for imported courses The previous fix only generated course-level skuilder.json files for newly scaffolded empty courses. Imported courses (via --import-course-data) were missing this file, causing the manifest hierarchy to be incomplete. Now packCourses() creates the course-level skuilder.json after each course is successfully packed, ensuring all courses have the complete 3-level structure. --- packages/cli/src/utils/pack-courses.ts | 33 ++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/packages/cli/src/utils/pack-courses.ts b/packages/cli/src/utils/pack-courses.ts index a5b90a28e..e704d9bff 100644 --- a/packages/cli/src/utils/pack-courses.ts +++ b/packages/cli/src/utils/pack-courses.ts @@ -1,5 +1,6 @@ import chalk from 'chalk'; import path from 'path'; +import { promises as fs } from 'fs'; export interface PackCoursesOptions { server: string; @@ -39,6 +40,38 @@ export async function packCourses(options: PackCoursesOptions): Promise { }); console.log(chalk.green(`✅ Successfully packed course: ${courseId}`)); + + // Create course-level skuilder.json for the packed course + const coursePath = path.join(outputDir, courseId); + const manifestPath = path.join(coursePath, 'manifest.json'); + + // Read the manifest to get course title + let courseTitle = courseId; + try { + const manifestContent = await fs.readFile(manifestPath, 'utf-8'); + const manifest = JSON.parse(manifestContent); + courseTitle = manifest.courseName || manifest.courseConfig?.name || courseId; + } catch (error) { + console.warn(chalk.yellow(`⚠️ Could not read manifest for course title, using courseId`)); + } + + // Create course-level skuilder.json + const courseSkuilderJson = { + name: `@skuilder/course-${courseId}`, + version: '1.0.0', + description: courseTitle, + content: { + type: 'static', + manifest: './manifest.json', + }, + }; + + await fs.writeFile( + path.join(coursePath, 'skuilder.json'), + JSON.stringify(courseSkuilderJson, null, 2) + ); + + console.log(chalk.gray(`📄 Created skuilder.json for course: ${courseId}`)); } catch (error: unknown) { console.error(chalk.red(`❌ Failed to pack course ${courseId}:`)); console.error(chalk.red(error instanceof Error ? error.message : String(error))); From a557447747d149cd3e0d8afc9296801864a334c3 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 4 Nov 2025 22:49:14 +0000 Subject: [PATCH 3/5] Fix course lookup in StaticDataLayerProvider by courseId The provider was storing courses by dependency name (e.g., '@skuilder/course-') but getCourseDB() was looking them up by raw courseId. This caused 'Course not found' errors when trying to access courses in static mode. Now extracts courseId from manifest and uses it as the map key for both courseUnpackers and manifests, ensuring consistent lookup. --- .../db/src/impl/static/StaticDataLayerProvider.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/db/src/impl/static/StaticDataLayerProvider.ts b/packages/db/src/impl/static/StaticDataLayerProvider.ts index 86bed82cd..2761b5e60 100644 --- a/packages/db/src/impl/static/StaticDataLayerProvider.ts +++ b/packages/db/src/impl/static/StaticDataLayerProvider.ts @@ -70,12 +70,18 @@ export class StaticDataLayerProvider implements DataLayerProvider { throw new Error(`Failed to fetch final content manifest for ${courseName} at ${finalManifestUrl}`); } const finalManifest = await finalManifestResponse.json(); - - this.manifests[courseName] = finalManifest; + + // Extract courseId from the manifest to use as the lookup key + const courseId = finalManifest.courseId || finalManifest.courseConfig?.courseID; + if (!courseId) { + throw new Error(`Course manifest for ${courseName} missing courseId`); + } + + this.manifests[courseId] = finalManifest; const unpacker = new StaticDataUnpacker(finalManifest, baseUrl); - this.courseUnpackers.set(courseName, unpacker); + this.courseUnpackers.set(courseId, unpacker); - logger.info(`[StaticDataLayerProvider] Successfully resolved and prepared course: ${courseName}`); + logger.info(`[StaticDataLayerProvider] Successfully resolved and prepared course: ${courseName} (courseId: ${courseId})`); } } catch (e) { logger.error(`[StaticDataLayerProvider] Failed to resolve dependency ${courseName}:`, e); From 5cf652a66f65323e00e38ca6da9d3855d688b1ee Mon Sep 17 00:00:00 2001 From: NiloCK Date: Wed, 5 Nov 2025 09:56:49 -0400 Subject: [PATCH 4/5] add cypress smoketest for scaffolded static crs... --- packages/cli/cypress.config.js | 17 ++++++++++ packages/cli/cypress/e2e/scaffolded-app.cy.js | 32 +++++++++++++++++++ packages/cli/cypress/support/commands.js | 9 ++++++ packages/cli/cypress/support/e2e.js | 17 ++++++++++ packages/cli/package.json | 8 +++-- yarn.lock | 2 ++ 6 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 packages/cli/cypress.config.js create mode 100644 packages/cli/cypress/e2e/scaffolded-app.cy.js create mode 100644 packages/cli/cypress/support/commands.js create mode 100644 packages/cli/cypress/support/e2e.js diff --git a/packages/cli/cypress.config.js b/packages/cli/cypress.config.js new file mode 100644 index 000000000..b1c85ba97 --- /dev/null +++ b/packages/cli/cypress.config.js @@ -0,0 +1,17 @@ +import { defineConfig } from 'cypress'; + +export default defineConfig({ + e2e: { + baseUrl: 'http://localhost:5173', // Default Vite dev server port + supportFile: 'cypress/support/e2e.js', + specPattern: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}', + setupNodeEvents(on, config) { + // implement node event listeners here + }, + }, + // Increase the default timeout for slower operations + defaultCommandTimeout: 10000, + // Viewport configuration + viewportWidth: 1280, + viewportHeight: 800, +}); diff --git a/packages/cli/cypress/e2e/scaffolded-app.cy.js b/packages/cli/cypress/e2e/scaffolded-app.cy.js new file mode 100644 index 000000000..345be4414 --- /dev/null +++ b/packages/cli/cypress/e2e/scaffolded-app.cy.js @@ -0,0 +1,32 @@ +// Test for CLI scaffolded application +describe('CLI Scaffolded App - Study View', () => { + it('should navigate to /study and render a card', () => { + // Visit the study page + cy.visit('/study'); + + // Wait for the application to load and render + // Check for the presence of a card view + // The cardView class is present on all rendered cards + cy.get('.cardView', { timeout: 15000 }) + .should('exist') + .and('be.visible'); + + // Additional validation: check that the card has a viewable data attribute + // This indicates it's a properly rendered card component + cy.get('[data-viewable]', { timeout: 15000 }) + .should('exist') + .and('be.visible'); + + // Verify that the card container (v-card) is present + cy.get('.v-card', { timeout: 15000 }) + .should('exist') + .and('be.visible'); + }); + + it('should load the home page successfully', () => { + cy.visit('/'); + + // Verify basic page load + cy.get('body').should('be.visible'); + }); +}); diff --git a/packages/cli/cypress/support/commands.js b/packages/cli/cypress/support/commands.js new file mode 100644 index 000000000..07164efbe --- /dev/null +++ b/packages/cli/cypress/support/commands.js @@ -0,0 +1,9 @@ +// *********************************************** +// This example commands.js shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** diff --git a/packages/cli/cypress/support/e2e.js b/packages/cli/cypress/support/e2e.js new file mode 100644 index 000000000..b0265634d --- /dev/null +++ b/packages/cli/cypress/support/e2e.js @@ -0,0 +1,17 @@ +// *********************************************************** +// This example support/e2e.js is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import './commands'; diff --git a/packages/cli/package.json b/packages/cli/package.json index 1f1140b55..df0211778 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -26,7 +26,9 @@ "lint": "npx eslint .", "lint:fix": "npx eslint . --fix", "lint:check": "npx eslint . --max-warnings 0", - "try:init": "node dist/cli.js init testproject --dangerously-clobber --no-interactive --data-layer static --import-course-data --import-server-url http://localhost:5984 --import-username admin --import-password password --import-course-ids 2aeb8315ef78f3e89ca386992d00825b && cd testproject && npm i && npm install --save-dev @vue-skuilder/cli@file:.. && npm install @vue-skuilder/db@file:../../db @vue-skuilder/courseware@file:../../courseware @vue-skuilder/common-ui@file:../../common-ui @vue-skuilder/express@file:../../express" + "try:init": "node dist/cli.js init testproject --dangerously-clobber --no-interactive --data-layer static --import-course-data --import-server-url http://localhost:5984 --import-username admin --import-password password --import-course-ids 2aeb8315ef78f3e89ca386992d00825b && cd testproject && npm i && npm install --save-dev @vue-skuilder/cli@file:.. && npm install @vue-skuilder/db@file:../../db @vue-skuilder/courseware@file:../../courseware @vue-skuilder/common-ui@file:../../common-ui @vue-skuilder/express@file:../../express", + "test:e2e": "cypress open", + "test:e2e:headless": "cypress run" }, "keywords": [ "cli", @@ -64,9 +66,11 @@ "@types/serve-static": "^1.15.0", "@vitejs/plugin-vue": "^5.2.1", "@vue-skuilder/studio-ui": "workspace:*", + "cypress": "14.1.0", "typescript": "~5.7.2", "vite": "^6.0.9", - "vue-tsc": "^1.8.0" + "vue-tsc": "^1.8.0", + "wait-on": "8.0.2" }, "engines": { "node": ">=18.0.0" diff --git a/yarn.lock b/yarn.lock index 2ef51d840..93ddd0e13 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6502,6 +6502,7 @@ __metadata: "@vue-skuilder/studio-ui": "workspace:*" chalk: "npm:^5.3.0" commander: "npm:^11.0.0" + cypress: "npm:14.1.0" fs-extra: "npm:^11.2.0" inquirer: "npm:^9.2.0" leveldown: "npm:^6.1.1" @@ -6514,6 +6515,7 @@ __metadata: vue-router: "npm:^4.2.0" vue-tsc: "npm:^1.8.0" vuetify: "npm:^3.7.0" + wait-on: "npm:8.0.2" bin: skuilder: ./dist/cli.js languageName: unknown From cee1e35765e0947813fa0b38f7475764de616080 Mon Sep 17 00:00:00 2001 From: NiloCK Date: Wed, 5 Nov 2025 09:58:38 -0400 Subject: [PATCH 5/5] add cypress smoketest for scaffolded static crs... --- .github/workflows/ci-pkg-cli.yml | 102 +++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 .github/workflows/ci-pkg-cli.yml diff --git a/.github/workflows/ci-pkg-cli.yml b/.github/workflows/ci-pkg-cli.yml new file mode 100644 index 000000000..ddfe50088 --- /dev/null +++ b/.github/workflows/ci-pkg-cli.yml @@ -0,0 +1,102 @@ +name: ci-pkg-cli +permissions: + contents: read + +on: + pull_request: + paths: + - 'packages/cli/**' + - 'packages/common/**' + - 'packages/db/**' + - 'packages/common-ui/**' + - 'packages/courseware/**' + - 'packages/express/**' + - 'packages/standalone-ui/**' + - '.github/workflows/ci-pkg-cli.yml' + - 'package.json' + - 'yarn.lock' + +jobs: + cli-try-init-e2e: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: 'recursive' + + - name: Display submodule information + run: | + echo "Submodule information:" + git submodule status + echo "CouchDB snapshot details:" + cd test-couch && git log -1 --pretty=format:'%h - %s (%cr) <%an>' + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + cache: 'yarn' + + - name: Setup Docker + uses: docker/setup-buildx-action@v2 + + - name: Install Yarn + run: corepack enable + + - name: Install dependencies + run: yarn install + + - name: Build library packages + run: | + yarn build:lib + yarn workspace @vue-skuilder/mcp build + yarn workspace @vue-skuilder/express build + yarn workspace @vue-skuilder/cli build + + - name: Install Cypress + run: yarn workspace @vue-skuilder/cli cypress install + + - name: Start CouchDB + run: yarn couchdb:start + + - name: Run try:init to create test project + working-directory: packages/cli + run: yarn try:init + + - name: Start scaffolded app dev server and wait for services + working-directory: packages/cli/testproject + run: | + # Start the dev server in background + npm run dev & + # Wait for the webserver to be ready + npx wait-on http://localhost:5173 --timeout 60000 + + - name: Run E2E tests on scaffolded app + working-directory: packages/cli + run: yarn test:e2e:headless + + - name: Cleanup services + if: always() + run: | + # Clean up dev server + kill $(lsof -t -i:5173) || true + # Clean up CouchDB + yarn couchdb:stop + + - name: Upload screenshots on failure + uses: actions/upload-artifact@v4 + if: failure() + with: + name: cypress-screenshots + path: packages/cli/cypress/screenshots + retention-days: 7 + + - name: Upload videos + uses: actions/upload-artifact@v4 + if: always() + with: + name: cypress-videos + path: packages/cli/cypress/videos + retention-days: 7