-
Notifications
You must be signed in to change notification settings - Fork 0
Description
Context
The docs module requires a running adapt-authoring instance to build documentation. This is because the swagger generator reads runtime data (route tree, resolved schemas, auth permissions), and the doc builder uses App.instance to discover module dependencies.
Investigation reveals the runtime data is entirely declarative — routes are hardcoded strings, permissions are hardcoded scope arrays, API metadata already lives in separate apidefs.js files, schemas are JSON files on disk, and errors are loaded from errors/*.json files. Runtime generation exists because no static declaration mechanism was designed, not because the data is inherently dynamic.
The solution has three parts:
- Add
loadDependencyFilesutility to core (adapt-authoring-core#95) — a shared function for scanning files across module dependencies, replacing duplicated logic in 5+ modules. - Externalise route config to static files (
routes.json) — used by both the API framework (for endpoint setup) and the doc builder (for swagger generation). Single source of truth. - Refactor the doc builder to scan module files on disk instead of starting the app. No
--staticflag needed — the app dependency is removed entirely.
Part 0: loadDependencyFiles utility (core#95)
A standalone utility function exported from core that replaces duplicated file-scanning logic across modules.
API
import { loadDependencyFiles } from 'adapt-authoring-core'
// Uses App.instance.dependencies by default, returns file paths grouped by module
const files = await loadDependencyFiles('errors/*.json')
// With JSON parsing — returns parsed content grouped by module
const parsed = await loadDependencyFiles('errors/*.json', { parse: true })
// Override dependencies (for testing or static doc builds)
const parsed = await loadDependencyFiles('errors/*.json', {
parse: true,
dependencies: customDeps
})- Standalone function (not an instance method) for easy unit testing
- Defaults to
App.instance.dependencieswhen nodependenciesoption provided parseoption (defaultfalse) handles the JSON read-and-parse stepdependenciesoption allows callers to pass custom dependency lists (for unit tests and static doc builds)
Modules to refactor to use this utility
| Module | Current code | Pattern |
|---|---|---|
| ErrorsModule | errors/lib/ErrorsModule.js |
glob('errors/*.json', { cwd: d.rootDir }) |
| LangModule | lang/lib/LangModule.js |
glob('lang/*.json', { cwd: d.rootDir }) |
| JsonSchemaModule | jsonschema/lib/JsonSchemaModule.js |
glob('schema/*.schema.json', { cwd: d.rootDir }) |
| ConfigModule | config/lib/ConfigModule.js |
direct read of conf/config.schema.json per dep |
| Doc plugins | docs/, config/docs/, core/docs/ |
various per-dep file reads |
Part 1: Externalise Route Definitions
New convention: routes.json per module
Location: A single routes.json file in the module root, alongside adapt-authoring.json. This follows the existing convention where module-level declarations (adapt-authoring.json, package.json) live at the root, while subdirectories hold specific concerns (conf/ for config schemas, schema/ for data schemas, errors/ for error definitions, lib/ for implementation code).
Each API module declares its REST API surface in this file:
{
"root": "content",
"schemaName": "content",
"collectionName": "content",
"useDefaultRoutes": true,
"routes": [
{
"route": "/insertrecursive",
"handlers": { "post": "insertRecursive" },
"permissions": { "post": ["write:content"] },
"meta": {
"post": {
"summary": "Insert hierarchical content data",
"requestBody": { "..." : "..." },
"responses": { "201": { "..." : "..." } }
}
}
},
{
"route": "/clone",
"handlers": { "post": "clone" },
"permissions": { "post": ["write:content"] },
"meta": { "..." : "..." }
}
]
}Named handler convention: Handler values are strings (e.g. "insertRecursive") that map to method names on the module class. AbstractApiModule.addRoutes() resolves "insertRecursive" to this.insertRecursive.bind(this). Special value "default" maps to this.requestHandler(), and "query" maps to this.queryHandler().
Default routes: When useDefaultRoutes: true, the standard CRUD routes (POST /, GET /, GET /:_id, PUT /:_id, PATCH /:_id, DELETE /:_id, POST /query, GET /schema) are included automatically with default handlers and permissions derived from root/schemaName. Custom routes in the routes array are merged with defaults.
This replaces:
- Route definitions in
setValues()— path, method, permission data moves toroutes.json apidefs.jsfiles — metadata moves intoroutes.jsonundermetauseDefaultRouteConfig()— replaced byuseDefaultRoutes: truein the JSON
What stays in code:
- Handler method implementations (the actual request handling logic)
- Any dynamic route setup that truly depends on runtime state (rare)
Changes to AbstractApiModule
setValues(): modules no longer need to setthis.root,this.routesetc. manually ifroutes.jsonexists. These are read from the file.addRoutes(): after loading routes fromroutes.json, resolve handler strings to bound methods onthis. Fall back to current behavior if noroutes.jsonexists (backward compatibility).generateApiMetadata(): no longer needed for modules withroutes.json— metadata is already in the file. Keep for backward compat with modules that haven't migrated.- Remove
DEFAULT_ROUTESgetter — replaced by a JSON template thataddRoutes()merges whenuseDefaultRoutes: true.
Changes to AbstractAuthModule
Auth modules are a special case — they create child routers under the auth module's router and have their own secureRoute/unsecureRoute calls. The routes.json format supports this:
{
"root": "local",
"parentRouter": "auth",
"routes": [
{
"route": "/",
"handlers": { "post": "authenticate" },
"unsecured": true
},
{
"route": "/invite",
"handlers": { "post": "invite" },
"permissions": { "post": ["register:users"] }
},
{
"route": "/registersuper",
"handlers": { "post": "registerSuper" },
"internal": true,
"unsecured": true
}
]
}Modules to update (each gets a routes.json)
| Module | Current route definition |
|---|---|
| adapt-authoring-api (base) | DEFAULT_ROUTES getter, generateApiMetadata() |
| adapt-authoring-content | setValues() + apidefs.js |
| adapt-authoring-assets | setValues() with inline meta |
| adapt-authoring-tags | setValues() with inline meta |
| adapt-authoring-users | setValues() |
| adapt-authoring-auth | init() + apidefs.js |
| adapt-authoring-auth-local | setValues() + apidefs.js |
| adapt-authoring-adaptframework | apidefs.js |
| adapt-authoring-contentplugin | apidefs.js |
| adapt-authoring-mongodblogger | apidefs.js |
| adapt-authoring-roles | setValues() |
| adapt-authoring-server | Router class, route registration |
| adapt-authoring-sessions | setValues() |
Backward compatibility
Modules without routes.json continue to work exactly as before — AbstractApiModule falls back to the current setValues() + addRoutes() pattern. Migration is incremental.
Part 2: Refactor Doc Builder
Once all modules have routes.json, every piece of data the doc builder needs exists on disk:
| Data | Source on disk |
|---|---|
| Module configs | package.json + adapt-authoring.json per module |
| Routes, permissions, metadata | routes.json per module (module root) |
| JSON schemas | schema/*.schema.json per module |
| Error definitions | errors/*.json per module |
| Source files (JSDoc) | lib/**/*.js per module |
| Manual pages | docs/*.md per module |
| Config schemas | conf/config.schema.json per module |
There is no remaining need to start the app. The adapt-authoring-core dependency is removed entirely.
New file: lib/StaticAppContext.js
Replaces App.instance by scanning the filesystem:
init(rootDir): GlobsrootDir/node_modules/**/adapt-authoring.json, reads + mergespackage.json+adapt-authoring.jsonfor each (replicatingDependencyLoader.loadConfigs()logic)pkg: merged rootpackage.json+adapt-authoring.jsondependencies: all discovered dependency configs, keyed by nameconfig.get(key): returns defaults fromconf/config.schema.json(resolves$TEMPtoos.tmpdir()). Also reads user config fromrootDir/conf/config.jsif present.rootDir: the provided root directorydependencyloader.instances: provides a mock exposingpermissions.routes(assembled frompermissionsfields in allroutes.jsonfiles)waitForModule('server'): returns mock{ api }with router tree assembled fromroutes.jsonfileswaitForModule('jsonschema'): returns mock{ schemas, getSchema }from rawschema/*.schema.jsonfiles (vialoadDependencyFiles)errors: loads and merges allerrors/*.jsonfiles from dependencies (vialoadDependencyFiles)onReady(): resolves immediately
Rewrite bin/docgen.js
- Remove
import { App } from 'adapt-authoring-core'entirely - Accept CLI args:
--rootDir <path>(defaults toprocess.cwd()),--outputDir <path>,--verbose - Create
StaticAppContext, callinit(rootDir), and proceed - In
cacheConfigs(): usedep.module !== falseinstead of!!app.dependencyloader.instances[dep.name] - All three generators (jsdoc3, docsify, swagger) run against the static context
Rewrite bin/docserve.js
- Remove
import { App } from 'adapt-authoring-core'entirely - Accept CLI args:
--outputDir <path>,--port <number>,--open - If
--outputDirprovided, use directly - If not, read default from
conf/config.schema.json(resolving$TEMP) - Start
http-serverdirectly — no app startup needed
No changes to generators
jsdoc3/jsdoc3.js— usesapp.pkg.version+ configs. Works withStaticAppContext.docsify/docsify.js— usesapp.config.get(). Works withStaticAppContext.swagger/swagger.js— useswaitForModule('server'),waitForModule('jsonschema'),dependencyloader.instances['adapt-authoring-auth']. All provided byStaticAppContextfrom static files. No changes needed.DocsifyPluginWrapper.js— unchanged. Plugin errors caught gracefully.
Plugin compatibility
All plugins now work statically:
| Plugin | Uses | Source |
|---|---|---|
configuration.js (config) |
app.dependencies + reads conf/config.schema.json |
Filesystem |
coremodules.js (core) |
app.pkg.version, app.dependencies |
StaticAppContext |
binscripts.js (core) |
app.dependencies + reads bin scripts |
Filesystem |
licensing.js (core) |
config.app.rootDir + file read |
Filesystem |
index-manual.js (core) |
No app usage |
N/A |
uidocs.js (ui) |
app.dependencies, app.pkg.version |
StaticAppContext |
at-utils.js |
No app usage |
N/A |
schemas-reference.js (jsonschema) |
app.waitForModule('jsonschema') |
StaticAppContext reads schema/*.schema.json |
errors.js (errors) |
app.errors |
StaticAppContext reads errors/*.json |
Update package.json
Remove the adapt-authoring-core peer dependency entirely.
New tests
tests/StaticAppContext.spec.js— fixture directory, dependency scanning, config defaults, route tree assembly, schema loading, error loading
Implementation Order
Phase 0: loadDependencyFiles utility (core#95)
- Add
loadDependencyFilesfunction to core - Export from
adapt-authoring-core - Add unit tests
- Refactor ErrorsModule, LangModule, JsonSchemaModule, ConfigModule to use it
Phase 1: Route externalisation (api module + all API modules)
- Define
routes.jsonschema/convention - Update
AbstractApiModule.addRoutes()to read fromroutes.json - Create
routes.jsonfor each module (migrate fromsetValues()+apidefs.js) - Update
AbstractAuthModulefor the auth-specific pattern - Test all modules still work with the new approach
Phase 2: Refactor docs module
- Create
lib/StaticAppContext.js— usesloadDependencyFilesfor schema/error/route loading - Rewrite
bin/docgen.js— removeAppimport, useStaticAppContext - Rewrite
bin/docserve.js— removeAppimport, accept--outputDir - Remove
adapt-authoring-corepeer dependency frompackage.json - Add
tests/StaticAppContext.spec.js
Phase 3: Cleanup (optional)
- Remove old
apidefs.jsfiles once all modules have migrated - Simplify
AbstractApiModule.setValues()for modules usingroutes.json
Verification
npm testin docs module — existing tests passnpx at-docgen --rootDir /path/to/adapt-authoring --outputDir /tmp/test— builds all docs without starting the appnpx at-docserve --outputDir /tmp/test— serves on port 9000 without starting the app- After Phase 1: modules boot and serve API correctly using
routes.json - Swagger output from static build matches previous live build output
See also: static-docs-plan.md in this repo.