@embeddable.com/sdk-core
Version:
Core Embeddable SDK module responsible for web-components bundling and publishing.
250 lines (212 loc) • 6.7 kB
text/typescript
import * as fs from "node:fs/promises";
import * as path from "node:path";
import { describe, it, expect, vi, beforeEach } from "vitest";
import buildTypes, { EMB_TYPE_FILE_REGEX } from "./buildTypes";
import { build } from "vite";
import { ResolvedEmbeddableConfig } from "./defineConfig";
import {
findFiles,
getContentHash,
getComponentLibraryConfig,
} from "@embeddable.com/sdk-utils";
import fg from "fast-glob";
// The same config you already have
const config = {
client: {
srcDir: "src",
rootDir: "root",
buildDir: "build",
componentLibraries: [],
},
outputOptions: {
typesEntryPointFilename: "typesEntryPointFilename",
},
};
const startMock = {
succeed: vi.fn(),
fail: vi.fn(),
};
vi.mock("ora", () => ({
default: () => ({
start: vi.fn().mockImplementation(() => startMock),
info: vi.fn(),
fail: vi.fn(),
}),
}));
vi.mock("@embeddable.com/sdk-utils", () => ({
findFiles: vi.fn(),
getContentHash: vi.fn(),
getComponentLibraryConfig: vi.fn(),
}));
vi.mock("node:path", async () => {
const actualPath =
await vi.importActual<typeof import("node:path")>("node:path");
return {
...actualPath,
resolve: vi.fn(),
relative: vi.fn(),
join: vi.fn(),
dirname: vi.fn(),
};
});
vi.mock("node:fs/promises", () => ({
writeFile: vi.fn(),
rm: vi.fn(),
readFile: vi.fn(),
rename: vi.fn(),
}));
vi.mock("vite", () => ({
build: vi.fn(),
}));
vi.mock("fast-glob", () => ({
default: {
sync: vi.fn(),
},
}));
describe("buildTypes", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(findFiles).mockResolvedValue([["fileName", "filePath"]]);
vi.mocked(path.relative).mockReturnValue("relativePath");
vi.mocked(path.resolve).mockReturnValue("resolvedPath");
vi.mocked(fs.readFile).mockResolvedValue("fileContent");
vi.mocked(getContentHash).mockReturnValue("somehash");
// By default, no libraries => no fast-glob usage
vi.mocked(fg.sync).mockReturnValue([]);
// We also default to returning an empty config from getComponentLibraryConfig
vi.mocked(getComponentLibraryConfig).mockReturnValue({
libraryName: "",
include: [],
exclude: [],
});
});
it("should build types", async () => {
await buildTypes(config as unknown as ResolvedEmbeddableConfig);
expect(findFiles).toHaveBeenCalledWith("src", EMB_TYPE_FILE_REGEX);
expect(build).toHaveBeenCalledWith({
build: {
emptyOutDir: false,
sourcemap: true,
minify: true,
rollupOptions: undefined,
lib: {
entry: "resolvedPath",
fileName: "embeddable-types",
formats: ["es"],
},
outDir: "build",
},
logLevel: "error",
});
expect(fs.readFile).toHaveBeenCalledWith("resolvedPath", "utf8");
expect(getContentHash).toHaveBeenCalledWith("fileContent");
expect(startMock.succeed).toHaveBeenCalledWith("Types built completed");
expect(fs.writeFile).toHaveBeenCalledWith(
"resolvedPath",
`import '../relativePath';
import '../relativePath';`,
);
expect(fs.rm).toHaveBeenCalledWith("resolvedPath");
});
it("should not add hash to the file name when watch is enabled", async () => {
await buildTypes({
...config,
dev: { watch: true, logger: undefined, sys: undefined },
} as unknown as ResolvedEmbeddableConfig);
expect(getContentHash).not.toHaveBeenCalled();
expect(fs.rename).not.toHaveBeenCalled();
});
it("should use dev optimizations when watch mode is enabled", async () => {
await buildTypes({
...config,
dev: { watch: true, logger: undefined, sys: undefined },
} as unknown as ResolvedEmbeddableConfig);
expect(build).toHaveBeenCalledWith({
build: {
emptyOutDir: false,
sourcemap: false,
minify: false,
rollupOptions: { treeshake: false },
lib: {
entry: "resolvedPath",
fileName: "embeddable-types",
formats: ["es"],
},
outDir: "build",
},
logLevel: "error",
});
});
it("should rename the built file with hash when not in watch mode", async () => {
// Reset the mock to control it better
vi.mocked(path.resolve).mockReset();
// Mock the calls in order they happen
vi.mocked(path.resolve)
.mockReturnValueOnce("resolvedPath") // typesFilePath in build()
.mockReturnValueOnce("resolvedPath") // typesFilePath for readFile
.mockReturnValueOnce("build/embeddable-types.js") // source file for rename
.mockReturnValueOnce("build/embeddable-types-somehash.js"); // target file for rename
await buildTypes(config as unknown as ResolvedEmbeddableConfig);
expect(fs.rename).toHaveBeenCalledWith(
"build/embeddable-types.js",
"build/embeddable-types-somehash.js"
);
});
it("should import types from installed libraries if present", async () => {
const configWithLibrary = {
...config,
client: {
...config.client,
componentLibraries: [
{
/* any library config object */
},
],
},
} as unknown as ResolvedEmbeddableConfig;
// Mock getComponentLibraryConfig => returns libraryName: "my-lib"
vi.mocked(getComponentLibraryConfig).mockReturnValue({
libraryName: "my-lib",
include: [],
exclude: [],
});
vi.mocked(fg.sync).mockReturnValue([
"/fake/path/node_modules/my-lib/dist/embeddable-types-abc123.js",
"/fake/path/node_modules/my-lib/dist/embeddable-types-xyz987.js",
]);
// Now run
await buildTypes(configWithLibrary);
const written = vi.mocked(fs.writeFile).mock.calls[0][1];
expect(written).toContain(
`import 'my-lib/dist/embeddable-types-abc123.js';`,
);
expect(written).toContain(
`import 'my-lib/dist/embeddable-types-xyz987.js';`,
);
expect(written).toContain(`import '../relativePath';`);
});
it("should throw if an error occurs when loading a library", async () => {
const configWithLibrary = {
...config,
client: {
...config.client,
componentLibraries: [
{
/* any library config object */
},
],
},
} as unknown as ResolvedEmbeddableConfig;
vi.mocked(getComponentLibraryConfig).mockReturnValue({
libraryName: "my-broken-lib",
include: [],
exclude: [],
});
vi.mocked(fg.sync).mockImplementation(() => {
throw new Error("some library error");
});
await expect(buildTypes(configWithLibrary)).rejects.toThrow(
"some library error",
);
});
});