@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 +36 -0
- package/src/cli.mjs +66 -0
- package/src/compiler.mjs +178 -0
- package/src/server.mjs +176 -0
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
|
+
}
|
package/src/compiler.mjs
ADDED
|
@@ -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
|
+
}
|