UNPKG

@embeddable.com/sdk-core

Version:

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

345 lines (305 loc) 11.5 kB
// buildGlobalHooks.unit.test.ts import { describe, it, expect, beforeEach, vi, Mock } from "vitest"; import * as fs from "node:fs/promises"; import * as fsSync from "node:fs"; import * as vite from "vite"; import { findFiles, getContentHash, getGlobalHooksMeta, getComponentLibraryConfig, } from "@embeddable.com/sdk-utils"; import buildGlobalHooks from "../src/buildGlobalHooks"; import { ResolvedEmbeddableConfig } from "./defineConfig"; import path from "node:path"; // Mock implementations vi.mock("node:fs/promises"); vi.mock("node:fs"); vi.mock("vite"); vi.mock("@embeddable.com/sdk-utils", async () => { const actual = await vi.importActual< typeof import("@embeddable.com/sdk-utils") >("@embeddable.com/sdk-utils"); return { ...actual, findFiles: vi.fn(), getContentHash: vi.fn(), getGlobalHooksMeta: vi.fn(), getComponentLibraryConfig: vi.fn(), }; }); const lifecyclePath = path.resolve( process.cwd(), "fake", "root", "embeddable.lifecycle.ts", ); const themePath = path.resolve( process.cwd(), "fake", "root", "embeddable.theme.ts", ); describe("buildGlobalHooks (Unit Tests)", () => { beforeEach(() => { vi.clearAllMocks(); }); it("should skip lifecycle building if file doesn't exist", async () => { vi.spyOn(fsSync, "existsSync").mockImplementation((p: any) => { // We want the code to see that /fake/root/embeddable.lifecycle.ts does NOT exist if (p === lifecyclePath) { return false; } // Otherwise, default to false return false; }); // Possibly not used, but let's mock anyway (findFiles as Mock).mockResolvedValue([]); // The aggregator template might be read if there's some library with a theme (fs.readFile as Mock).mockResolvedValue("some template content"); (getContentHash as Mock).mockReturnValue("abc123"); const ctx: ResolvedEmbeddableConfig = { client: { srcDir: path.resolve(process.cwd(), "fake", "src"), buildDir: path.resolve(process.cwd(), "fake", "build"), tmpDir: path.resolve(process.cwd(), "fake", "tmp"), rootDir: path.resolve(process.cwd(), "fake", "root"), lifecycleHooksFile: lifecyclePath, componentLibraries: [], customizationFile: themePath, }, core: { templatesDir: "/fake/templates", }, } as any; await buildGlobalHooks(ctx); // Because we skip building the repo lifecycle, no call to vite.build with that entry expect(vite.build).not.toHaveBeenCalledWith( expect.objectContaining({ build: expect.objectContaining({ lib: expect.objectContaining({ entry: lifecyclePath, }), }), }), ); }); it("should build lifecycle file if it exists", async () => { // Now we say "repo lifecycle does exist" vi.spyOn(fsSync, "existsSync").mockImplementation((p: any) => { // If path is exactly the lifecycle file => true return p === lifecyclePath; }); // Just in case, but not strictly used here (findFiles as Mock).mockResolvedValue([]); // The aggregator template might or might not be read (fs.readFile as Mock).mockResolvedValue("some file content"); (getContentHash as Mock).mockReturnValue("abc123"); (fs.rename as Mock).mockResolvedValue(undefined); (vite.build as Mock).mockResolvedValue(undefined); const ctx: ResolvedEmbeddableConfig = { client: { srcDir: path.resolve(process.cwd(), "fake", "src"), buildDir: path.resolve(process.cwd(), "fake", "build"), tmpDir: path.resolve(process.cwd(), "fake", "tmp"), rootDir: path.resolve(process.cwd(), "fake", "root"), lifecycleHooksFile: lifecyclePath, customizationFile: themePath, componentLibraries: [], }, core: { templatesDir: "/fake/templates", }, } as any; await buildGlobalHooks(ctx); // We expect a call to build the lifecycle expect(vite.build).toHaveBeenCalledWith( expect.objectContaining({ build: expect.objectContaining({ lib: expect.objectContaining({ entry: lifecyclePath, fileName: "embeddable-lifecycle", }), }), }), ); }); it("should build theme aggregator if libraries exist with themeProvider references", async () => { // aggregator template read is guaranteed (fs.readFile as Mock).mockResolvedValue("template content"); (getContentHash as Mock).mockReturnValue("xyz777"); (fs.rename as Mock).mockResolvedValue(undefined); (vite.build as Mock).mockResolvedValue(undefined); // Suppose we skip the lifecycle, but aggregator is still built vi.spyOn(fsSync, "existsSync").mockImplementation((p: any) => { // lifecycle => false if (p === lifecyclePath) return false; // local theme => true if (p === themePath) return true; return false; }); (getComponentLibraryConfig as Mock).mockImplementation((cfg: any) => ({ libraryName: cfg.name, })); // Each library call to getGlobalHooksMeta occurs twice: // 1) buildThemeHook -> aggregator // 2) buildLifecycleHooks -> but we skip it if lifecycle doesn't exist // The code still calls getGlobalHooksMeta for each library in buildLifecycleHooks // So we must provide 4 total mock results for 2 libraries => aggregator + lifecycle each. (getGlobalHooksMeta as Mock) // aggregator call #1: libA .mockResolvedValueOnce({ themeProvider: "libA-theme.js", lifecycleHooks: [], }) // aggregator call #2: libB .mockResolvedValueOnce({ themeProvider: "libB-theme.js", lifecycleHooks: [], }) // lifecycle call #1: libA .mockResolvedValueOnce({ themeProvider: "libA-theme.js", lifecycleHooks: [], }) // lifecycle call #2: libB .mockResolvedValueOnce({ themeProvider: "libB-theme.js", lifecycleHooks: [], }); const ctx: ResolvedEmbeddableConfig = { client: { srcDir: path.resolve(process.cwd(), "fake", "src"), buildDir: path.resolve(process.cwd(), "fake", "build"), tmpDir: path.resolve(process.cwd(), "fake", "tmp"), rootDir: path.resolve(process.cwd(), "fake", "root"), lifecycleHooksFile: lifecyclePath, customizationFile: themePath, componentLibraries: [{ name: "libA" }, { name: "libB" }], }, core: { templatesDir: "/fake/templates", }, } as any; await buildGlobalHooks(ctx); // aggregator => build with entry = /fake/build/embeddableThemeHook.js expect(vite.build).toHaveBeenCalledWith( expect.objectContaining({ build: expect.objectContaining({ lib: expect.objectContaining({ entry: expect.stringContaining("embeddableThemeHook.js"), fileName: `embeddable-theme-xyz777`, // from getContentHash }), }), }), ); }); it("should skip aggregator if no library has themeProvider and no local theme", async () => { (fs.readFile as Mock).mockResolvedValue("template content"); (getContentHash as Mock).mockReturnValue("someHash"); (fs.rename as Mock).mockResolvedValue(undefined); (vite.build as Mock).mockResolvedValue(undefined); vi.spyOn(fsSync, "existsSync").mockImplementation((p: any) => { // Suppose no local theme => false if (p === themePath) return false; if (p === lifecyclePath) return false; return false; }); // Each library is missing a themeProvider (getComponentLibraryConfig as Mock).mockImplementation((cfg: any) => ({ libraryName: cfg.name, })); // no theme, so aggregator is skipped (getGlobalHooksMeta as Mock).mockResolvedValue({ themeProvider: null, lifecycleHooks: [], }); const ctx: ResolvedEmbeddableConfig = { client: { srcDir: path.resolve(process.cwd(), "fake", "src"), buildDir: path.resolve(process.cwd(), "fake", "build"), tmpDir: path.resolve(process.cwd(), "fake", "tmp"), rootDir: path.resolve(process.cwd(), "fake", "root"), lifecycleHooksFile: lifecyclePath, customizationFile: themePath, componentLibraries: [ { name: "libA" }, // no theme ], }, core: { templatesDir: "/fake/templates", }, } as any; await buildGlobalHooks(ctx); // aggregator not built // We do an exact check: "not toHaveBeenCalledWith" // But the code might still do a build for the lifecycle if it existed. // We said the lifecycle is false => so no build for aggregator expect(vite.build).not.toHaveBeenCalledWith( expect.objectContaining({ build: expect.objectContaining({ lib: expect.objectContaining({ entry: expect.stringContaining("embeddableThemeHook.js"), }), }), }), ); }); it("should normalize Windows paths in theme import statements", async () => { // Mock template content that has the placeholder for local theme import const templateContent = `{{LIBRARY_THEME_IMPORTS}} {{LOCAL_THEME_IMPORT}} // rest of template`; (fs.readFile as Mock) .mockResolvedValueOnce(templateContent) // Template read .mockResolvedValueOnce("file content"); // Temp file read for hash (getContentHash as Mock).mockReturnValue("hash123"); (vite.build as Mock).mockResolvedValue(undefined); (fs.writeFile as Mock).mockResolvedValue(undefined); (fs.rm as Mock).mockResolvedValue(undefined); // Simulate Windows-style path with backslashes const windowsThemePath = "C:\\work\\code\\embeddable-boilerplate\\embeddable.theme.ts"; vi.spyOn(fsSync, "existsSync").mockImplementation((p: any) => { if (p === windowsThemePath) return true; // Theme file exists if (p === lifecyclePath) return false; // No lifecycle return false; }); (getComponentLibraryConfig as Mock).mockReturnValue({ libraryName: "testLib", }); (getGlobalHooksMeta as Mock).mockResolvedValue({ themeProvider: null, // No library theme lifecycleHooks: [], }); const ctx: ResolvedEmbeddableConfig = { client: { srcDir: path.resolve(process.cwd(), "fake", "src"), buildDir: path.resolve(process.cwd(), "fake", "build"), tmpDir: path.resolve(process.cwd(), "fake", "tmp"), rootDir: path.resolve(process.cwd(), "fake", "root"), lifecycleHooksFile: lifecyclePath, customizationFile: windowsThemePath, // Windows path with backslashes componentLibraries: [], }, core: { templatesDir: "/fake/templates", }, } as any; await buildGlobalHooks(ctx); // Verify that the temporary file was written with normalized path expect(fs.writeFile).toHaveBeenCalledWith( expect.stringContaining("embeddableThemeHook.js"), expect.stringContaining( "C:/work/code/embeddable-boilerplate/embeddable.theme.ts", ), // Forward slashes "utf8", ); // Verify that the content does NOT contain backslashes (which would break imports) const writeFileCall = (fs.writeFile as Mock).mock.calls.find((call) => call[0].includes("embeddableThemeHook.js"), ); expect(writeFileCall).toBeDefined(); expect(writeFileCall![1]).not.toContain("C:\\work\\code"); // Should not have backslashes }); });