Skip to content

Refactor docs module for static builds #47

@taylortom

Description

@taylortom

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:

  1. Add loadDependencyFiles utility to core (adapt-authoring-core#95) — a shared function for scanning files across module dependencies, replacing duplicated logic in 5+ modules.
  2. 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.
  3. Refactor the doc builder to scan module files on disk instead of starting the app. No --static flag 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.dependencies when no dependencies option provided
  • parse option (default false) handles the JSON read-and-parse step
  • dependencies option 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 to routes.json
  • apidefs.js files — metadata moves into routes.json under meta
  • useDefaultRouteConfig() — replaced by useDefaultRoutes: true in 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 set this.root, this.routes etc. manually if routes.json exists. These are read from the file.
  • addRoutes(): after loading routes from routes.json, resolve handler strings to bound methods on this. Fall back to current behavior if no routes.json exists (backward compatibility).
  • generateApiMetadata(): no longer needed for modules with routes.json — metadata is already in the file. Keep for backward compat with modules that haven't migrated.
  • Remove DEFAULT_ROUTES getter — replaced by a JSON template that addRoutes() merges when useDefaultRoutes: 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): Globs rootDir/node_modules/**/adapt-authoring.json, reads + merges package.json + adapt-authoring.json for each (replicating DependencyLoader.loadConfigs() logic)
  • pkg: merged root package.json + adapt-authoring.json
  • dependencies: all discovered dependency configs, keyed by name
  • config.get(key): returns defaults from conf/config.schema.json (resolves $TEMP to os.tmpdir()). Also reads user config from rootDir/conf/config.js if present.
  • rootDir: the provided root directory
  • dependencyloader.instances: provides a mock exposing permissions.routes (assembled from permissions fields in all routes.json files)
  • waitForModule('server'): returns mock { api } with router tree assembled from routes.json files
  • waitForModule('jsonschema'): returns mock { schemas, getSchema } from raw schema/*.schema.json files (via loadDependencyFiles)
  • errors: loads and merges all errors/*.json files from dependencies (via loadDependencyFiles)
  • onReady(): resolves immediately

Rewrite bin/docgen.js

  • Remove import { App } from 'adapt-authoring-core' entirely
  • Accept CLI args: --rootDir <path> (defaults to process.cwd()), --outputDir <path>, --verbose
  • Create StaticAppContext, call init(rootDir), and proceed
  • In cacheConfigs(): use dep.module !== false instead 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 --outputDir provided, use directly
  • If not, read default from conf/config.schema.json (resolving $TEMP)
  • Start http-server directly — no app startup needed

No changes to generators

  • jsdoc3/jsdoc3.js — uses app.pkg.version + configs. Works with StaticAppContext.
  • docsify/docsify.js — uses app.config.get(). Works with StaticAppContext.
  • swagger/swagger.js — uses waitForModule('server'), waitForModule('jsonschema'), dependencyloader.instances['adapt-authoring-auth']. All provided by StaticAppContext from 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)

  1. Add loadDependencyFiles function to core
  2. Export from adapt-authoring-core
  3. Add unit tests
  4. Refactor ErrorsModule, LangModule, JsonSchemaModule, ConfigModule to use it

Phase 1: Route externalisation (api module + all API modules)

  1. Define routes.json schema/convention
  2. Update AbstractApiModule.addRoutes() to read from routes.json
  3. Create routes.json for each module (migrate from setValues() + apidefs.js)
  4. Update AbstractAuthModule for the auth-specific pattern
  5. Test all modules still work with the new approach

Phase 2: Refactor docs module

  1. Create lib/StaticAppContext.js — uses loadDependencyFiles for schema/error/route loading
  2. Rewrite bin/docgen.js — remove App import, use StaticAppContext
  3. Rewrite bin/docserve.js — remove App import, accept --outputDir
  4. Remove adapt-authoring-core peer dependency from package.json
  5. Add tests/StaticAppContext.spec.js

Phase 3: Cleanup (optional)

  1. Remove old apidefs.js files once all modules have migrated
  2. Simplify AbstractApiModule.setValues() for modules using routes.json

Verification

  1. npm test in docs module — existing tests pass
  2. npx at-docgen --rootDir /path/to/adapt-authoring --outputDir /tmp/test — builds all docs without starting the app
  3. npx at-docserve --outputDir /tmp/test — serves on port 9000 without starting the app
  4. After Phase 1: modules boot and serve API correctly using routes.json
  5. Swagger output from static build matches previous live build output

See also: static-docs-plan.md in this repo.

Sub-issues

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions