@octane-xplat/workshop 0.1.0 → 0.16.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/LICENSE +21 -0
- package/MOBILE.md +143 -0
- package/README.md +305 -1
- package/dist/build/discover.js +404 -0
- package/dist/build/macos-node.js +77 -0
- package/dist/build/mobile-node.js +96 -0
- package/dist/build/mobile-regex.js +40 -0
- package/dist/build/node.js +426 -0
- package/dist/build/vite-mobile.js +75 -0
- package/dist/build/vite.js +230 -0
- package/dist/build/windows/preflight.js +83 -0
- package/dist/contract.js +50 -0
- package/dist/index.js +2 -0
- package/package.json +118 -5
- package/src/build/discover.ts +585 -0
- package/src/build/macos/shell.macos.ts +674 -0
- package/src/build/macos-node.ts +98 -0
- package/src/build/macos-vite.ts +48 -0
- package/src/build/mobile-node.ts +127 -0
- package/src/build/mobile-regex.ts +60 -0
- package/src/build/node.d.ts +65 -0
- package/src/build/node.ts +537 -0
- package/src/build/vite-mobile.d.ts +21 -0
- package/src/build/vite-mobile.ts +102 -0
- package/src/build/vite.d.ts +11 -0
- package/src/build/vite.ts +316 -0
- package/src/build/windows/preflight.ts +113 -0
- package/src/catalog.ts +58 -0
- package/src/contract.ts +257 -0
- package/src/controls.ts +159 -0
- package/src/errors.ts +47 -0
- package/src/fixture.ts +158 -0
- package/src/index.ts +6 -0
- package/src/macos/controller.ts +170 -0
- package/src/macos/index.macos.ts +2 -0
- package/src/macos/stage.macos.ts +182 -0
- package/src/mobile/controller.ts +134 -0
- package/src/mobile/entry.mobile.ts +607 -0
- package/src/mobile/index.mobile.ts +3 -0
- package/src/mobile/mount.mobile.ts +60 -0
- package/src/mobile/tree.mobile.tsrx +17 -0
- package/src/session.ts +520 -0
- package/src/web/entry.web.ts +270 -0
- package/src/web/index.web.ts +7 -0
- package/src/web/mount.web.ts +86 -0
- package/src/web/shell.css +449 -0
- package/src/web/shell.web.tsrx +759 -0
- package/types/generated/.tsrx-typegen-manifest.json +24 -0
- package/types/generated/catalog.d.ts +30 -0
- package/types/generated/contract.d.ts +145 -0
- package/types/generated/controls.d.ts +28 -0
- package/types/generated/errors.d.ts +13 -0
- package/types/generated/fixture.d.ts +53 -0
- package/types/generated/index.d.ts +6 -0
- package/types/generated/macos/controller.d.ts +52 -0
- package/types/generated/macos/index.macos.d.ts +2 -0
- package/types/generated/macos/stage.macos.d.ts +10 -0
- package/types/generated/mobile/controller.d.ts +31 -0
- package/types/generated/mobile/entry.mobile.d.ts +22 -0
- package/types/generated/mobile/index.mobile.d.ts +3 -0
- package/types/generated/mobile/mount.mobile.d.ts +4 -0
- package/types/generated/mobile/tree.mobile.d.ts +4 -0
- package/types/generated/session.d.ts +84 -0
- package/types/generated/web/entry.web.d.ts +53 -0
- package/types/generated/web/index.web.d.ts +7 -0
- package/types/generated/web/mount.web.d.ts +2 -0
- package/types/generated/web/shell.web.d.ts +4 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
// @ts-nocheck — .mjs→.ts port: annotations pending (transitional)
|
|
2
|
+
/**
|
|
3
|
+
* xplatWorkshop — the workshop dev-server plugin.
|
|
4
|
+
*
|
|
5
|
+
* Emits a virtual catalog module whose metadata comes from static discovery
|
|
6
|
+
* (never module evaluation), a virtual entry that boots the adaptive shell,
|
|
7
|
+
* and a virtual index.html. Story modules stay lazily imported per selection;
|
|
8
|
+
* loaders exist only for stories applicable to the build target, so a
|
|
9
|
+
* platform-specific leaf is never imported into an incompatible bundle.
|
|
10
|
+
*/
|
|
11
|
+
import { existsSync, readFileSync, statSync } from 'node:fs'
|
|
12
|
+
import { dirname, join, resolve, sep } from 'node:path'
|
|
13
|
+
import { fileURLToPath } from 'node:url'
|
|
14
|
+
import { discoverStories, normalizeInclude } from './discover.ts'
|
|
15
|
+
|
|
16
|
+
const CATALOG_ID = '\0xplat-workshop-catalog'
|
|
17
|
+
const ENTRY_ID = '\0xplat-workshop-entry'
|
|
18
|
+
const STORY_FILE_RE =
|
|
19
|
+
/\.stories(?:\.(?:web|mobile|ios|android|macos|windows|linux))?\.(?:[cm]?[jt]s|[jt]sx|[jt]srx)$/
|
|
20
|
+
|
|
21
|
+
const WEB_EXTENSIONS = [
|
|
22
|
+
'.web.tsrx',
|
|
23
|
+
'.tsrx',
|
|
24
|
+
'.web.tsx',
|
|
25
|
+
'.tsx',
|
|
26
|
+
'.web.ts',
|
|
27
|
+
'.ts',
|
|
28
|
+
'.mts',
|
|
29
|
+
'.ts',
|
|
30
|
+
'.jsx',
|
|
31
|
+
'.js',
|
|
32
|
+
'.json',
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
// Per-target resolution for DOM-rendering hosts. Linux renders the same DOM
|
|
36
|
+
// inside the system webview, so its chain mirrors the consumer Linux preset
|
|
37
|
+
// (apps/linux/vite.linux.config.ts): .linux → .web → shared, conditions
|
|
38
|
+
// 'linux' then 'web'.
|
|
39
|
+
const TARGET_RESOLUTION = {
|
|
40
|
+
web: { conditions: ['web'], extensions: WEB_EXTENSIONS },
|
|
41
|
+
linux: {
|
|
42
|
+
conditions: ['linux', 'web'],
|
|
43
|
+
extensions: [
|
|
44
|
+
'.linux.tsrx',
|
|
45
|
+
'.web.tsrx',
|
|
46
|
+
'.tsrx',
|
|
47
|
+
'.linux.tsx',
|
|
48
|
+
'.web.tsx',
|
|
49
|
+
'.tsx',
|
|
50
|
+
'.linux.ts',
|
|
51
|
+
'.web.ts',
|
|
52
|
+
'.ts',
|
|
53
|
+
'.mts',
|
|
54
|
+
'.ts',
|
|
55
|
+
'.jsx',
|
|
56
|
+
'.js',
|
|
57
|
+
'.json',
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const fsUrl = (file) => `/@fs/${file.split(sep).join('/')}`
|
|
63
|
+
|
|
64
|
+
function catalogCode(root, config, target) {
|
|
65
|
+
const storiesConfig = config?.stories ?? {}
|
|
66
|
+
const { entries, diagnostics } = discoverStories(root, {
|
|
67
|
+
include: storiesConfig.include,
|
|
68
|
+
exclude: storiesConfig.exclude,
|
|
69
|
+
target,
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
const include = JSON.stringify(normalizeInclude(storiesConfig.include))
|
|
73
|
+
const loaders = entries
|
|
74
|
+
.filter((entry) => entry.applicable && diagnostics.length === 0)
|
|
75
|
+
.map(
|
|
76
|
+
(entry) => `\t${JSON.stringify(entry.id)}: (stamp) => stamp === undefined
|
|
77
|
+
\t\t? import(${JSON.stringify(fsUrl(entry.file))})
|
|
78
|
+
\t\t: import(/* @vite-ignore */ ${JSON.stringify(fsUrl(entry.file))} + '?t=' + encodeURIComponent(String(stamp))),`,
|
|
79
|
+
)
|
|
80
|
+
.join('\n')
|
|
81
|
+
|
|
82
|
+
return `import { catalogDidUpdate } from '@octane-xplat/workshop/web'
|
|
83
|
+
|
|
84
|
+
export const catalogMeta = ${JSON.stringify(entries)}
|
|
85
|
+
export const diagnostics = ${JSON.stringify(diagnostics)}
|
|
86
|
+
export const includePatterns = ${include}
|
|
87
|
+
|
|
88
|
+
const loaders = {
|
|
89
|
+
${loaders}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export const catalog = catalogMeta.map((entry) =>
|
|
93
|
+
loaders[entry.id] ? { ...entry, load: loaders[entry.id] } : entry,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
export function loadStory(id, stamp) {
|
|
97
|
+
const entry = catalog.find((item) => item.id === id)
|
|
98
|
+
if (!entry?.load) {
|
|
99
|
+
return Promise.reject(
|
|
100
|
+
new Error(
|
|
101
|
+
entry
|
|
102
|
+
? 'story "' + id + '" is not supported on this target'
|
|
103
|
+
: 'story "' + id + '" is not in the catalog',
|
|
104
|
+
),
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
return entry.load(stamp)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
catalogDidUpdate(catalog, diagnostics)
|
|
111
|
+
if (import.meta.hot) {
|
|
112
|
+
import.meta.hot.accept((next) => catalogDidUpdate(next?.catalog ?? catalog, next?.diagnostics ?? diagnostics))
|
|
113
|
+
}
|
|
114
|
+
`
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function entryCode(config, target, story, packageDir, root) {
|
|
118
|
+
const providerModules = config?.providerModules ?? []
|
|
119
|
+
const imports = providerModules
|
|
120
|
+
.map((specifier, index) => {
|
|
121
|
+
const abs = resolve(root, specifier)
|
|
122
|
+
return `import * as __workshopPreset${index} from ${JSON.stringify(fsUrl(abs))}`
|
|
123
|
+
})
|
|
124
|
+
.join('\n')
|
|
125
|
+
|
|
126
|
+
const providers = providerModules
|
|
127
|
+
.map(
|
|
128
|
+
(_, index) =>
|
|
129
|
+
`...(__workshopPreset${index}.providers ?? __workshopPreset${index}.default ?? [])`,
|
|
130
|
+
)
|
|
131
|
+
.join(', ')
|
|
132
|
+
|
|
133
|
+
const shellCss = fsUrl(join(packageDir, 'web', 'shell.css'))
|
|
134
|
+
|
|
135
|
+
return `import { catalog, loadStory, diagnostics } from ${JSON.stringify(CATALOG_ID)}
|
|
136
|
+
import { startWorkshop } from '@octane-xplat/workshop/web'
|
|
137
|
+
import '@octane-xplat/ui/theme/tokens.css'
|
|
138
|
+
import ${JSON.stringify(shellCss)}
|
|
139
|
+
${imports}
|
|
140
|
+
|
|
141
|
+
startWorkshop({
|
|
142
|
+
catalog,
|
|
143
|
+
diagnostics,
|
|
144
|
+
loadStory,
|
|
145
|
+
target: ${JSON.stringify(target)},
|
|
146
|
+
initialStoryId: ${JSON.stringify(story ?? null)},
|
|
147
|
+
globalProviders: [${providers}],
|
|
148
|
+
hot: import.meta.hot,
|
|
149
|
+
})
|
|
150
|
+
`
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const WORKSHOP_HTML = `<!doctype html>
|
|
154
|
+
<html lang="en">
|
|
155
|
+
<head>
|
|
156
|
+
<meta charset="utf-8" />
|
|
157
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
158
|
+
<title>octane-xplat workshop</title>
|
|
159
|
+
<style>html,body{margin:0;height:100%}#workshop-root{height:100%}</style>
|
|
160
|
+
</head>
|
|
161
|
+
<body>
|
|
162
|
+
<div id="workshop-root"></div>
|
|
163
|
+
<script type="module" src="/@id/__x00__xplat-workshop-entry"></script>
|
|
164
|
+
</body>
|
|
165
|
+
</html>
|
|
166
|
+
`
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* @param {object} [options]
|
|
170
|
+
* @param {string} options.cwd - consumer project root
|
|
171
|
+
* @param {object} [options.config] - resolved workshop.config.* contents
|
|
172
|
+
* @param {string} [options.target] - launch target (DOM hosts: 'web' | 'linux')
|
|
173
|
+
* @param {string} [options.story] - initial stable story id
|
|
174
|
+
*/
|
|
175
|
+
export function xplatWorkshop(options = {}) {
|
|
176
|
+
const target = options.target ?? 'web'
|
|
177
|
+
const story = options.story ?? null
|
|
178
|
+
const resolution = TARGET_RESOLUTION[target] ?? TARGET_RESOLUTION.web
|
|
179
|
+
let root = options.cwd ?? process.cwd()
|
|
180
|
+
let config = options.config ?? {}
|
|
181
|
+
|
|
182
|
+
const virtualModules = {
|
|
183
|
+
name: 'xplat-workshop-virtual',
|
|
184
|
+
configResolved(resolved) {
|
|
185
|
+
root = options.cwd ?? resolved.root
|
|
186
|
+
},
|
|
187
|
+
resolveId(id) {
|
|
188
|
+
if (id === CATALOG_ID || id === ENTRY_ID) {
|
|
189
|
+
return id
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return null
|
|
193
|
+
},
|
|
194
|
+
load(id) {
|
|
195
|
+
if (id === CATALOG_ID) {
|
|
196
|
+
return catalogCode(root, config, target)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (id === ENTRY_ID) {
|
|
200
|
+
return entryCode(config, target, story, dirname(fileURLToPath(import.meta.url)), root)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return null
|
|
204
|
+
},
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const shell = {
|
|
208
|
+
name: 'xplat-workshop-shell',
|
|
209
|
+
config() {
|
|
210
|
+
return {
|
|
211
|
+
optimizeDeps: {
|
|
212
|
+
exclude: ['@octane-xplat/workshop'],
|
|
213
|
+
// The packed workshop is excluded from compilation, so explicitly
|
|
214
|
+
// optimize its portable composition dependency and CJS transitive graph.
|
|
215
|
+
include: ['@octane-xplat/workshop > storybook/preview-api'],
|
|
216
|
+
},
|
|
217
|
+
resolve: {
|
|
218
|
+
conditions: [...resolution.conditions],
|
|
219
|
+
},
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
configResolved(resolved) {
|
|
223
|
+
// The octane plugin's extension list merges first, which would let the
|
|
224
|
+
// unsuffixed .tsrx leaf shadow the target's leaf. Environments carry
|
|
225
|
+
// their own resolved lists — rewrite every one.
|
|
226
|
+
const apply = (scope) => {
|
|
227
|
+
scope.resolve.extensions = [
|
|
228
|
+
...resolution.extensions,
|
|
229
|
+
...scope.resolve.extensions.filter((ext) => !resolution.extensions.includes(ext)),
|
|
230
|
+
]
|
|
231
|
+
|
|
232
|
+
scope.resolve.conditions = [
|
|
233
|
+
...resolution.conditions,
|
|
234
|
+
...scope.resolve.conditions.filter((cond) => !resolution.conditions.includes(cond)),
|
|
235
|
+
]
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
apply(resolved)
|
|
239
|
+
for (const env of Object.values(resolved.environments ?? {})) {
|
|
240
|
+
apply(env)
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
hotUpdate: {
|
|
244
|
+
// 'pre' plus registration ahead of the octane plugin: octane's own
|
|
245
|
+
// hotUpdate full-reloads every octane file whose modules are not all
|
|
246
|
+
// self-accepting, which would reset the workshop page on every story
|
|
247
|
+
// save. Answering with the accepting catalog alone makes octane's
|
|
248
|
+
// every-self-accepting early return match, while the catalog update
|
|
249
|
+
// re-discovers metadata and remounts the story through a fresh
|
|
250
|
+
// stamped loader import.
|
|
251
|
+
order: 'pre',
|
|
252
|
+
handler(ctx) {
|
|
253
|
+
if (this.environment.name !== 'client' || !STORY_FILE_RE.test(ctx.file)) {
|
|
254
|
+
return
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const catalog = this.environment.moduleGraph.getModuleById(CATALOG_ID)
|
|
258
|
+
if (!catalog) {
|
|
259
|
+
return
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
this.environment.moduleGraph.invalidateModule(catalog)
|
|
263
|
+
// The environment-aware hook includes create/delete. Stamped story
|
|
264
|
+
// nodes carry no graph importer edge (@vite-ignore loader import),
|
|
265
|
+
// so seeding them would dead-end propagation into a full reload.
|
|
266
|
+
return [catalog]
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
configureServer(s) {
|
|
270
|
+
// The workshop page is a separate dev entry — intercept '/' before
|
|
271
|
+
// vite's own index fallback can serve the consumer app's document.
|
|
272
|
+
s.middlewares.use((req, res, next) => {
|
|
273
|
+
const url = (req.url ?? '').split('?')[0]
|
|
274
|
+
if (url === '/' || url === '/index.html') {
|
|
275
|
+
res.statusCode = 200
|
|
276
|
+
res.setHeader('content-type', 'text/html; charset=utf-8')
|
|
277
|
+
res.end(WORKSHOP_HTML)
|
|
278
|
+
return
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (url.startsWith('/@workshop-assets/')) {
|
|
282
|
+
return serveAsset(config, root, url, res, next)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
next()
|
|
286
|
+
})
|
|
287
|
+
},
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return [virtualModules, shell]
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function serveAsset(config, root, url, res, next) {
|
|
294
|
+
const assets = config?.assets ?? {}
|
|
295
|
+
const rest = url.slice('/@workshop-assets/'.length)
|
|
296
|
+
const name = rest.split('/')[0]
|
|
297
|
+
const dir = assets[name] ? resolve(root, assets[name]) : null
|
|
298
|
+
if (!dir) {
|
|
299
|
+
next()
|
|
300
|
+
return
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const file = resolve(dir, rest.slice(name.length + 1))
|
|
304
|
+
if (
|
|
305
|
+
!(file === dir || file.startsWith(dir + sep)) ||
|
|
306
|
+
!existsSync(file) ||
|
|
307
|
+
!statSync(file).isFile()
|
|
308
|
+
) {
|
|
309
|
+
res.statusCode = 404
|
|
310
|
+
res.end('not found')
|
|
311
|
+
return
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
res.statusCode = 200
|
|
315
|
+
res.end(readFileSync(file))
|
|
316
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/** Read-only Windows setup evidence. Prerequisites never qualify a host. */
|
|
2
|
+
import { execFileSync } from 'node:child_process'
|
|
3
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
4
|
+
import { createRequire } from 'node:module'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
|
|
7
|
+
function commandOutput(command, args) {
|
|
8
|
+
try {
|
|
9
|
+
return execFileSync(command, args, {
|
|
10
|
+
encoding: 'utf8',
|
|
11
|
+
timeout: 5000,
|
|
12
|
+
maxBuffer: 65536,
|
|
13
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
14
|
+
windowsHide: true,
|
|
15
|
+
}).trim()
|
|
16
|
+
} catch {
|
|
17
|
+
return null
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function windowsPreflight(root, { platform = process.platform, run = commandOutput } = {}) {
|
|
22
|
+
const manifest = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))
|
|
23
|
+
const require = createRequire(join(root, 'package.json'))
|
|
24
|
+
const installed = (name) => {
|
|
25
|
+
for (const specifier of [`${name}/package.json`, name]) {
|
|
26
|
+
try {
|
|
27
|
+
require.resolve(specifier)
|
|
28
|
+
return true
|
|
29
|
+
} catch {
|
|
30
|
+
/* Try the public entry. */
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return false
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const runtime =
|
|
38
|
+
manifest.devDependencies?.['@nativescript/windows'] ??
|
|
39
|
+
manifest.dependencies?.['@nativescript/windows']
|
|
40
|
+
|
|
41
|
+
const requirements = {
|
|
42
|
+
'Windows host': platform === 'win32',
|
|
43
|
+
'nativescript.config': ['ts', 'mts', 'js', 'mjs'].some((ext) =>
|
|
44
|
+
existsSync(join(root, `nativescript.config.${ext}`)),
|
|
45
|
+
),
|
|
46
|
+
'App_Resources/Windows/Package.appxmanifest': existsSync(
|
|
47
|
+
join(root, 'App_Resources/Windows/Package.appxmanifest'),
|
|
48
|
+
),
|
|
49
|
+
'@nativescript/windows exact version':
|
|
50
|
+
typeof runtime === 'string' && /^\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?$/.test(runtime),
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
for (const name of [
|
|
54
|
+
'@nativescript/windows',
|
|
55
|
+
'@nativescript/core',
|
|
56
|
+
'@nativescript-community/octane',
|
|
57
|
+
'@nativescript/vite',
|
|
58
|
+
'nativescript',
|
|
59
|
+
'vite',
|
|
60
|
+
]) {
|
|
61
|
+
requirements[name] = installed(name)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Never run Windows commands on another host, boot a device, or change settings.
|
|
65
|
+
if (platform === 'win32') {
|
|
66
|
+
const sdk = run('dotnet', ['--version'])
|
|
67
|
+
requirements['.NET SDK >= 10'] =
|
|
68
|
+
typeof sdk === 'string' && /^\d+\.\d+\.\d+/.test(sdk) && Number(sdk.split('.')[0]) >= 10
|
|
69
|
+
|
|
70
|
+
const devMode = run('powershell', [
|
|
71
|
+
'-NoProfile',
|
|
72
|
+
'-NonInteractive',
|
|
73
|
+
'-Command',
|
|
74
|
+
'(Get-ItemProperty "HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\AppModelUnlock" -Name AllowDevelopmentWithoutDevLicense -ErrorAction SilentlyContinue).AllowDevelopmentWithoutDevLicense',
|
|
75
|
+
])
|
|
76
|
+
|
|
77
|
+
requirements['Developer Mode (development launch)'] = devMode?.trim() === '1'
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
backend: 'experimental nativescript/winui',
|
|
82
|
+
executionHost: { required: 'win32', current: platform, reachable: platform === 'win32' },
|
|
83
|
+
qualification: 'unverified',
|
|
84
|
+
requirements,
|
|
85
|
+
status:
|
|
86
|
+
platform !== 'win32'
|
|
87
|
+
? 'remote-host-required'
|
|
88
|
+
: Object.values(requirements).every(Boolean)
|
|
89
|
+
? 'unqualified-backend'
|
|
90
|
+
: 'missing-prerequisites',
|
|
91
|
+
note:
|
|
92
|
+
'Windows workshop mounting, shell and reload are not implemented or host-qualified. ' +
|
|
93
|
+
(platform !== 'win32'
|
|
94
|
+
? 'Run these commands in a Windows checkout; SDK and Developer Mode checks were not run. '
|
|
95
|
+
: '') +
|
|
96
|
+
'Prerequisites do not prove renderer compatibility, input, accessibility or lifecycle support. No web fallback is used.',
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function windowsLaunchBlocker(report) {
|
|
101
|
+
if (report.status === 'remote-host-required') {
|
|
102
|
+
return 'Windows workshop requires a real Windows host. Run `xplat workshop doctor` in that host’s checkout. No web fallback is used.'
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const missing = Object.entries(report.requirements)
|
|
106
|
+
.filter(([, ok]) => !ok)
|
|
107
|
+
.map(([name]) => name)
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
(missing.length ? `Windows prerequisites missing: ${missing.join(', ')}. ` : '') +
|
|
111
|
+
'Windows workshop mounting, shell and reload remain unimplemented and unqualified; launch is blocked. Run `xplat workshop doctor` for setup evidence.'
|
|
112
|
+
)
|
|
113
|
+
}
|
package/src/catalog.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catalog model shared by Node-side discovery, the catalog virtual module,
|
|
3
|
+
* and the shell. Metadata is literal and statically readable — discovery
|
|
4
|
+
* never evaluates story modules.
|
|
5
|
+
*/
|
|
6
|
+
import type { WorkshopTarget } from './contract'
|
|
7
|
+
|
|
8
|
+
export interface StoryMeta {
|
|
9
|
+
id: string
|
|
10
|
+
title: string
|
|
11
|
+
/** Effective targets after declared targets ∩ file-suffix applicability. */
|
|
12
|
+
targets: readonly WorkshopTarget[]
|
|
13
|
+
capabilities: readonly string[]
|
|
14
|
+
/** Absolute file path of the story module. */
|
|
15
|
+
file: string
|
|
16
|
+
/** Export binding the definition is attached to. */
|
|
17
|
+
exportName: string
|
|
18
|
+
/** Whether this story can mount on the current build target. */
|
|
19
|
+
applicable: boolean
|
|
20
|
+
/** Why the story cannot mount here (unsupported-by-story / leaf mismatch). */
|
|
21
|
+
reason?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface CatalogEntry extends StoryMeta {
|
|
25
|
+
/** Present only when applicable: loads the story definition. */
|
|
26
|
+
load?: (stamp?: string | number) => Promise<unknown>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Platform suffix on a story file narrows applicability to those targets. */
|
|
30
|
+
export const SUFFIX_TARGETS: Record<string, readonly WorkshopTarget[]> = {
|
|
31
|
+
web: ['web'],
|
|
32
|
+
mobile: ['ios', 'android'],
|
|
33
|
+
ios: ['ios'],
|
|
34
|
+
android: ['android'],
|
|
35
|
+
macos: ['macos'],
|
|
36
|
+
windows: ['windows'],
|
|
37
|
+
linux: ['linux'],
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const PLATFORM_SUFFIX_RE =
|
|
41
|
+
/\.stories\.(web|mobile|ios|android|macos|windows|linux)\.[cm]?[jt]srx?$/
|
|
42
|
+
|
|
43
|
+
export function storyFileTag(file: string): string | null {
|
|
44
|
+
return PLATFORM_SUFFIX_RE.exec(file)?.[1] ?? null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function applicableTargets(
|
|
48
|
+
declared: readonly WorkshopTarget[],
|
|
49
|
+
file: string,
|
|
50
|
+
): readonly WorkshopTarget[] {
|
|
51
|
+
const tag = storyFileTag(file)
|
|
52
|
+
if (!tag) {
|
|
53
|
+
return declared
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const implied = SUFFIX_TARGETS[tag] ?? []
|
|
57
|
+
return declared.filter((t) => implied.includes(t))
|
|
58
|
+
}
|