From 6b23863be8af73f786b5b9eee81e52f728a7ab8f Mon Sep 17 00:00:00 2001 From: SanikaA3 <147941536+SanikaA3@users.noreply.github.com> Date: Wed, 23 Sep 2026 21:55:54 -0400 Subject: [PATCH] feat(site): add openstaxToSite to --import-structure Refs haxtheweb/issues#2912. Step 3 of the plan on that issue: the converter and its dispatcher case merged in haxtheweb/haxcms-nodejs#40, but `hax site --import-site --import-structure openstaxToSite` had no mapping, so the name fell through to the hidden-methodologies branch and silently did nothing. Maps openstaxToSite to the site/import/:platform dispatcher case 'openstax' - the same in-process on-prem route call the other platform converters make - and lists it in the --import-structure help for both commands and in the man page. xlsxToSite was mapped but absent from both lists, so it is listed now too. IMPORT_STRUCTURE_MAP is exported so the wiring can be asserted, and test/unit/site-imports.test.cjs covers it: every platform importer maps to its dispatcher platform, docx and xlsx keep their own action routes, and every mapped importer appears in both the help text and the man page. That last check is the drift that let the OpenStax importer ship unreachable. The CLI path depends on a published haxcms-nodejs that carries the converter; @haxtheweb/haxcms-nodejs 26.8.1, the version this repo installs today, predates it. Co-Authored-By: Claude Opus 5 --- src/create.js | 4 +- src/docs/hax.1 | 4 ++ src/lib/programs/site.js | 3 +- test/unit/site-imports.test.cjs | 66 +++++++++++++++++++++++++++++++++ 4 files changed, 74 insertions(+), 3 deletions(-) create mode 100644 test/unit/site-imports.test.cjs diff --git a/src/create.js b/src/create.js index 6bf3f0a..2147e27 100644 --- a/src/create.js +++ b/src/create.js @@ -78,7 +78,7 @@ async function main() { // options for site .option('--import-site ', 'URL of site to import') - .option('--import-structure ', `import method to use:\n\rpressbooksToSite\n\relmslnToSite\n\rhaxcmsToSite\n\rnotionToSite\n\rgitbookToSite\n\revolutionToSite\n\rploneToSite\n\rwordpressPagesToSite\n\rdrupalBookToSite\n\rhtmlToSite\n\rdocxToSite`) + .option('--import-structure ', `import method to use:\n\rpressbooksToSite\n\relmslnToSite\n\rhaxcmsToSite\n\rnotionToSite\n\rgitbookToSite\n\revolutionToSite\n\rploneToSite\n\rwordpressPagesToSite\n\rdrupalBookToSite\n\ropenstaxToSite\n\rhtmlToSite\n\rdocxToSite\n\rxlsxToSite`) .option('--node-op ', 'node operation to perform') .option('--item-id ', 'node ID to operate on') .option('--domain ', 'published domain name') @@ -192,7 +192,7 @@ async function main() { .option('--root ', 'root location to execute the command from') .option('--import-site ', 'URL of site to import') - .option('--import-structure ', `import method to use:\n\rpressbooksToSite\n\relmslnToSite\n\rhaxcmsToSite\n\rnotionToSite\n\rgitbookToSite\n\revolutionToSite\n\rploneToSite\n\rwordpressPagesToSite\n\rdrupalBookToSite\n\rhtmlToSite\n\rdocxToSite`) + .option('--import-structure ', `import method to use:\n\rpressbooksToSite\n\relmslnToSite\n\rhaxcmsToSite\n\rnotionToSite\n\rgitbookToSite\n\revolutionToSite\n\rploneToSite\n\rwordpressPagesToSite\n\rdrupalBookToSite\n\ropenstaxToSite\n\rhtmlToSite\n\rdocxToSite\n\rxlsxToSite`) .option('--name ', 'name of the site (when creating a new one)') .option('--domain ', 'published domain name') .option('--node-op ', 'node operation to perform') diff --git a/src/docs/hax.1 b/src/docs/hax.1 index ad6e9cd..00e1848 100644 --- a/src/docs/hax.1 +++ b/src/docs/hax.1 @@ -225,9 +225,13 @@ wordpressPagesToSite .br drupalBookToSite .br +openstaxToSite +.br htmlToSite .br docxToSite +.br +xlsxToSite .TP \--node\-op node operation to perform diff --git a/src/lib/programs/site.js b/src/lib/programs/site.js index 6b38b8a..88295af 100644 --- a/src/lib/programs/site.js +++ b/src/lib/programs/site.js @@ -182,7 +182,7 @@ async function invokeRoute(routeHandler, body = {}, query = {}, params = {}) { // converters go through the site/import/:platform dispatcher; docx/xlsx use their // own actions routes. evolutionToSite (custom zip upload) is intentionally absent // and falls through to the hidden-methodologies branch below. -const IMPORT_STRUCTURE_MAP = { +export const IMPORT_STRUCTURE_MAP = { haxcmsToSite: { platform: 'haxcms' }, pressbooksToSite: { platform: 'pressbooks' }, gitbookToSite: { platform: 'gitbook' }, @@ -191,6 +191,7 @@ const IMPORT_STRUCTURE_MAP = { ploneToSite: { platform: 'plone' }, wordpressPagesToSite: { platform: 'wordpress' }, drupalBookToSite: { platform: 'drupal-book' }, + openstaxToSite: { platform: 'openstax' }, htmlToSite: { platform: 'html' }, docxToSite: { routeKey: 'actions/import-docx' }, xlsxToSite: { routeKey: 'actions/import-xlsx' }, diff --git a/test/unit/site-imports.test.cjs b/test/unit/site-imports.test.cjs new file mode 100644 index 0000000..51981d9 --- /dev/null +++ b/test/unit/site-imports.test.cjs @@ -0,0 +1,66 @@ +'use strict' + +// Isolate HOME BEFORE requiring site.js (via the canary) so HAXCMS init lands in +// a temp dir, not the real ~/.haxtheweb. Same pattern as site-helpers-2.test.cjs. +const fs = require('node:fs') +const os = require('node:os') +const path = require('node:path') +const ISOLATED_HOME = fs.mkdtempSync(path.join(os.tmpdir(), 'hax-site-imports-')) +process.env.HOME = ISOLATED_HOME +process.env.USERPROFILE = ISOLATED_HOME + +const test = require('node:test') +const assert = require('node:assert/strict') + +const { available, skipReason, module: siteModule } = require('../_helpers/site-canary.cjs') + +const { IMPORT_STRUCTURE_MAP } = available ? siteModule : {} + +const opts = { skip: skipReason, timeout: 15000 } + +// --- IMPORT_STRUCTURE_MAP --- +// --import-structure names resolve to on-prem haxcms-nodejs routes: platform +// converters through the site/import/:platform dispatcher, docx/xlsx through +// their own actions routes. A converter missing from this map is unreachable +// from the CLI even when the backend route exists, which is what happened to +// the OpenStax importer from haxtheweb/issues#2912. + +test('every platform importer maps to its dispatcher platform', opts, () => { + const platforms = {} + Object.keys(IMPORT_STRUCTURE_MAP).forEach((name) => { + if (IMPORT_STRUCTURE_MAP[name].platform) { + platforms[name] = IMPORT_STRUCTURE_MAP[name].platform + } + }) + assert.deepEqual(platforms, { + haxcmsToSite: 'haxcms', + pressbooksToSite: 'pressbooks', + gitbookToSite: 'gitbook', + notionToSite: 'notion', + elmslnToSite: 'elmsln', + ploneToSite: 'plone', + wordpressPagesToSite: 'wordpress', + drupalBookToSite: 'drupal-book', + openstaxToSite: 'openstax', + htmlToSite: 'html', + }) +}) + +test('openstaxToSite reaches the openstax dispatcher case', opts, () => { + assert.deepEqual(IMPORT_STRUCTURE_MAP.openstaxToSite, { platform: 'openstax' }) +}) + +test('docx and xlsx keep their own action routes', opts, () => { + assert.deepEqual(IMPORT_STRUCTURE_MAP.docxToSite, { routeKey: 'actions/import-docx' }) + assert.deepEqual(IMPORT_STRUCTURE_MAP.xlsxToSite, { routeKey: 'actions/import-xlsx' }) +}) + +test('the --import-structure help and man page list every mapped importer', opts, () => { + const root = path.join(__dirname, '..', '..') + const help = fs.readFileSync(path.join(root, 'src', 'create.js'), 'utf8') + const manPage = fs.readFileSync(path.join(root, 'src', 'docs', 'hax.1'), 'utf8') + Object.keys(IMPORT_STRUCTURE_MAP).forEach((name) => { + assert.ok(help.includes(name), name + ' is offered by --import-structure') + assert.ok(manPage.includes(name), name + ' is documented in the man page') + }) +})