@orbit-work/oem-dev-host 0.1.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/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@orbit-work/oem-dev-host",
3
+ "version": "0.1.0",
4
+ "description": "Live OEM bridge for full Desktop, isolated-profile sandbox, and Slot workbench modes.",
5
+ "type": "module",
6
+ "publishConfig": {
7
+ "access": "public",
8
+ "registry": "https://registry.npmjs.org/"
9
+ },
10
+ "bin": {
11
+ "orbit-oem-dev-host": "./src/cli.mjs"
12
+ },
13
+ "exports": {
14
+ ".": "./src/compiler.mjs"
15
+ },
16
+ "files": [
17
+ "src/cli.mjs",
18
+ "src/compiler.mjs",
19
+ "src/server.mjs"
20
+ ],
21
+ "dependencies": {
22
+ "@orbit-work/oem-sdk": "0.1.0",
23
+ "@orbit-work/ui": "0.1.2",
24
+ "@vitejs/plugin-react": "^4.3.4",
25
+ "react": "^19.0.0",
26
+ "react-dom": "^19.0.0",
27
+ "typescript": "^5.6.3",
28
+ "vite": "^6.0.0"
29
+ },
30
+ "engines": {
31
+ "node": ">=20.0.0"
32
+ },
33
+ "scripts": {
34
+ "test": "node --test src/*.test.mjs"
35
+ }
36
+ }
package/src/cli.mjs ADDED
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { realpathSync } from "node:fs"
4
+ import { access } from "node:fs/promises"
5
+ import path from "node:path"
6
+ import { fileURLToPath } from "node:url"
7
+ import { startDevelopmentServer } from "./server.mjs"
8
+
9
+ export function parseArgs(argv, env = process.env) {
10
+ const result = { project: process.cwd(), host: env.ORBIT_OEM_DEV_HOST_EXECUTABLE, mode: "full" }
11
+ let selectedMode
12
+ for (let index = 0; index < argv.length; index += 1) {
13
+ const arg = argv[index]
14
+ if (arg === "--help" || arg === "-h") return { help: true }
15
+ if (arg === "--full" || arg === "--sandbox" || arg === "--workbench") {
16
+ const mode = arg.slice(2)
17
+ if (selectedMode && selectedMode !== mode) throw new Error("only one Dev Host mode can be selected")
18
+ selectedMode = mode
19
+ result.mode = mode
20
+ continue
21
+ }
22
+ if (arg !== "--project" && arg !== "--host") throw new Error(`unknown option: ${arg}`)
23
+ const value = argv[(index += 1)]
24
+ if (!value) throw new Error(`${arg} requires a value`)
25
+ result[arg.slice(2)] = value
26
+ }
27
+ return result
28
+ }
29
+
30
+ export async function main(argv = process.argv.slice(2)) {
31
+ const args = parseArgs(argv)
32
+ if (args.help) {
33
+ console.log(
34
+ "Usage: orbit-oem-dev-host [--project <directory>] [--host <compiled-dev-host-executable>] [--full|--sandbox|--workbench]",
35
+ )
36
+ console.log(" --full Launch the complete Orbitwork application with live OEM composition (default)")
37
+ console.log(" --sandbox Launch the complete Desktop and Orbit APIs in an isolated local profile")
38
+ console.log(" --workbench Launch the lightweight Slot contract workbench")
39
+ return
40
+ }
41
+ if (!args.host) {
42
+ throw new Error("Install the compiled Orbitwork OEM Dev Host, then set ORBIT_OEM_DEV_HOST_EXECUTABLE or pass --host")
43
+ }
44
+ const executable = path.resolve(args.host)
45
+ await access(executable)
46
+ const server = await startDevelopmentServer(path.resolve(args.project), { launch: executable, mode: args.mode })
47
+ console.log(
48
+ `[oem-dev-host] ${server.mode} mode connected at ${server.origin}; successful saves reload Desktop, failed builds keep the current renderer`,
49
+ )
50
+ const stop = async () => {
51
+ await server.close()
52
+ process.exit(0)
53
+ }
54
+ process.once("SIGINT", stop)
55
+ process.once("SIGTERM", stop)
56
+ server.child?.once("exit", (code) => {
57
+ void server.close().finally(() => process.exit(code ?? 0))
58
+ })
59
+ }
60
+
61
+ if (process.argv[1] && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))) {
62
+ main().catch((error) => {
63
+ console.error(error.stack ?? error.message)
64
+ process.exitCode = 1
65
+ })
66
+ }
@@ -0,0 +1,178 @@
1
+ import { readFile, realpath } from "node:fs/promises"
2
+ import path from "node:path"
3
+ import { auditBrandPackage } from "@orbit-work/oem-sdk/audit"
4
+ import react from "@vitejs/plugin-react"
5
+ import { OEM_VITE_CONFIG } from "@orbit-work/oem-sdk/vite"
6
+ import { build } from "vite"
7
+ import ts from "typescript"
8
+
9
+ const EXTERNALS = new Set([
10
+ "react",
11
+ "react-dom",
12
+ "react-dom/client",
13
+ "react/jsx-runtime",
14
+ "@orbit-work/ui",
15
+ "@orbit-work/ui/appearance",
16
+ "@orbit-work/ui/slot/core",
17
+ "@orbit-work/ui/slot/react",
18
+ "@orbit-work/oem-sdk",
19
+ "@orbit-work/oem-sdk/runtime",
20
+ ])
21
+ function exactDevOrigin(value, label) {
22
+ let url
23
+ try {
24
+ url = new URL(value)
25
+ } catch {
26
+ throw new Error(`${label} must be an exact HTTP(S) origin`)
27
+ }
28
+ if (!new Set(["http:", "https:"]).has(url.protocol) || url.username || url.password || url.pathname !== "/" || url.search || url.hash) {
29
+ throw new Error(`${label} must be an exact HTTP(S) origin without credentials, path, query, or fragment`)
30
+ }
31
+ if (url.protocol === "http:" && !new Set(["localhost", "127.0.0.1", "::1"]).has(url.hostname)) {
32
+ throw new Error(`${label} may use HTTP only for loopback`)
33
+ }
34
+ return url.origin
35
+ }
36
+
37
+ export function parseDevelopmentConfig(value) {
38
+ if (value === undefined) return { network: { allowedOrigins: [] } }
39
+ if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some((key) => key !== "network")) {
40
+ throw new Error("orbit-dev.local.json supports only the network object")
41
+ }
42
+ if (!value.network || typeof value.network !== "object" || Array.isArray(value.network)) {
43
+ throw new Error("orbit-dev.local.json network must be an object")
44
+ }
45
+ if (Object.keys(value.network).some((key) => key !== "allowedOrigins") || !Array.isArray(value.network.allowedOrigins)) {
46
+ throw new Error("orbit-dev.local.json network.allowedOrigins must be an array")
47
+ }
48
+ const allowedOrigins = value.network.allowedOrigins.map((origin, index) => exactDevOrigin(origin, `network.allowedOrigins[${index}]`))
49
+ if (new Set(allowedOrigins).size !== allowedOrigins.length) throw new Error("development origins must not contain duplicates")
50
+ return { network: { allowedOrigins } }
51
+ }
52
+
53
+ async function readJsonIfPresent(file) {
54
+ try {
55
+ return JSON.parse(await readFile(file, "utf8"))
56
+ } catch (error) {
57
+ if (error?.code === "ENOENT") return undefined
58
+ throw error
59
+ }
60
+ }
61
+
62
+ function moduleShim(specifier, namespace) {
63
+ const names = Object.keys(namespace).filter((name) => name !== "default" && /^[A-Za-z_$][\w$]*$/.test(name))
64
+ const lines = [`const namespace = globalThis.__ORBIT_OEM_DEV_EXTERNALS__[${JSON.stringify(specifier)}];`]
65
+ if ("default" in namespace) lines.push("export default namespace.default;")
66
+ for (const name of names) lines.push(`export const ${name} = namespace[${JSON.stringify(name)}];`)
67
+ return lines.join("\n")
68
+ }
69
+
70
+ async function externalPlugin() {
71
+ const shims = new Map()
72
+ for (const specifier of EXTERNALS) {
73
+ const namespace = await import(specifier)
74
+ shims.set(specifier, moduleShim(specifier, namespace))
75
+ }
76
+ return {
77
+ name: "orbit-oem-dev-host-singletons",
78
+ enforce: "pre",
79
+ resolveId(id) {
80
+ return EXTERNALS.has(id) ? `\0orbit-host:${id}` : null
81
+ },
82
+ load(id) {
83
+ return id.startsWith("\0orbit-host:") ? shims.get(id.slice("\0orbit-host:".length)) : null
84
+ },
85
+ }
86
+ }
87
+
88
+ function dataUrl(file, bytes) {
89
+ const extension = path.extname(file).toLowerCase()
90
+ const mime = extension === ".svg" ? "image/svg+xml" : extension === ".webp" ? "image/webp" : "image/png"
91
+ return `data:${mime};base64,${bytes.toString("base64")}`
92
+ }
93
+
94
+ export function immutableDevelopmentIdentity(manifest) {
95
+ return JSON.stringify({ identity: manifest.identity, appIcon: manifest.assets.appIcon, tray: manifest.assets.tray ?? null })
96
+ }
97
+
98
+ function typecheckProject(root) {
99
+ const configFile = ts.findConfigFile(root, ts.sys.fileExists, "tsconfig.json")
100
+ if (!configFile) throw new Error("OEM Dev Host requires a tsconfig.json")
101
+ const loaded = ts.readConfigFile(configFile, ts.sys.readFile)
102
+ if (loaded.error) throw new Error(ts.flattenDiagnosticMessageText(loaded.error.messageText, "\n"))
103
+ const parsed = ts.parseJsonConfigFileContent(loaded.config, ts.sys, root)
104
+ const program = ts.createProgram(parsed.fileNames, { ...parsed.options, noEmit: true })
105
+ const diagnostic = ts.getPreEmitDiagnostics(program)[0]
106
+ if (!diagnostic) return
107
+ const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n")
108
+ if (!diagnostic.file || diagnostic.start === undefined) throw new Error(message)
109
+ const position = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start)
110
+ const error = new Error(`${diagnostic.file.fileName}:${position.line + 1}:${position.character + 1}: ${message}`)
111
+ error.name = "OemTypecheckError"
112
+ throw error
113
+ }
114
+
115
+ export async function compileOemDevelopmentProject(projectRoot, { expectedIdentity } = {}) {
116
+ const root = await realpath(path.resolve(projectRoot))
117
+ const manifestFile = path.join(root, "orbit-brand.json")
118
+ const development = parseDevelopmentConfig(await readJsonIfPresent(path.join(root, "orbit-dev.local.json")))
119
+ const audit = await auditBrandPackage(manifestFile, { developmentAllowedOrigins: development.network.allowedOrigins })
120
+ typecheckProject(root)
121
+ const identity = immutableDevelopmentIdentity(audit.manifest)
122
+ if (expectedIdentity && identity !== expectedIdentity) {
123
+ throw new Error("Dev Host identity, app icon, or tray changed; rebuild and reinstall the dedicated Dev Host")
124
+ }
125
+ const locales = Object.fromEntries(
126
+ await Promise.all(
127
+ Object.entries(audit.manifest.locales ?? {}).map(async ([locale, relative]) => [
128
+ locale,
129
+ JSON.parse(await readFile(path.resolve(root, relative), "utf8")),
130
+ ]),
131
+ ),
132
+ )
133
+ const logoFile = path.resolve(root, audit.manifest.assets.logo)
134
+ const logo = dataUrl(logoFile, await readFile(logoFile))
135
+ let javascript = ""
136
+ let css = ""
137
+ if (audit.manifest.entry) {
138
+ const result = await build({
139
+ configFile: false,
140
+ root,
141
+ plugins: [await externalPlugin(), react()],
142
+ resolve: { dedupe: [...OEM_VITE_CONFIG.resolve.dedupe] },
143
+ css: { modules: { ...OEM_VITE_CONFIG.css.modules } },
144
+ build: {
145
+ ...OEM_VITE_CONFIG.build,
146
+ write: false,
147
+ minify: false,
148
+ sourcemap: "inline",
149
+ lib: {
150
+ entry: path.resolve(root, audit.manifest.entry),
151
+ name: "OrbitOemDevelopmentEntry",
152
+ formats: ["iife"],
153
+ fileName: () => "bundle.js",
154
+ },
155
+ rollupOptions: {
156
+ output: { footer: "globalThis.__ORBIT_OEM_DEV_ACCEPT__?.(OrbitOemDevelopmentEntry.default);" },
157
+ },
158
+ },
159
+ })
160
+ const output = Array.isArray(result) ? result.flatMap((item) => item.output) : result.output
161
+ javascript = output.find((item) => item.type === "chunk" && item.fileName.endsWith(".js"))?.code ?? ""
162
+ const cssAsset = output.find((item) => item.type === "asset" && item.fileName.endsWith(".css"))
163
+ css = cssAsset ? String(cssAsset.source) : ""
164
+ }
165
+ return {
166
+ identity,
167
+ snapshot: { manifest: audit.manifest, locales, logo, development },
168
+ javascript,
169
+ css,
170
+ }
171
+ }
172
+
173
+ export function formatBuildError(error) {
174
+ const diagnostic = error?.errors?.[0]
175
+ if (!diagnostic) return error instanceof Error ? error.message : String(error)
176
+ const location = diagnostic.location
177
+ return `${location?.file ?? "OEM build"}${location ? `:${location.line}:${location.column}` : ""}: ${diagnostic.text}`
178
+ }
package/src/server.mjs ADDED
@@ -0,0 +1,176 @@
1
+ import { spawn } from "node:child_process"
2
+ import { createHash } from "node:crypto"
3
+ import { watch } from "node:fs"
4
+ import http from "node:http"
5
+ import path from "node:path"
6
+ import { compileOemDevelopmentProject, formatBuildError } from "./compiler.mjs"
7
+
8
+ export const OEM_DEV_HOST = "127.0.0.1"
9
+ export const OEM_DEV_HOST_PORT = 43127
10
+ export const OEM_DEV_HOST_RENDERER_ORIGIN = "app://orbit"
11
+
12
+ function json(response, status, value) {
13
+ response.writeHead(status, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" })
14
+ response.end(JSON.stringify(value))
15
+ }
16
+
17
+ const DEV_HOST_MODES = new Set(["full", "sandbox", "workbench"])
18
+ const WEBSOCKET_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
19
+
20
+ async function startFixtureSidecarStub() {
21
+ const sockets = new Set()
22
+ const server = http.createServer()
23
+ server.on("upgrade", (request, socket) => {
24
+ const key = String(request.headers["sec-websocket-key"] ?? "")
25
+ if (!key) {
26
+ socket.destroy()
27
+ return
28
+ }
29
+ const accept = createHash("sha1").update(`${key}${WEBSOCKET_GUID}`).digest("base64")
30
+ socket.write(
31
+ [
32
+ "HTTP/1.1 101 Switching Protocols",
33
+ "Upgrade: websocket",
34
+ "Connection: Upgrade",
35
+ `Sec-WebSocket-Accept: ${accept}`,
36
+ "",
37
+ "",
38
+ ].join("\r\n"),
39
+ )
40
+ sockets.add(socket)
41
+ socket.on("close", () => sockets.delete(socket))
42
+ socket.on("error", () => sockets.delete(socket))
43
+ })
44
+ await new Promise((resolve, reject) => {
45
+ server.once("error", reject)
46
+ server.listen(0, OEM_DEV_HOST, resolve)
47
+ })
48
+ const address = server.address()
49
+ if (!address || typeof address === "string") throw new Error("fixture Sidecar stub did not bind a TCP port")
50
+ return {
51
+ url: `ws://${OEM_DEV_HOST}:${address.port}`,
52
+ close: async () => {
53
+ for (const socket of sockets) socket.destroy()
54
+ await new Promise((resolve) => server.close(resolve))
55
+ },
56
+ }
57
+ }
58
+
59
+ export function developmentHostEnvironment(mode, env = process.env) {
60
+ if (!DEV_HOST_MODES.has(mode)) throw new Error(`unsupported OEM Dev Host mode: ${mode}`)
61
+ return { ...env, ORBIT_OEM_DEV_HOST: "1", ORBIT_OEM_DEV_HOST_MODE: mode }
62
+ }
63
+
64
+ export async function startDevelopmentServer(
65
+ projectRoot,
66
+ { port = OEM_DEV_HOST_PORT, launch, mode = "full", fixtureRuntime = false } = {},
67
+ ) {
68
+ if (!DEV_HOST_MODES.has(mode)) throw new Error(`unsupported OEM Dev Host mode: ${mode}`)
69
+ let current = null
70
+ let identity = null
71
+ let revision = 0
72
+ let rebuilding = false
73
+ let rebuildAgain = false
74
+ const clients = new Set()
75
+ const notify = (event, data) => {
76
+ for (const response of clients) response.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`)
77
+ }
78
+ const rebuild = async () => {
79
+ if (rebuilding) {
80
+ rebuildAgain = true
81
+ return
82
+ }
83
+ rebuilding = true
84
+ try {
85
+ const next = await compileOemDevelopmentProject(projectRoot, { expectedIdentity: identity })
86
+ identity ??= next.identity
87
+ current = next
88
+ revision += 1
89
+ notify("ready", { revision })
90
+ console.log(`[oem-dev-host] build ${revision} ready`)
91
+ } catch (error) {
92
+ const message = formatBuildError(error)
93
+ notify("error", { message })
94
+ console.error(`[oem-dev-host] ${message}`)
95
+ } finally {
96
+ rebuilding = false
97
+ if (rebuildAgain) {
98
+ rebuildAgain = false
99
+ void rebuild()
100
+ }
101
+ }
102
+ }
103
+ await rebuild()
104
+ if (!current) throw new Error("initial OEM build failed; fix the reported error before starting Dev Host")
105
+
106
+ const server = http.createServer((request, response) => {
107
+ const url = new URL(request.url ?? "/", `http://${OEM_DEV_HOST}:${port}`)
108
+ const requestOrigin = request.headers.origin
109
+ if (requestOrigin && requestOrigin !== OEM_DEV_HOST_RENDERER_ORIGIN) {
110
+ return json(response, 403, { error: "browser origin is not allowed" })
111
+ }
112
+ response.setHeader("Access-Control-Allow-Origin", OEM_DEV_HOST_RENDERER_ORIGIN)
113
+ response.setHeader("Vary", "Origin")
114
+ if (url.pathname === "/status") return json(response, 200, { revision, snapshot: current.snapshot })
115
+ if (url.pathname === "/bundle.js") {
116
+ response.writeHead(200, { "Content-Type": "text/javascript; charset=utf-8", "Cache-Control": "no-store" })
117
+ return response.end(current.javascript)
118
+ }
119
+ if (url.pathname === "/bundle.css") {
120
+ response.writeHead(200, { "Content-Type": "text/css; charset=utf-8", "Cache-Control": "no-store" })
121
+ return response.end(current.css)
122
+ }
123
+ if (url.pathname === "/events") {
124
+ response.writeHead(200, {
125
+ "Content-Type": "text/event-stream",
126
+ "Cache-Control": "no-store",
127
+ Connection: "keep-alive",
128
+ "Access-Control-Allow-Origin": OEM_DEV_HOST_RENDERER_ORIGIN,
129
+ Vary: "Origin",
130
+ })
131
+ response.write(`event: connected\ndata: ${JSON.stringify({ revision })}\n\n`)
132
+ clients.add(response)
133
+ request.on("close", () => clients.delete(response))
134
+ return
135
+ }
136
+ json(response, 404, { error: "not found" })
137
+ })
138
+ await new Promise((resolve, reject) => {
139
+ server.once("error", reject)
140
+ server.listen(port, OEM_DEV_HOST, resolve)
141
+ })
142
+
143
+ const fixtureSidecar = fixtureRuntime ? await startFixtureSidecarStub() : null
144
+
145
+ let timer
146
+ const watcher = watch(path.resolve(projectRoot), { recursive: true }, (_event, filename) => {
147
+ const relative = String(filename ?? "")
148
+ if (!relative || /(?:^|[\\/])(?:node_modules|dist|preview-dist)(?:[\\/]|$)/.test(relative)) return
149
+ clearTimeout(timer)
150
+ timer = setTimeout(() => void rebuild(), 90)
151
+ })
152
+ const child = launch
153
+ ? spawn(launch, [], {
154
+ stdio: "inherit",
155
+ env: developmentHostEnvironment(mode, {
156
+ ...process.env,
157
+ ...(fixtureSidecar ? { ORBIT_SIDECAR_WS_URL: fixtureSidecar.url } : {}),
158
+ }),
159
+ })
160
+ : null
161
+ const close = async () => {
162
+ clearTimeout(timer)
163
+ watcher.close()
164
+ for (const client of clients) client.end()
165
+ child?.kill()
166
+ await new Promise((resolve) => server.close(resolve))
167
+ await fixtureSidecar?.close()
168
+ }
169
+ return {
170
+ origin: `http://${OEM_DEV_HOST}:${port}`,
171
+ mode,
172
+ fixtureSidecarUrl: fixtureSidecar?.url,
173
+ close,
174
+ child,
175
+ }
176
+ }