@embeddable.com/sdk-core 3.7.2 → 3.8.0-next.0

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.
@@ -59,6 +59,8 @@ const currentDir = dirname(fileURLToPath(import.meta.url));
59
59
 
60
60
  const isWindows = process.platform === "win32";
61
61
 
62
+ const moduleCache = new Map();
63
+
62
64
  export const resolve = async (specifier, context, nextResolve) => {
63
65
  if (NON_JS_TS_EXTENSIONS.test(specifier)) {
64
66
  const mockModulePath = pathToFileURL(
@@ -72,6 +74,10 @@ export const resolve = async (specifier, context, nextResolve) => {
72
74
  };
73
75
  }
74
76
 
77
+ if (moduleCache.has(specifier)) {
78
+ return moduleCache.get(specifier);
79
+ }
80
+
75
81
  const isTS = EXTENSIONS.some((ext) => specifier.endsWith(ext));
76
82
 
77
83
  // Entry point
@@ -82,11 +88,15 @@ export const resolve = async (specifier, context, nextResolve) => {
82
88
  ? pathToFileURL(specifier.replace(`${processCwd}/file:/`, "")).href
83
89
  : specifier;
84
90
 
85
- return {
91
+ const resolved = {
86
92
  format: isTS ? "ts" : undefined,
87
93
  url: entryPointPath,
88
94
  shortCircuit: true,
89
95
  };
96
+
97
+ moduleCache.set(specifier, resolved);
98
+
99
+ return resolved;
90
100
  }
91
101
 
92
102
  // Determine if this is a package import
@@ -121,13 +131,17 @@ export const resolve = async (specifier, context, nextResolve) => {
121
131
  !resolvedModule.resolvedFileName.includes("/node_modules/") &&
122
132
  EXTENSIONS.includes(resolvedModule.extension)
123
133
  ) {
124
- return {
134
+ const resolved = {
125
135
  format: "ts",
126
136
  url:
127
137
  pathToFileURL(resolvedModule.resolvedFileName).href +
128
138
  `?update=${Date.now()}`,
129
139
  shortCircuit: true,
130
140
  };
141
+
142
+ moduleCache.set(specifier, resolved);
143
+
144
+ return resolved;
131
145
  }
132
146
 
133
147
  // Fallback for non-TS files or unresolved modules
@@ -139,7 +153,13 @@ export const resolve = async (specifier, context, nextResolve) => {
139
153
  return nextResolve(specifierPathOrUrl);
140
154
  };
141
155
 
156
+ const compiledModuleCache = new Map();
157
+
142
158
  export const load = async (url, context, nextLoad) => {
159
+ if (compiledModuleCache.has(url)) {
160
+ return compiledModuleCache.get(url);
161
+ }
162
+
143
163
  if (context.format === "ts") {
144
164
  const { source } = await nextLoad(url, context);
145
165
  const code =
@@ -147,11 +167,15 @@ export const load = async (url, context, nextLoad) => {
147
167
 
148
168
  // Compile the code using @swc-node with the customer's tsconfig
149
169
  const compiled = await compile(code, fileURLToPath(url), tsconfig, true);
150
- return {
170
+ const resolved = {
151
171
  format: "module",
152
172
  source: compiled,
153
173
  shortCircuit: true,
154
174
  };
175
+
176
+ compiledModuleCache.set(url, resolved);
177
+
178
+ return resolved;
155
179
  } else {
156
180
  if (
157
181
  isWindows &&
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@embeddable.com/sdk-core",
3
- "version": "3.7.2",
3
+ "version": "3.8.0-next.0",
4
4
  "description": "Core Embeddable SDK module responsible for web-components bundling and publishing.",
5
5
  "keywords": [
6
6
  "embeddable",
@@ -41,7 +41,7 @@
41
41
  "@embeddable.com/sdk-utils": "*",
42
42
  "@inquirer/select": "^1.3.0",
43
43
  "@stencil/core": "^4.18.2",
44
- "@swc-node/register": "^1.9.0",
44
+ "@swc-node/register": "^1.10.9",
45
45
  "archiver": "^5.3.1",
46
46
  "axios": "^1.7.2",
47
47
  "chokidar": "^3.6.0",
@@ -0,0 +1,102 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach, Mock } from "vitest";
2
+ import * as http from "node:http";
3
+ import axios from "axios";
4
+ import provideConfig from "./provideConfig";
5
+ import { getToken } from "./login";
6
+ import * as chokidar from "chokidar";
7
+ import dev from "./dev";
8
+ import { checkNodeVersion } from "./utils";
9
+ import { createManifest } from "./cleanup";
10
+ import prepare from "./prepare";
11
+ import { WebSocketServer } from "ws";
12
+
13
+ // Mock dependencies
14
+ vi.mock("./buildTypes", () => ({ default: vi.fn() }));
15
+ vi.mock("./prepare", () => ({ default: vi.fn(), removeIfExists: vi.fn() }));
16
+ vi.mock("./generate", () => ({ default: vi.fn() }));
17
+ vi.mock("./provideConfig", () => ({ default: vi.fn() }));
18
+ vi.mock("@stencil/core/sys/node", () => ({
19
+ createNodeLogger: vi.fn(),
20
+ createNodeSys: vi.fn(),
21
+ }));
22
+ vi.mock("ws", () => ({ WebSocketServer: vi.fn() }));
23
+ vi.mock("chokidar", () => ({ watch: vi.fn() }));
24
+ vi.mock("./login", () => ({ getToken: vi.fn(), default: vi.fn() }));
25
+ vi.mock("axios", () => ({ default: { get: vi.fn() } }));
26
+ vi.mock("@embeddable.com/sdk-utils", () => ({ findFiles: vi.fn() }));
27
+ vi.mock("./push", () => ({ archive: vi.fn(), sendBuild: vi.fn() }));
28
+ vi.mock("./validate", () => ({ default: vi.fn() }));
29
+ vi.mock("./utils", () => ({ checkNodeVersion: vi.fn() }));
30
+ vi.mock("./cleanup", () => ({ createManifest: vi.fn() }));
31
+ vi.mock("node:http", () => ({
32
+ createServer: vi.fn(() => ({ listen: vi.fn() })),
33
+ }));
34
+
35
+ const mockConfig = {
36
+ client: {
37
+ rootDir: "/mock/root",
38
+ buildDir: "/mock/root/.embeddable-dev-build",
39
+ componentDir: "/mock/root/.embeddable-dev-build/component",
40
+ stencilBuild: "/mock/root/.embeddable-dev-build/dist/embeddable-wrapper",
41
+ tmpDir: "/mock/root/.embeddable-dev-tmp",
42
+ selfServeCustomizationDir: "/mock/root/self-serve",
43
+ modelsSrc: "/mock/root/models",
44
+ },
45
+ plugins: [],
46
+ previewBaseUrl: "http://preview.example.com",
47
+ pushBaseUrl: "http://push.example.com",
48
+ };
49
+
50
+ describe("dev command", () => {
51
+ let listenMock: Mock;
52
+ beforeEach(() => {
53
+ listenMock = vi.fn();
54
+ vi.mocked(http.createServer).mockImplementation(
55
+ () =>
56
+ ({
57
+ listen: listenMock,
58
+ }) as any,
59
+ );
60
+ vi.mocked(WebSocketServer).mockImplementation(() => {
61
+ return {
62
+ clients: [],
63
+ on: vi.fn(),
64
+ } as any;
65
+ });
66
+ vi.mocked(chokidar.watch).mockReturnValue({
67
+ on: vi.fn(),
68
+ } as any);
69
+ // Mock process.on to avoid actually setting up process listeners
70
+ vi.spyOn(process, "on").mockImplementation(() => process);
71
+
72
+ vi.mocked(provideConfig).mockResolvedValue(mockConfig);
73
+ vi.mocked(getToken).mockResolvedValue("mock-token");
74
+ vi.mocked(axios.get).mockResolvedValue({
75
+ data: "mock-workspace",
76
+ });
77
+ });
78
+
79
+ afterEach(() => {
80
+ vi.restoreAllMocks();
81
+ });
82
+
83
+ it("should set up the development environment", async () => {
84
+ // Run the dev command
85
+ await dev();
86
+
87
+ // Verify that the necessary functions were called
88
+ expect(checkNodeVersion).toHaveBeenCalled();
89
+ expect(prepare).toHaveBeenCalled();
90
+ expect(http.createServer).toHaveBeenCalled();
91
+ expect(WebSocketServer).toHaveBeenCalled();
92
+
93
+ // Verify that the server was set up to listen on the correct port
94
+ expect(listenMock).toHaveBeenCalledWith(8926, expect.any(Function));
95
+
96
+ // Call the listen callback to simulate the server being set up
97
+ listenMock.mock.calls[0][1]();
98
+ expect(createManifest).toHaveBeenCalled();
99
+
100
+ await expect.poll(() => chokidar.watch).toBeCalledTimes(2);
101
+ });
102
+ });
package/src/dev.ts CHANGED
@@ -11,6 +11,7 @@ import {
11
11
  createNodeSys,
12
12
  } from "@stencil/core/sys/node";
13
13
  import { RollupWatcher } from "rollup";
14
+ import * as http from "node:http";
14
15
  import { IncomingMessage, Server, ServerResponse } from "http";
15
16
  import { ChildProcess } from "node:child_process";
16
17
  import { WebSocketServer, Server as WSServer } from "ws";
@@ -56,16 +57,24 @@ const addToGitingore = async () => {
56
57
  }
57
58
  };
58
59
 
60
+ const chokidarWatchOptions = {
61
+ ignoreInitial: true,
62
+ usePolling: false, // Ensure polling is disabled
63
+ awaitWriteFinish: {
64
+ stabilityThreshold: 200,
65
+ pollInterval: 100,
66
+ },
67
+ };
68
+
59
69
  export default async () => {
60
70
  checkNodeVersion();
61
71
  addToGitingore();
62
72
 
63
- const http = require("http");
64
73
  ora = (await oraP).default;
65
74
 
66
75
  process.on("warning", (e) => console.warn(e.stack));
67
76
 
68
- const logger = createNodeLogger({ process });
77
+ const logger = createNodeLogger();
69
78
  const sys = createNodeSys({ process });
70
79
 
71
80
  const defaultConfig = await provideConfig();
@@ -173,6 +182,7 @@ export default async () => {
173
182
 
174
183
  await plugin.validate(config);
175
184
  const watcher = await plugin.build(config);
185
+
176
186
  await configureWatcher(watcher, config);
177
187
  watchers.push(watcher);
178
188
  }
@@ -243,9 +253,7 @@ const onBundleBuildEnd = async (ctx: any) => {
243
253
  const dataModelAndSecurityContextWatcher = (ctx: any): FSWatcher => {
244
254
  const fsWatcher = chokidar.watch(
245
255
  [path.resolve(ctx.client.modelsSrc, "**/*.{cube,sc}.{yaml,yml,js}")],
246
- {
247
- ignoreInitial: true,
248
- },
256
+ chokidarWatchOptions,
249
257
  );
250
258
  fsWatcher.on("all", async () => {
251
259
  await sendDataModelsAndSecurityContextsChanges(ctx);
@@ -260,9 +268,7 @@ const customSelfServeWatcher = (ctx: any): FSWatcher => {
260
268
  path.resolve(ctx.client.selfServeCustomizationDir, "style.css"),
261
269
  path.resolve(ctx.client.selfServeCustomizationDir, "*.svg"),
262
270
  ],
263
- {
264
- ignoreInitial: true,
265
- },
271
+ chokidarWatchOptions,
266
272
  );
267
273
 
268
274
  fsWatcher.on("all", async () => {
@@ -79,7 +79,9 @@ describe("generate", () => {
79
79
  config: {},
80
80
  } as any);
81
81
  vi.mocked(createCompiler).mockResolvedValue({
82
- build: vi.fn(),
82
+ build: vi.fn().mockResolvedValue({
83
+ hasError: false,
84
+ }),
83
85
  destroy: vi.fn(),
84
86
  } as any);
85
87
 
@@ -112,4 +114,51 @@ describe("generate", () => {
112
114
  "stencilBuild/embeddable-wrapper.esm-hash.js",
113
115
  );
114
116
  });
117
+
118
+ it("should generate bundle in dev mode", async () => {
119
+ const ctx = {
120
+ ...config,
121
+ dev: {
122
+ logger: vi.fn(),
123
+ sys: vi.fn(),
124
+ },
125
+ };
126
+
127
+ vi.mocked(fs.readFile).mockResolvedValue(
128
+ "replace-this-with-component-name",
129
+ );
130
+ await generate(ctx, "sdk-react");
131
+
132
+ expect(createCompiler).toHaveBeenCalled();
133
+
134
+ expect(fs.writeFile).toHaveBeenCalledWith(
135
+ "componentDir/component.tsx",
136
+ expect.stringContaining("embeddable-component"),
137
+ );
138
+
139
+ expect(loadConfig).toHaveBeenCalledWith({
140
+ config: {
141
+ configPath: "buildDir/stencil.config.ts",
142
+ devMode: true,
143
+ enableCache: true,
144
+ maxConcurrentWorkers: 8,
145
+ minifyCss: false,
146
+ minifyJs: false,
147
+ namespace: "embeddable-wrapper",
148
+ outputTargets: [
149
+ {
150
+ type: "dist",
151
+ },
152
+ ],
153
+ rootDir: "buildDir",
154
+ sourceMap: false,
155
+ srcDir: "buildDir/component",
156
+ tsconfig: "buildDir/tsconfig.json",
157
+ watchDirs: ["buildDir/buildName"],
158
+ },
159
+ initTsConfig: true,
160
+ logger: expect.any(Function),
161
+ sys: expect.any(Function),
162
+ });
163
+ });
115
164
  });
package/src/generate.ts CHANGED
@@ -96,8 +96,8 @@ async function addComponentTagName(filePath: string, bundleHash: string) {
96
96
  ]);
97
97
  }
98
98
 
99
- async function runStencil(ctx: any) {
100
- const logger = ctx.dev?.logger || createNodeLogger({ process });
99
+ async function runStencil(ctx: any): Promise<void> {
100
+ const logger = ctx.dev?.logger || createNodeLogger();
101
101
  const sys = ctx.dev?.sys || createNodeSys({ process });
102
102
  const devMode = !!ctx.dev;
103
103
 
@@ -109,23 +109,56 @@ async function runStencil(ctx: any) {
109
109
  sys,
110
110
  config: {
111
111
  devMode,
112
+ enableCache: true,
112
113
  maxConcurrentWorkers: isWindows ? 0 : 8, // workers break on windows
113
114
  rootDir: ctx.client.buildDir,
114
115
  configPath: path.resolve(ctx.client.buildDir, "stencil.config.ts"),
115
116
  tsconfig: path.resolve(ctx.client.buildDir, "tsconfig.json"),
116
117
  namespace: "embeddable-wrapper",
117
118
  srcDir: path.resolve(ctx.client.buildDir, "component"),
119
+ sourceMap: !devMode,
120
+ minifyJs: !devMode,
121
+ minifyCss: !devMode,
118
122
  outputTargets: [
119
123
  {
120
124
  type: "dist",
121
125
  },
122
126
  ],
127
+ watchDirs: [
128
+ path.resolve(
129
+ ctx.client.buildDir,
130
+ ctx["sdk-react"].outputOptions.buildName,
131
+ ),
132
+ ],
123
133
  },
124
134
  });
125
135
 
126
136
  const compiler = await createCompiler(validated.config);
137
+ const buildResults = await compiler.build();
138
+
139
+ if (devMode) {
140
+ // Handle process exit to clean up resources
141
+ const cleanUp = async () => {
142
+ await compiler.destroy();
143
+ process.exit(0);
144
+ };
145
+
146
+ process.on("SIGINT", cleanUp);
147
+ process.on("SIGTERM", cleanUp);
148
+ } else {
149
+ if (buildResults.hasError) {
150
+ console.error("Stencil build error:", buildResults.diagnostics);
151
+ throw new Error("Stencil build error");
152
+ } else {
153
+ await handleStencilBuildOutput(ctx);
154
+ }
155
+ await compiler.destroy();
156
+ }
127
157
 
128
- await compiler.build();
158
+ process.chdir(ctx.client.rootDir);
159
+ }
160
+
161
+ async function handleStencilBuildOutput(ctx: any) {
129
162
  const entryFilePath = path.resolve(
130
163
  ctx.client.stencilBuild,
131
164
  "embeddable-wrapper.esm.js",
@@ -149,9 +182,6 @@ async function runStencil(ctx: any) {
149
182
  entryFilePath,
150
183
  path.resolve(ctx.client.stencilBuild, fileName),
151
184
  );
152
-
153
- await compiler.destroy();
154
- process.chdir(ctx.client.rootDir);
155
185
  }
156
186
 
157
187
  async function generateSourceMap(ctx: any, pluginName: string) {