Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
080b6a1
Fix: Map duplicate assets during import instead of warning (#172)
taylortom Mar 23, 2026
74276ab
New: Run adapt-migrations directly for all trigger points (#174)
taylortom Mar 23, 2026
491081f
New: Pre-built compilation cache for preview builds (refs #176)
taylortom Mar 18, 2026
c9cd741
Fix: Defer contentplugin dependency to break circular deadlock
taylortom Mar 18, 2026
cc0071a
Fix: Cache all build artifacts and apply schema defaults on cache hit
taylortom Mar 18, 2026
30dd01d
New: Eager shared cache prebuild after invalidation (refs #176)
taylortom Mar 18, 2026
b2cb531
Fix: Apply schema defaults to all content types on cache hit (refs #176)
taylortom Mar 19, 2026
ecd72b9
Update: Replace courseassets dependency with content._assetIds querie…
taylortom Mar 23, 2026
3531709
Fix: Update rollback tests to reflect courseassets removal
taylortom Mar 23, 2026
2336a5b
Update: Adapt to synchronous adapt-schemas v3 API
taylortom Mar 20, 2026
99898c2
Fix: Merge master and address PR review comments (refs #186)
taylortom Mar 27, 2026
fb0f5eb
Merge branch 'master' into feature/combined-updates
taylortom Mar 28, 2026
147a2f7
Fix: Make in-memory migration cache prod-writable and concurrency-safe
taylortom Apr 22, 2026
778a354
Fix: Use JSON-normalized content on write in migrateExistingCourses
taylortom Apr 22, 2026
871a11a
Fix: Apply patchCustomStyle/patchThemeName to in-memory content
taylortom Apr 22, 2026
dc2c1cf
Fix: Guard _shareWithUsers optional chaining in checkContentAccess
taylortom Apr 23, 2026
0066a58
Fix: surface real errors in import dry-run and asset resolution
taylortom Apr 29, 2026
ee49d3c
New: import migration to drop invalid vanilla _backgroundStyles values
taylortom Apr 29, 2026
b4f52b9
Fix: include all installed plugins in preview prebuilt cache
taylortom Apr 30, 2026
d1f8f13
New: prebuild shared cache on boot when missing, with build diagnostics
taylortom Apr 30, 2026
18068c7
Fix: Exclude disabled themes/menus from build sources
taylortom May 5, 2026
b485c23
Update: Collapse prebuilt cache to single dir, prebuild every theme/m…
taylortom May 6, 2026
b04b239
Refactor: Promote prebuiltCache utils to lib/BuildCache.js class
taylortom May 6, 2026
ebb1c11
Fix: Defer updateEnabledPlugins until import completes (refs #109)
taylortom May 7, 2026
97987c8
Fix: Address several import correctness issues
taylortom May 8, 2026
5a47f41
Fix: Use hierarchy index as canonical _sortOrder during import
taylortom May 8, 2026
a87b310
Chore: Update resolveAssets test for drop-and-warn behaviour
taylortom May 8, 2026
a60e41e
Build: Bump deps to released majors
taylortom May 8, 2026
345864b
Build: Revert errant adapt-migrations bump
taylortom May 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions conf/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@
"description": "URL of the Adapt framework git repository to install",
"type": "string"
},
"prebuildCache": {
"description": "When enabled, eagerly rebuilds the prebuilt cache for every (theme, menu) combination in the background after invalidation (e.g. plugin install/update/delete)",
"type": "boolean",
"default": false
},
"importMaxFileSize": {
"description": "Maximum file upload size for course imports",
"type": "string",
Expand Down
2 changes: 1 addition & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
* @namespace adaptframework
*/
export { default } from './lib/AdaptFrameworkModule.js'
export { copyFrameworkSource } from './lib/utils.js'
export { copyFrameworkSource, readFrameworkPluginVersions } from './lib/utils.js'
export { default as AdaptFrameworkBuild } from './lib/AdaptFrameworkBuild.js'
export { default as AdaptFrameworkImport } from './lib/AdaptFrameworkImport.js'
142 changes: 96 additions & 46 deletions lib/AdaptFrameworkBuild.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { App, Hook, ensureDir, writeJson } from 'adapt-authoring-core'
import { parseObjectId } from 'adapt-authoring-mongodb'
import { createWriteStream } from 'node:fs'
import AdaptCli from 'adapt-cli'
import { log, logDir, logMemory, copyFrameworkSource } from './utils.js'
import { log, logDir, logMemory, copyFrameworkSource, generateLanguageManifest, applyBuildReplacements } from './utils.js'
import BuildCache from './BuildCache.js'
import fs from 'node:fs/promises'
import path from 'upath'
import semver from 'semver'
Expand Down Expand Up @@ -199,11 +200,48 @@ class AdaptFrameworkBuild {

await this.loadCourseData()

// Check for cached preview build
if (this.isPreview && !contentOnly) {
const cache = new BuildCache(path.join(framework.getConfig('buildDir'), 'prebuilt-cache'))
const pluginHash = await framework.getPluginHash()
const theme = this.courseData.config.data._theme
const menu = this.courseData.config.data._menu

if (await cache.has(pluginHash, theme, menu)) {
await cache.restore(pluginHash, theme, menu, this.buildDir)
await this.applySchemaDefaults()
await this.copyAssets()
await this.preBuildHook.invoke(this)
await this.writeContentJson()
await this.writeLanguageManifest()
await applyBuildReplacements(this.buildDir, {
defaultLanguage: this.courseData.config.data._defaultLanguage ?? 'en',
defaultDirection: this.courseData.config.data._defaultDirection ?? 'ltr',
buildType: 'development',
timestamp: Date.now()
})
this.location = path.join(this.dir, 'build')
await this.postBuildHook.invoke(this)
this.buildData = await this.recordBuildAttempt()
return this
}
}

const tasks = [this.copyAssets()]
if (!contentOnly) {
// preview cache is shared across courses, so include all installed plugins
// — except disabled themes/menus, since only one of each can be active per
// build and the framework's less:dev task globs every theme/menu in src/,
// which OOMs when more than one is present (see adapt_framework#3802).
const pluginsToInclude = this.isPreview
? [
...this.enabledPlugins,
...this.disabledPlugins.filter(p => p.type !== 'theme' && p.type !== 'menu')
]
: this.enabledPlugins
tasks.push(copyFrameworkSource({
destDir: this.dir,
enabledPlugins: this.enabledPlugins.map(p => p.name),
enabledPlugins: pluginsToInclude.map(p => p.name),
linkNodeModules: !this.isExport
}))
}
Expand Down Expand Up @@ -233,6 +271,18 @@ class AdaptFrameworkBuild {
.setData(e)
}
}
// Populate prebuilt cache after successful grunt build for preview
if (this.isPreview && !contentOnly) {
const cache = new BuildCache(path.join(framework.getConfig('buildDir'), 'prebuilt-cache'))
const pluginHash = await framework.getPluginHash()
const theme = this.courseData.config.data._theme
const menu = this.courseData.config.data._menu
try {
await cache.populate(this.buildDir, pluginHash, theme, menu)
} catch (e) {
log('warn', 'CACHE', `failed to populate prebuilt cache: ${e.message}`)
}
}
if (this.compress) {
this.location = await this.prepareZip()
} else {
Expand Down Expand Up @@ -275,10 +325,10 @@ class AdaptFrameworkBuild {
* @return {Promise}
*/
async loadAssetData () {
const [assets, courseassets, tags] = await App.instance.waitForModule('assets', 'courseassets', 'tags')
const [assets, content, tags] = await App.instance.waitForModule('assets', 'content', 'tags')

const caRecs = await courseassets.find({ courseId: this.courseId })
const uniqueAssetIds = new Set(caRecs.map(c => parseObjectId(c.assetId)))
const courseContent = await content.find({ _courseId: this.courseId }, { validate: false }, { projection: { _assetIds: 1 } })
const uniqueAssetIds = new Set(courseContent.flatMap(c => (c._assetIds ?? []).map(id => parseObjectId(id))))
const usedAssets = await assets.find({ _id: { $in: [...uniqueAssetIds] } })

const usedTagIds = new Set(usedAssets.reduce((m, a) => [...m, ...(a.tags ?? [])], []))
Expand Down Expand Up @@ -426,12 +476,35 @@ class AdaptFrameworkBuild {
}))
}

/**
* Outputs all course data to the required JSON files
* @return {Promise}
*/
async writeContentJson () {
const data = Object.values(this.courseData)
if (this.isExport && this.assetData.data.length) {
this.assetData.data = this.assetData.data.map(d => {
return {
title: d.title,
description: d.description,
filename: d.path,
tags: d.tags
}
})
data.push(this.assetData)
}
return Promise.all(data.map(async ({ dir, fileName, data }) => {
await ensureDir(dir)
const filepath = path.join(dir, fileName)
const returnData = await writeJson(filepath, data)
log('verbose', 'WRITE', filepath)
return returnData
}))
}

/**
* Applies schema defaults to the in-memory course and config data using
* the jsonschema module. Replicates what grunt's schema-defaults task does.
*
* TODO: replace validateWithDefaults workaround with schema.validate(data, { ignoreErrors: true })
* once migrated to adapt-schemas v3.x (see #184)
* @return {Promise}
*/
async applySchemaDefaults () {
Expand All @@ -442,31 +515,19 @@ class AdaptFrameworkBuild {
const extensionFilter = s => contentplugin.isPluginSchema(s) ? enabledPluginSchemas.includes(s) : true
const getSchema = name => jsonschema.getSchema(name, { useCache: false, extensionFilter })

/**
* Applies defaults via validate(), catching and ignoring validation errors.
* The validated+defaulted data is returned from validate() on success, or
* extracted from the error on failure (validate clones internally).
*/
const validateWithDefaults = (schema, data) => {
try {
return schema.validate(data, { useDefaults: true, ignoreRequired: true })
} catch (e) {
return e.data.data
}
}

// Apply defaults without running full validation (which rejects ObjectIds etc.)
const [courseSchema, configSchema] = await Promise.all([
getSchema('course'),
getSchema('config')
])
Object.assign(this.courseData.course.data, validateWithDefaults(courseSchema, this.courseData.course.data))
Object.assign(this.courseData.config.data, validateWithDefaults(configSchema, this.courseData.config.data))
courseSchema.compiledWithDefaults(this.courseData.course.data)
configSchema.compiledWithDefaults(this.courseData.config.data)

for (const type of ['contentObject', 'article', 'block']) {
const schemaName = type === 'contentObject' ? 'contentobject' : type
const schema = await getSchema(schemaName)
for (const item of this.courseData[type].data) {
Object.assign(item, validateWithDefaults(schema, item))
schema.compiledWithDefaults(item)
}
}

Expand All @@ -476,34 +537,23 @@ class AdaptFrameworkBuild {
if (!componentSchemas[schemaName]) {
componentSchemas[schemaName] = await getSchema(schemaName)
}
Object.assign(item, validateWithDefaults(componentSchemas[schemaName], item))
componentSchemas[schemaName].compiledWithDefaults(item)
}
}

/**
* Outputs all course data to the required JSON files
* Writes the language_data_manifest.js for each language dir.
* Only needed on cache-hit builds where grunt is skipped.
* @return {Promise}
*/
async writeContentJson () {
const data = Object.values(this.courseData)
if (this.isExport && this.assetData.data.length) {
this.assetData.data = this.assetData.data.map(d => {
return {
title: d.title,
description: d.description,
filename: d.path,
tags: d.tags
}
})
data.push(this.assetData)
}
return Promise.all(data.map(async ({ dir, fileName, data }) => {
await ensureDir(dir)
const filepath = path.join(dir, fileName)
const returnData = await writeJson(filepath, data)
log('verbose', 'WRITE', filepath)
return returnData
}))
async writeLanguageManifest () {
const langDir = this.courseData.course.dir
const fileNames = Object.values(this.courseData)
.filter(d => d.dir === langDir)
.map(d => d.fileName)
const manifest = generateLanguageManifest(fileNames)
await ensureDir(langDir)
await writeJson(path.join(langDir, 'language_data_manifest.js'), manifest)
}

/**
Expand Down
Loading
Loading