@embeddable.com/sdk-core 4.3.3 → 4.4.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/defineConfig.d.ts +10 -0
- package/lib/index.d.ts +1 -1
- package/lib/index.esm.js +44 -10
- package/lib/index.esm.js.map +1 -1
- package/lib/push.d.ts +1 -0
- package/package.json +1 -1
- package/src/defineConfig.test.ts +1 -0
- package/src/defineConfig.ts +10 -0
- package/src/dev.test.ts +149 -2
- package/src/dev.ts +28 -3
- package/src/generate.test.ts +65 -3
- package/src/generate.ts +5 -2
- package/src/index.ts +1 -1
- package/src/push.test.ts +163 -3
- package/src/push.ts +20 -2
- package/templates/component.tsx.template +13 -4
package/lib/push.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { ResolvedEmbeddableConfig } from "./defineConfig";
|
|
|
3
3
|
export declare const CUBE_FILES: RegExp;
|
|
4
4
|
export declare const CLIENT_CONTEXT_FILES: RegExp;
|
|
5
5
|
export declare const SECURITY_CONTEXT_FILES: RegExp;
|
|
6
|
+
export declare const EMBEDDABLE_FILES: RegExp;
|
|
6
7
|
declare const _default: () => Promise<void>;
|
|
7
8
|
export default _default;
|
|
8
9
|
export declare function buildArchive(config: ResolvedEmbeddableConfig): Promise<Ora>;
|
package/package.json
CHANGED
package/src/defineConfig.test.ts
CHANGED
package/src/defineConfig.ts
CHANGED
|
@@ -19,6 +19,7 @@ export type EmbeddableConfig = {
|
|
|
19
19
|
})[];
|
|
20
20
|
pushModels?: boolean;
|
|
21
21
|
pushComponents?: boolean;
|
|
22
|
+
pushEmbeddables?: boolean;
|
|
22
23
|
pushBaseUrl?: string;
|
|
23
24
|
audienceUrl?: string;
|
|
24
25
|
authDomain?: string;
|
|
@@ -83,6 +84,7 @@ export type ResolvedEmbeddableConfig = {
|
|
|
83
84
|
};
|
|
84
85
|
pushModels: boolean;
|
|
85
86
|
pushComponents: boolean;
|
|
87
|
+
pushEmbeddables: boolean;
|
|
86
88
|
pushBaseUrl: string;
|
|
87
89
|
audienceUrl: string;
|
|
88
90
|
previewBaseUrl: string;
|
|
@@ -107,9 +109,14 @@ export type ResolvedEmbeddableConfig = {
|
|
|
107
109
|
buildName: string;
|
|
108
110
|
componentsEntryPointFilename: string;
|
|
109
111
|
};
|
|
112
|
+
pluginFlags?: PluginFlags;
|
|
110
113
|
};
|
|
111
114
|
};
|
|
112
115
|
|
|
116
|
+
export type PluginFlags = {
|
|
117
|
+
supportsOnComponentReadyHook: boolean;
|
|
118
|
+
}
|
|
119
|
+
|
|
113
120
|
const REGION_CONFIGS = {
|
|
114
121
|
EU: {
|
|
115
122
|
pushBaseUrl: "https://api.eu.embeddable.com",
|
|
@@ -152,6 +159,7 @@ export const embeddableConfigSchema = z
|
|
|
152
159
|
).optional(),
|
|
153
160
|
pushModels: z.boolean().optional(),
|
|
154
161
|
pushComponents: z.boolean().optional(),
|
|
162
|
+
pushEmbeddables: z.boolean().optional(),
|
|
155
163
|
pushBaseUrl: z.string().optional(),
|
|
156
164
|
audienceUrl: z.string().optional(),
|
|
157
165
|
authDomain: z.string().optional(),
|
|
@@ -202,6 +210,7 @@ export default (config: EmbeddableConfig) => {
|
|
|
202
210
|
pushModels = true,
|
|
203
211
|
starterEmbeddables,
|
|
204
212
|
pushComponents = true,
|
|
213
|
+
pushEmbeddables = true,
|
|
205
214
|
pushBaseUrl,
|
|
206
215
|
audienceUrl,
|
|
207
216
|
authDomain,
|
|
@@ -308,6 +317,7 @@ export default (config: EmbeddableConfig) => {
|
|
|
308
317
|
starterEmbeddables,
|
|
309
318
|
pushModels,
|
|
310
319
|
pushComponents,
|
|
320
|
+
pushEmbeddables,
|
|
311
321
|
pushBaseUrl: pushBaseUrl ?? regionConfig.pushBaseUrl,
|
|
312
322
|
audienceUrl: audienceUrl ?? regionConfig.audienceUrl,
|
|
313
323
|
previewBaseUrl: previewBaseUrl ?? regionConfig.previewBaseUrl,
|
package/src/dev.test.ts
CHANGED
|
@@ -29,7 +29,8 @@ import { logError } from "./logger";
|
|
|
29
29
|
import { ResolvedEmbeddableConfig } from "./defineConfig";
|
|
30
30
|
import { RollupWatcher } from "rollup";
|
|
31
31
|
import ora from "ora";
|
|
32
|
-
import { archive } from "./push";
|
|
32
|
+
import { archive, EMBEDDABLE_FILES } from "./push";
|
|
33
|
+
import fg from "fast-glob";
|
|
33
34
|
import { selectWorkspace } from "./workspaceUtils";
|
|
34
35
|
import serveStatic from "serve-static";
|
|
35
36
|
import { createNodeSys } from "@stencil/core/sys/node";
|
|
@@ -107,6 +108,8 @@ vi.mock("serve-static", () => ({
|
|
|
107
108
|
default: vi.fn(() => vi.fn()),
|
|
108
109
|
}));
|
|
109
110
|
|
|
111
|
+
vi.mock("fast-glob", () => ({ default: vi.fn() }));
|
|
112
|
+
|
|
110
113
|
vi.mock("./dev", async (importOriginal) => {
|
|
111
114
|
const actual = await importOriginal<typeof dev>();
|
|
112
115
|
return {
|
|
@@ -221,6 +224,8 @@ describe("dev command", () => {
|
|
|
221
224
|
["mock-model.json", "/mock/root/models/mock-model.json"],
|
|
222
225
|
]);
|
|
223
226
|
|
|
227
|
+
vi.mocked(fg).mockResolvedValue([]);
|
|
228
|
+
|
|
224
229
|
// Mock fs functions
|
|
225
230
|
vi.mocked(fs.readFile).mockResolvedValue("default content");
|
|
226
231
|
vi.mocked(fs.appendFile).mockResolvedValue(undefined);
|
|
@@ -417,6 +422,49 @@ describe("dev command", () => {
|
|
|
417
422
|
isDev: true,
|
|
418
423
|
});
|
|
419
424
|
});
|
|
425
|
+
|
|
426
|
+
it("should include embeddable files in archive when pushEmbeddables is true", async () => {
|
|
427
|
+
const embeddableConfig = {
|
|
428
|
+
...mockConfig,
|
|
429
|
+
pushEmbeddables: true,
|
|
430
|
+
client: { ...mockConfig.client, srcDir: "/mock/src" },
|
|
431
|
+
} as unknown as ResolvedEmbeddableConfig;
|
|
432
|
+
|
|
433
|
+
vi.mocked(findFiles).mockImplementation(async (_dir, pattern) => {
|
|
434
|
+
if (pattern === EMBEDDABLE_FILES) {
|
|
435
|
+
return [["dashboard.embeddable.yaml", "/mock/src/dashboard.embeddable.yaml"]];
|
|
436
|
+
}
|
|
437
|
+
return [["mock-model.json", "/mock/root/models/mock-model.json"]];
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
await sendBuildChanges(embeddableConfig);
|
|
441
|
+
|
|
442
|
+
expect(findFiles).toHaveBeenCalledWith("/mock/src", EMBEDDABLE_FILES);
|
|
443
|
+
expect(archive).toHaveBeenCalledWith(
|
|
444
|
+
expect.objectContaining({
|
|
445
|
+
filesList: expect.arrayContaining([
|
|
446
|
+
["dashboard.embeddable.yaml", "/mock/src/dashboard.embeddable.yaml"],
|
|
447
|
+
]),
|
|
448
|
+
}),
|
|
449
|
+
);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it("should not call findFiles with EMBEDDABLE_FILES when pushEmbeddables is false", async () => {
|
|
453
|
+
const embeddableConfig = {
|
|
454
|
+
...mockConfig,
|
|
455
|
+
pushEmbeddables: false,
|
|
456
|
+
client: { ...mockConfig.client, srcDir: "/mock/src" },
|
|
457
|
+
} as unknown as ResolvedEmbeddableConfig;
|
|
458
|
+
|
|
459
|
+
vi.mocked(findFiles).mockClear();
|
|
460
|
+
|
|
461
|
+
await sendBuildChanges(embeddableConfig);
|
|
462
|
+
|
|
463
|
+
const embeddableFilesCall = vi.mocked(findFiles).mock.calls.find(
|
|
464
|
+
(call) => call[1] === EMBEDDABLE_FILES,
|
|
465
|
+
);
|
|
466
|
+
expect(embeddableFilesCall).toBeUndefined();
|
|
467
|
+
});
|
|
420
468
|
});
|
|
421
469
|
|
|
422
470
|
describe("sendBuildChanges error handling", () => {
|
|
@@ -439,7 +487,7 @@ describe("dev command", () => {
|
|
|
439
487
|
await sendBuildChanges(mockConfig as unknown as ResolvedEmbeddableConfig);
|
|
440
488
|
|
|
441
489
|
expect(ora().fail).toHaveBeenCalledWith(
|
|
442
|
-
`Data models and/or security context synchronization failed with error: ${error.message}`,
|
|
490
|
+
`Data models and/or security context and/or embeddables synchronization failed with error: ${error.message}`,
|
|
443
491
|
);
|
|
444
492
|
expect(mockWss.clients[0].send).toHaveBeenCalledWith(
|
|
445
493
|
JSON.stringify({ type: "dataModelsAndOrSecurityContextUpdateError", error: error.message }),
|
|
@@ -447,6 +495,77 @@ describe("dev command", () => {
|
|
|
447
495
|
});
|
|
448
496
|
});
|
|
449
497
|
|
|
498
|
+
describe("cubeSecurityContextAndClientContextWatcher (pushEmbeddables)", () => {
|
|
499
|
+
it("should call fg with embeddable pattern and pass results to chokidar.watch when pushEmbeddables is true", async () => {
|
|
500
|
+
const mockEmbeddableFiles = [
|
|
501
|
+
"/mock/src/dashboard.embeddable.yaml",
|
|
502
|
+
"/mock/src/report.embeddable.yml",
|
|
503
|
+
];
|
|
504
|
+
|
|
505
|
+
vi.mocked(fg).mockImplementation(async (pattern: any) => {
|
|
506
|
+
if (pattern === "**/*.embeddable.{yaml,yml}") {
|
|
507
|
+
return mockEmbeddableFiles as any;
|
|
508
|
+
}
|
|
509
|
+
return [];
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
vi.mocked(provideConfig).mockResolvedValue({
|
|
513
|
+
...mockConfig,
|
|
514
|
+
pushEmbeddables: true,
|
|
515
|
+
client: { ...mockConfig.client, srcDir: "/mock/src" },
|
|
516
|
+
} as unknown as ResolvedEmbeddableConfig);
|
|
517
|
+
|
|
518
|
+
await dev();
|
|
519
|
+
await listenMock.mock.calls[0][1]();
|
|
520
|
+
|
|
521
|
+
expect(vi.mocked(fg)).toHaveBeenCalledWith(
|
|
522
|
+
"**/*.embeddable.{yaml,yml}",
|
|
523
|
+
expect.objectContaining({ cwd: "/mock/src", absolute: true }),
|
|
524
|
+
);
|
|
525
|
+
|
|
526
|
+
expect(chokidar.watch).toHaveBeenCalledWith(
|
|
527
|
+
expect.arrayContaining(mockEmbeddableFiles),
|
|
528
|
+
expect.anything(),
|
|
529
|
+
);
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
it("should not call fg with embeddable pattern when pushEmbeddables is false", async () => {
|
|
533
|
+
vi.mocked(provideConfig).mockResolvedValue({
|
|
534
|
+
...mockConfig,
|
|
535
|
+
pushEmbeddables: false,
|
|
536
|
+
client: { ...mockConfig.client, srcDir: "/mock/src" },
|
|
537
|
+
} as unknown as ResolvedEmbeddableConfig);
|
|
538
|
+
|
|
539
|
+
vi.mocked(fg).mockClear();
|
|
540
|
+
|
|
541
|
+
await dev();
|
|
542
|
+
await listenMock.mock.calls[0][1]();
|
|
543
|
+
|
|
544
|
+
const embeddableCall = vi.mocked(fg).mock.calls.find(
|
|
545
|
+
(call) => call[0] === "**/*.embeddable.{yaml,yml}",
|
|
546
|
+
);
|
|
547
|
+
expect(embeddableCall).toBeUndefined();
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
it("should not call fg with embeddable pattern when pushEmbeddables is undefined", async () => {
|
|
551
|
+
vi.mocked(provideConfig).mockResolvedValue({
|
|
552
|
+
...mockConfig,
|
|
553
|
+
// pushEmbeddables not set → undefined → falsy
|
|
554
|
+
client: { ...mockConfig.client, srcDir: "/mock/src" },
|
|
555
|
+
} as unknown as ResolvedEmbeddableConfig);
|
|
556
|
+
|
|
557
|
+
vi.mocked(fg).mockClear();
|
|
558
|
+
|
|
559
|
+
await dev();
|
|
560
|
+
await listenMock.mock.calls[0][1]();
|
|
561
|
+
|
|
562
|
+
const embeddableCall = vi.mocked(fg).mock.calls.find(
|
|
563
|
+
(call) => call[0] === "**/*.embeddable.{yaml,yml}",
|
|
564
|
+
);
|
|
565
|
+
expect(embeddableCall).toBeUndefined();
|
|
566
|
+
});
|
|
567
|
+
});
|
|
568
|
+
|
|
450
569
|
describe("Plugin build coordination", () => {
|
|
451
570
|
it("should handle configs with no plugins when pushComponents is true", async () => {
|
|
452
571
|
vi.mocked(provideConfig).mockResolvedValue({
|
|
@@ -984,6 +1103,34 @@ describe("dev command", () => {
|
|
|
984
1103
|
|
|
985
1104
|
process.argv = originalArgv;
|
|
986
1105
|
});
|
|
1106
|
+
|
|
1107
|
+
it("should include pushEmbeddables in the dev-workspace request body when true", async () => {
|
|
1108
|
+
vi.mocked(provideConfig).mockResolvedValue({
|
|
1109
|
+
...mockConfig,
|
|
1110
|
+
pushEmbeddables: true,
|
|
1111
|
+
} as unknown as ResolvedEmbeddableConfig);
|
|
1112
|
+
|
|
1113
|
+
await dev();
|
|
1114
|
+
|
|
1115
|
+
const devWorkspaceCall = vi.mocked(axios.post).mock.calls.find(
|
|
1116
|
+
(call) => String(call[0]).includes("dev-workspace"),
|
|
1117
|
+
);
|
|
1118
|
+
expect(devWorkspaceCall?.[1]).toMatchObject({ pushEmbeddables: true });
|
|
1119
|
+
});
|
|
1120
|
+
|
|
1121
|
+
it("should include pushEmbeddables in the dev-workspace request body when false", async () => {
|
|
1122
|
+
vi.mocked(provideConfig).mockResolvedValue({
|
|
1123
|
+
...mockConfig,
|
|
1124
|
+
pushEmbeddables: false,
|
|
1125
|
+
} as unknown as ResolvedEmbeddableConfig);
|
|
1126
|
+
|
|
1127
|
+
await dev();
|
|
1128
|
+
|
|
1129
|
+
const devWorkspaceCall = vi.mocked(axios.post).mock.calls.find(
|
|
1130
|
+
(call) => String(call[0]).includes("dev-workspace"),
|
|
1131
|
+
);
|
|
1132
|
+
expect(devWorkspaceCall?.[1]).toMatchObject({ pushEmbeddables: false });
|
|
1133
|
+
});
|
|
987
1134
|
});
|
|
988
1135
|
|
|
989
1136
|
describe("addToGitignore", () => {
|
package/src/dev.ts
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
sendBuild,
|
|
29
29
|
SECURITY_CONTEXT_FILES,
|
|
30
30
|
CLIENT_CONTEXT_FILES,
|
|
31
|
+
EMBEDDABLE_FILES,
|
|
31
32
|
} from "./push";
|
|
32
33
|
import validate from "./validate";
|
|
33
34
|
import { checkNodeVersion } from "./utils";
|
|
@@ -494,6 +495,14 @@ const cubeSecurityContextAndClientContextWatcher = async (
|
|
|
494
495
|
filesToWatch = [...filesToWatch, ...cubeFiles, ...securityContextFiles];
|
|
495
496
|
}
|
|
496
497
|
|
|
498
|
+
if (ctx.pushEmbeddables) {
|
|
499
|
+
const embeddableFiles = await fg("**/*.embeddable.{yaml,yml}", {
|
|
500
|
+
cwd: ctx.client.srcDir,
|
|
501
|
+
absolute: true,
|
|
502
|
+
});
|
|
503
|
+
filesToWatch = [...filesToWatch, ...embeddableFiles];
|
|
504
|
+
}
|
|
505
|
+
|
|
497
506
|
const fsWatcher = chokidar.watch(filesToWatch, chokidarWatchOptions);
|
|
498
507
|
|
|
499
508
|
fsWatcher.on("all", () => sendBuildChanges(ctx));
|
|
@@ -526,7 +535,7 @@ export const sendBuildChanges = async (ctx: ResolvedEmbeddableConfig) => {
|
|
|
526
535
|
sendMessage("dataModelsAndOrSecurityContextUpdateStart");
|
|
527
536
|
|
|
528
537
|
const sending = ora(
|
|
529
|
-
"Synchronising data models and/or security contexts...",
|
|
538
|
+
"Synchronising data models and/or security contexts and/or embeddables...",
|
|
530
539
|
).start();
|
|
531
540
|
|
|
532
541
|
let filesList: [string, string][] = [];
|
|
@@ -567,6 +576,21 @@ export const sendBuildChanges = async (ctx: ResolvedEmbeddableConfig) => {
|
|
|
567
576
|
filesList = [...filesList, ...cubeAndSecurityContextFileList];
|
|
568
577
|
}
|
|
569
578
|
|
|
579
|
+
if (ctx.pushEmbeddables) {
|
|
580
|
+
const embeddableFilesList = await findFiles(
|
|
581
|
+
ctx.client.srcDir,
|
|
582
|
+
EMBEDDABLE_FILES,
|
|
583
|
+
);
|
|
584
|
+
|
|
585
|
+
filesList = [
|
|
586
|
+
...filesList,
|
|
587
|
+
...embeddableFilesList.map((entry): [string, string] => [
|
|
588
|
+
path.basename(entry[1]),
|
|
589
|
+
entry[1],
|
|
590
|
+
]),
|
|
591
|
+
];
|
|
592
|
+
}
|
|
593
|
+
|
|
570
594
|
try {
|
|
571
595
|
const token = await getToken();
|
|
572
596
|
await archive({
|
|
@@ -578,12 +602,12 @@ export const sendBuildChanges = async (ctx: ResolvedEmbeddableConfig) => {
|
|
|
578
602
|
} catch (e: any) {
|
|
579
603
|
const errorMessage = e.response?.data?.errorMessage ?? e.message ?? "Unknown error";
|
|
580
604
|
sending.fail(
|
|
581
|
-
`Data models and/or security context synchronization failed with error: ${errorMessage}`,
|
|
605
|
+
`Data models and/or security context and/or embeddables synchronization failed with error: ${errorMessage}`,
|
|
582
606
|
);
|
|
583
607
|
return sendMessage("dataModelsAndOrSecurityContextUpdateError", { error: errorMessage });
|
|
584
608
|
}
|
|
585
609
|
|
|
586
|
-
sending.succeed(`Data models and/or security context synchronized`);
|
|
610
|
+
sending.succeed(`Data models and/or security context and/or embeddables synchronized`);
|
|
587
611
|
sendMessage("dataModelsAndOrSecurityContextUpdateSuccess");
|
|
588
612
|
};
|
|
589
613
|
|
|
@@ -637,6 +661,7 @@ const getPreviewWorkspace = async (
|
|
|
637
661
|
instanceUrl,
|
|
638
662
|
pushModels: ctx.pushModels,
|
|
639
663
|
pushComponents: ctx.pushComponents,
|
|
664
|
+
pushEmbeddables: ctx.pushEmbeddables,
|
|
640
665
|
},
|
|
641
666
|
{
|
|
642
667
|
headers: {
|
package/src/generate.test.ts
CHANGED
|
@@ -254,9 +254,9 @@ describe("generateDTS", () => {
|
|
|
254
254
|
"embeddable-wrapper.esm.js",
|
|
255
255
|
] as any);
|
|
256
256
|
vi.mocked(path.resolve).mockImplementation((...args) => args.join("/"));
|
|
257
|
-
// Template contains
|
|
257
|
+
// Template contains all tokens so we can verify replacement
|
|
258
258
|
vi.mocked(fs.readFile).mockResolvedValue(
|
|
259
|
-
"replace-this-with-component-name {{RENDER_IMPORT}}",
|
|
259
|
+
"replace-this-with-component-name {{RENDER_IMPORT}} {{PLUGIN_FLAGS}}",
|
|
260
260
|
);
|
|
261
261
|
vi.mocked(loadConfig).mockResolvedValue({ config: {} } as any);
|
|
262
262
|
vi.mocked(createCompiler).mockResolvedValue({
|
|
@@ -290,6 +290,19 @@ describe("generateDTS", () => {
|
|
|
290
290
|
);
|
|
291
291
|
});
|
|
292
292
|
|
|
293
|
+
it("should replace {{PLUGIN_FLAGS}} token with empty pluginFlags", async () => {
|
|
294
|
+
await generateDTS(config as unknown as ResolvedEmbeddableConfig);
|
|
295
|
+
|
|
296
|
+
expect(fs.writeFile).toHaveBeenCalledWith(
|
|
297
|
+
"componentDir/component.tsx",
|
|
298
|
+
expect.stringContaining("const pluginFlags: Partial<PluginFlags> = {}"),
|
|
299
|
+
);
|
|
300
|
+
expect(fs.writeFile).toHaveBeenCalledWith(
|
|
301
|
+
"componentDir/component.tsx",
|
|
302
|
+
expect.not.stringContaining("{{PLUGIN_FLAGS}}"),
|
|
303
|
+
);
|
|
304
|
+
});
|
|
305
|
+
|
|
293
306
|
it("should call loadConfig with devMode=false and sourceMap=false", async () => {
|
|
294
307
|
await generateDTS(config as unknown as ResolvedEmbeddableConfig);
|
|
295
308
|
|
|
@@ -333,7 +346,7 @@ describe("injectBundleRender cross-platform paths", () => {
|
|
|
333
346
|
|
|
334
347
|
beforeEach(() => {
|
|
335
348
|
vi.mocked(path.resolve).mockImplementation((...args) => args.join("/"));
|
|
336
|
-
vi.mocked(fs.readFile).mockResolvedValue("{{RENDER_IMPORT}}");
|
|
349
|
+
vi.mocked(fs.readFile).mockResolvedValue("{{RENDER_IMPORT}}\n{{PLUGIN_FLAGS}}");
|
|
337
350
|
});
|
|
338
351
|
|
|
339
352
|
it("should use forward slashes in import when path.relative returns unix path", async () => {
|
|
@@ -363,6 +376,55 @@ describe("injectBundleRender cross-platform paths", () => {
|
|
|
363
376
|
expect.stringContaining("import render from '../../buildDir/buildName/render.js'"),
|
|
364
377
|
);
|
|
365
378
|
});
|
|
379
|
+
|
|
380
|
+
it("should inject pluginFlags from config into component.tsx", async () => {
|
|
381
|
+
vi.mocked(path.relative).mockReturnValue("../../buildDir/buildName");
|
|
382
|
+
const ctxWithPluginFlags = {
|
|
383
|
+
...ctxWithFileName,
|
|
384
|
+
"sdk-react": {
|
|
385
|
+
...ctxWithFileName["sdk-react"],
|
|
386
|
+
pluginFlags: { supportsOnComponentReadyHook: true },
|
|
387
|
+
},
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
await injectBundleRender(
|
|
391
|
+
ctxWithPluginFlags as unknown as ResolvedEmbeddableConfig,
|
|
392
|
+
"sdk-react",
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
expect(fs.writeFile).toHaveBeenCalledWith(
|
|
396
|
+
expect.any(String),
|
|
397
|
+
expect.stringContaining('const pluginFlags: Partial<PluginFlags> = {"supportsOnComponentReadyHook":true}'),
|
|
398
|
+
);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it("should inject empty pluginFlags when not present in config", async () => {
|
|
402
|
+
vi.mocked(path.relative).mockReturnValue("../../buildDir/buildName");
|
|
403
|
+
|
|
404
|
+
await injectBundleRender(
|
|
405
|
+
ctxWithFileName as unknown as ResolvedEmbeddableConfig,
|
|
406
|
+
"sdk-react",
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
expect(fs.writeFile).toHaveBeenCalledWith(
|
|
410
|
+
expect.any(String),
|
|
411
|
+
expect.stringContaining("const pluginFlags: Partial<PluginFlags> = {}"),
|
|
412
|
+
);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it("should not leave {{PLUGIN_FLAGS}} token in output", async () => {
|
|
416
|
+
vi.mocked(path.relative).mockReturnValue("../../buildDir/buildName");
|
|
417
|
+
|
|
418
|
+
await injectBundleRender(
|
|
419
|
+
ctxWithFileName as unknown as ResolvedEmbeddableConfig,
|
|
420
|
+
"sdk-react",
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
expect(fs.writeFile).toHaveBeenCalledWith(
|
|
424
|
+
expect.any(String),
|
|
425
|
+
expect.not.stringContaining("{{PLUGIN_FLAGS}}"),
|
|
426
|
+
);
|
|
427
|
+
});
|
|
366
428
|
});
|
|
367
429
|
|
|
368
430
|
describe("injectCSS cross-platform paths", () => {
|
package/src/generate.ts
CHANGED
|
@@ -18,6 +18,7 @@ import type { Logger } from "@stencil/core/internal";
|
|
|
18
18
|
|
|
19
19
|
const STYLE_IMPORTS_TOKEN = "{{STYLES_IMPORT}}";
|
|
20
20
|
const RENDER_IMPORT_TOKEN = "{{RENDER_IMPORT}}";
|
|
21
|
+
const PLUGIN_FLAGS_TOKEN = "{{PLUGIN_FLAGS}}";
|
|
21
22
|
|
|
22
23
|
// stencil doesn't support dynamic component tag name, so we need to replace it manually
|
|
23
24
|
const COMPONENT_TAG_TOKEN = "replace-this-with-component-name";
|
|
@@ -166,6 +167,8 @@ export async function injectBundleRender(
|
|
|
166
167
|
)
|
|
167
168
|
.replaceAll("\\", "/");
|
|
168
169
|
const importStr = `import render from '${importFilePath}/${ctx[pluginName].outputOptions.fileName}';`;
|
|
170
|
+
const pluginFlags = ctx[pluginName].pluginFlags ?? {};
|
|
171
|
+
const pluginFlagsStr = `const pluginFlags: Partial<PluginFlags> = ${JSON.stringify(pluginFlags)}`;
|
|
169
172
|
|
|
170
173
|
let content = await fs.readFile(
|
|
171
174
|
path.resolve(ctx.core.templatesDir, "component.tsx.template"),
|
|
@@ -178,7 +181,7 @@ export async function injectBundleRender(
|
|
|
178
181
|
|
|
179
182
|
await fs.writeFile(
|
|
180
183
|
path.resolve(ctx.client.componentDir, "component.tsx"),
|
|
181
|
-
content.replace(RENDER_IMPORT_TOKEN, importStr),
|
|
184
|
+
content.replace(RENDER_IMPORT_TOKEN, importStr).replace(PLUGIN_FLAGS_TOKEN, pluginFlagsStr),
|
|
182
185
|
);
|
|
183
186
|
}
|
|
184
187
|
|
|
@@ -199,7 +202,7 @@ async function injectBundleRenderStub(
|
|
|
199
202
|
content = content.replace(COMPONENT_TAG_TOKEN, "embeddable-component");
|
|
200
203
|
await fs.writeFile(
|
|
201
204
|
path.resolve(ctx.client.componentDir, "component.tsx"),
|
|
202
|
-
content.replace(RENDER_IMPORT_TOKEN, stubStr),
|
|
205
|
+
content.replace(RENDER_IMPORT_TOKEN, stubStr).replace(PLUGIN_FLAGS_TOKEN, "const pluginFlags: Partial<PluginFlags> = {}"),
|
|
203
206
|
);
|
|
204
207
|
}
|
|
205
208
|
|
package/src/index.ts
CHANGED
|
@@ -4,4 +4,4 @@ export { default as push } from "./push";
|
|
|
4
4
|
export { default as dev } from "./dev";
|
|
5
5
|
export { default as defineConfig } from "./defineConfig";
|
|
6
6
|
export { default as buildPackage } from "./buildPackage";
|
|
7
|
-
export type { ResolvedEmbeddableConfig } from "./defineConfig";
|
|
7
|
+
export type { ResolvedEmbeddableConfig, PluginFlags } from "./defineConfig";
|
package/src/push.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import push, { buildArchive } from "./push";
|
|
1
|
+
import push, { buildArchive, EMBEDDABLE_FILES, sendBuild, sendBuildByApiKey } from "./push";
|
|
2
2
|
import provideConfig from "./provideConfig";
|
|
3
3
|
import { fileFromPath } from "formdata-node/file-from-path";
|
|
4
4
|
import * as path from "path";
|
|
@@ -222,18 +222,19 @@ describe("push", () => {
|
|
|
222
222
|
});
|
|
223
223
|
|
|
224
224
|
describe("push configuration", () => {
|
|
225
|
-
it("should fail if
|
|
225
|
+
it("should fail if pushModels, pushComponents, and pushEmbeddables are all disabled", async () => {
|
|
226
226
|
vi.spyOn(console, "error").mockImplementation(() => undefined);
|
|
227
227
|
vi.mocked(provideConfig).mockResolvedValue({
|
|
228
228
|
...config,
|
|
229
229
|
pushModels: false,
|
|
230
230
|
pushComponents: false,
|
|
231
|
+
pushEmbeddables: false,
|
|
231
232
|
} as ResolvedEmbeddableConfig);
|
|
232
233
|
|
|
233
234
|
await push();
|
|
234
235
|
|
|
235
236
|
expect(startMock.fail).toHaveBeenCalledWith(
|
|
236
|
-
"Cannot push:
|
|
237
|
+
"Cannot push: pushModels, pushComponents, and pushEmbeddables are all disabled"
|
|
237
238
|
);
|
|
238
239
|
expect(process.exit).toHaveBeenCalledWith(1);
|
|
239
240
|
});
|
|
@@ -633,5 +634,164 @@ describe("push", () => {
|
|
|
633
634
|
expect(findFiles).toHaveBeenCalledWith("/src", expect.any(RegExp));
|
|
634
635
|
expect(findFiles).toHaveBeenCalledWith("/src", expect.any(RegExp));
|
|
635
636
|
});
|
|
637
|
+
|
|
638
|
+
it("should include embeddable files when pushEmbeddables is true", async () => {
|
|
639
|
+
vi.mocked(findFiles)
|
|
640
|
+
.mockResolvedValueOnce([]) // cube files
|
|
641
|
+
.mockResolvedValueOnce([]) // security context files
|
|
642
|
+
.mockResolvedValueOnce([]) // client context files
|
|
643
|
+
.mockResolvedValueOnce([
|
|
644
|
+
["dashboard.embeddable.yaml", "/src/dashboard.embeddable.yaml"],
|
|
645
|
+
["report.embeddable.yml", "/src/report.embeddable.yml"],
|
|
646
|
+
]); // embeddable files
|
|
647
|
+
|
|
648
|
+
const testConfig = {
|
|
649
|
+
...config,
|
|
650
|
+
pushModels: true,
|
|
651
|
+
pushComponents: true,
|
|
652
|
+
pushEmbeddables: true,
|
|
653
|
+
client: {
|
|
654
|
+
...config.client,
|
|
655
|
+
srcDir: "/src",
|
|
656
|
+
},
|
|
657
|
+
} as ResolvedEmbeddableConfig;
|
|
658
|
+
|
|
659
|
+
await buildArchive(testConfig);
|
|
660
|
+
|
|
661
|
+
expect(findFiles).toHaveBeenCalledWith("/src", EMBEDDABLE_FILES);
|
|
662
|
+
expect(mockZipLocal.addFile).toHaveBeenCalledWith(
|
|
663
|
+
"/src/dashboard.embeddable.yaml",
|
|
664
|
+
"dashboard.embeddable.yaml",
|
|
665
|
+
expect.objectContaining({ compress: true }),
|
|
666
|
+
);
|
|
667
|
+
expect(mockZipLocal.addFile).toHaveBeenCalledWith(
|
|
668
|
+
"/src/report.embeddable.yml",
|
|
669
|
+
"report.embeddable.yml",
|
|
670
|
+
expect.objectContaining({ compress: true }),
|
|
671
|
+
);
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
it("should not search for embeddable files when pushEmbeddables is false", async () => {
|
|
675
|
+
// Reset call history to avoid interference from previous tests
|
|
676
|
+
vi.mocked(findFiles).mockClear();
|
|
677
|
+
|
|
678
|
+
const testConfig = {
|
|
679
|
+
...config,
|
|
680
|
+
pushModels: true,
|
|
681
|
+
pushComponents: true,
|
|
682
|
+
pushEmbeddables: false,
|
|
683
|
+
client: {
|
|
684
|
+
...config.client,
|
|
685
|
+
srcDir: "/src",
|
|
686
|
+
},
|
|
687
|
+
} as ResolvedEmbeddableConfig;
|
|
688
|
+
|
|
689
|
+
await buildArchive(testConfig);
|
|
690
|
+
|
|
691
|
+
// findFiles should have been called for models and components but NOT for embeddable files
|
|
692
|
+
const embeddableFilesCall = vi
|
|
693
|
+
.mocked(findFiles)
|
|
694
|
+
.mock.calls.find((call) => call[1] === EMBEDDABLE_FILES);
|
|
695
|
+
expect(embeddableFilesCall).toBeUndefined();
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
it("should exit when all three push flags are disabled", async () => {
|
|
699
|
+
vi.spyOn(process, "exit").mockImplementation(() => null as never);
|
|
700
|
+
|
|
701
|
+
const testConfig = {
|
|
702
|
+
...config,
|
|
703
|
+
pushModels: false,
|
|
704
|
+
pushComponents: false,
|
|
705
|
+
pushEmbeddables: false,
|
|
706
|
+
client: {
|
|
707
|
+
...config.client,
|
|
708
|
+
srcDir: "/src",
|
|
709
|
+
},
|
|
710
|
+
} as ResolvedEmbeddableConfig;
|
|
711
|
+
|
|
712
|
+
await buildArchive(testConfig);
|
|
713
|
+
|
|
714
|
+
expect(process.exit).toHaveBeenCalledWith(1);
|
|
715
|
+
});
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
describe("EMBEDDABLE_FILES", () => {
|
|
719
|
+
it("should match .embeddable.yaml files", () => {
|
|
720
|
+
expect(EMBEDDABLE_FILES.test("my-dashboard.embeddable.yaml")).toBe(true);
|
|
721
|
+
expect(EMBEDDABLE_FILES.test("src/dashboards/sales.embeddable.yaml")).toBe(
|
|
722
|
+
true,
|
|
723
|
+
);
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
it("should match .embeddable.yml files", () => {
|
|
727
|
+
expect(EMBEDDABLE_FILES.test("my-dashboard.embeddable.yml")).toBe(true);
|
|
728
|
+
expect(EMBEDDABLE_FILES.test("src/widgets/chart.embeddable.yml")).toBe(
|
|
729
|
+
true,
|
|
730
|
+
);
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
it("should not match unrelated yaml files", () => {
|
|
734
|
+
expect(EMBEDDABLE_FILES.test("config.yaml")).toBe(false);
|
|
735
|
+
expect(EMBEDDABLE_FILES.test("my-dashboard.cc.yaml")).toBe(false);
|
|
736
|
+
expect(EMBEDDABLE_FILES.test("my-dashboard.sc.yaml")).toBe(false);
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
it("should not match cube files", () => {
|
|
740
|
+
expect(EMBEDDABLE_FILES.test("my-model.cube.yaml")).toBe(false);
|
|
741
|
+
expect(EMBEDDABLE_FILES.test("my-model.cube.yml")).toBe(false);
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
it("should not match non-yaml extensions", () => {
|
|
745
|
+
expect(EMBEDDABLE_FILES.test("my-dashboard.embeddable.json")).toBe(false);
|
|
746
|
+
expect(EMBEDDABLE_FILES.test("my-dashboard.embeddable.ts")).toBe(false);
|
|
747
|
+
});
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
describe.each([
|
|
751
|
+
{
|
|
752
|
+
label: "sendBuild",
|
|
753
|
+
endpoint: "**/bundle/:workspaceId/upload",
|
|
754
|
+
invoke: (testConfig: ResolvedEmbeddableConfig) =>
|
|
755
|
+
sendBuild(testConfig, { workspaceId: "test-workspace", token: "test-token" }),
|
|
756
|
+
},
|
|
757
|
+
{
|
|
758
|
+
label: "sendBuildByApiKey",
|
|
759
|
+
endpoint: "**/bundle/upload",
|
|
760
|
+
invoke: (testConfig: ResolvedEmbeddableConfig) =>
|
|
761
|
+
sendBuildByApiKey(testConfig, { apiKey: "test-api-key", email: "test@example.com" }),
|
|
762
|
+
},
|
|
763
|
+
])("$label", ({ endpoint, invoke }) => {
|
|
764
|
+
async function captureMetadata(pushEmbeddables: boolean) {
|
|
765
|
+
let capturedMetadata: Record<string, any> | undefined;
|
|
766
|
+
|
|
767
|
+
server.use(
|
|
768
|
+
http.post(endpoint, async ({ request }) => {
|
|
769
|
+
const formData = await request.formData();
|
|
770
|
+
const requestBlob = formData.get("request") as Blob;
|
|
771
|
+
capturedMetadata = JSON.parse(await requestBlob.text());
|
|
772
|
+
return HttpResponse.json({ bundleId: "mocked-bundle-id" });
|
|
773
|
+
}),
|
|
774
|
+
);
|
|
775
|
+
|
|
776
|
+
const testConfig = {
|
|
777
|
+
...config,
|
|
778
|
+
pushEmbeddables,
|
|
779
|
+
region: "us",
|
|
780
|
+
starterEmbeddables: {},
|
|
781
|
+
} as unknown as ResolvedEmbeddableConfig;
|
|
782
|
+
|
|
783
|
+
await invoke(testConfig);
|
|
784
|
+
return capturedMetadata;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
it("should include pushEmbeddables in form data metadata when true", async () => {
|
|
788
|
+
const metadata = await captureMetadata(true);
|
|
789
|
+
expect(metadata?.pushEmbeddables).toBe(true);
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
it("should include pushEmbeddables in form data metadata when false", async () => {
|
|
793
|
+
const metadata = await captureMetadata(false);
|
|
794
|
+
expect(metadata?.pushEmbeddables).toBe(false);
|
|
795
|
+
});
|
|
636
796
|
});
|
|
637
797
|
});
|