UNPKG

everything-dev

Version:

A consolidated product package for building Module Federation apps with oRPC APIs.

1 lines 14 kB
{"version":3,"file":"app.mjs","names":["configBuildRuntimeConfig"],"sources":["../src/app.ts"],"sourcesContent":["import { existsSync } from \"node:fs\";\nimport { createServer } from \"node:net\";\nimport { join } from \"node:path\";\nimport { Context, Data, Effect, Layer } from \"effect\";\nimport type { DevPortState } from \"./cli/infra\";\nimport {\n buildRuntimeConfig as configBuildRuntimeConfig,\n getProjectRoot,\n resolveLocalDevelopmentPath,\n} from \"./config\";\nimport { claimedPorts } from \"./process-registry\";\nimport type { AppOrchestrator } from \"./service-descriptor\";\nimport type { BosConfig, RuntimeConfig, RuntimePluginConfig } from \"./types\";\n\nexport type { AppOrchestrator };\n\nconst DEFAULT_HOST_PORT = 3000;\nconst DEFAULT_API_PORT = 3001;\nconst DEFAULT_AUTH_PORT = 3002;\nconst DEFAULT_UI_PORT = 3003;\nconst DEFAULT_PLUGIN_PORT_START = 3010;\n\nconst PROBE_TIMEOUT_MS = 250;\nconst MAX_PORT_SCAN_STEPS = 1000;\nconst PARALLEL_PROBE_WINDOW = 8;\n\nexport type PortBudget = { min: number; max: number };\n\nexport class PortAllocationError extends Data.TaggedError(\"PortAllocationError\")<{\n preferred: number;\n budget?: PortBudget;\n cause?: unknown;\n}> {}\n\nexport class PortAllocator extends Context.Tag(\"PortAllocator\")<\n PortAllocator,\n {\n pickAvailable: (\n preferred: number,\n budget?: PortBudget,\n ) => Effect.Effect<number, PortAllocationError>;\n }\n>() {}\n\nexport function detectLocalPackages(\n bosConfig?: BosConfig,\n runtimeConfig?: RuntimeConfig,\n): string[] {\n const packages: string[] = [];\n const configDir = getProjectRoot();\n\n const uiLocalPath =\n runtimeConfig?.ui.localPath ??\n resolveLocalDevelopmentPath(bosConfig?.app.ui.development, configDir);\n if (uiLocalPath && existsSync(join(uiLocalPath, \"package.json\"))) {\n packages.push(\"ui\");\n }\n\n const apiLocalPath =\n runtimeConfig?.api.localPath ??\n resolveLocalDevelopmentPath(bosConfig?.app.api.development, configDir);\n if (apiLocalPath && existsSync(join(apiLocalPath, \"package.json\"))) {\n packages.push(\"api\");\n }\n\n const hostLocalPath =\n runtimeConfig?.host?.localPath ??\n resolveLocalDevelopmentPath(bosConfig?.app.host.development, configDir);\n if (hostLocalPath && existsSync(join(hostLocalPath, \"package.json\"))) {\n packages.push(\"host\");\n } else if (existsSync(join(configDir, \"host\", \"package.json\"))) {\n packages.push(\"host\");\n }\n\n for (const [pluginId, pluginConfig] of Object.entries(runtimeConfig?.plugins ?? {})) {\n if (pluginConfig.localPath && existsSync(join(pluginConfig.localPath, \"package.json\"))) {\n packages.push(`plugin:${pluginId}`);\n }\n if (pluginConfig.ui?.localPath && existsSync(join(pluginConfig.ui.localPath, \"package.json\"))) {\n packages.push(`plugin-ui:${pluginId}`);\n }\n }\n\n const authLocalPath =\n runtimeConfig?.auth?.localPath ??\n resolveLocalDevelopmentPath(bosConfig?.app.auth?.development, configDir);\n if (authLocalPath && existsSync(join(authLocalPath, \"package.json\"))) {\n packages.push(\"auth\");\n }\n\n return packages;\n}\n\nexport function buildRuntimeConfig(\n bosConfig: BosConfig,\n options: {\n hostSource?: \"local\" | \"remote\";\n uiSource?: \"local\" | \"remote\";\n apiSource?: \"local\" | \"remote\";\n authSource?: \"local\" | \"remote\";\n proxy?: string;\n env?: \"development\" | \"production\";\n plugins?: Record<string, RuntimePluginConfig>;\n },\n): RuntimeConfig {\n return configBuildRuntimeConfig(bosConfig, getProjectRoot(), options.env ?? \"development\", {\n hostSource: options.hostSource,\n uiSource: options.uiSource,\n apiSource: options.apiSource,\n authSource: options.authSource,\n proxy: options.proxy,\n plugins: options.plugins,\n });\n}\n\nfunction probePortBindable(port: number): Effect.Effect<boolean> {\n return Effect.async<boolean>((resume) => {\n const server = createServer();\n\n server.once(\"listening\", () => {\n server.close(() => {\n resume(Effect.succeed(true));\n });\n });\n\n server.once(\"error\", () => {\n server.removeAllListeners();\n // EADDRINUSE, EACCES, or any other bind error → not available\n resume(Effect.succeed(false));\n });\n\n server.listen(port, \"127.0.0.1\");\n\n const timer = setTimeout(() => {\n server.removeAllListeners();\n try {\n server.close();\n } catch {\n // ignore\n }\n resume(Effect.succeed(false));\n }, PROBE_TIMEOUT_MS);\n\n server.once(\"listening\", () => clearTimeout(timer));\n server.once(\"error\", () => clearTimeout(timer));\n });\n}\n\nfunction pickAvailablePort(\n preferred: number,\n usedPorts: Set<number>,\n budget?: PortBudget,\n): Effect.Effect<number, PortAllocationError> {\n return Effect.gen(function* () {\n const within = (candidate: number): boolean =>\n !budget || (candidate >= budget.min && candidate <= budget.max);\n\n let port = preferred;\n if (!within(port)) {\n port = budget ? budget.min : port;\n }\n\n const ceiling = budget ? budget.max + 1 : Number.MAX_SAFE_INTEGER;\n let steps = 0;\n\n const fail = () =>\n Effect.fail(\n new PortAllocationError({\n preferred,\n budget,\n cause: budget\n ? `No free port in budget [${budget.min}, ${budget.max}] starting from ${preferred}`\n : `No free port found starting from ${preferred} within ${MAX_PORT_SCAN_STEPS} steps`,\n }),\n );\n\n while (true) {\n if (port >= ceiling || steps > MAX_PORT_SCAN_STEPS) {\n yield* fail();\n }\n\n const candidates: number[] = [];\n for (let i = 0; i < PARALLEL_PROBE_WINDOW && port + i < ceiling; i++) {\n const candidate = port + i;\n if (!usedPorts.has(candidate)) {\n candidates.push(candidate);\n }\n }\n\n if (candidates.length === 0) {\n port += PARALLEL_PROBE_WINDOW;\n steps += PARALLEL_PROBE_WINDOW;\n continue;\n }\n\n const results = yield* Effect.forEach(\n candidates,\n (c) => probePortBindable(c).pipe(Effect.map((free) => ({ port: c, free }))),\n { concurrency: \"unbounded\" },\n );\n\n const firstFree = results.find((r) => r.free);\n if (firstFree) {\n usedPorts.add(firstFree.port);\n return firstFree.port;\n }\n\n port += PARALLEL_PROBE_WINDOW;\n steps += PARALLEL_PROBE_WINDOW;\n }\n });\n}\n\nexport const PortAllocatorLive: Layer.Layer<PortAllocator> = Layer.sync(PortAllocator, () => {\n const usedPorts = claimedPorts();\n return {\n pickAvailable: (preferred, budget) => pickAvailablePort(preferred, usedPorts, budget),\n };\n});\n\nfunction withLocalRuntimeUrl<\n T extends { url: string; entry: string; port?: number; localPath?: string },\n>(entry: T, port: number): T {\n const url = `http://localhost:${port}`;\n return {\n ...entry,\n url,\n entry: `${url}/mf-manifest.json`,\n port,\n };\n}\n\nexport interface DevPortOptions {\n hostPort?: number;\n apiPort?: number;\n uiPort?: number;\n authPort?: number;\n pluginPortStart?: number;\n ssr?: boolean;\n portBudget?: PortBudget;\n}\n\nexport interface PreparedDevRuntime {\n runtimeConfig: RuntimeConfig;\n devPorts: DevPortState;\n}\n\nexport function prepareDevelopmentRuntimeConfig(\n runtimeConfig: RuntimeConfig,\n options?: DevPortOptions,\n): Effect.Effect<PreparedDevRuntime, PortAllocationError, PortAllocator> {\n return Effect.gen(function* () {\n const allocator = yield* PortAllocator;\n const budget = options?.portBudget;\n\n const pickedHostPort = yield* allocator.pickAvailable(\n options?.hostPort ?? DEFAULT_HOST_PORT,\n budget,\n );\n\n const hostIsLocal = runtimeConfig.host.source === \"local\";\n const next: RuntimeConfig = {\n ...runtimeConfig,\n host: hostIsLocal\n ? {\n ...runtimeConfig.host,\n url: `http://localhost:${pickedHostPort}`,\n port: pickedHostPort,\n }\n : { ...runtimeConfig.host },\n ui: { ...runtimeConfig.ui },\n api: { ...runtimeConfig.api },\n auth: runtimeConfig.auth ? { ...runtimeConfig.auth } : undefined,\n plugins: runtimeConfig.plugins ? { ...runtimeConfig.plugins } : undefined,\n };\n\n const devPorts: DevPortState = {\n host: hostIsLocal ? pickedHostPort : undefined,\n api: undefined,\n ui: undefined,\n auth: undefined,\n pluginPortStart: undefined,\n };\n\n if (next.api.source === \"local\" && next.api.localPath) {\n const apiPort = yield* allocator.pickAvailable(\n options?.apiPort ?? next.api.port ?? DEFAULT_API_PORT,\n budget,\n );\n next.api = withLocalRuntimeUrl(next.api, apiPort);\n devPorts.api = apiPort;\n }\n\n if (next.auth?.source === \"local\" && next.auth.localPath) {\n const authPort = yield* allocator.pickAvailable(\n options?.authPort ?? next.auth.port ?? DEFAULT_AUTH_PORT,\n budget,\n );\n next.auth = withLocalRuntimeUrl(next.auth, authPort);\n devPorts.auth = authPort;\n }\n\n if (next.ui.source === \"local\" && next.ui.localPath) {\n const uiPort = yield* allocator.pickAvailable(\n options?.uiPort ?? next.ui.port ?? DEFAULT_UI_PORT,\n budget,\n );\n next.ui = withLocalRuntimeUrl(next.ui, uiPort);\n devPorts.ui = uiPort;\n if (options?.ssr) {\n const ssrPort = yield* allocator.pickAvailable(uiPort + 1, budget);\n next.ui.ssrUrl = `http://localhost:${ssrPort}`;\n } else {\n next.ui.ssrUrl = undefined;\n }\n }\n\n if (next.plugins) {\n const entries = Object.entries(next.plugins).sort(([a], [b]) => a.localeCompare(b));\n let pluginBasePort = options?.pluginPortStart ?? DEFAULT_PLUGIN_PORT_START;\n let firstLocalPluginPort: number | undefined;\n\n for (const [pluginId, plugin] of entries) {\n if (plugin.source === \"local\" && plugin.localPath) {\n const pluginPort = yield* allocator.pickAvailable(plugin.port ?? pluginBasePort, budget);\n next.plugins[pluginId] = withLocalRuntimeUrl(plugin, pluginPort);\n if (firstLocalPluginPort === undefined) firstLocalPluginPort = pluginPort;\n pluginBasePort = pluginPort + 1;\n }\n\n if (plugin.ui?.source === \"local\" && plugin.ui.localPath) {\n const pluginUiPort = yield* allocator.pickAvailable(\n plugin.ui.port ?? pluginBasePort,\n budget,\n );\n next.plugins[pluginId] = {\n ...next.plugins[pluginId]!,\n ui: withLocalRuntimeUrl(plugin.ui, pluginUiPort),\n };\n if (firstLocalPluginPort === undefined) firstLocalPluginPort = pluginUiPort;\n pluginBasePort = pluginUiPort + 1;\n }\n }\n\n devPorts.pluginPortStart = firstLocalPluginPort;\n }\n\n return { runtimeConfig: next, devPorts };\n });\n}\n"],"mappings":"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"}