@nan0web/ui 1.9.0 → 1.11.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 +97 -12
- package/package.json +54 -25
- package/src/App/Command/DepsCommand.js +3 -4
- package/src/Frame/Props.js +12 -18
- package/src/InterfaceTemplate/InterfaceTemplate.js +9 -7
- package/src/Model/index.js +86 -2
- package/src/StdIn.js +2 -6
- package/src/cli.js +1 -0
- package/src/core/Form/Form.js +8 -7
- package/src/core/Form/Message.js +1 -1
- package/src/core/GeneratorRunner.js +77 -7
- package/src/core/InputAdapter.js +3 -1
- package/src/core/Intent.js +214 -16
- package/src/core/IntentErrorModel.js +6 -1
- package/src/core/Message/Message.js +4 -7
- package/src/core/Message/OutputMessage.js +4 -9
- package/src/core/Stream.js +16 -5
- package/src/core/StreamEntry.js +20 -28
- package/src/core/index.js +2 -1
- package/src/domain/Content.js +196 -0
- package/src/domain/Document.js +17 -0
- package/src/domain/FooterModel.js +37 -19
- package/src/domain/HeaderModel.js +47 -21
- package/src/domain/HeroModel.js +24 -22
- package/src/domain/LayoutModel.js +43 -0
- package/src/domain/ModelAsApp.js +46 -0
- package/src/domain/SandboxModel.js +19 -16
- package/src/domain/app/GalleryCommand.js +53 -0
- package/src/domain/app/GalleryRenderIntent.js +77 -0
- package/src/domain/app/SnapshotAuditor.js +401 -0
- package/src/domain/app/SnapshotRunner.js +264 -0
- package/src/domain/app/UIApp.js +78 -0
- package/src/domain/components/BreadcrumbModel.js +10 -6
- package/src/domain/components/FeatureGridModel.js +62 -0
- package/src/domain/components/MarkdownModel.js +24 -0
- package/src/domain/components/ShellModel.js +243 -0
- package/src/domain/components/TableModel.js +10 -6
- package/src/domain/components/ToastModel.js +10 -6
- package/src/domain/components/index.js +3 -1
- package/src/domain/index.js +14 -4
- package/src/index.js +21 -2
- package/src/inspect.js +2 -0
- package/src/test/ScenarioAdapter.js +59 -0
- package/src/test/ScenarioTest.js +51 -0
- package/src/test/ScenarioTest.story.js +56 -0
- package/src/testing/CrashReporter.js +56 -0
- package/src/testing/GalleryGenerator.js +29 -0
- package/src/testing/LogicInspector.js +55 -0
- package/src/testing/SnapshotRunner.js +22 -0
- package/src/testing/SpecAdapter.js +115 -0
- package/src/testing/SpecRunner.js +121 -0
- package/src/testing/VisualAdapter.js +46 -0
- package/src/testing/index.js +7 -0
- package/src/testing/verifySnapshot.js +17 -0
- package/types/App/Command/DepsCommand.d.ts +0 -2
- package/types/Model/index.d.ts +56 -4
- package/types/StdIn.d.ts +3 -3
- package/types/cli.d.ts +1 -0
- package/types/core/Form/Form.d.ts +2 -2
- package/types/core/GeneratorRunner.d.ts +18 -1
- package/types/core/InputAdapter.d.ts +2 -1
- package/types/core/Intent.d.ts +232 -26
- package/types/core/IntentErrorModel.d.ts +4 -0
- package/types/core/Message/Message.d.ts +2 -2
- package/types/core/Message/OutputMessage.d.ts +0 -2
- package/types/core/index.d.ts +2 -1
- package/types/domain/Content.d.ts +340 -0
- package/types/domain/Document.d.ts +21 -0
- package/types/domain/FooterModel.d.ts +22 -12
- package/types/domain/HeaderModel.d.ts +36 -13
- package/types/domain/HeroModel.d.ts +19 -17
- package/types/domain/LayoutModel.d.ts +34 -0
- package/types/domain/ModelAsApp.d.ts +23 -0
- package/types/domain/SandboxModel.d.ts +10 -0
- package/types/domain/app/GalleryCommand.d.ts +55 -0
- package/types/domain/app/GalleryRenderIntent.d.ts +31 -0
- package/types/domain/app/SnapshotAuditor.d.ts +99 -0
- package/types/domain/app/SnapshotRunner.d.ts +45 -0
- package/types/domain/app/UIApp.d.ts +60 -0
- package/types/domain/components/BreadcrumbModel.d.ts +6 -8
- package/types/domain/components/FeatureGridModel.d.ts +50 -0
- package/types/domain/components/MarkdownModel.d.ts +19 -0
- package/types/domain/components/ShellModel.d.ts +56 -0
- package/types/domain/components/TableModel.d.ts +4 -0
- package/types/domain/components/ToastModel.d.ts +4 -0
- package/types/domain/components/index.d.ts +3 -0
- package/types/domain/index.d.ts +10 -4
- package/types/index.d.ts +19 -1
- package/types/inspect.d.ts +2 -0
- package/types/test/ScenarioAdapter.d.ts +43 -0
- package/types/test/ScenarioTest.d.ts +24 -0
- package/types/test/ScenarioTest.story.d.ts +1 -0
- package/types/testing/CrashReporter.d.ts +13 -0
- package/types/testing/GalleryGenerator.d.ts +1 -0
- package/types/testing/LogicInspector.d.ts +22 -0
- package/types/testing/SnapshotRunner.d.ts +7 -0
- package/types/testing/SpecAdapter.d.ts +57 -0
- package/types/testing/SpecRunner.d.ts +41 -0
- package/types/testing/VisualAdapter.d.ts +9 -0
- package/types/testing/index.d.ts +7 -0
- package/types/testing/verifySnapshot.d.ts +14 -0
- package/src/README.md.js +0 -436
- package/types/App/Command/Options.d.ts +0 -43
- package/types/App/Command/index.d.ts +0 -8
- package/types/App/User/Command/Options.d.ts +0 -34
- package/types/core/Message/InputMessage.d.ts +0 -71
- package/types/domain/components/HeroModel.d.ts +0 -24
- package/types/domain/components/ShowcaseAppModel.d.ts +0 -32
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { Model } from '@nan0web/types'
|
|
2
|
+
import { ask, log } from '../../core/Intent.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* ShellModel — OLMUI Component Model for CLI Orchestration
|
|
6
|
+
* Canonical CLI entry that describes available operations as a schema.
|
|
7
|
+
*/
|
|
8
|
+
export class ShellModel extends Model {
|
|
9
|
+
static $id = '@nan0web/ui/ShellModel'
|
|
10
|
+
|
|
11
|
+
static command = {
|
|
12
|
+
help: 'What do you want to do?',
|
|
13
|
+
type: 'select',
|
|
14
|
+
default: null,
|
|
15
|
+
positional: true,
|
|
16
|
+
// options: [
|
|
17
|
+
// BootEngine,
|
|
18
|
+
// InteractiveCLI,
|
|
19
|
+
// DevMode,
|
|
20
|
+
// BuildProject,
|
|
21
|
+
// TestSSG,
|
|
22
|
+
// SSGGallery,
|
|
23
|
+
// TestWeb,
|
|
24
|
+
// WebGallery,
|
|
25
|
+
// ConfigWizard,
|
|
26
|
+
// ],
|
|
27
|
+
options: [
|
|
28
|
+
{ label: '📡 Boot Engine (Run OS)', value: 'run' },
|
|
29
|
+
{ label: '🖥️ Interactive CLI', value: 'cli' },
|
|
30
|
+
{ label: '🧬 Dev Mode (Hot-Reload)', value: 'dev' },
|
|
31
|
+
{ label: '📦 Build Project (Data & UI)', value: 'build' },
|
|
32
|
+
{ label: '🧪 Test SSG', value: 'test:ssg' },
|
|
33
|
+
{ label: '🔭 SSG Gallery', value: 'ssg:gallery' },
|
|
34
|
+
{ label: '🧪 Test Web', value: 'test:web' },
|
|
35
|
+
{ label: '🔭 Web Gallery', value: 'web:gallery' },
|
|
36
|
+
{ label: '🔧 Config Wizard', value: 'config' },
|
|
37
|
+
],
|
|
38
|
+
required: true,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static data = {
|
|
42
|
+
help: 'Data source (DSN)',
|
|
43
|
+
type: 'string',
|
|
44
|
+
default: 'data/',
|
|
45
|
+
alias: 'dsn',
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
static index = {
|
|
49
|
+
help: 'Directory index file name (e.g. README or index)',
|
|
50
|
+
type: 'string',
|
|
51
|
+
default: 'index',
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
static locale = {
|
|
55
|
+
help: 'Application locale',
|
|
56
|
+
type: 'string',
|
|
57
|
+
default: 'en',
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
static port = {
|
|
61
|
+
help: 'Server port',
|
|
62
|
+
type: 'string',
|
|
63
|
+
default: '3000',
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
#options = {}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @param {object} data
|
|
71
|
+
* @param {object} [options] External dependencies (AppRunner, SSRServer, etc.)
|
|
72
|
+
*/
|
|
73
|
+
constructor(data = {}, options = {}) {
|
|
74
|
+
super(data)
|
|
75
|
+
this.#options = options
|
|
76
|
+
/** @type {string|null} */ this.command
|
|
77
|
+
/** @type {string} */ this.data
|
|
78
|
+
/** @type {string} */ this.index
|
|
79
|
+
/** @type {string} */ this.locale
|
|
80
|
+
/** @type {string} */ this.port
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async *run() {
|
|
84
|
+
yield log('info', '📡 NaN0Web Engine OLMUI Shell Ready')
|
|
85
|
+
|
|
86
|
+
if (!this.command || this.command === 'help') {
|
|
87
|
+
const res = yield ask('Shell', ShellModel)
|
|
88
|
+
if (res.cancelled) return
|
|
89
|
+
Object.assign(this, res.value)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (this.command !== 'cli' && this.command !== 'help') {
|
|
93
|
+
yield log('info', `📡 Executing command: ${this.command}...`)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
switch (this.command) {
|
|
97
|
+
case 'run':
|
|
98
|
+
return yield* this.#runEngine()
|
|
99
|
+
case 'cli':
|
|
100
|
+
return yield* this.#runCli()
|
|
101
|
+
case 'config':
|
|
102
|
+
return yield* this.#runConfig()
|
|
103
|
+
case 'build':
|
|
104
|
+
return yield* this.#runBuild()
|
|
105
|
+
case 'dev':
|
|
106
|
+
return yield* this.#runDev()
|
|
107
|
+
case 'test:ssg':
|
|
108
|
+
case 'ssg:gallery':
|
|
109
|
+
case 'test:web':
|
|
110
|
+
case 'web:gallery':
|
|
111
|
+
return yield* this.#runNpmScript(this.command)
|
|
112
|
+
default:
|
|
113
|
+
yield log('error', `Unknown command: ${this.command}`)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
this.command = null
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async *#runCli() {
|
|
120
|
+
const { spawn, locale, dsn } = this.#options
|
|
121
|
+
if (!spawn) {
|
|
122
|
+
yield log('error', 'Spawn utility missing. CLI mode requires Node environment.')
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const bankFrame = ' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ '
|
|
127
|
+
yield log(
|
|
128
|
+
'success',
|
|
129
|
+
`\x1b[1m\n${bankFrame}\n📡 Launching Sub-App CLI...\n${bankFrame}\n\x1b[22m`,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
const absPath = (await import('node:path')).resolve('src/ui/cli/index.js')
|
|
133
|
+
const { existsSync } = await import('node:fs')
|
|
134
|
+
if (!existsSync(absPath)) {
|
|
135
|
+
yield log(
|
|
136
|
+
'error',
|
|
137
|
+
`No CLI runner found at ${absPath}. Ensure you are in the application root.`,
|
|
138
|
+
)
|
|
139
|
+
return
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const args = [absPath]
|
|
143
|
+
if (this.locale) args.push('--locale', this.locale)
|
|
144
|
+
if (this.data) args.push('--data', this.data)
|
|
145
|
+
|
|
146
|
+
const extra = (process.argv || []).slice(3)
|
|
147
|
+
args.push(...extra)
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
const code = await spawn('node', args, { stdio: 'inherit' })
|
|
151
|
+
if (code !== 0) yield log('error', `CLI exited with code ${code}`)
|
|
152
|
+
} catch (/** @type {any} */ e) {
|
|
153
|
+
yield log('error', `Failed to spawn CLI: ${e.message}`)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async *#runEngine() {
|
|
158
|
+
const { AppRunner, SSRServer } = this.#options
|
|
159
|
+
if (!AppRunner) return yield log('error', 'AppRunner dependency missing')
|
|
160
|
+
|
|
161
|
+
const runner = new AppRunner({
|
|
162
|
+
dsn: this.data,
|
|
163
|
+
port: this.port,
|
|
164
|
+
locale: this.locale,
|
|
165
|
+
directoryIndex: this.index,
|
|
166
|
+
})
|
|
167
|
+
for await (const msg of runner.run()) {
|
|
168
|
+
yield log('info', msg)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const server = new SSRServer(runner)
|
|
172
|
+
const port = runner.config?.port || 3000
|
|
173
|
+
const { protocol } = await server.listen(port)
|
|
174
|
+
|
|
175
|
+
yield log('success', `\n🌐 Server running on ${protocol}://localhost:${port}`)
|
|
176
|
+
|
|
177
|
+
// Keep alive in CLI mode
|
|
178
|
+
if (typeof process !== 'undefined') {
|
|
179
|
+
while (true) {
|
|
180
|
+
await new Promise((r) => setTimeout(r, 60000))
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async *#runConfig() {
|
|
186
|
+
const { NaN0WebConfig, DBwithFSDriver } = this.#options
|
|
187
|
+
const res = yield ask('config', NaN0WebConfig)
|
|
188
|
+
if (res.cancelled) return
|
|
189
|
+
|
|
190
|
+
const data = res.value
|
|
191
|
+
if (typeof process !== 'undefined' && DBwithFSDriver) {
|
|
192
|
+
const db = new DBwithFSDriver({ cwd: process.cwd() })
|
|
193
|
+
await db.connect()
|
|
194
|
+
await db.saveDocument('nan0web.config.yaml', {
|
|
195
|
+
name: data.name,
|
|
196
|
+
dsn: data.data || data.dsn,
|
|
197
|
+
locale: data.locale,
|
|
198
|
+
port: data.port,
|
|
199
|
+
directoryIndex: data.index,
|
|
200
|
+
})
|
|
201
|
+
yield log('success', '\n✅ Config saved to nan0web.config.yaml')
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async *#runBuild() {
|
|
206
|
+
const { spawn, AppRunner, SSRServer } = this.#options
|
|
207
|
+
if (!spawn) return yield log('error', 'Spawn missing')
|
|
208
|
+
|
|
209
|
+
const { existsSync } = await import('node:fs')
|
|
210
|
+
const viteConfig = existsSync('vite.docs.js') ? 'vite.docs.js' :
|
|
211
|
+
existsSync('vite.config.js') ? 'vite.config.js' : null
|
|
212
|
+
|
|
213
|
+
if (viteConfig) {
|
|
214
|
+
yield log('info', `🛠 Building UI (Vite using ${viteConfig})...`)
|
|
215
|
+
const exitCode = await spawn('npx', ['vite', 'build', '-c', viteConfig])
|
|
216
|
+
if (exitCode !== 0) yield log('error', '⚠️ Vite build failed.')
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const runner = new AppRunner({
|
|
220
|
+
dsn: this.data,
|
|
221
|
+
locale: this.locale,
|
|
222
|
+
directoryIndex: this.index,
|
|
223
|
+
})
|
|
224
|
+
for await (const msg of runner.run()) yield log('info', msg)
|
|
225
|
+
const server = new SSRServer(runner)
|
|
226
|
+
const stats = await server.exportStatic('dist')
|
|
227
|
+
yield log('success', `✅ Built ${stats.count}/${stats.total} pages into /dist`)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async *#runDev() {
|
|
231
|
+
const { spawn } = this.#options
|
|
232
|
+
if (!spawn) return yield log('error', 'Dev mode requires spawn')
|
|
233
|
+
yield log('info', '🧬 Starting VITE Dev Server...')
|
|
234
|
+
await spawn('npx', ['vite'], { stdio: 'inherit' })
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async *#runNpmScript(script) {
|
|
238
|
+
const { spawn } = this.#options
|
|
239
|
+
if (!spawn) return
|
|
240
|
+
yield log('info', `🔭 Running npm run ${script}...`)
|
|
241
|
+
await spawn('npm', ['run', script], { stdio: 'inherit' })
|
|
242
|
+
}
|
|
243
|
+
}
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import { Model } from '@nan0web/types'
|
|
2
|
+
import { show, result } from '../../core/Intent.js'
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Model-as-Schema for Table Data component.
|
|
5
6
|
* Displays tabular string data in rows and columns.
|
|
6
7
|
*/
|
|
7
8
|
export class TableModel extends Model {
|
|
9
|
+
static $id = '@nan0web/ui/TableModel'
|
|
10
|
+
|
|
11
|
+
static UI = {
|
|
12
|
+
displayingTable: 'Displaying table with {count} rows',
|
|
13
|
+
}
|
|
14
|
+
|
|
8
15
|
static columns = {
|
|
9
16
|
help: 'Array of column headers',
|
|
10
17
|
type: 'string[]',
|
|
@@ -34,14 +41,11 @@ export class TableModel extends Model {
|
|
|
34
41
|
* @returns {AsyncGenerator<any, any, any>}
|
|
35
42
|
*/
|
|
36
43
|
async *run() {
|
|
37
|
-
yield {
|
|
38
|
-
type: 'log',
|
|
39
|
-
level: 'info',
|
|
40
|
-
message: `Displaying table with ${this.rows?.length || 0} rows`,
|
|
44
|
+
yield show(this._.t(TableModel.UI.displayingTable, { count: this.rows?.length || 0 }), 'info', {
|
|
41
45
|
component: 'Table',
|
|
42
46
|
model: this,
|
|
43
|
-
}
|
|
47
|
+
})
|
|
44
48
|
|
|
45
|
-
return
|
|
49
|
+
return result({ rowsCount: this.rows?.length || 0 })
|
|
46
50
|
}
|
|
47
51
|
}
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import { Model } from '@nan0web/types'
|
|
2
|
+
import { show, result } from '../../core/Intent.js'
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Model-as-Schema for Toast notification.
|
|
5
6
|
*/
|
|
6
7
|
export class ToastModel extends Model {
|
|
8
|
+
static $id = '@nan0web/ui/ToastModel'
|
|
9
|
+
|
|
10
|
+
static UI = {
|
|
11
|
+
toastLog: '{message}',
|
|
12
|
+
}
|
|
13
|
+
|
|
7
14
|
static variant = {
|
|
8
15
|
help: 'Notification color scheme',
|
|
9
16
|
default: 'info',
|
|
@@ -37,14 +44,11 @@ export class ToastModel extends Model {
|
|
|
37
44
|
* @returns {AsyncGenerator<any, any, any>}
|
|
38
45
|
*/
|
|
39
46
|
async *run() {
|
|
40
|
-
yield {
|
|
41
|
-
type: 'log',
|
|
42
|
-
level: this.variant === 'error' ? 'error' : 'info',
|
|
43
|
-
message: this.message,
|
|
47
|
+
yield show(this._.t(ToastModel.UI.toastLog, { message: this.message }), this.variant === 'error' ? 'error' : 'info', {
|
|
44
48
|
component: 'Toast',
|
|
45
49
|
model: this,
|
|
46
|
-
}
|
|
50
|
+
})
|
|
47
51
|
|
|
48
|
-
return
|
|
52
|
+
return result({ dismissed: true })
|
|
49
53
|
}
|
|
50
54
|
}
|
|
@@ -20,6 +20,7 @@ export { GalleryModel } from './GalleryModel.js'
|
|
|
20
20
|
export { PriceModel } from './PriceModel.js'
|
|
21
21
|
export { PricingModel } from './PricingModel.js'
|
|
22
22
|
export { PricingSectionModel } from './PricingSectionModel.js'
|
|
23
|
+
export { FeatureGridModel, FeatureItemModel } from './FeatureGridModel.js'
|
|
23
24
|
|
|
24
25
|
// Hero
|
|
25
26
|
|
|
@@ -39,7 +40,8 @@ export { HeaderConfigModel } from './HeaderConfigModel.js'
|
|
|
39
40
|
export { FooterVisibilityModel } from './FooterVisibilityModel.js'
|
|
40
41
|
export { FooterConfigModel } from './FooterConfigModel.js'
|
|
41
42
|
|
|
42
|
-
// Business Critical
|
|
43
43
|
export { EmptyStateModel } from './EmptyStateModel.js'
|
|
44
44
|
export { BannerModel } from './BannerModel.js'
|
|
45
45
|
export { ProfileDropdownModel } from './ProfileDropdownModel.js'
|
|
46
|
+
export { MarkdownModel } from './MarkdownModel.js'
|
|
47
|
+
export { ShellModel } from './ShellModel.js'
|
package/src/domain/index.js
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
|
+
// Model runner (Model as App)
|
|
2
|
+
/** @typedef {import('./ModelAsApp.js').ModelAsAppOptions} ModelAsAppOptions */
|
|
3
|
+
export { ModelAsApp } from './ModelAsApp.js'
|
|
4
|
+
|
|
1
5
|
// Domain Models — OLMUI Model-as-Schema
|
|
2
6
|
export { SandboxModel } from './SandboxModel.js'
|
|
7
|
+
export { Content } from './Content.js'
|
|
8
|
+
export { Document } from './Document.js'
|
|
3
9
|
export { ShowcaseAppModel } from './ShowcaseAppModel.js'
|
|
4
10
|
export { default as Navigation } from './Navigation.js'
|
|
5
|
-
export { Language } from '@nan0web/i18n/src/domain/Language.js'
|
|
6
11
|
|
|
7
12
|
// Layout Models (Phase 1)
|
|
8
|
-
export {
|
|
9
|
-
export {
|
|
10
|
-
export {
|
|
13
|
+
export { LayoutModel } from './LayoutModel.js'
|
|
14
|
+
export { HeaderModel } from './HeaderModel.js'
|
|
15
|
+
export { FooterModel } from './FooterModel.js'
|
|
16
|
+
export { HeroModel } from './HeroModel.js'
|
|
11
17
|
|
|
12
18
|
// Component Models
|
|
13
19
|
export {
|
|
@@ -24,6 +30,8 @@ export {
|
|
|
24
30
|
AccordionModel,
|
|
25
31
|
GalleryModel,
|
|
26
32
|
PriceModel,
|
|
33
|
+
FeatureGridModel,
|
|
34
|
+
FeatureItemModel,
|
|
27
35
|
PricingModel,
|
|
28
36
|
CommentModel,
|
|
29
37
|
TestimonialModel,
|
|
@@ -39,3 +47,5 @@ export {
|
|
|
39
47
|
BannerModel,
|
|
40
48
|
ProfileDropdownModel,
|
|
41
49
|
} from './components/index.js'
|
|
50
|
+
|
|
51
|
+
export { ShellModel } from './components/ShellModel.js'
|
package/src/index.js
CHANGED
|
@@ -12,7 +12,6 @@ import App from './App/index.js'
|
|
|
12
12
|
export { Frame, FrameProps, Locale, StdIn, StdOut, View, RenderOptions, Model, Component, App }
|
|
13
13
|
export { format } from './format.js'
|
|
14
14
|
export { default as Navigation } from './domain/Navigation.js'
|
|
15
|
-
export { Language } from '@nan0web/i18n/src/domain/Language.js'
|
|
16
15
|
|
|
17
16
|
// export default App
|
|
18
17
|
export { default as FormMessage } from './core/Form/Message.js'
|
|
@@ -27,6 +26,26 @@ export { default as Error, CancelError } from './core/Error/index.js'
|
|
|
27
26
|
export { default as UiAdapter } from './core/UiAdapter.js'
|
|
28
27
|
|
|
29
28
|
// OLMUI Generator Engine
|
|
30
|
-
|
|
29
|
+
/** @typedef {import('./core/Intent.js').LogLevel} LogLevel */
|
|
30
|
+
/** @typedef {import('./core/Intent.js').ShowLevel} ShowLevel */
|
|
31
|
+
/** @typedef {import('./core/Intent.js').FieldSchema} FieldSchema */
|
|
32
|
+
/** @typedef {import('./core/Intent.js').Intent} Intent */
|
|
33
|
+
/** @typedef {import('./core/Intent.js').IntentResponse} IntentResponse */
|
|
34
|
+
/** @typedef {import('./core/Intent.js').AskIntent} AskIntent */
|
|
35
|
+
/** @typedef {import('./core/Intent.js').ProgressIntent} ProgressIntent */
|
|
36
|
+
/** @typedef {import('./core/Intent.js').LogIntent} LogIntent */
|
|
37
|
+
/** @typedef {import('./core/Intent.js').ShowIntent} ShowIntent */
|
|
38
|
+
/** @typedef {import('./core/Intent.js').RenderIntent} RenderIntent */
|
|
39
|
+
/** @typedef {import('./core/Intent.js').ResultIntent} ResultIntent */
|
|
40
|
+
/** @typedef {import('./core/Intent.js').IntentType} IntentType */
|
|
41
|
+
/** @typedef {import('./core/Intent.js').AskResponse} AskResponse */
|
|
42
|
+
/** @typedef {import('./core/Intent.js').AbortResponse} AbortResponse */
|
|
43
|
+
/** @typedef {import('./core/Intent.js').ShowData} ShowData */
|
|
44
|
+
export * from './core/Intent.js'
|
|
45
|
+
|
|
31
46
|
export { IntentErrorModel } from './core/IntentErrorModel.js'
|
|
32
47
|
export { runGenerator } from './core/GeneratorRunner.js'
|
|
48
|
+
export { buildNan0SpecFromTrace } from './testing/CrashReporter.js'
|
|
49
|
+
|
|
50
|
+
/** @typedef {import('./domain/index.js').ModelAsAppOptions} ModelAsAppOptions */
|
|
51
|
+
export * from './domain/index.js'
|
package/src/inspect.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import InputAdapter from '../core/InputAdapter.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Deterministic Scenario Adapter for OLMUI Testing.
|
|
5
|
+
*
|
|
6
|
+
* Drives the Model generator through a predefined script of responses,
|
|
7
|
+
* enabling millisecond-fast verification of complex business logic.
|
|
8
|
+
*/
|
|
9
|
+
export default class ScenarioAdapter extends InputAdapter {
|
|
10
|
+
/**
|
|
11
|
+
* @param {Array<{field: string, value: any, cancelled?: boolean}>} [scenario=[]]
|
|
12
|
+
*/
|
|
13
|
+
constructor(scenario = []) {
|
|
14
|
+
super()
|
|
15
|
+
this.scenario = scenario
|
|
16
|
+
this.intents = [] // Recorded intents for verification
|
|
17
|
+
this.console = {
|
|
18
|
+
info: () => {},
|
|
19
|
+
warn: () => {},
|
|
20
|
+
error: () => {},
|
|
21
|
+
debug: () => {},
|
|
22
|
+
log: () => {},
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {import('../core/Intent.js').AskIntent} intent
|
|
28
|
+
* @returns {Promise<import('../core/Intent.js').AskResponse>}
|
|
29
|
+
*/
|
|
30
|
+
async askIntent(intent) {
|
|
31
|
+
this.intents.push(intent)
|
|
32
|
+
const match = this.scenario.find((s) => s.field === intent.field)
|
|
33
|
+
if (match) {
|
|
34
|
+
return { value: match.value, cancelled: !!match.cancelled }
|
|
35
|
+
}
|
|
36
|
+
// If no specific match, try to use the first available answer or default
|
|
37
|
+
return { value: null }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** @param {import('../core/Intent.js').ProgressIntent} intent */
|
|
41
|
+
async progressIntent(intent) {
|
|
42
|
+
this.intents.push(intent)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** @param {import('../core/Intent.js').ShowIntent} intent */
|
|
46
|
+
async showIntent(intent) {
|
|
47
|
+
this.intents.push(intent)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** @param {import('../core/Intent.js').RenderIntent} intent */
|
|
51
|
+
async renderIntent(intent) {
|
|
52
|
+
this.intents.push(intent)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** @param {import('../core/Intent.js').ResultIntent} intent */
|
|
56
|
+
async resultIntent(intent) {
|
|
57
|
+
this.intents.push(intent)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { runGenerator } from '../core/GeneratorRunner.js'
|
|
2
|
+
import OutputAdapter from '../core/OutputAdapter.js'
|
|
3
|
+
import ScenarioAdapter from './ScenarioAdapter.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Deterministic Scenario Test Runner.
|
|
7
|
+
* Orchestrates a model against a predefined scenario, mocking I/O immediately.
|
|
8
|
+
*/
|
|
9
|
+
export class ScenarioTest {
|
|
10
|
+
/**
|
|
11
|
+
* Runs an application model with a specific set of answers.
|
|
12
|
+
*
|
|
13
|
+
* @param {typeof import('../domain/ModelAsApp.js').ModelAsApp} AppClass
|
|
14
|
+
* @param {Array<{field: string, value: any, cancelled?: boolean}>} scenario
|
|
15
|
+
* @param {any} [appData={}]
|
|
16
|
+
* @returns {Promise<{ value: any, intents: any[], error?: Error | undefined }>}
|
|
17
|
+
*/
|
|
18
|
+
static async run(AppClass, scenario = [], appData = {}) {
|
|
19
|
+
const inputAdapter = new ScenarioAdapter(scenario)
|
|
20
|
+
|
|
21
|
+
const app = new AppClass(appData, { adapter: inputAdapter })
|
|
22
|
+
|
|
23
|
+
let value
|
|
24
|
+
let error
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
// runGenerator executes the Intents returned by AppClass.run()
|
|
28
|
+
value = await runGenerator(
|
|
29
|
+
app.run(),
|
|
30
|
+
{
|
|
31
|
+
ask: inputAdapter.askIntent.bind(inputAdapter),
|
|
32
|
+
progress: inputAdapter.progressIntent.bind(inputAdapter),
|
|
33
|
+
show: inputAdapter.showIntent.bind(inputAdapter),
|
|
34
|
+
render: inputAdapter.renderIntent.bind(inputAdapter),
|
|
35
|
+
result: inputAdapter.resultIntent.bind(inputAdapter),
|
|
36
|
+
},
|
|
37
|
+
{ timeoutMs: 3333 },
|
|
38
|
+
)
|
|
39
|
+
} catch (err) {
|
|
40
|
+
error = /** @type {Error} */ (err)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
value,
|
|
45
|
+
intents: inputAdapter.intents,
|
|
46
|
+
error,
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export default ScenarioTest
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import DB from '@nan0web/db-fs'
|
|
4
|
+
import { SpecRunner } from '../testing/index.js'
|
|
5
|
+
import { ModelAsApp, ask, show, result } from '../index.js'
|
|
6
|
+
|
|
7
|
+
const db = new DB({ root: 'tests/uk' })
|
|
8
|
+
|
|
9
|
+
async function loadStory(name) {
|
|
10
|
+
const doc = await db.loadDocument(`${name}.nan0`)
|
|
11
|
+
return doc
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe('Workflow Story: Shopping Cart Purchase', () => {
|
|
15
|
+
class ShoppingCartApp extends ModelAsApp {
|
|
16
|
+
async *run() {
|
|
17
|
+
const { value: product } = yield ask('product', { help: 'Select product' })
|
|
18
|
+
if (product === 'laptop') {
|
|
19
|
+
yield show('Good choice!')
|
|
20
|
+
}
|
|
21
|
+
const { value: confirm } = yield ask('confirm', { help: 'Confirm purchase?' })
|
|
22
|
+
return result({ product: product, confirm: confirm })
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
it('should successfully buy a laptop and show a message via SpecRunner', async () => {
|
|
27
|
+
const doc = await loadStory('shopping.story')
|
|
28
|
+
assert.ok(Array.isArray(doc.story), 'Story must be an array')
|
|
29
|
+
|
|
30
|
+
await assert.doesNotReject(() => SpecRunner.execute(doc.story, { ShoppingCartApp }))
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
describe('Workflow Story: Validated Input Form', () => {
|
|
35
|
+
class ValidatedApp extends ModelAsApp {
|
|
36
|
+
async *run() {
|
|
37
|
+
const { value: code } = yield ask('code', { help: 'Enter code', required: true })
|
|
38
|
+
if (!code) throw new Error('Code is mandatory')
|
|
39
|
+
return result({ code: code })
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
it('should successfully process a valid code via SpecRunner', async () => {
|
|
44
|
+
const doc = await loadStory('validated.story')
|
|
45
|
+
assert.ok(Array.isArray(doc.story), 'Story must be an array')
|
|
46
|
+
|
|
47
|
+
await assert.doesNotReject(() => SpecRunner.execute(doc.story, { ValidatedApp }))
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('should fail if user provides empty input (Node.js Test)', async () => {
|
|
51
|
+
// Testing error conditions are better suited as explicit scenarios or node:test
|
|
52
|
+
const stream = [{ ValidatedApp: {} }, { ask: 'code', $value: '' }]
|
|
53
|
+
|
|
54
|
+
await assert.rejects(SpecRunner.execute(stream, { ValidatedApp }), /Code is mandatory/)
|
|
55
|
+
})
|
|
56
|
+
})
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file CrashReporter.js — Nan0Spec serialization.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Transforms an execution trace from GeneratorRunner into a strict Nan0Spec model
|
|
7
|
+
* ready to be saved as a .nan0 file for Crash Reporting and Integration Tests.
|
|
8
|
+
*
|
|
9
|
+
* @param {string} appName The name of the root model/app (e.g. 'ShoppingCartApp').
|
|
10
|
+
* @param {object} appData The initial data provided to the app.
|
|
11
|
+
* @param {import('../core/Intent.js').Intent[]} trace The array of intents executed.
|
|
12
|
+
* @returns {Array<object>} The serializable Nan0Spec array.
|
|
13
|
+
*/
|
|
14
|
+
export function buildNan0SpecFromTrace(appName, appData, trace) {
|
|
15
|
+
const spec = []
|
|
16
|
+
|
|
17
|
+
// 1. Initial State
|
|
18
|
+
spec.push({
|
|
19
|
+
[appName]: appData ? JSON.parse(JSON.stringify(appData)) : {},
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
/** @type {Record<string, string>} Mapping from intent type to its primary payload field in Nan0Spec */
|
|
23
|
+
const PAYLOAD_MAP = {
|
|
24
|
+
ask: 'field',
|
|
25
|
+
show: 'message',
|
|
26
|
+
log: 'message',
|
|
27
|
+
progress: 'message',
|
|
28
|
+
render: 'component',
|
|
29
|
+
agent: 'task'
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 2. Map trace
|
|
33
|
+
for (const intent of trace) {
|
|
34
|
+
if (intent.type === 'result') {
|
|
35
|
+
spec.push({ result: intent.data || null })
|
|
36
|
+
continue
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const key = PAYLOAD_MAP[intent.type]
|
|
40
|
+
if (!key) continue
|
|
41
|
+
|
|
42
|
+
// Create the primary step { type: payload }
|
|
43
|
+
const step = { [intent.type]: intent[key] }
|
|
44
|
+
|
|
45
|
+
// Automatically include all metadata/response fields (starting with $)
|
|
46
|
+
for (const [k, v] of Object.entries(intent)) {
|
|
47
|
+
if (k.startsWith('$') && v !== undefined) {
|
|
48
|
+
step[k] = v
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
spec.push(step)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return spec
|
|
56
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import { fileURLToPath } from 'url'
|
|
3
|
+
import { SnapshotRunner } from './SnapshotRunner.js'
|
|
4
|
+
|
|
5
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
6
|
+
const rootDir = path.resolve(__dirname, '../../')
|
|
7
|
+
const dataDir = path.resolve(rootDir, 'docs/data')
|
|
8
|
+
const snapshotsDir = path.resolve(rootDir, 'snapshots/core')
|
|
9
|
+
|
|
10
|
+
const groups = {
|
|
11
|
+
Actions: ['Button', 'Toggle'],
|
|
12
|
+
Forms: ['Input', 'Select', 'Slider', 'Autocomplete', 'Color', 'Shadow'],
|
|
13
|
+
Data: ['Accordion', 'Card', 'Sortable', 'Table', 'Tree', 'CodeBlock', 'Markdown', 'Badge'],
|
|
14
|
+
Feedback: ['Alert', 'Confirm', 'Modal', 'ProgressBar', 'Spinner', 'Toast'],
|
|
15
|
+
System: ['LangSelect', 'ThemeToggle'],
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function getCategory(comp) {
|
|
19
|
+
for (const [cat, comps] of Object.entries(groups)) {
|
|
20
|
+
if (comps.includes(comp)) return cat
|
|
21
|
+
}
|
|
22
|
+
return 'Other'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
SnapshotRunner.generateAndAudit({
|
|
26
|
+
dataDir,
|
|
27
|
+
snapshotsDir,
|
|
28
|
+
getCategory,
|
|
29
|
+
}).catch(console.error)
|