@nan0web/release 3.3.0 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -35
- package/bin/nan0release.js +33 -2
- package/bin/serve.js +73 -0
- package/package.json +46 -17
- package/src/Release/Document.js +1 -0
- package/src/Release/Scanner.js +3 -2
- package/src/Release.js +7 -7
- package/src/domain/App.js +33 -2
- package/src/domain/BumpCommand.js +10 -3
- package/src/domain/CheckCommand.js +504 -146
- package/src/domain/CheckCommand.story.js +92 -0
- package/src/domain/CloseCommand.js +13 -9
- package/src/domain/Dashboard/Dashboard.story.js +155 -0
- package/src/domain/Dashboard/DashboardModel.js +308 -0
- package/src/domain/Dashboard/ReleaseDiscovery.js +132 -0
- package/src/domain/Dashboard/ReleaseParser.js +226 -0
- package/src/domain/Dashboard/index.js +19 -0
- package/src/domain/DepsCommand.js +5 -2
- package/src/domain/InitCommand.js +18 -16
- package/src/domain/ListCommand.js +45 -0
- package/src/domain/PMaCParser.js +418 -0
- package/src/domain/PMaCParser.story.js +152 -0
- package/src/domain/PreviewCommand.js +39 -0
- package/src/domain/PublishCommand.js +7 -3
- package/src/domain/Registry/Registry.story.js +82 -0
- package/src/domain/Registry/RegistryModel.js +124 -0
- package/src/domain/ServeCommand.js +165 -0
- package/src/domain/ServeCommand.story.js +88 -0
- package/src/domain/SpecCommand.js +3 -1
- package/src/domain/StatusCommand.js +249 -3
- package/src/domain/ValidateCommand.js +426 -0
- package/src/domain/ValidateCommand.story.js +92 -0
- package/src/domain/ViewCommand.js +99 -0
- package/src/domain/WebCommand.js +140 -0
- package/src/domain/inspect/ReleaseAuditor.js +508 -0
- package/src/domain/inspect/ReleaseAuditor.story.js +136 -0
- package/src/domain/models/ReleaseIssueModel.js +27 -0
- package/src/domain/models/ReleaseMemberModel.js +21 -0
- package/src/domain/models/ReleaseModel.js +191 -0
- package/src/domain/models/ReleaseParityModel.js +27 -0
- package/src/domain/models/ReleaseTaskModel.js +33 -0
- package/src/domain/models/index.js +6 -0
- package/src/domain/templates/release.html +34 -0
- package/src/domain/templates/release.html.template.js +34 -0
- package/src/index.js +15 -0
- package/src/inspectors/inspect-js.js +32 -0
- package/src/releases/1/0/v1.0.3/task.test.js +1 -1
- package/src/utils/nodejs/GitTelemetry.js +154 -0
- package/types/Release/Document.d.ts +24 -27
- package/types/Release/MarkdownToTest.d.ts +1 -1
- package/types/Release/Person.d.ts +9 -7
- package/types/Release/Scanner.d.ts +41 -41
- package/types/Release.d.ts +42 -42
- package/types/architecture/ProjectManagementAsCode.d.ts +59 -64
- package/types/domain/App.d.ts +25 -7
- package/types/domain/BumpCommand.d.ts +11 -6
- package/types/domain/CheckCommand.d.ts +128 -16
- package/types/domain/CheckCommand.story.d.ts +1 -0
- package/types/domain/CloseCommand.d.ts +4 -6
- package/types/domain/Dashboard/Dashboard.story.d.ts +1 -0
- package/types/domain/Dashboard/DashboardModel.d.ts +273 -0
- package/types/domain/Dashboard/GitTelemetry.d.ts +23 -0
- package/types/domain/Dashboard/ReleaseDiscovery.d.ts +47 -0
- package/types/domain/Dashboard/ReleaseParser.d.ts +79 -0
- package/types/domain/Dashboard/index.d.ts +3 -0
- package/types/domain/DepsCommand.d.ts +6 -6
- package/types/domain/InitCommand.d.ts +4 -6
- package/types/domain/ListCommand.d.ts +18 -0
- package/types/domain/PMaCParser.d.ts +64 -0
- package/types/domain/PMaCParser.story.d.ts +1 -0
- package/types/domain/PreviewCommand.d.ts +6 -0
- package/types/domain/PublishCommand.d.ts +8 -6
- package/types/domain/Registry/Registry.story.d.ts +1 -0
- package/types/domain/Registry/RegistryModel.d.ts +61 -0
- package/types/domain/ServeCommand.d.ts +59 -0
- package/types/domain/ServeCommand.story.d.ts +1 -0
- package/types/domain/SpecCommand.d.ts +4 -6
- package/types/domain/StatusCommand.d.ts +38 -3
- package/types/domain/ValidateCommand.d.ts +69 -0
- package/types/domain/ValidateCommand.story.d.ts +1 -0
- package/types/domain/ViewCommand.d.ts +27 -0
- package/types/domain/WebCommand.d.ts +47 -0
- package/types/domain/inspect/ReleaseAuditor.d.ts +134 -0
- package/types/domain/inspect/ReleaseAuditor.story.d.ts +1 -0
- package/types/domain/models/ReleaseIssueModel.d.ts +35 -0
- package/types/domain/models/ReleaseMemberModel.d.ts +25 -0
- package/types/domain/models/ReleaseModel.d.ts +122 -0
- package/types/domain/models/ReleaseParityModel.d.ts +37 -0
- package/types/domain/models/ReleaseTaskModel.d.ts +46 -0
- package/types/domain/models/index.d.ts +5 -0
- package/types/domain/templates/release.html.template.d.ts +2 -0
- package/types/index.d.ts +21 -10
- package/types/inspectors/inspect-js.d.ts +2 -0
- package/types/utils/nodejs/GitTelemetry.d.ts +23 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { describe, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import DB from '@nan0web/db'
|
|
4
|
+
import { RegistryModel } from './RegistryModel.js'
|
|
5
|
+
|
|
6
|
+
describe('RegistryModel OLMUI Scenario', () => {
|
|
7
|
+
it('parses releases.txt and extracts valid project paths ignoring comments and empty lines', async () => {
|
|
8
|
+
const rawContent = `# Global Registry
|
|
9
|
+
.
|
|
10
|
+
# packages
|
|
11
|
+
packages/release
|
|
12
|
+
|
|
13
|
+
# apps
|
|
14
|
+
apps/release.app
|
|
15
|
+
apps/3rdparty/industrialbank/bank
|
|
16
|
+
`
|
|
17
|
+
const db = new DB({
|
|
18
|
+
predefined: [
|
|
19
|
+
['releases.txt', rawContent],
|
|
20
|
+
['package.json', { name: '@nan0web/monorepo', version: '1.0.0' }],
|
|
21
|
+
['packages/release/package.json', { name: '@nan0web/release', version: '3.3.0' }],
|
|
22
|
+
['apps/release.app/package.json', { name: 'release.app', version: '0.1.0' }],
|
|
23
|
+
['apps/3rdparty/industrialbank/bank/package.json', { name: '@industrialbank/bank', version: '3.3.0' }],
|
|
24
|
+
],
|
|
25
|
+
})
|
|
26
|
+
await db.connect()
|
|
27
|
+
|
|
28
|
+
const registry = new RegistryModel({ filePath: 'releases.txt' }, { db })
|
|
29
|
+
const projects = await registry.loadProjects()
|
|
30
|
+
|
|
31
|
+
assert.equal(projects.length, 4)
|
|
32
|
+
assert.deepEqual(projects[0], {
|
|
33
|
+
path: '.',
|
|
34
|
+
name: '@nan0web/monorepo',
|
|
35
|
+
version: '1.0.0',
|
|
36
|
+
})
|
|
37
|
+
assert.deepEqual(projects[1], {
|
|
38
|
+
path: 'packages/release',
|
|
39
|
+
name: '@nan0web/release',
|
|
40
|
+
version: '3.3.0',
|
|
41
|
+
})
|
|
42
|
+
assert.deepEqual(projects[2], {
|
|
43
|
+
path: 'apps/release.app',
|
|
44
|
+
name: 'release.app',
|
|
45
|
+
version: '0.1.0',
|
|
46
|
+
})
|
|
47
|
+
assert.deepEqual(projects[3], {
|
|
48
|
+
path: 'apps/3rdparty/industrialbank/bank',
|
|
49
|
+
name: '@industrialbank/bank',
|
|
50
|
+
version: '3.3.0',
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('handles projects without package.json gracefully', async () => {
|
|
55
|
+
const db = new DB({
|
|
56
|
+
predefined: [
|
|
57
|
+
['releases.txt', 'packages/custom-pkg\n'],
|
|
58
|
+
],
|
|
59
|
+
})
|
|
60
|
+
await db.connect()
|
|
61
|
+
|
|
62
|
+
const registry = new RegistryModel({ filePath: 'releases.txt' }, { db })
|
|
63
|
+
const projects = await registry.loadProjects()
|
|
64
|
+
|
|
65
|
+
assert.equal(projects.length, 1)
|
|
66
|
+
assert.equal(projects[0].name, 'packages/custom-pkg')
|
|
67
|
+
assert.equal(projects[0].version, '0.0.0')
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('parses raw text paths via static parsePaths utility', () => {
|
|
71
|
+
const text = `
|
|
72
|
+
# Comment 1
|
|
73
|
+
/path/one
|
|
74
|
+
|
|
75
|
+
/path/two # inline comment
|
|
76
|
+
# Comment 2
|
|
77
|
+
/path/three
|
|
78
|
+
`
|
|
79
|
+
const paths = RegistryModel.parsePaths(text)
|
|
80
|
+
assert.deepEqual(paths, ['/path/one', '/path/two', '/path/three'])
|
|
81
|
+
})
|
|
82
|
+
})
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { Model } from '@nan0web/types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {Object} ProjectEntry
|
|
5
|
+
* @property {string} path - Project relative or absolute path
|
|
6
|
+
* @property {string} name - Package name from package.json or folder basename
|
|
7
|
+
* @property {string} version - Package version from package.json
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* RegistryModel - PM-as-Code Project Registry manager.
|
|
12
|
+
* Reads project paths from releases.txt and discovers package metadata.
|
|
13
|
+
*/
|
|
14
|
+
export class RegistryModel extends Model {
|
|
15
|
+
static filePath = {
|
|
16
|
+
help: 'Path to releases.txt registry file',
|
|
17
|
+
default: 'releases.txt',
|
|
18
|
+
type: 'string',
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
static projects = {
|
|
22
|
+
help: 'List of registered projects',
|
|
23
|
+
default: () => [],
|
|
24
|
+
type: 'array',
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
static UI = {
|
|
28
|
+
fileNotFound: 'Registry file {$path} not found',
|
|
29
|
+
loadingProjects: 'Loading projects from registry {$path}...',
|
|
30
|
+
projectsLoaded: 'Loaded {$count} projects from registry',
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {Partial<RegistryModel>} [data]
|
|
35
|
+
* @param {import('@nan0web/types').ModelOptions} [options]
|
|
36
|
+
*/
|
|
37
|
+
constructor(data = {}, options = {}) {
|
|
38
|
+
super(data, options)
|
|
39
|
+
/** @type {string} Path to registry file */ this.filePath
|
|
40
|
+
/** @type {ProjectEntry[]} List of registered projects */ this.projects
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Parse raw content of releases.txt into array of clean paths
|
|
45
|
+
* @param {string} content
|
|
46
|
+
* @returns {string[]}
|
|
47
|
+
*/
|
|
48
|
+
static parsePaths(content = '') {
|
|
49
|
+
if (typeof content !== 'string') return []
|
|
50
|
+
return content
|
|
51
|
+
.split('\n')
|
|
52
|
+
.map((line) => {
|
|
53
|
+
// Strip comments starting with #
|
|
54
|
+
const withoutComment = line.split('#')[0]
|
|
55
|
+
return withoutComment.trim()
|
|
56
|
+
})
|
|
57
|
+
.filter((line) => line.length > 0)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Load project entries from registry file using injected DB instance
|
|
62
|
+
* @returns {Promise<ProjectEntry[]>}
|
|
63
|
+
*/
|
|
64
|
+
async loadProjects() {
|
|
65
|
+
const db = this._.db
|
|
66
|
+
if (!db) {
|
|
67
|
+
throw new Error('Database instance is required in options ({ db }) to load projects')
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let content = await db.loadDocument(this.filePath, '')
|
|
71
|
+
let rootPrefix = ''
|
|
72
|
+
if (!content && this.filePath === 'releases.txt') {
|
|
73
|
+
try {
|
|
74
|
+
const parent2 = await db.loadDocument('../../releases.txt', '')
|
|
75
|
+
if (parent2) {
|
|
76
|
+
content = parent2
|
|
77
|
+
rootPrefix = '../../'
|
|
78
|
+
} else {
|
|
79
|
+
const parent1 = await db.loadDocument('../releases.txt', '')
|
|
80
|
+
if (parent1) {
|
|
81
|
+
content = parent1
|
|
82
|
+
rootPrefix = '../'
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
} catch {
|
|
86
|
+
// ignore
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const paths = RegistryModel.parsePaths(String(content || ''))
|
|
91
|
+
|
|
92
|
+
/** @type {ProjectEntry[]} */
|
|
93
|
+
const result = []
|
|
94
|
+
|
|
95
|
+
for (const projectPath of paths) {
|
|
96
|
+
const effectivePath = rootPrefix
|
|
97
|
+
? projectPath === '.'
|
|
98
|
+
? rootPrefix.slice(0, -1)
|
|
99
|
+
: `${rootPrefix}${projectPath}`
|
|
100
|
+
: projectPath
|
|
101
|
+
const pkgPath = effectivePath === '.' ? 'package.json' : `${effectivePath.replace(/\/+$/, '')}/package.json`
|
|
102
|
+
let pkg = null
|
|
103
|
+
try {
|
|
104
|
+
pkg = await db.loadDocument(pkgPath, null)
|
|
105
|
+
} catch {
|
|
106
|
+
pkg = null
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const name = pkg?.name || projectPath
|
|
110
|
+
const version = pkg?.version || '0.0.0'
|
|
111
|
+
|
|
112
|
+
result.push({
|
|
113
|
+
path: projectPath,
|
|
114
|
+
name,
|
|
115
|
+
version,
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
this.projects = result
|
|
120
|
+
return result
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export default RegistryModel
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { ModelAsApp, result, show } from '@nan0web/ui'
|
|
2
|
+
import Markdown from '@nan0web/markdown'
|
|
3
|
+
import DEFAULT_TEMPLATE from './templates/release.html.template.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* ServeCommand - Universal ModelAsApp for data or release catalogs.
|
|
7
|
+
*
|
|
8
|
+
* Generates HTML content from README.md and release templates using $db and Markdown.
|
|
9
|
+
* Pure OLMUI model: does not start HTTP server, does not call node:fs or child_process.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* pnpm exec serve releases/ # Runs via CLI bootstrap
|
|
13
|
+
*/
|
|
14
|
+
export default class ServeCommand extends ModelAsApp {
|
|
15
|
+
static alias = 'serve'
|
|
16
|
+
|
|
17
|
+
static UI = {
|
|
18
|
+
title: 'serve',
|
|
19
|
+
help: 'Serve data or releases catalog over HTTP',
|
|
20
|
+
serving: '🌐 Serving {$dir} on http://localhost:{$port} (Press Ctrl+C to stop)',
|
|
21
|
+
dirNotFound: 'Directory {$dir} does not exist.',
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
static dir = {
|
|
25
|
+
help: 'Directory to serve (e.g. releases/ or data/)',
|
|
26
|
+
positional: true,
|
|
27
|
+
type: 'string',
|
|
28
|
+
default: 'releases',
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
static port = {
|
|
32
|
+
help: 'HTTP server port',
|
|
33
|
+
default: 3131,
|
|
34
|
+
type: 'number',
|
|
35
|
+
alias: 'p',
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
static open = {
|
|
39
|
+
help: 'Automatically open in browser',
|
|
40
|
+
default: true,
|
|
41
|
+
type: 'boolean',
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {Partial<ServeCommand>} [data]
|
|
46
|
+
* @param {import('@nan0web/types').ModelOptions} [options]
|
|
47
|
+
*/
|
|
48
|
+
constructor(data = {}, options = {}) {
|
|
49
|
+
super(data, options)
|
|
50
|
+
/** @type {string} */
|
|
51
|
+
this.dir = String(data.dir || (options && 'dir' in options ? /** @type {any} */ (options).dir : 'releases'))
|
|
52
|
+
/** @type {number} */
|
|
53
|
+
this.port = Number(data.port ?? (options && 'port' in options ? /** @type {any} */ (options).port : 3131))
|
|
54
|
+
/** @type {boolean} */
|
|
55
|
+
this.open = Boolean(data.open ?? (options && 'open' in options ? /** @type {any} */ (options).open : true))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Convert markdown string to HTML using @nan0web/markdown
|
|
60
|
+
* @param {string} mdRaw
|
|
61
|
+
* @returns {string}
|
|
62
|
+
*/
|
|
63
|
+
renderMarkdownToHtml(mdRaw) {
|
|
64
|
+
if (!mdRaw || typeof mdRaw !== 'string') return ''
|
|
65
|
+
const md = new Markdown(mdRaw)
|
|
66
|
+
return md.stringify()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Render HTML content for the target directory
|
|
71
|
+
* @returns {Promise<string>}
|
|
72
|
+
*/
|
|
73
|
+
async renderHtml() {
|
|
74
|
+
const db = this.$db
|
|
75
|
+
let readmeContent = ''
|
|
76
|
+
const readmePath = db.resolveSync ? db.resolveSync(this.dir, 'README.md') : `${this.dir}/README.md`
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const mdRaw = await db.loadDocument(readmePath, null)
|
|
80
|
+
if (mdRaw) {
|
|
81
|
+
readmeContent = this.renderMarkdownToHtml(typeof mdRaw === 'string' ? mdRaw : mdRaw.content || String(mdRaw))
|
|
82
|
+
}
|
|
83
|
+
} catch {}
|
|
84
|
+
|
|
85
|
+
if (!readmeContent) {
|
|
86
|
+
// If no README.md in directory, scan directory entries and list them nicely
|
|
87
|
+
let listItems = []
|
|
88
|
+
try {
|
|
89
|
+
if (typeof db.listDir === 'function') {
|
|
90
|
+
const entries = await db.listDir(this.dir)
|
|
91
|
+
if (entries && Array.isArray(entries)) {
|
|
92
|
+
listItems = entries
|
|
93
|
+
.map((e) => e?.name || e?.path || String(e))
|
|
94
|
+
.filter((name) => !name.startsWith('.'))
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
} catch {}
|
|
98
|
+
|
|
99
|
+
const title = this.dir === 'releases' ? 'Releases Catalog' : `Index of ${this.dir}`
|
|
100
|
+
let md = `# ${title}\n\n`
|
|
101
|
+
if (listItems.length > 0) {
|
|
102
|
+
md += `Found **${listItems.length}** item(s) in \`${this.dir}\`:\n\n`
|
|
103
|
+
for (const item of listItems) {
|
|
104
|
+
md += `- **\`${item}\`**\n`
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
md += `Directory \`${this.dir}\` is currently empty or contains no catalog entries.\n`
|
|
108
|
+
}
|
|
109
|
+
readmeContent = this.renderMarkdownToHtml(md)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Load template via DB or fallback to bundled template
|
|
113
|
+
let templateRaw = null
|
|
114
|
+
try {
|
|
115
|
+
templateRaw = await db.loadDocument('packages/release/src/domain/templates/release.html', null)
|
|
116
|
+
} catch {}
|
|
117
|
+
|
|
118
|
+
if (!templateRaw) {
|
|
119
|
+
templateRaw = DEFAULT_TEMPLATE
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return String(templateRaw).replace('{{CONTENT}}', readmeContent)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async *run() {
|
|
126
|
+
const db = this.$db
|
|
127
|
+
|
|
128
|
+
// Check if directory exists via db.listDir or db.stat
|
|
129
|
+
let dirExists = false
|
|
130
|
+
try {
|
|
131
|
+
if (typeof db.listDir === 'function') {
|
|
132
|
+
const entries = await db.listDir(this.dir)
|
|
133
|
+
dirExists = entries && entries.length > 0
|
|
134
|
+
}
|
|
135
|
+
if (!dirExists && typeof db.stat === 'function') {
|
|
136
|
+
const stat = await db.stat(this.dir)
|
|
137
|
+
dirExists = Boolean(stat && (stat.exists || stat.isDirectory || stat.isFile))
|
|
138
|
+
}
|
|
139
|
+
} catch {
|
|
140
|
+
dirExists = false
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (!dirExists) {
|
|
144
|
+
yield show(ServeCommand.UI.dirNotFound.replace('{$dir}', this.dir), 'error')
|
|
145
|
+
return result({ success: false })
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const html = await this.renderHtml()
|
|
149
|
+
|
|
150
|
+
yield show(
|
|
151
|
+
ServeCommand.UI.serving.replace('{$dir}', this.dir).replace('{$port}', String(this.port)),
|
|
152
|
+
'success'
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
return result({
|
|
156
|
+
success: true,
|
|
157
|
+
dir: this.dir,
|
|
158
|
+
port: this.port,
|
|
159
|
+
open: this.open,
|
|
160
|
+
html,
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export { ServeCommand }
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { describe, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import DB from '@nan0web/db'
|
|
4
|
+
import ServeCommand from './ServeCommand.js'
|
|
5
|
+
|
|
6
|
+
describe('ServeCommand OLMUI Scenario & Model Test', () => {
|
|
7
|
+
it('generates HTML from releases/README.md when directory exists', async () => {
|
|
8
|
+
const readmeMd = `# Releases
|
|
9
|
+
|
|
10
|
+
This is the main catalog of releases.
|
|
11
|
+
|
|
12
|
+
## Active Projects
|
|
13
|
+
- @nan0web/ui
|
|
14
|
+
- @nan0web/db
|
|
15
|
+
`
|
|
16
|
+
const templateHtml = `<!DOCTYPE html><html><body><main>{{CONTENT}}</main></body></html>`
|
|
17
|
+
|
|
18
|
+
const db = new DB({
|
|
19
|
+
predefined: [
|
|
20
|
+
['releases/README.md', readmeMd],
|
|
21
|
+
['packages/release/src/domain/templates/release.html', templateHtml],
|
|
22
|
+
],
|
|
23
|
+
})
|
|
24
|
+
await db.connect()
|
|
25
|
+
|
|
26
|
+
const cmd = new ServeCommand({ dir: 'releases', port: 3131 }, { db })
|
|
27
|
+
const gen = cmd.run()
|
|
28
|
+
|
|
29
|
+
let step = await gen.next()
|
|
30
|
+
const outputs = []
|
|
31
|
+
while (!step.done) {
|
|
32
|
+
outputs.push(step.value)
|
|
33
|
+
step = await gen.next()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const res = step.value?.data || step.value
|
|
37
|
+
assert.ok(res.success !== false, 'Execution should succeed')
|
|
38
|
+
assert.equal(res.dir, 'releases')
|
|
39
|
+
assert.equal(res.port, 3131)
|
|
40
|
+
assert.ok(typeof res.html === 'string', 'Should return html string')
|
|
41
|
+
assert.ok(res.html.includes('Releases'), 'HTML should contain rendered markdown title')
|
|
42
|
+
assert.ok(res.html.includes('main'), 'HTML should use the template')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('yields error when target directory does not exist', async () => {
|
|
46
|
+
const db = new DB({
|
|
47
|
+
predefined: [],
|
|
48
|
+
})
|
|
49
|
+
await db.connect()
|
|
50
|
+
|
|
51
|
+
const cmd = new ServeCommand({ dir: 'nonexistent-dir' }, { db })
|
|
52
|
+
const gen = cmd.run()
|
|
53
|
+
|
|
54
|
+
let step = await gen.next()
|
|
55
|
+
const outputs = []
|
|
56
|
+
while (!step.done) {
|
|
57
|
+
outputs.push(step.value)
|
|
58
|
+
step = await gen.next()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const res = step.value?.data || step.value
|
|
62
|
+
assert.equal(res.success, false)
|
|
63
|
+
assert.ok(outputs.some((o) => o?.type === 'show' || o?.tag === 'show' || String(o).includes('Directory') || o?.content?.includes('nonexistent-dir')))
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('handles missing README.md with fallback content', async () => {
|
|
67
|
+
const templateHtml = `<!DOCTYPE html><html><body>{{CONTENT}}</body></html>`
|
|
68
|
+
const db = new DB({
|
|
69
|
+
predefined: [
|
|
70
|
+
['releases/.gitkeep', ''],
|
|
71
|
+
['packages/release/src/domain/templates/release.html', templateHtml],
|
|
72
|
+
],
|
|
73
|
+
})
|
|
74
|
+
await db.connect()
|
|
75
|
+
|
|
76
|
+
const cmd = new ServeCommand({ dir: 'releases' }, { db })
|
|
77
|
+
const gen = cmd.run()
|
|
78
|
+
|
|
79
|
+
let step = await gen.next()
|
|
80
|
+
while (!step.done) {
|
|
81
|
+
step = await gen.next()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const res = step.value?.data || step.value
|
|
85
|
+
assert.ok(res.success !== false)
|
|
86
|
+
assert.ok(res.html.includes('Releases'))
|
|
87
|
+
})
|
|
88
|
+
})
|
|
@@ -3,6 +3,9 @@ import { runSpawn } from '@nan0web/test'
|
|
|
3
3
|
import Scanner from '../Release/Scanner.js'
|
|
4
4
|
|
|
5
5
|
export default class SpecCommand extends ModelAsApp {
|
|
6
|
+
/** @type {string} Release version filter */
|
|
7
|
+
version = ''
|
|
8
|
+
|
|
6
9
|
static version = {
|
|
7
10
|
help: 'Release version filter',
|
|
8
11
|
default: '',
|
|
@@ -18,7 +21,6 @@ export default class SpecCommand extends ModelAsApp {
|
|
|
18
21
|
*/
|
|
19
22
|
constructor(data = {}, options = {}) {
|
|
20
23
|
super(data, options)
|
|
21
|
-
/** @type {string} Release version filter */ this.version
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
/**
|