@embeddable.com/sdk-core 3.13.6 → 3.14.0-next.1

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/lib/push.d.ts CHANGED
@@ -9,7 +9,7 @@ export declare function archive(args: {
9
9
  ctx: ResolvedEmbeddableConfig;
10
10
  filesList: [string, string][];
11
11
  isDev: boolean;
12
- }): Promise<unknown>;
12
+ }): Promise<void>;
13
13
  export declare function createFormData(filePath: string, metadata: Record<string, any>): Promise<import("formdata-node").FormData>;
14
14
  export declare function sendBuildByApiKey(ctx: ResolvedEmbeddableConfig, { apiKey, email, message, }: {
15
15
  apiKey: string;
@@ -1,4 +1,11 @@
1
- import { build, defineConfig, dev, login, push } from "../lib/index.esm.js";
1
+ import {
2
+ build,
3
+ buildPackage,
4
+ defineConfig,
5
+ dev,
6
+ login,
7
+ push,
8
+ } from "../lib/index.esm.js";
2
9
  import { spawn } from "child_process";
3
10
  import path from "path";
4
11
  import fs from "fs";
@@ -9,6 +16,7 @@ const COMMANDS_MAP = {
9
16
  push,
10
17
  dev,
11
18
  defineConfig,
19
+ buildPackage,
12
20
  };
13
21
 
14
22
  export async function main() {
@@ -13,6 +13,7 @@ vi.mock("../lib/index.esm.js", () => ({
13
13
  push: vi.fn().mockResolvedValue(undefined),
14
14
  dev: vi.fn().mockResolvedValue(undefined),
15
15
  defineConfig: vi.fn().mockResolvedValue(undefined),
16
+ buildPackage: vi.fn().mockResolvedValue(undefined),
16
17
  }));
17
18
 
18
19
  describe("entryPoint", () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@embeddable.com/sdk-core",
3
- "version": "3.13.6",
3
+ "version": "3.14.0-next.1",
4
4
  "description": "Core Embeddable SDK module responsible for web-components bundling and publishing.",
5
5
  "keywords": [
6
6
  "embeddable",
@@ -40,7 +40,7 @@
40
40
  },
41
41
  "license": "MIT",
42
42
  "dependencies": {
43
- "@embeddable.com/sdk-utils": "0.7.4",
43
+ "@embeddable.com/sdk-utils": "0.8.0-next.0",
44
44
  "@inquirer/prompts": "^7.2.1",
45
45
  "@stencil/core": "^4.23.0",
46
46
  "@swc-node/register": "^1.10.9",
@@ -51,12 +51,13 @@
51
51
  "fast-glob": "^3.3.2",
52
52
  "finalhandler": "^1.3.1",
53
53
  "formdata-node": "^6.0.3",
54
+ "mergician": "^2.0.2",
54
55
  "minimist": "^1.2.8",
55
56
  "open": "^9.1.0",
56
57
  "ora": "^8.1.1",
57
58
  "serve-static": "^1.16.2",
58
59
  "sorcery": "^1.0.0",
59
- "vite": "^6.2.1",
60
+ "vite": "^6.2.2",
60
61
  "ws": "^8.18.0",
61
62
  "yaml": "^2.6.1"
62
63
  },
package/src/build.test.ts CHANGED
@@ -5,6 +5,7 @@ import buildTypes from "./buildTypes";
5
5
  import provideConfig from "./provideConfig";
6
6
  import generate from "./generate";
7
7
  import cleanup from "./cleanup";
8
+ import buildGlobalHooks from "./buildGlobalHooks";
8
9
 
9
10
  // @ts-ignore
10
11
  import reportErrorToRollbar from "./rollbar.mjs";
@@ -26,6 +27,10 @@ vi.mock("./provideConfig", () => ({
26
27
  default: vi.fn(),
27
28
  }));
28
29
 
30
+ vi.mock("./buildGlobalHooks", () => ({
31
+ default: vi.fn(),
32
+ }));
33
+
29
34
  vi.mock("./buildTypes", () => ({
30
35
  default: vi.fn(),
31
36
  }));
@@ -75,6 +80,7 @@ describe("build", () => {
75
80
  expect(validate).toHaveBeenCalledWith(config);
76
81
  expect(prepare).toHaveBeenCalledWith(config);
77
82
  expect(buildTypes).toHaveBeenCalledWith(config);
83
+ expect(buildGlobalHooks).toHaveBeenCalledWith(config);
78
84
 
79
85
  // Plugin
80
86
  expect(mockPlugin.validate).toHaveBeenCalledWith(config);
package/src/build.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import buildTypes from "./buildTypes";
2
+ import buildGlobalHooks from "./buildGlobalHooks";
2
3
  import prepare from "./prepare";
3
4
  import generate from "./generate";
4
5
  import cleanup from "./cleanup";
@@ -29,6 +30,7 @@ export default async () => {
29
30
  await prepare(config);
30
31
 
31
32
  await buildTypes(config);
33
+ await buildGlobalHooks(config);
32
34
 
33
35
  for (const getPlugin of config.plugins) {
34
36
  const plugin = getPlugin();
@@ -0,0 +1,215 @@
1
+ // buildGlobalHooks.int.test.ts
2
+ import { describe, it, expect, beforeEach, vi } from "vitest";
3
+ import * as fs from "node:fs/promises";
4
+ import * as fsSync from "node:fs";
5
+ import * as vite from "vite";
6
+ import {
7
+ getGlobalHooksMeta,
8
+ getComponentLibraryConfig,
9
+ getContentHash,
10
+ } from "@embeddable.com/sdk-utils";
11
+
12
+ import buildGlobalHooks from "../src/buildGlobalHooks";
13
+ import type { ResolvedEmbeddableConfig } from "./defineConfig";
14
+ import path from "node:path";
15
+
16
+ const rootDir = path.resolve("fake", "root");
17
+ const buildDir = path.resolve("fake", "build");
18
+ const srcDir = path.resolve("fake", "src");
19
+ const lifecycleFile = path.resolve("fake", "root", "embeddable.lifecycle.ts");
20
+ const themFile = path.resolve("fake", "root", "embeddable.theme.ts");
21
+
22
+ // Potential partial mocks, or we can let some logic run real
23
+ vi.mock("node:fs/promises");
24
+ vi.mock("node:fs");
25
+ vi.mock("vite");
26
+ vi.mock("@embeddable.com/sdk-utils", async () => {
27
+ const actual = await vi.importActual<
28
+ typeof import("@embeddable.com/sdk-utils")
29
+ >("@embeddable.com/sdk-utils");
30
+ return {
31
+ ...actual,
32
+ getGlobalHooksMeta: vi.fn(),
33
+ getComponentLibraryConfig: vi.fn(),
34
+ getContentHash: vi.fn(),
35
+ };
36
+ });
37
+
38
+ describe("buildGlobalHooks (Integration Tests)", () => {
39
+ beforeEach(() => {
40
+ vi.clearAllMocks();
41
+ });
42
+
43
+ it("builds aggregator and lifecycle for multiple libraries, then saves meta", async () => {
44
+ // We pretend that the lifecycle file does exist
45
+ vi.spyOn(fsSync, "existsSync").mockImplementation((p: any) => {
46
+ // We'll say yes for lifecycle + local theme
47
+ if (p === lifecycleFile) return true;
48
+ if (p === themFile) return true;
49
+ return false;
50
+ });
51
+
52
+ // aggregator template or any read
53
+ (fs.readFile as any).mockResolvedValueOnce(
54
+ "{{LIBRARY_THEME_IMPORTS}}{{LOCAL_THEME_IMPORT}}",
55
+ );
56
+ // Suppose subsequent reads for the entry files also return "some content"
57
+ (fs.readFile as any).mockResolvedValue("some content");
58
+ // We hash them all
59
+ (getContentHash as any).mockReturnValue("123abc");
60
+ // Suppose vite.build calls are successful
61
+ (vite.build as any).mockResolvedValue(undefined);
62
+
63
+ // For each library
64
+ (getComponentLibraryConfig as any).mockImplementation((cfg: any) => ({
65
+ libraryName: cfg.name,
66
+ }));
67
+
68
+ (getGlobalHooksMeta as any)
69
+ // aggregator calls for library #1
70
+ .mockResolvedValueOnce({
71
+ themeProvider: "libA-theme.js",
72
+ lifecycleHooks: ["libA-lifecycle.js"],
73
+ })
74
+ // aggregator calls for library #2
75
+ .mockResolvedValueOnce({
76
+ themeProvider: "libB-theme.js",
77
+ lifecycleHooks: [],
78
+ })
79
+ // lifecycle calls for library #1
80
+ .mockResolvedValueOnce({
81
+ themeProvider: "libA-theme.js",
82
+ lifecycleHooks: ["libA-lifecycle.js"],
83
+ })
84
+ // lifecycle calls for library #2
85
+ .mockResolvedValueOnce({
86
+ themeProvider: "libB-theme.js",
87
+ lifecycleHooks: ["libB-lifecycle.js"],
88
+ });
89
+
90
+ const ctx: ResolvedEmbeddableConfig = {
91
+ client: {
92
+ srcDir,
93
+ buildDir,
94
+ rootDir,
95
+ lifecycleHooksFile: lifecycleFile,
96
+ customizationFile: themFile,
97
+ componentLibraries: [{ name: "libA" }, { name: "libB" }],
98
+ },
99
+ core: {
100
+ templatesDir: "/fake/templates",
101
+ },
102
+ dev: {
103
+ watch: false, // so we do hashing, no watchers
104
+ },
105
+ } as any;
106
+
107
+ await buildGlobalHooks(ctx);
108
+
109
+ // aggregator => built with "embeddable-theme-123abc"
110
+ expect(vite.build).toHaveBeenCalledWith(
111
+ expect.objectContaining({
112
+ build: expect.objectContaining({
113
+ lib: expect.objectContaining({
114
+ entry: expect.stringContaining("embeddableThemeHook.js"),
115
+ fileName: "embeddable-theme-123abc",
116
+ }),
117
+ }),
118
+ }),
119
+ );
120
+
121
+ // We also expect the lifecycle for the repo => "embeddable.lifecycle.ts"
122
+ expect(vite.build).toHaveBeenCalledWith(
123
+ expect.objectContaining({
124
+ build: expect.objectContaining({
125
+ lib: expect.objectContaining({
126
+ entry: lifecycleFile,
127
+ fileName: "embeddable-lifecycle", // or with hash if code does that
128
+ }),
129
+ }),
130
+ }),
131
+ );
132
+
133
+ // library #1 => has "libA-lifecycle.js"
134
+ // library #2 => has "libB-lifecycle.js"
135
+ // so we expect them to build also
136
+ expect(vite.build).toHaveBeenCalledWith(
137
+ expect.objectContaining({
138
+ build: expect.objectContaining({
139
+ lib: expect.objectContaining({
140
+ entry: path.resolve(
141
+ rootDir,
142
+ "node_modules",
143
+ "libA",
144
+ "dist",
145
+ "libA-lifecycle.js",
146
+ ),
147
+ }),
148
+ }),
149
+ }),
150
+ );
151
+ expect(vite.build).toHaveBeenCalledWith(
152
+ expect.objectContaining({
153
+ build: expect.objectContaining({
154
+ lib: expect.objectContaining({
155
+ entry: path.resolve(
156
+ rootDir,
157
+ "node_modules",
158
+ "libB",
159
+ "dist",
160
+ "libB-lifecycle.js",
161
+ ),
162
+ }),
163
+ }),
164
+ }),
165
+ );
166
+
167
+ // You might also check the final "saveGlobalHooksMeta" by reading the file or checking fsSync calls
168
+ // ...
169
+ });
170
+
171
+ it("skips aggregator if no library has themeProvider, no local theme", async () => {
172
+ vi.spyOn(fsSync, "existsSync").mockImplementation((p: any) => false);
173
+ (fs.readFile as any).mockResolvedValueOnce("{{LIBRARY_THEME_IMPORTS}}");
174
+ (getContentHash as any).mockReturnValue("someHash");
175
+ (vite.build as any).mockResolvedValue(undefined);
176
+
177
+ (getGlobalHooksMeta as any)
178
+ // aggregator calls:
179
+ .mockResolvedValueOnce({ themeProvider: null, lifecycleHooks: [] })
180
+ // lifecycle calls:
181
+ .mockResolvedValueOnce({ themeProvider: null, lifecycleHooks: [] });
182
+
183
+ (getComponentLibraryConfig as any).mockImplementation((cfg: any) => ({
184
+ libraryName: cfg.name,
185
+ }));
186
+
187
+ const ctx: ResolvedEmbeddableConfig = {
188
+ client: {
189
+ srcDir,
190
+ buildDir,
191
+ rootDir,
192
+ lifecycleHooksFile: lifecycleFile,
193
+ customizationFile: themFile,
194
+ componentLibraries: [{ name: "libA" }],
195
+ },
196
+ core: {
197
+ templatesDir: "/fake/templates",
198
+ },
199
+ dev: { watch: false },
200
+ } as any;
201
+
202
+ await buildGlobalHooks(ctx);
203
+
204
+ // aggregator not built
205
+ expect(vite.build).not.toHaveBeenCalledWith(
206
+ expect.objectContaining({
207
+ build: expect.objectContaining({
208
+ lib: expect.objectContaining({
209
+ entry: expect.stringContaining("embeddableThemeHook.js"),
210
+ }),
211
+ }),
212
+ }),
213
+ );
214
+ });
215
+ });
@@ -0,0 +1,252 @@
1
+ import * as fsSync from "node:fs";
2
+ import * as fs from "node:fs/promises";
3
+ import * as path from "node:path";
4
+ import * as vite from "vite";
5
+ import { existsSync } from "node:fs";
6
+ import ora from "ora";
7
+ import {
8
+ EXTERNAL_LIBRARY_GLOBAL_HOOKS_META_NAME,
9
+ getComponentLibraryConfig,
10
+ getContentHash,
11
+ getGlobalHooksMeta,
12
+ } from "@embeddable.com/sdk-utils";
13
+
14
+ import { ResolvedEmbeddableConfig } from "./defineConfig";
15
+ import { RollupWatcher, RollupWatcherEvent } from "rollup";
16
+
17
+ const TEMP_JS_HOOK_FILE = "embeddableThemeHook.js";
18
+ const LIFECYCLE_OUTPUT_NAME = "embeddable-lifecycle";
19
+ const THEME_PROVIDER_OUTPUT_NAME = "embeddable-theme";
20
+
21
+ export default async (ctx: ResolvedEmbeddableConfig) => {
22
+ const watch = ctx.dev?.watch;
23
+ const progress = watch ? undefined : ora("Building global hooks...").start();
24
+
25
+ try {
26
+ const { fileName: themeProvider, watcher: themeWatcher } =
27
+ await buildThemeHook(ctx);
28
+ const { lifecycleHooks, watcher: lifecycleWatcher } =
29
+ await buildLifecycleHooks(ctx);
30
+
31
+ await saveGlobalHooksMeta(ctx, themeProvider, lifecycleHooks);
32
+
33
+ progress?.succeed("Global hooks build completed");
34
+
35
+ return { themeWatcher, lifecycleWatcher };
36
+ } catch (error) {
37
+ progress?.fail("Global hooks build failed");
38
+ throw error;
39
+ }
40
+ };
41
+
42
+ /**
43
+ * Build theme hooks for a given component library.
44
+ */
45
+ async function buildThemeHook(ctx: ResolvedEmbeddableConfig) {
46
+ const componentLibraries = ctx.client.componentLibraries;
47
+ const repoThemeHookExists = existsSync(ctx.client.customizationFile);
48
+ const imports = [];
49
+ const functionNames = [];
50
+ for (let i = 0; i < componentLibraries.length; i++) {
51
+ const libraryConfig = componentLibraries[i];
52
+ const { libraryName } = getComponentLibraryConfig(libraryConfig);
53
+ const libMeta = await getGlobalHooksMeta(ctx, libraryName);
54
+
55
+ const themeProvider = libMeta.themeProvider;
56
+
57
+ if (!themeProvider) continue;
58
+
59
+ // Prepare imports: library theme + repo theme (if exists)
60
+ const functionName = `libraryThemeProvider${i}`;
61
+ const libraryThemeImport = `import ${functionName} from '${libraryName}/dist/${themeProvider}'`;
62
+ functionNames.push(functionName);
63
+ imports.push(libraryThemeImport);
64
+ }
65
+
66
+ if (!imports.length && !repoThemeHookExists) {
67
+ return { fileName: undefined, watcher: undefined };
68
+ }
69
+
70
+ const repoThemeImport = repoThemeHookExists
71
+ ? `import localThemeProvider from '${ctx.client.customizationFile}';`
72
+ : "const localThemeProvider = () => {};";
73
+
74
+ // Generate a temporary file that imports both library and repo theme
75
+ await generateTemporaryHookFile(ctx, imports, functionNames, repoThemeImport);
76
+
77
+ // Build the temporary file with Vite
78
+ const buildResults = await buildWithVite(
79
+ ctx,
80
+ getTempHookFilePath(ctx),
81
+ THEME_PROVIDER_OUTPUT_NAME,
82
+ ctx.dev?.watch,
83
+ !ctx.dev?.watch,
84
+ );
85
+ // Cleanup temporary file
86
+ if (!ctx.dev?.watch) {
87
+ await cleanupTemporaryHookFile(ctx);
88
+ }
89
+
90
+ return buildResults;
91
+ }
92
+
93
+ /**
94
+ * Build theme hooks for a given component library.
95
+ */
96
+ async function buildLifecycleHooks(ctx: ResolvedEmbeddableConfig) {
97
+ const componentLibraries = ctx.client.componentLibraries;
98
+ const builtLifecycleHooks: string[] = [];
99
+ const repoLifecycleExist = existsSync(ctx.client.lifecycleHooksFile);
100
+
101
+ let lifecycleWatcher: RollupWatcher | undefined = undefined;
102
+
103
+ // If lifecycle exists, build it right away to get the hashed output
104
+ if (repoLifecycleExist) {
105
+ const { fileName: repoLifecycleFileName, watcher } = await buildWithVite(
106
+ ctx,
107
+ ctx.client.lifecycleHooksFile,
108
+ LIFECYCLE_OUTPUT_NAME,
109
+ ctx.dev?.watch,
110
+ false,
111
+ );
112
+ if (ctx.dev?.watch) {
113
+ lifecycleWatcher = watcher;
114
+ }
115
+ builtLifecycleHooks.push(repoLifecycleFileName);
116
+ }
117
+ for (const libraryConfig of componentLibraries) {
118
+ const { libraryName } = getComponentLibraryConfig(libraryConfig);
119
+ const libMeta = await getGlobalHooksMeta(ctx, libraryName);
120
+
121
+ const lifecycleHooks = libMeta.lifecycleHooks;
122
+
123
+ for (const lifecycleHook of lifecycleHooks) {
124
+ const libLifecycleHook = path.resolve(
125
+ ctx.client.rootDir,
126
+ "node_modules",
127
+ libraryName,
128
+ "dist",
129
+ lifecycleHook,
130
+ );
131
+ const { fileName: lifecycleHookFileName } = await buildWithVite(
132
+ ctx,
133
+ libLifecycleHook,
134
+ LIFECYCLE_OUTPUT_NAME,
135
+ );
136
+
137
+ builtLifecycleHooks.push(lifecycleHookFileName);
138
+ }
139
+ }
140
+
141
+ return { lifecycleHooks: builtLifecycleHooks, watcher: lifecycleWatcher };
142
+ }
143
+
144
+ /**
145
+ * Write the final global hooks metadata to disk (themeHooksMeta, lifecycleHookMeta).
146
+ */
147
+ async function saveGlobalHooksMeta(
148
+ ctx: ResolvedEmbeddableConfig,
149
+ themeProvider?: string,
150
+ lifecycleHooks?: string[],
151
+ ) {
152
+ const metaFilePath = path.resolve(
153
+ ctx.client.buildDir,
154
+ EXTERNAL_LIBRARY_GLOBAL_HOOKS_META_NAME,
155
+ );
156
+ const data = JSON.stringify({ themeProvider, lifecycleHooks }, null, 2);
157
+ fsSync.writeFileSync(metaFilePath, data);
158
+ }
159
+
160
+ /**
161
+ * Generate a temporary file which imports the library theme and repository theme,
162
+ * replacing template placeholders.
163
+ */
164
+ async function generateTemporaryHookFile(
165
+ ctx: ResolvedEmbeddableConfig,
166
+ libraryThemeImports: string[],
167
+ functionNames: string[],
168
+ repoThemeImport: string,
169
+ ) {
170
+ const templatePath = path.resolve(
171
+ ctx.core.templatesDir,
172
+ "embeddableThemeHook.js.template",
173
+ );
174
+ const templateContent = await fs.readFile(templatePath, "utf8");
175
+
176
+ const newContent = templateContent
177
+ .replace("{{LIBRARY_THEME_IMPORTS}}", libraryThemeImports.join("\n"))
178
+ .replace("{{ARRAY_OF_LIBRARY_THEME_PROVIDERS}}", functionNames.join("\n"))
179
+ .replace("{{LOCAL_THEME_IMPORT}}", repoThemeImport);
180
+
181
+ // Write to temporary hook file
182
+ await fs.writeFile(getTempHookFilePath(ctx), newContent, "utf8");
183
+ }
184
+
185
+ /**
186
+ * Build a file with Vite and return the hashed output file name (e.g., embeddable-theme-xxxx.js).
187
+ */
188
+ async function buildWithVite(
189
+ ctx: ResolvedEmbeddableConfig,
190
+ entryFile: string,
191
+ outputFile: string,
192
+ watch = false,
193
+ useHash = true,
194
+ ) {
195
+ const fileContent = await fs.readFile(entryFile, "utf8");
196
+ const fileHash = getContentHash(fileContent);
197
+ // Bundle using Vite
198
+ const fileName = useHash ? `${outputFile}-${fileHash}` : outputFile;
199
+ const fileWatcher = await vite.build({
200
+ logLevel: watch ? "info" : "error",
201
+ build: {
202
+ emptyOutDir: false,
203
+ lib: {
204
+ entry: entryFile,
205
+ formats: ["es"],
206
+ fileName: fileName,
207
+ },
208
+ outDir: ctx.client.buildDir,
209
+ watch: watch ? {} : undefined,
210
+ },
211
+ });
212
+
213
+ if (watch) {
214
+ await waitForInitialBuild(fileWatcher as RollupWatcher);
215
+ }
216
+
217
+ const watcher: RollupWatcher | undefined = watch
218
+ ? (fileWatcher as RollupWatcher)
219
+ : undefined;
220
+
221
+ return { fileName: `${fileName}.js`, watcher };
222
+ }
223
+
224
+ /**
225
+ * Remove the temporary hook file after building.
226
+ */
227
+ async function cleanupTemporaryHookFile(ctx: ResolvedEmbeddableConfig) {
228
+ await fs.rm(getTempHookFilePath(ctx), { force: true });
229
+ }
230
+
231
+ /**
232
+ * Get the path to the temporary hook file in the build directory.
233
+ */
234
+ function getTempHookFilePath(ctx: ResolvedEmbeddableConfig): string {
235
+ return path.resolve(ctx.client.buildDir, TEMP_JS_HOOK_FILE);
236
+ }
237
+
238
+ function waitForInitialBuild(watcher: RollupWatcher): Promise<void> {
239
+ return new Promise((resolve, reject) => {
240
+ function onEvent(event: RollupWatcherEvent) {
241
+ if (event.code === "END") {
242
+ watcher.off("event", onEvent);
243
+ resolve();
244
+ } else if (event.code === "ERROR") {
245
+ watcher.off("event", onEvent);
246
+ reject(event.error);
247
+ }
248
+ }
249
+
250
+ watcher.on("event", onEvent);
251
+ });
252
+ }