UNPKG

@embeddable.com/sdk-core

Version:

Core Embeddable SDK module responsible for web-components bundling and publishing.

505 lines (435 loc) 14.1 kB
import buildTypes, { EMB_OPTIONS_FILE_REGEX, EMB_TYPE_FILE_REGEX, } from "./buildTypes"; import prepare, { removeIfExists } from "./prepare"; import generate from "./generate"; import provideConfig from "./provideConfig"; import { CompilerSystem, createNodeLogger, createNodeSys, } from "@stencil/core/sys/node"; import { RollupWatcher } from "rollup"; import * as http from "node:http"; import { IncomingMessage, Server, ServerResponse } from "http"; import { ChildProcess } from "node:child_process"; import { WebSocketServer } from "ws"; import * as chokidar from "chokidar"; import * as path from "path"; import { getToken, default as login } from "./login"; import axios from "axios"; import { findFiles } from "@embeddable.com/sdk-utils"; import { archive, CUBE_FILES, sendBuild, SECURITY_CONTEXT_FILES, CLIENT_CONTEXT_FILES, } from "./push"; import validate from "./validate"; import { checkNodeVersion } from "./utils"; import { createManifest } from "./cleanup"; import { selectWorkspace } from "./workspaceUtils"; import * as fs from "node:fs/promises"; import minimist from "minimist"; import { initLogger, logError } from "./logger"; import fg from "fast-glob"; import * as dotenv from "dotenv"; import ora, { Ora } from "ora"; import finalhandler from "finalhandler"; import serveStatic from "serve-static"; import { ResolvedEmbeddableConfig } from "./defineConfig"; import buildGlobalHooks from "./buildGlobalHooks"; type FSWatcher = chokidar.FSWatcher; dotenv.config(); let wss: WebSocketServer; let changedFiles: string[] = []; let browserWindow: ChildProcess | null = null; let previewWorkspace: string; const SERVER_PORT = 8926; const BUILD_DEV_DIR = ".embeddable-dev-build"; const GLOBAL_CSS = "/global.css"; const buildWebComponent = async (config: any) => { await generate(config, "sdk-react"); }; const addToGitingore = async () => { try { const gitignorePath = path.resolve(process.cwd(), ".gitignore"); const gitignoreContent = await fs.readFile(gitignorePath, "utf8"); if (!gitignoreContent.includes(BUILD_DEV_DIR)) { await fs.appendFile(gitignorePath, `\n${BUILD_DEV_DIR}\n`); } } catch (e) { // ignore } }; const chokidarWatchOptions = { ignoreInitial: true, usePolling: false, // Ensure polling is disabled awaitWriteFinish: { stabilityThreshold: 200, pollInterval: 100, }, }; export default async () => { await initLogger("dev"); const breadcrumbs: string[] = []; try { breadcrumbs.push("run dev"); checkNodeVersion(); addToGitingore(); process.on("warning", (e) => console.warn(e.stack)); const logger = createNodeLogger(); const sys = createNodeSys({ process }); const defaultConfig = await provideConfig(); const buildDir = path.resolve(defaultConfig.client.rootDir, BUILD_DEV_DIR); const config = { ...defaultConfig, dev: { watch: true, logger, sys, }, client: { ...defaultConfig.client, buildDir, componentDir: path.resolve(buildDir, "component"), stencilBuild: path.resolve(buildDir, "dist", "embeddable-wrapper"), tmpDir: path.resolve( defaultConfig.client.rootDir, ".embeddable-dev-tmp" ), }, }; breadcrumbs.push("prepare config"); await prepare(config); const serve = serveStatic(config.client.buildDir); let workspacePreparation = ora("Preparing workspace...").start(); breadcrumbs.push("get preview workspace"); try { previewWorkspace = await getPreviewWorkspace( workspacePreparation, config ); } catch (e: any) { if (e.response?.status === 401) { // login and retry await login(); workspacePreparation = ora("Preparing workspace...").start(); previewWorkspace = await getPreviewWorkspace( workspacePreparation, config ); } else { workspacePreparation.fail( e.response?.data?.errorMessage || "Unknown error: " + e.message ); process.exit(1); } } workspacePreparation.succeed("Workspace is ready"); const server = http.createServer( async (request: IncomingMessage, res: ServerResponse) => { res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader( "Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS" ); res.setHeader( "Access-Control-Allow-Headers", "Content-Type, Authorization" ); if (request.method === "OPTIONS") { // Respond to OPTIONS requests with just the CORS headers and a 200 status code res.writeHead(200); res.end(); return; } const done = finalhandler(request, res); try { if (request.url?.endsWith(GLOBAL_CSS)) { res.writeHead(200, { "Content-Type": "text/css" }); res.end(await fs.readFile(config.client.globalCss)); return; } } catch {} serve(request, res, done); } ); const { themeWatcher, lifecycleWatcher } = await buildGlobalHooks(config); wss = new WebSocketServer({ server }); server.listen(SERVER_PORT, async () => { const watchers: Array<RollupWatcher | FSWatcher> = []; if (sys?.onProcessInterrupt) { sys.onProcessInterrupt( async () => await onClose(server, sys, watchers, config) ); } breadcrumbs.push("create manifest"); await createManifest({ ctx: { ...config, client: { ...config.client, tmpDir: buildDir, }, }, typesFileName: "embeddable-types.js", stencilWrapperFileName: "embeddable-wrapper.js", metaFileName: "embeddable-components-meta.js", editorsMetaFileName: "embeddable-editors-meta.js", }); await sendBuildChanges(config); if (config.pushComponents) { for (const getPlugin of config.plugins) { const plugin = getPlugin(); breadcrumbs.push("validate plugin"); await plugin.validate(config); breadcrumbs.push("build plugin"); const watcher = await plugin.build(config); breadcrumbs.push("configure watcher"); await configureWatcher(watcher as RollupWatcher, config); watchers.push(watcher as RollupWatcher); } const customGlobalCssWatch = globalCssWatcher(config); watchers.push(customGlobalCssWatch); if (themeWatcher) { await globalHookWatcher(themeWatcher, config); watchers.push(themeWatcher); } if (lifecycleWatcher) { await globalHookWatcher(lifecycleWatcher, config); watchers.push(lifecycleWatcher); } } else { await openDevWorkspacePage(config.previewBaseUrl); } const cubeSecurityContextAndClientContextWatch = await cubeSecurityContextAndClientContextWatcher(config); watchers.push(cubeSecurityContextAndClientContextWatch); }); } catch (error: any) { await logError({ command: "dev", breadcrumbs, error }); console.log(error); process.exit(1); } }; const configureWatcher = async ( watcher: RollupWatcher, ctx: ResolvedEmbeddableConfig ) => { watcher.on("change", (path) => { changedFiles.push(path); }); watcher.on("event", async (e) => { if (e.code === "BUNDLE_START") { await onBuildStart(ctx); } if (e.code === "BUNDLE_END") { await onBundleBuildEnd(ctx); changedFiles = []; } if (e.code === "ERROR") { sendMessage("componentsBuildError", { error: e.error?.message }); changedFiles = []; } }); }; const globalHookWatcher = async (watcher: RollupWatcher, ctx: any) => { watcher.on("change", (path) => { changedFiles.push(path); }); watcher.on("event", async (e) => { if (e.code === "BUNDLE_START") { sendMessage("componentsBuildStart", { changedFiles }); } if (e.code === "BUNDLE_END") { sendMessage("componentsBuildSuccess"); changedFiles = []; } if (e.code === "ERROR") { sendMessage("componentsBuildError", { error: e.error?.message }); changedFiles = []; } }); }; const sendMessage = (type: string, meta = {}) => { wss?.clients?.forEach((ws) => { ws.send(JSON.stringify({ type, ...meta })); }); }; const typeFilesFilter = (f: string) => EMB_OPTIONS_FILE_REGEX.test(f) || EMB_TYPE_FILE_REGEX.test(f); const onlyTypesChanged = () => changedFiles.length !== 0 && changedFiles.filter(typeFilesFilter).length === changedFiles.length; const isTypeFileChanged = () => changedFiles.findIndex(typeFilesFilter) >= 0; const onBuildStart = async (ctx: ResolvedEmbeddableConfig) => { if (changedFiles.length === 0 || isTypeFileChanged()) { await buildTypes(ctx); } sendMessage("componentsBuildStart", { changedFiles }); }; const openDevWorkspacePage = async (previewBaseUrl: string) => { const open = (await import("open")).default; return await open(`${previewBaseUrl}/workspace/${previewWorkspace}`); }; const onBundleBuildEnd = async (ctx: ResolvedEmbeddableConfig) => { if (!onlyTypesChanged() || changedFiles.length === 0) { await buildWebComponent(ctx); } if (browserWindow == null) { browserWindow = await openDevWorkspacePage(ctx.previewBaseUrl); } else { sendMessage("componentsBuildSuccess"); } }; const cubeSecurityContextAndClientContextWatcher = async ( ctx: ResolvedEmbeddableConfig ): Promise<FSWatcher> => { let filesToWatch: any = []; if (ctx.pushComponents) { const clientContextFiles = await fg("**/*.cc.{yaml,yml}", { cwd: ctx.client.presetsSrc, absolute: true, }); filesToWatch = [...filesToWatch, ...clientContextFiles]; } if (ctx.pushModels) { const [cubeFiles, securityContextFiles] = await Promise.all([ fg("**/*.cube.{yaml,yml,js}", { cwd: ctx.client.modelsSrc, absolute: true, }), fg("**/*.sc.{yaml,yml}", { cwd: ctx.client.presetsSrc, absolute: true, }), ]); filesToWatch = [...filesToWatch, ...cubeFiles, ...securityContextFiles]; } const fsWatcher = chokidar.watch(filesToWatch, chokidarWatchOptions); fsWatcher.on("all", () => sendBuildChanges(ctx)); return fsWatcher; }; const globalCssWatcher = (ctx: ResolvedEmbeddableConfig): FSWatcher => { const fsWatcher = chokidar.watch(ctx.client.globalCss, chokidarWatchOptions); fsWatcher.on("all", async () => { sendMessage("globalCssUpdateSuccess"); }); return fsWatcher; }; const sendBuildChanges = async (ctx: ResolvedEmbeddableConfig) => { const isValid = await validate(ctx); if (!isValid) { return sendMessage("dataModelsAndOrSecurityContextUpdateError"); } const sending = ora( "Synchronising data models and/or security contexts..." ).start(); let filesList: [string, string][] = []; if (ctx.pushComponents) { const clientContextFilesList = await findFiles( ctx.client.presetsSrc, CLIENT_CONTEXT_FILES ); // Map the files to include their full filenames const clientContextFileList = [...clientContextFilesList].map( (entry): [string, string] => [path.basename(entry[1]), entry[1]] ); filesList = [...clientContextFileList]; } if (ctx.pushModels) { const cubeFilesList = await findFiles(ctx.client.modelsSrc, CUBE_FILES); const securityContextFilesList = await findFiles( ctx.client.presetsSrc, SECURITY_CONTEXT_FILES ); // Map the files to include their full filenames const cubeAndSecurityContextFileList = [ ...cubeFilesList, ...securityContextFilesList, ].map((entry): [string, string] => [path.basename(entry[1]), entry[1]]); filesList = [ ...filesList, ...cubeAndSecurityContextFileList, // add manifest to the archive [ "embeddable-manifest.json", path.resolve(ctx.client.buildDir, "embeddable-manifest.json"), ], ]; } const token = await getToken(); await archive({ ctx, filesList, isDev: true, }); await sendBuild(ctx, { workspaceId: previewWorkspace, token }); sending.succeed(`Data models and/or security context synchronized`); sendMessage("dataModelsAndOrSecurityContextUpdateSuccess"); }; const onClose = async ( server: Server, sys: CompilerSystem, watchers: Array<RollupWatcher | FSWatcher>, config: ResolvedEmbeddableConfig ) => { server.close(); wss.close(); browserWindow?.unref(); for (const watcher of watchers) { if (watcher.close) { await watcher.close(); } } for (const getPlugin of config.plugins) { const plugin = getPlugin(); await plugin.cleanup(config); } await removeIfExists(config); await sys.destroy(); process.exit(0); }; const getPreviewWorkspace = async ( startedOra: Ora, ctx: ResolvedEmbeddableConfig ): Promise<string> => { const token = await getToken(); const params = minimist(process.argv.slice(2)); let primaryWorkspaceId = params.w || params.workspace; if (!primaryWorkspaceId) { startedOra.stop(); // Stop current Ora, otherwise the last option will get hidden by it. const { workspaceId } = await selectWorkspace(ora, ctx, token); primaryWorkspaceId = workspaceId; startedOra.start(); } try { const instanceUrl = process.env.CUBE_CLOUD_ENDPOINT; const response = await axios.post( `${ctx.pushBaseUrl}/workspace/dev-workspace`, { primaryWorkspaceId, instanceUrl, pushModels: ctx.pushModels, pushComponents: ctx.pushComponents, }, { headers: { Authorization: `Bearer ${token}`, }, } ); return response.data; } catch (e: any) { if (e.response.status === 401) { // login and retry await login(); return await getPreviewWorkspace(startedOra, ctx); } else { throw e; } } };