@embeddable.com/sdk-core 2.6.0-next.2 → 2.6.0-next.3
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/package.json +2 -1
- package/src/build.ts +38 -0
- package/src/buildTypes.ts +69 -0
- package/src/cleanup.ts +40 -0
- package/src/createContext.ts +26 -0
- package/src/credentials.ts +5 -0
- package/src/defineConfig.ts +70 -0
- package/src/dev.ts +265 -0
- package/src/entryPoint.ts +16 -0
- package/src/generate.ts +124 -0
- package/src/globalCleanup.ts +19 -0
- package/src/index.ts +5 -0
- package/src/login.ts +111 -0
- package/src/prepare.ts +23 -0
- package/src/provideConfig.ts +14 -0
- package/src/push.ts +189 -0
- package/src/rollbar.mjs +85 -0
- package/src/utils.ts +24 -0
- package/src/validate.ts +180 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@embeddable.com/sdk-core",
|
|
3
|
-
"version": "2.6.0-next.
|
|
3
|
+
"version": "2.6.0-next.3",
|
|
4
4
|
"description": "Core Embeddable SDK module responsible for web-components bundling and publishing.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"embeddable",
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"author": "Embeddable.com <engineering@embeddable.com>",
|
|
22
22
|
"files": [
|
|
23
23
|
"bin/",
|
|
24
|
+
"src/",
|
|
24
25
|
"lib/",
|
|
25
26
|
"loader/",
|
|
26
27
|
"templates/",
|
package/src/build.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import buildTypes from "./buildTypes";
|
|
2
|
+
import prepare from "./prepare";
|
|
3
|
+
import generate from "./generate";
|
|
4
|
+
import cleanup from "./cleanup";
|
|
5
|
+
import validate from "./validate";
|
|
6
|
+
import provideConfig from "./provideConfig";
|
|
7
|
+
// @ts-ignore
|
|
8
|
+
import reportErrorToRollbar from "./rollbar.mjs";
|
|
9
|
+
import { checkNodeVersion } from "./utils";
|
|
10
|
+
|
|
11
|
+
export default async () => {
|
|
12
|
+
try {
|
|
13
|
+
checkNodeVersion();
|
|
14
|
+
const config = await provideConfig();
|
|
15
|
+
|
|
16
|
+
await validate(config);
|
|
17
|
+
|
|
18
|
+
await prepare(config);
|
|
19
|
+
|
|
20
|
+
await buildTypes(config);
|
|
21
|
+
|
|
22
|
+
for (const getPlugin of config.plugins) {
|
|
23
|
+
const plugin = getPlugin();
|
|
24
|
+
|
|
25
|
+
await plugin.validate(config);
|
|
26
|
+
await plugin.build(config);
|
|
27
|
+
await plugin.cleanup(config);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// NOTE: likely this will be called inside the loop above if we decide to support clients with mixed frameworks simultaneously.
|
|
31
|
+
await generate(config, "sdk-react");
|
|
32
|
+
await cleanup(config);
|
|
33
|
+
} catch (error: any) {
|
|
34
|
+
await reportErrorToRollbar(error);
|
|
35
|
+
console.log(error);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import * as vite from "vite";
|
|
4
|
+
const oraP = import("ora");
|
|
5
|
+
import { findFiles } from "@embeddable.com/sdk-utils";
|
|
6
|
+
|
|
7
|
+
export const EMB_TYPE_FILE_REGEX = /^(.*)\.type\.emb\.[jt]s$/;
|
|
8
|
+
export const EMB_OPTIONS_FILE_REGEX = /^(.*)\.options\.emb\.[jt]s$/;
|
|
9
|
+
|
|
10
|
+
export default async (ctx: any) => {
|
|
11
|
+
const ora = (await oraP).default;
|
|
12
|
+
|
|
13
|
+
const progress = ora("Building types...").start();
|
|
14
|
+
|
|
15
|
+
await generate(ctx);
|
|
16
|
+
|
|
17
|
+
await build(ctx);
|
|
18
|
+
|
|
19
|
+
await cleanup(ctx);
|
|
20
|
+
|
|
21
|
+
progress.succeed("Types built completed");
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
async function generate(ctx: any) {
|
|
25
|
+
const typeFiles = await findFiles(ctx.client.srcDir, EMB_TYPE_FILE_REGEX);
|
|
26
|
+
const optionsFiles = await findFiles(
|
|
27
|
+
ctx.client.srcDir,
|
|
28
|
+
EMB_OPTIONS_FILE_REGEX,
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const typeImports = typeFiles
|
|
32
|
+
.concat(optionsFiles)
|
|
33
|
+
.map(
|
|
34
|
+
([_fileName, filePath]) =>
|
|
35
|
+
`import './${path
|
|
36
|
+
.relative(ctx.client.rootDir, filePath)
|
|
37
|
+
.replaceAll("\\", "/")}';`,
|
|
38
|
+
)
|
|
39
|
+
.join("\n");
|
|
40
|
+
|
|
41
|
+
await fs.writeFile(
|
|
42
|
+
path.resolve(ctx.client.rootDir, ctx.outputOptions.typesEntryPointFilename),
|
|
43
|
+
typeImports,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function build(ctx: any) {
|
|
48
|
+
await vite.build({
|
|
49
|
+
logLevel: "error",
|
|
50
|
+
build: {
|
|
51
|
+
emptyOutDir: false,
|
|
52
|
+
lib: {
|
|
53
|
+
entry: path.resolve(
|
|
54
|
+
ctx.client.rootDir,
|
|
55
|
+
ctx.outputOptions.typesEntryPointFilename,
|
|
56
|
+
),
|
|
57
|
+
formats: ["es"],
|
|
58
|
+
fileName: "embeddable-types",
|
|
59
|
+
},
|
|
60
|
+
outDir: ctx.client.buildDir,
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function cleanup(ctx: any) {
|
|
66
|
+
await fs.rm(
|
|
67
|
+
path.resolve(ctx.client.rootDir, "embeddable-types-entry-point.js"),
|
|
68
|
+
);
|
|
69
|
+
}
|
package/src/cleanup.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
|
|
4
|
+
export default async (ctx: any) => {
|
|
5
|
+
await extractBuild(ctx);
|
|
6
|
+
|
|
7
|
+
await removeObsoleteDir(ctx.client.buildDir);
|
|
8
|
+
|
|
9
|
+
await moveBuildTOBuildDir(ctx);
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
async function extractBuild(ctx: any) {
|
|
13
|
+
await fs.rename(
|
|
14
|
+
path.resolve(ctx.client.buildDir, ctx.client.stencilBuild),
|
|
15
|
+
ctx.client.tmpDir,
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
await fs.rename(
|
|
19
|
+
path.resolve(ctx.client.buildDir, "embeddable-types.js"),
|
|
20
|
+
path.join(ctx.client.tmpDir, "embeddable-types.js"),
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
await fs.rename(
|
|
24
|
+
path.resolve(ctx.client.buildDir, "embeddable-components-meta.js"),
|
|
25
|
+
path.join(ctx.client.tmpDir, "embeddable-components-meta.js"),
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
await fs.rename(
|
|
29
|
+
path.resolve(ctx.client.buildDir, "embeddable-editors-meta.js"),
|
|
30
|
+
path.join(ctx.client.tmpDir, "embeddable-editors-meta.js"),
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function removeObsoleteDir(dir: string) {
|
|
35
|
+
await fs.rm(dir, { recursive: true });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function moveBuildTOBuildDir(ctx: any) {
|
|
39
|
+
await fs.rename(ctx.client.tmpDir, ctx.client.buildDir);
|
|
40
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
|
|
3
|
+
export default (coreRoot: string, clientRoot: string) => ({
|
|
4
|
+
core: {
|
|
5
|
+
rootDir: coreRoot,
|
|
6
|
+
templatesDir: path.resolve(coreRoot, "templates"),
|
|
7
|
+
configsDir: path.resolve(coreRoot, "configs"),
|
|
8
|
+
},
|
|
9
|
+
client: {
|
|
10
|
+
rootDir: clientRoot,
|
|
11
|
+
buildDir: path.resolve(clientRoot, ".embeddable-build"),
|
|
12
|
+
srcDir: path.resolve(clientRoot, "src"),
|
|
13
|
+
tmpDir: path.resolve(clientRoot, ".embeddable-tmp"),
|
|
14
|
+
componentDir: path.resolve(clientRoot, ".embeddable-build", "component"),
|
|
15
|
+
stencilBuild: path.resolve(
|
|
16
|
+
clientRoot,
|
|
17
|
+
".embeddable-build",
|
|
18
|
+
"dist",
|
|
19
|
+
"embeddable-wrapper",
|
|
20
|
+
),
|
|
21
|
+
archiveFile: path.resolve(clientRoot, "embeddable-build.zip"),
|
|
22
|
+
},
|
|
23
|
+
outputOptions: {
|
|
24
|
+
typesEntryPointFilename: "embeddable-types-entry-point.js",
|
|
25
|
+
},
|
|
26
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
|
|
3
|
+
export type EmbeddableConfig = {
|
|
4
|
+
plugins: (() => {
|
|
5
|
+
pluginName: string;
|
|
6
|
+
build: (config: EmbeddableConfig) => Promise<void>;
|
|
7
|
+
cleanup: (config: EmbeddableConfig) => Promise<void>;
|
|
8
|
+
validate: (config: EmbeddableConfig) => Promise<void>;
|
|
9
|
+
})[];
|
|
10
|
+
pushBaseUrl?: string;
|
|
11
|
+
audienceUrl?: string;
|
|
12
|
+
authDomain?: string;
|
|
13
|
+
authClientId?: string;
|
|
14
|
+
errorFallbackComponent?: string;
|
|
15
|
+
applicationEnvironment?: string;
|
|
16
|
+
rollbarAccessToken?: string;
|
|
17
|
+
previewBaseUrl?: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export default ({
|
|
21
|
+
plugins,
|
|
22
|
+
pushBaseUrl,
|
|
23
|
+
audienceUrl,
|
|
24
|
+
authDomain,
|
|
25
|
+
authClientId,
|
|
26
|
+
errorFallbackComponent,
|
|
27
|
+
applicationEnvironment,
|
|
28
|
+
rollbarAccessToken,
|
|
29
|
+
previewBaseUrl,
|
|
30
|
+
}: EmbeddableConfig) => {
|
|
31
|
+
const coreRoot = path.resolve(__dirname, "..");
|
|
32
|
+
const clientRoot = process.cwd();
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
core: {
|
|
36
|
+
rootDir: coreRoot,
|
|
37
|
+
templatesDir: path.resolve(coreRoot, "templates"),
|
|
38
|
+
configsDir: path.resolve(coreRoot, "configs"),
|
|
39
|
+
},
|
|
40
|
+
client: {
|
|
41
|
+
rootDir: clientRoot,
|
|
42
|
+
buildDir: path.resolve(clientRoot, ".embeddable-build"),
|
|
43
|
+
srcDir: path.resolve(clientRoot, "src"),
|
|
44
|
+
tmpDir: path.resolve(clientRoot, ".embeddable-tmp"),
|
|
45
|
+
componentDir: path.resolve(clientRoot, ".embeddable-build", "component"),
|
|
46
|
+
stencilBuild: path.resolve(
|
|
47
|
+
clientRoot,
|
|
48
|
+
".embeddable-build",
|
|
49
|
+
"dist",
|
|
50
|
+
"embeddable-wrapper",
|
|
51
|
+
),
|
|
52
|
+
archiveFile: path.resolve(clientRoot, "embeddable-build.zip"),
|
|
53
|
+
errorFallbackComponent: errorFallbackComponent
|
|
54
|
+
? path.resolve(clientRoot, errorFallbackComponent)
|
|
55
|
+
: undefined,
|
|
56
|
+
},
|
|
57
|
+
outputOptions: {
|
|
58
|
+
typesEntryPointFilename: "embeddable-types-entry-point.js",
|
|
59
|
+
},
|
|
60
|
+
pushBaseUrl: pushBaseUrl ?? "https://api.embeddable.com",
|
|
61
|
+
audienceUrl: audienceUrl ?? "https://auth.embeddable.com",
|
|
62
|
+
previewBaseUrl: previewBaseUrl ?? "https://app.embeddable.com",
|
|
63
|
+
authDomain: authDomain ?? "auth.embeddable.com",
|
|
64
|
+
authClientId: authClientId ?? "dygrSUmI6HmgY5ymVbEAoLDEBxIOyr1V",
|
|
65
|
+
applicationEnvironment: applicationEnvironment ?? "production",
|
|
66
|
+
rollbarAccessToken:
|
|
67
|
+
rollbarAccessToken ?? "5c6028038d844bf1835a0f4db5f55d9e",
|
|
68
|
+
plugins,
|
|
69
|
+
};
|
|
70
|
+
};
|
package/src/dev.ts
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import buildTypes, {
|
|
2
|
+
EMB_OPTIONS_FILE_REGEX,
|
|
3
|
+
EMB_TYPE_FILE_REGEX,
|
|
4
|
+
} from "./buildTypes";
|
|
5
|
+
import prepare, { removeIfExists } from "./prepare";
|
|
6
|
+
import generate from "./generate";
|
|
7
|
+
import provideConfig from "./provideConfig";
|
|
8
|
+
import {
|
|
9
|
+
CompilerSystem,
|
|
10
|
+
createNodeLogger,
|
|
11
|
+
createNodeSys,
|
|
12
|
+
} from "@stencil/core/sys/node";
|
|
13
|
+
import { RollupWatcher } from "rollup";
|
|
14
|
+
import { IncomingMessage, Server, ServerResponse } from "http";
|
|
15
|
+
import { ChildProcess } from "node:child_process";
|
|
16
|
+
import { WebSocketServer, Server as WSServer } from "ws";
|
|
17
|
+
import * as chokidar from "chokidar";
|
|
18
|
+
import * as path from "path";
|
|
19
|
+
import { FSWatcher } from "chokidar";
|
|
20
|
+
import { getToken, default as login } from "./login";
|
|
21
|
+
import axios from "axios";
|
|
22
|
+
import { findFiles } from "@embeddable.com/sdk-utils";
|
|
23
|
+
import { archive, YAML_OR_JS_FILES, sendBuild } from "./push";
|
|
24
|
+
import validate from "./validate";
|
|
25
|
+
import { checkNodeVersion } from "./utils";
|
|
26
|
+
const minimist = require("minimist");
|
|
27
|
+
|
|
28
|
+
const oraP = import("ora");
|
|
29
|
+
let wss: WSServer;
|
|
30
|
+
let changedFiles: string[] = [];
|
|
31
|
+
let browserWindow: ChildProcess | null = null;
|
|
32
|
+
|
|
33
|
+
let ora: any;
|
|
34
|
+
let previewWorkspace: string;
|
|
35
|
+
|
|
36
|
+
const SERVER_PORT = 8926;
|
|
37
|
+
|
|
38
|
+
const buildWebComponent = async (config: any) => {
|
|
39
|
+
await generate(config, "sdk-react");
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export default async () => {
|
|
43
|
+
checkNodeVersion();
|
|
44
|
+
|
|
45
|
+
const http = require("http");
|
|
46
|
+
ora = (await oraP).default;
|
|
47
|
+
|
|
48
|
+
process.on("warning", (e) => console.warn(e.stack));
|
|
49
|
+
|
|
50
|
+
const logger = createNodeLogger({ process });
|
|
51
|
+
const sys = createNodeSys({ process });
|
|
52
|
+
|
|
53
|
+
const config = {
|
|
54
|
+
dev: {
|
|
55
|
+
watch: true,
|
|
56
|
+
logger,
|
|
57
|
+
sys,
|
|
58
|
+
},
|
|
59
|
+
...(await provideConfig()),
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
await prepare(config);
|
|
63
|
+
|
|
64
|
+
const finalhandler = require("finalhandler");
|
|
65
|
+
const serveStatic = require("serve-static");
|
|
66
|
+
|
|
67
|
+
const serve = serveStatic(config.client.buildDir);
|
|
68
|
+
|
|
69
|
+
const workspacePreparation = ora("Preparing workspace...").start();
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
previewWorkspace = await getPreviewWorkspace(config);
|
|
73
|
+
} catch (e: any) {
|
|
74
|
+
workspacePreparation.fail(e.response.data?.errorMessage);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
workspacePreparation.succeed("Workspace is ready");
|
|
79
|
+
|
|
80
|
+
const server = http.createServer(
|
|
81
|
+
(request: IncomingMessage, res: ServerResponse) => {
|
|
82
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
83
|
+
res.setHeader(
|
|
84
|
+
"Access-Control-Allow-Methods",
|
|
85
|
+
"GET, POST, PUT, DELETE, OPTIONS",
|
|
86
|
+
);
|
|
87
|
+
res.setHeader(
|
|
88
|
+
"Access-Control-Allow-Headers",
|
|
89
|
+
"Content-Type, Authorization",
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
if (request.method === "OPTIONS") {
|
|
93
|
+
// Respond to OPTIONS requests with just the CORS headers and a 200 status code
|
|
94
|
+
res.writeHead(200);
|
|
95
|
+
res.end();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const done = finalhandler(request, res);
|
|
100
|
+
serve(request, res, done);
|
|
101
|
+
},
|
|
102
|
+
);
|
|
103
|
+
wss = new WebSocketServer({ server });
|
|
104
|
+
|
|
105
|
+
server.listen(SERVER_PORT, async () => {
|
|
106
|
+
const watchers: Array<RollupWatcher | FSWatcher> = [];
|
|
107
|
+
if (sys?.onProcessInterrupt) {
|
|
108
|
+
sys.onProcessInterrupt(
|
|
109
|
+
async () => await onClose(server, sys, watchers, config),
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
await sendDataModelsAndSecurityContextsChanges(config);
|
|
114
|
+
|
|
115
|
+
for (const getPlugin of config.plugins) {
|
|
116
|
+
const plugin = getPlugin();
|
|
117
|
+
|
|
118
|
+
await plugin.validate(config);
|
|
119
|
+
const watcher = await plugin.build(config);
|
|
120
|
+
await configureWatcher(watcher, config);
|
|
121
|
+
watchers.push(watcher);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const dataModelAndSecurityContextWatch =
|
|
125
|
+
dataModelAndSecurityContextWatcher(config);
|
|
126
|
+
watchers.push(dataModelAndSecurityContextWatch);
|
|
127
|
+
});
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const configureWatcher = async (watcher: RollupWatcher, ctx: any) => {
|
|
131
|
+
watcher.on("change", (path) => {
|
|
132
|
+
changedFiles.push(path);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
watcher.on("event", async (e) => {
|
|
136
|
+
if (e.code === "BUNDLE_START") {
|
|
137
|
+
await onBuildStart(ctx);
|
|
138
|
+
}
|
|
139
|
+
if (e.code === "BUNDLE_END") {
|
|
140
|
+
await onBundleBuildEnd(ctx);
|
|
141
|
+
changedFiles = [];
|
|
142
|
+
}
|
|
143
|
+
if (e.code === "ERROR") {
|
|
144
|
+
sendMessage("componentsBuildError", { error: e.error?.message });
|
|
145
|
+
changedFiles = [];
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const sendMessage = (type: string, meta = {}) => {
|
|
151
|
+
wss.clients.forEach((ws) => {
|
|
152
|
+
ws.send(JSON.stringify({ type, ...meta }));
|
|
153
|
+
});
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const typeFilesFilter = (f: string) =>
|
|
157
|
+
EMB_OPTIONS_FILE_REGEX.test(f) || EMB_TYPE_FILE_REGEX.test(f);
|
|
158
|
+
const onlyTypesChanged = () =>
|
|
159
|
+
changedFiles.length !== 0 &&
|
|
160
|
+
changedFiles.filter(typeFilesFilter).length === changedFiles.length;
|
|
161
|
+
const isTypeFileChanged = () => changedFiles.findIndex(typeFilesFilter) >= 0;
|
|
162
|
+
|
|
163
|
+
const onBuildStart = async (ctx: any) => {
|
|
164
|
+
if (changedFiles.length === 0 || isTypeFileChanged()) {
|
|
165
|
+
await buildTypes(ctx);
|
|
166
|
+
}
|
|
167
|
+
sendMessage("componentsBuildStart", { changedFiles });
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const onBundleBuildEnd = async (ctx: any) => {
|
|
171
|
+
if (!onlyTypesChanged() || changedFiles.length === 0) {
|
|
172
|
+
await buildWebComponent(ctx);
|
|
173
|
+
}
|
|
174
|
+
if (browserWindow == null) {
|
|
175
|
+
const open = (await import("open")).default;
|
|
176
|
+
browserWindow = await open(
|
|
177
|
+
`${ctx.previewBaseUrl}/workspace/${previewWorkspace}`,
|
|
178
|
+
);
|
|
179
|
+
} else {
|
|
180
|
+
sendMessage("componentsBuildSuccess");
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const dataModelAndSecurityContextWatcher = (ctx: any): FSWatcher => {
|
|
185
|
+
const fsWatcher = chokidar.watch(
|
|
186
|
+
[path.resolve(ctx.client.srcDir, "**/*.{cube,sc}.{yaml,yml,js}")],
|
|
187
|
+
{
|
|
188
|
+
ignoreInitial: true,
|
|
189
|
+
},
|
|
190
|
+
);
|
|
191
|
+
fsWatcher.on("all", async () => {
|
|
192
|
+
await sendDataModelsAndSecurityContextsChanges(ctx);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
return fsWatcher;
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const sendDataModelsAndSecurityContextsChanges = async (ctx: any) => {
|
|
199
|
+
sendMessage("dataModelsAndOrSecurityContextUpdateStart");
|
|
200
|
+
const isValid = await validate(ctx, false);
|
|
201
|
+
if (isValid) {
|
|
202
|
+
const token = await getToken();
|
|
203
|
+
const sending = ora(
|
|
204
|
+
"Synchronising data models and/or security contexts...",
|
|
205
|
+
).start();
|
|
206
|
+
const filesList = await findFiles(ctx.client.srcDir, YAML_OR_JS_FILES);
|
|
207
|
+
await archive(ctx, filesList, false);
|
|
208
|
+
await sendBuild(ctx, { workspaceId: previewWorkspace, token });
|
|
209
|
+
sending.succeed(`Data models and/or security context synchronized`);
|
|
210
|
+
sendMessage("dataModelsAndOrSecurityContextUpdateSuccess");
|
|
211
|
+
} else {
|
|
212
|
+
sendMessage("dataModelsAndOrSecurityContextUpdateError");
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const onClose = async (
|
|
217
|
+
server: Server,
|
|
218
|
+
sys: CompilerSystem,
|
|
219
|
+
watchers: Array<RollupWatcher | FSWatcher>,
|
|
220
|
+
config: any,
|
|
221
|
+
) => {
|
|
222
|
+
server.close();
|
|
223
|
+
wss.close();
|
|
224
|
+
browserWindow?.unref();
|
|
225
|
+
for (const watcher of watchers) {
|
|
226
|
+
if (watcher.close) {
|
|
227
|
+
await watcher.close();
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
for (const getPlugin of config.plugins) {
|
|
231
|
+
const plugin = getPlugin();
|
|
232
|
+
await plugin.cleanup(config);
|
|
233
|
+
}
|
|
234
|
+
await removeIfExists(config);
|
|
235
|
+
await sys.destroy();
|
|
236
|
+
process.exit(0);
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const getPreviewWorkspace = async (ctx: any): Promise<string> => {
|
|
240
|
+
const token = await getToken();
|
|
241
|
+
|
|
242
|
+
const params = minimist(process.argv.slice(2));
|
|
243
|
+
const primaryWorkspace = params.w || params.workspace;
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
const response = await axios.get(
|
|
247
|
+
`${ctx.pushBaseUrl}/workspace/dev-workspace`,
|
|
248
|
+
{
|
|
249
|
+
params: { primaryWorkspace },
|
|
250
|
+
headers: {
|
|
251
|
+
Authorization: `Bearer ${token}`,
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
);
|
|
255
|
+
return response.data;
|
|
256
|
+
} catch (e: any) {
|
|
257
|
+
if (e.response.status === 401) {
|
|
258
|
+
// login and retry
|
|
259
|
+
await login();
|
|
260
|
+
return await getPreviewWorkspace(ctx);
|
|
261
|
+
} else {
|
|
262
|
+
throw e;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { fileURLToPath } from "url";
|
|
3
|
+
|
|
4
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
5
|
+
const COMMANDS_MAP = await import(path.join(__dirname, "../lib/index.js"));
|
|
6
|
+
|
|
7
|
+
async function main() {
|
|
8
|
+
const command = process.argv[2];
|
|
9
|
+
const runScript = COMMANDS_MAP[command];
|
|
10
|
+
|
|
11
|
+
if (!runScript) process.exit(1);
|
|
12
|
+
|
|
13
|
+
await runScript();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
main();
|
package/src/generate.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { createNodeLogger, createNodeSys } from "@stencil/core/sys/node";
|
|
4
|
+
import { createCompiler, loadConfig } from "@stencil/core/compiler";
|
|
5
|
+
|
|
6
|
+
const sorcery = require("sorcery");
|
|
7
|
+
|
|
8
|
+
const STYLE_IMPORTS_TOKEN = "{{STYLES_IMPORT}}";
|
|
9
|
+
const RENDER_IMPORT_TOKEN = "{{RENDER_IMPORT}}";
|
|
10
|
+
|
|
11
|
+
export default async (ctx: any, pluginName: string) => {
|
|
12
|
+
await injectCSS(ctx, pluginName);
|
|
13
|
+
|
|
14
|
+
await injectBundleRender(ctx, pluginName);
|
|
15
|
+
|
|
16
|
+
await runStencil(ctx);
|
|
17
|
+
|
|
18
|
+
await generateSourceMap(ctx, pluginName);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
async function injectCSS(ctx: any, pluginName: string) {
|
|
22
|
+
const CUSTOMER_BUILD = path.resolve(
|
|
23
|
+
ctx.client.buildDir,
|
|
24
|
+
ctx[pluginName].outputOptions.buildName,
|
|
25
|
+
);
|
|
26
|
+
const allFiles = await fs.readdir(CUSTOMER_BUILD);
|
|
27
|
+
|
|
28
|
+
const cssFilesImportsStr = allFiles
|
|
29
|
+
.filter((fileName) => fileName.endsWith(".css"))
|
|
30
|
+
.map(
|
|
31
|
+
(fileName) =>
|
|
32
|
+
`@import '../${ctx[pluginName].outputOptions.buildName}/${fileName}';`,
|
|
33
|
+
)
|
|
34
|
+
.join("\n");
|
|
35
|
+
|
|
36
|
+
const content = await fs.readFile(
|
|
37
|
+
path.resolve(ctx.core.templatesDir, "style.css.template"),
|
|
38
|
+
"utf8",
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
await fs.writeFile(
|
|
42
|
+
path.resolve(ctx.client.componentDir, "style.css"),
|
|
43
|
+
content.replace(STYLE_IMPORTS_TOKEN, cssFilesImportsStr),
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function injectBundleRender(ctx: any, pluginName: string) {
|
|
48
|
+
const importStr = `import render from '../${ctx[pluginName].outputOptions.buildName}/${ctx[pluginName].outputOptions.fileName}';`;
|
|
49
|
+
|
|
50
|
+
const content = await fs.readFile(
|
|
51
|
+
path.resolve(ctx.core.templatesDir, "component.tsx.template"),
|
|
52
|
+
"utf8",
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
await fs.writeFile(
|
|
56
|
+
path.resolve(ctx.client.componentDir, "component.tsx"),
|
|
57
|
+
content.replace(RENDER_IMPORT_TOKEN, importStr),
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function runStencil(ctx: any) {
|
|
62
|
+
const logger = ctx.dev?.logger || createNodeLogger({ process });
|
|
63
|
+
const sys = ctx.dev?.sys || createNodeSys({ process });
|
|
64
|
+
const devMode = !!ctx.dev;
|
|
65
|
+
|
|
66
|
+
const validated = await loadConfig({
|
|
67
|
+
initTsConfig: true,
|
|
68
|
+
logger,
|
|
69
|
+
sys,
|
|
70
|
+
config: {
|
|
71
|
+
devMode,
|
|
72
|
+
rootDir: ctx.client.buildDir,
|
|
73
|
+
configPath: path.resolve(ctx.client.buildDir, "stencil.config.ts"),
|
|
74
|
+
tsconfig: path.resolve(ctx.client.buildDir, "tsconfig.json"),
|
|
75
|
+
namespace: "embeddable-wrapper",
|
|
76
|
+
srcDir: path.resolve(ctx.client.buildDir, "component"),
|
|
77
|
+
outputTargets: [
|
|
78
|
+
{
|
|
79
|
+
type: "dist",
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const compiler = await createCompiler(validated.config);
|
|
86
|
+
|
|
87
|
+
await compiler.build();
|
|
88
|
+
|
|
89
|
+
await compiler.destroy();
|
|
90
|
+
process.chdir(ctx.client.rootDir);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function generateSourceMap(ctx: any, pluginName: string) {
|
|
94
|
+
const componentBuildDir = path.resolve(
|
|
95
|
+
ctx.client.buildDir,
|
|
96
|
+
ctx[pluginName].outputOptions.buildName,
|
|
97
|
+
);
|
|
98
|
+
const stencilBuild = path.resolve(ctx.client.stencilBuild);
|
|
99
|
+
|
|
100
|
+
const tmpComponentDir = path.resolve(
|
|
101
|
+
stencilBuild,
|
|
102
|
+
ctx[pluginName].outputOptions.buildName,
|
|
103
|
+
);
|
|
104
|
+
await fs.cp(componentBuildDir, tmpComponentDir, { recursive: true });
|
|
105
|
+
|
|
106
|
+
const stencilFiles = await fs.readdir(stencilBuild);
|
|
107
|
+
const jsFiles = stencilFiles.filter((file) =>
|
|
108
|
+
file.toLowerCase().endsWith(".js"),
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
await Promise.all(
|
|
112
|
+
jsFiles.map(async (jsFile) => {
|
|
113
|
+
try {
|
|
114
|
+
const chain = await sorcery.load(path.resolve(stencilBuild, jsFile));
|
|
115
|
+
// overwrite the existing file
|
|
116
|
+
await chain.write();
|
|
117
|
+
} catch (e) {
|
|
118
|
+
// do nothing if a map file can not be generated
|
|
119
|
+
}
|
|
120
|
+
}),
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
await fs.rm(tmpComponentDir, { recursive: true });
|
|
124
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as fsP from "node:fs/promises";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
|
|
5
|
+
export default async (ctx: any) => {
|
|
6
|
+
const componentsEntryPath = path.resolve(
|
|
7
|
+
ctx.client.rootDir,
|
|
8
|
+
ctx.outputOptions.componentsEntryPointFilename,
|
|
9
|
+
);
|
|
10
|
+
const typesEntryPath = path.resolve(
|
|
11
|
+
ctx.client.rootDir,
|
|
12
|
+
ctx.outputOptions.typesEntryPointFilename,
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
if (fs.existsSync(ctx.client.buildDir))
|
|
16
|
+
await fsP.rm(ctx.client.buildDir, { recursive: true });
|
|
17
|
+
if (fs.existsSync(componentsEntryPath)) await fsP.rm(componentsEntryPath);
|
|
18
|
+
if (fs.existsSync(typesEntryPath)) await fsP.rm(typesEntryPath);
|
|
19
|
+
};
|
package/src/index.ts
ADDED
package/src/login.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as fs from "node:fs/promises";
|
|
4
|
+
import axios from "axios";
|
|
5
|
+
import provideConfig from "./provideConfig";
|
|
6
|
+
import { jwtDecode } from "jwt-decode";
|
|
7
|
+
// @ts-ignore
|
|
8
|
+
import reportErrorToRollbar from "./rollbar.mjs";
|
|
9
|
+
import { CREDENTIALS_DIR, CREDENTIALS_FILE } from "./credentials";
|
|
10
|
+
const oraP = import("ora");
|
|
11
|
+
const openP = import("open");
|
|
12
|
+
|
|
13
|
+
export default async () => {
|
|
14
|
+
const ora = (await oraP).default;
|
|
15
|
+
const authenticationSpinner = ora("Waiting for code verification...").start();
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const open = (await openP).default;
|
|
19
|
+
const config = await provideConfig();
|
|
20
|
+
|
|
21
|
+
await resolveFiles();
|
|
22
|
+
|
|
23
|
+
const deviceCodePayload = {
|
|
24
|
+
client_id: config.authClientId,
|
|
25
|
+
audience: config.audienceUrl,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const deviceCodeResponse = await axios.post(
|
|
29
|
+
`https://${config.authDomain}/oauth/device/code`,
|
|
30
|
+
deviceCodePayload,
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
ora(
|
|
34
|
+
"Confirm this code on your browser: " +
|
|
35
|
+
deviceCodeResponse.data["user_code"],
|
|
36
|
+
).info();
|
|
37
|
+
|
|
38
|
+
const tokenPayload = {
|
|
39
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
40
|
+
device_code: deviceCodeResponse.data["device_code"],
|
|
41
|
+
client_id: config.authClientId,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
await open(deviceCodeResponse.data["verification_uri_complete"]);
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* This is a recommended way to poll, since it take some time for a user to enter a `user_code` in a browser.
|
|
48
|
+
* deviceCodeResponse.data['interval'] is a recommended/calculated polling interval specified in seconds.
|
|
49
|
+
*/
|
|
50
|
+
while (true) {
|
|
51
|
+
try {
|
|
52
|
+
const tokenResponse = await axios.post(
|
|
53
|
+
`https://${config.authDomain}/oauth/token`,
|
|
54
|
+
tokenPayload,
|
|
55
|
+
);
|
|
56
|
+
await fs.writeFile(
|
|
57
|
+
CREDENTIALS_FILE,
|
|
58
|
+
JSON.stringify(tokenResponse.data),
|
|
59
|
+
);
|
|
60
|
+
authenticationSpinner.succeed(
|
|
61
|
+
"You are successfully authenticated now!",
|
|
62
|
+
);
|
|
63
|
+
break;
|
|
64
|
+
} catch (e: any) {
|
|
65
|
+
if (e.response.data?.error !== "authorization_pending") {
|
|
66
|
+
authenticationSpinner.fail(
|
|
67
|
+
"Authentication failed. Please try again.",
|
|
68
|
+
);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
await sleep(deviceCodeResponse.data["interval"] * 1000);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
} catch (error) {
|
|
76
|
+
authenticationSpinner.fail("Authentication failed. Please try again.");
|
|
77
|
+
await reportErrorToRollbar(error);
|
|
78
|
+
console.log(error);
|
|
79
|
+
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export async function getToken() {
|
|
85
|
+
try {
|
|
86
|
+
const rawCredentials = await fs.readFile(CREDENTIALS_FILE, "utf-8");
|
|
87
|
+
const credentials = JSON.parse(rawCredentials.toString());
|
|
88
|
+
|
|
89
|
+
return credentials?.access_token ?? "";
|
|
90
|
+
} catch (_e) {
|
|
91
|
+
return "";
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function sleep(ms: number) {
|
|
96
|
+
return new Promise((res) => setTimeout(res, ms));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function resolveFiles() {
|
|
100
|
+
try {
|
|
101
|
+
await fs.access(CREDENTIALS_DIR);
|
|
102
|
+
} catch (_e) {
|
|
103
|
+
await fs.mkdir(CREDENTIALS_DIR);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
await fs.access(CREDENTIALS_FILE);
|
|
108
|
+
} catch (e) {
|
|
109
|
+
await fs.writeFile(CREDENTIALS_FILE, "");
|
|
110
|
+
}
|
|
111
|
+
}
|
package/src/prepare.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import * as fsSync from "node:fs";
|
|
2
|
+
import * as fs from "node:fs/promises";
|
|
3
|
+
|
|
4
|
+
export default async (ctx: any) => {
|
|
5
|
+
await removeIfExists(ctx);
|
|
6
|
+
|
|
7
|
+
await copyStencilConfigsToClient(ctx);
|
|
8
|
+
|
|
9
|
+
await createComponentDir(ctx.client.componentDir);
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export async function removeIfExists(ctx: any) {
|
|
13
|
+
if (fsSync.existsSync(ctx.client.buildDir))
|
|
14
|
+
await fs.rm(ctx.client.buildDir, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function copyStencilConfigsToClient(ctx: any) {
|
|
18
|
+
await fs.cp(ctx.core.configsDir, ctx.client.buildDir, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function createComponentDir(dir: string) {
|
|
22
|
+
await fs.mkdir(dir);
|
|
23
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as url from "node:url";
|
|
3
|
+
|
|
4
|
+
export default async () => {
|
|
5
|
+
const configFilePath = `${process.cwd()}/embeddable.config.js`;
|
|
6
|
+
|
|
7
|
+
if (!fs.existsSync(configFilePath)) {
|
|
8
|
+
console.log("Please create a proper `embeddable.config.js` file first.");
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const configFileUrl = url.pathToFileURL(configFilePath).href;
|
|
13
|
+
return (await import(configFileUrl)).default;
|
|
14
|
+
};
|
package/src/push.ts
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as fsSync from "node:fs";
|
|
3
|
+
import * as archiver from "archiver";
|
|
4
|
+
import axios from "axios";
|
|
5
|
+
const oraP = import("ora");
|
|
6
|
+
const inquirerSelect = import("@inquirer/select");
|
|
7
|
+
import provideConfig from "./provideConfig";
|
|
8
|
+
// @ts-ignore
|
|
9
|
+
import reportErrorToRollbar from "./rollbar.mjs";
|
|
10
|
+
|
|
11
|
+
import { findFiles } from "@embeddable.com/sdk-utils";
|
|
12
|
+
import { getToken } from "./login";
|
|
13
|
+
import { checkNodeVersion } from "./utils";
|
|
14
|
+
|
|
15
|
+
// grab .cube.yml|js and .sc.yml|js files
|
|
16
|
+
export const YAML_OR_JS_FILES = /^(.*)\.(cube|sc)\.(ya?ml|js)$/;
|
|
17
|
+
|
|
18
|
+
let ora: any;
|
|
19
|
+
export default async () => {
|
|
20
|
+
try {
|
|
21
|
+
checkNodeVersion();
|
|
22
|
+
ora = (await oraP).default;
|
|
23
|
+
|
|
24
|
+
const config = await provideConfig();
|
|
25
|
+
|
|
26
|
+
const token = await verify(config);
|
|
27
|
+
|
|
28
|
+
const { workspaceId, name: workspaceName } = await selectWorkspace(
|
|
29
|
+
config,
|
|
30
|
+
token,
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const spinnerArchive = ora("Building...").start();
|
|
34
|
+
|
|
35
|
+
const filesList = await findFiles(config.client.srcDir, YAML_OR_JS_FILES);
|
|
36
|
+
|
|
37
|
+
await archive(config, filesList);
|
|
38
|
+
spinnerArchive.succeed("Bundling completed");
|
|
39
|
+
|
|
40
|
+
const spinnerPushing = ora(
|
|
41
|
+
`Publishing to ${workspaceName} using ${config.pushBaseUrl}...`,
|
|
42
|
+
).start();
|
|
43
|
+
|
|
44
|
+
await sendBuild(config, { workspaceId, token });
|
|
45
|
+
spinnerPushing.succeed(
|
|
46
|
+
`Published to ${workspaceName} using ${config.pushBaseUrl}`,
|
|
47
|
+
);
|
|
48
|
+
} catch (error: any) {
|
|
49
|
+
console.log(error);
|
|
50
|
+
await reportErrorToRollbar(error);
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
async function selectWorkspace(ctx: any, token: string) {
|
|
56
|
+
const workspaceSpinner = ora({
|
|
57
|
+
text: `Fetching workspaces using ${ctx.pushBaseUrl}...`,
|
|
58
|
+
color: "green",
|
|
59
|
+
discardStdin: false,
|
|
60
|
+
}).start();
|
|
61
|
+
|
|
62
|
+
const availableWorkspaces = await getWorkspaces(ctx, token, workspaceSpinner);
|
|
63
|
+
|
|
64
|
+
let selectedWorkspace;
|
|
65
|
+
|
|
66
|
+
if (availableWorkspaces.length === 0) {
|
|
67
|
+
workspaceSpinner.fail("No workspaces found");
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
workspaceSpinner.info(`Found ${availableWorkspaces.length} workspace(s)`);
|
|
72
|
+
|
|
73
|
+
if (availableWorkspaces.length === 1) {
|
|
74
|
+
selectedWorkspace = availableWorkspaces[0];
|
|
75
|
+
} else {
|
|
76
|
+
const select = (await inquirerSelect).default;
|
|
77
|
+
selectedWorkspace = await select({
|
|
78
|
+
message: "Select workspace to push changes",
|
|
79
|
+
choices: availableWorkspaces.map((workspace: any) => ({
|
|
80
|
+
name: `${workspace.name} (${workspace.workspaceId})`,
|
|
81
|
+
value: workspace,
|
|
82
|
+
})),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
workspaceSpinner.succeed(
|
|
87
|
+
`Workspace: ${selectedWorkspace.name} (${selectedWorkspace.workspaceId})`,
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
return selectedWorkspace;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function verify(ctx: any) {
|
|
94
|
+
try {
|
|
95
|
+
await fs.access(ctx.client.buildDir);
|
|
96
|
+
} catch (_e) {
|
|
97
|
+
console.error("No embeddable build was produced.");
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// TODO: initiate login if no/invalid token.
|
|
102
|
+
const token = await getToken();
|
|
103
|
+
|
|
104
|
+
if (!token) {
|
|
105
|
+
console.error("Expired token. Please login again.");
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return token;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function archive(
|
|
113
|
+
ctx: any,
|
|
114
|
+
yamlFiles: [string, string][],
|
|
115
|
+
includeBuild: boolean = true,
|
|
116
|
+
) {
|
|
117
|
+
const output = fsSync.createWriteStream(ctx.client.archiveFile);
|
|
118
|
+
|
|
119
|
+
const _archiver = archiver.create("zip", {
|
|
120
|
+
zlib: { level: 9 },
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
_archiver.pipe(output);
|
|
124
|
+
if (includeBuild) {
|
|
125
|
+
_archiver.directory(ctx.client.buildDir, false);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
for (const fileData of yamlFiles) {
|
|
129
|
+
const fileName = fileData[1].split("/").pop();
|
|
130
|
+
|
|
131
|
+
_archiver.file(fileData[1], {
|
|
132
|
+
name: fileName!,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
await _archiver.finalize();
|
|
137
|
+
|
|
138
|
+
return new Promise((resolve, _reject) => {
|
|
139
|
+
output.on("close", resolve);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export async function sendBuild(
|
|
144
|
+
ctx: any,
|
|
145
|
+
{ workspaceId, token }: { workspaceId: string; token: string },
|
|
146
|
+
) {
|
|
147
|
+
const { FormData } = await import("formdata-node");
|
|
148
|
+
const { fileFromPath } = await import("formdata-node/file-from-path");
|
|
149
|
+
|
|
150
|
+
const file = await fileFromPath(
|
|
151
|
+
ctx.client.archiveFile,
|
|
152
|
+
"embeddable-build.zip",
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const form = new FormData();
|
|
156
|
+
form.set("file", file, "embeddable-build.zip");
|
|
157
|
+
|
|
158
|
+
await axios.post(`${ctx.pushBaseUrl}/bundle/${workspaceId}/upload`, form, {
|
|
159
|
+
headers: {
|
|
160
|
+
"Content-Type": "multipart/form-data",
|
|
161
|
+
Authorization: `Bearer ${token}`,
|
|
162
|
+
},
|
|
163
|
+
maxContentLength: Infinity,
|
|
164
|
+
maxBodyLength: Infinity,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
await fs.rm(ctx.client.archiveFile);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function getWorkspaces(ctx: any, token: string, workspaceSpinner: any) {
|
|
171
|
+
try {
|
|
172
|
+
const response = await axios.get(`${ctx.pushBaseUrl}/workspace`, {
|
|
173
|
+
headers: {
|
|
174
|
+
Authorization: `Bearer ${token}`,
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
return response.data?.filter((w: any) => !w.devWorkspace);
|
|
179
|
+
} catch (e: any) {
|
|
180
|
+
if (e.response.status === 401) {
|
|
181
|
+
workspaceSpinner.fail(
|
|
182
|
+
'Unauthorized. Please login using "embeddable login" command.',
|
|
183
|
+
);
|
|
184
|
+
} else {
|
|
185
|
+
workspaceSpinner.fail("Failed to fetch workspaces");
|
|
186
|
+
}
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
189
|
+
}
|
package/src/rollbar.mjs
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import * as path from "path";
|
|
2
|
+
import Rollbar from 'rollbar';
|
|
3
|
+
import provideConfig from "./provideConfig";
|
|
4
|
+
import fs from "node:fs/promises";
|
|
5
|
+
import {CREDENTIALS_FILE} from "./credentials";
|
|
6
|
+
import {jwtDecode} from "jwt-decode";
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
const reportErrorToRollbar = async (error) => {
|
|
10
|
+
const config = await provideConfig();
|
|
11
|
+
|
|
12
|
+
const rollbar = new Rollbar({
|
|
13
|
+
accessToken: config.rollbarAccessToken,
|
|
14
|
+
captureUncaught: true,
|
|
15
|
+
captureUnhandledRejections: true,
|
|
16
|
+
payload: {
|
|
17
|
+
environment: config.applicationEnvironment,
|
|
18
|
+
source: "sdk",
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const userData = await getUserData()
|
|
23
|
+
|
|
24
|
+
rollbar.configure({
|
|
25
|
+
payload: {
|
|
26
|
+
person: {
|
|
27
|
+
id: userData.globalUserId
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
rollbar.error(error, {
|
|
33
|
+
custom: {
|
|
34
|
+
code_version: getSdkPackageVersionInfo(config)
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const getSdkPackageVersionInfo = (config) => {
|
|
40
|
+
try {
|
|
41
|
+
const packageJsonFilePath = path.resolve(
|
|
42
|
+
config.client.rootDir,
|
|
43
|
+
"package.json",
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const packageJson = require(packageJsonFilePath);
|
|
47
|
+
const devDependencies = packageJson.devDependencies;
|
|
48
|
+
const dependencies = packageJson.dependencies;
|
|
49
|
+
|
|
50
|
+
const getDependencyVersion = (key, dependencies, devDependencies) => {
|
|
51
|
+
return dependencies?.[key] || devDependencies?.[key];
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const packageVersionInfo = {
|
|
55
|
+
core: getDependencyVersion("@embeddable.com/core", dependencies, devDependencies),
|
|
56
|
+
sdk_core: getDependencyVersion("@embeddable.com/sdk-core", dependencies, devDependencies),
|
|
57
|
+
react: getDependencyVersion("@embeddable.com/react", dependencies, devDependencies),
|
|
58
|
+
sdk_react: getDependencyVersion("@embeddable.com/sdk-react", dependencies, devDependencies),
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
return JSON.stringify(packageVersionInfo);
|
|
62
|
+
} catch (e) {
|
|
63
|
+
console.warn("Could not get SDK package version info", e)
|
|
64
|
+
return "unknown";
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export async function getUserData() {
|
|
69
|
+
try {
|
|
70
|
+
const token = await fs
|
|
71
|
+
.readFile(CREDENTIALS_FILE)
|
|
72
|
+
.then((data) => JSON.parse(data.toString()));
|
|
73
|
+
|
|
74
|
+
const decodedToken = jwtDecode(token.access_token);
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
globalUserId: `${decodedToken.iss}@${decodedToken.sub}`,
|
|
78
|
+
};
|
|
79
|
+
} catch (error) {
|
|
80
|
+
console.warn("Failed to get user data from credentials file");
|
|
81
|
+
return { globalUserId: "unknown" };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export default reportErrorToRollbar;
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const oraP = import("ora");
|
|
2
|
+
|
|
3
|
+
let ora: any;
|
|
4
|
+
export const checkNodeVersion = async () => {
|
|
5
|
+
ora = (await oraP).default;
|
|
6
|
+
ora("Checking node version...");
|
|
7
|
+
const [major, minor] = process.versions.node.split(".").map(Number);
|
|
8
|
+
|
|
9
|
+
const engines = require("../package.json").engines.node;
|
|
10
|
+
|
|
11
|
+
const [minMajor, minMinor] = engines
|
|
12
|
+
.split(".")
|
|
13
|
+
.map((v: string) => v.replace(/[^\d]/g, ""))
|
|
14
|
+
.map(Number);
|
|
15
|
+
|
|
16
|
+
if (major < minMajor || (major === minMajor && minor < minMinor)) {
|
|
17
|
+
ora({
|
|
18
|
+
text: `Node version ${minMajor}.${minMinor} or higher is required. You are running ${major}.${minor}.`,
|
|
19
|
+
color: "red",
|
|
20
|
+
}).fail();
|
|
21
|
+
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
};
|
package/src/validate.ts
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as YAML from "yaml";
|
|
3
|
+
import { errorFormatter, findFiles } from "@embeddable.com/sdk-utils";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
|
|
6
|
+
const CUBE_YAML_FILE_REGEX = /^(.*)\.cube\.ya?ml$/;
|
|
7
|
+
const SECURITY_CONTEXT_FILE_REGEX = /^(.*)\.sc\.ya?ml$/;
|
|
8
|
+
|
|
9
|
+
const checkNodeVersion = () => {
|
|
10
|
+
const [major, minor] = process.versions.node.split(".").map(Number);
|
|
11
|
+
|
|
12
|
+
const engines = require("../package.json").engines.node;
|
|
13
|
+
|
|
14
|
+
const [minMajor, minMinor] = engines.split(".").map(Number);
|
|
15
|
+
|
|
16
|
+
if (major < minMajor || (major === minMajor && minor < minMinor)) {
|
|
17
|
+
throw new Error(
|
|
18
|
+
`Node version ${minMajor}.${minMinor} or higher is required. You are running ${major}.${minor}.`,
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export default async (ctx: any, exitIfInvalid = true) => {
|
|
24
|
+
checkNodeVersion();
|
|
25
|
+
const ora = (await import("ora")).default;
|
|
26
|
+
|
|
27
|
+
const spinnerValidate = ora("Data model validation...").start();
|
|
28
|
+
|
|
29
|
+
const filesList = await findFiles(ctx.client.srcDir, CUBE_YAML_FILE_REGEX);
|
|
30
|
+
const securityContextFilesList = await findFiles(
|
|
31
|
+
ctx.client.srcDir,
|
|
32
|
+
SECURITY_CONTEXT_FILE_REGEX,
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const dataModelErrors = await dataModelsValidation(filesList);
|
|
36
|
+
|
|
37
|
+
if (dataModelErrors.length) {
|
|
38
|
+
spinnerValidate.fail("One or more cube.yaml files are invalid:");
|
|
39
|
+
|
|
40
|
+
dataModelErrors.forEach((errorMessage) =>
|
|
41
|
+
spinnerValidate.info(errorMessage),
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
if (exitIfInvalid) {
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
spinnerValidate.succeed("Data model validation completed");
|
|
50
|
+
|
|
51
|
+
const securityContextErrors = await securityContextValidation(
|
|
52
|
+
securityContextFilesList,
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
if (securityContextErrors.length) {
|
|
56
|
+
spinnerValidate.fail("One or more security context files are invalid:");
|
|
57
|
+
|
|
58
|
+
securityContextErrors.forEach((errorMessage) =>
|
|
59
|
+
spinnerValidate.info(errorMessage),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
if (exitIfInvalid) {
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return dataModelErrors.length === 0 && securityContextErrors.length === 0;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export async function dataModelsValidation(filesList: [string, string][]) {
|
|
71
|
+
const errors: string[] = [];
|
|
72
|
+
|
|
73
|
+
for (const [_, filePath] of filesList) {
|
|
74
|
+
const fileContentRaw = await fs.readFile(filePath, "utf8");
|
|
75
|
+
|
|
76
|
+
const cube = YAML.parse(fileContentRaw);
|
|
77
|
+
|
|
78
|
+
const safeParse = cubeModelSchema.safeParse(cube);
|
|
79
|
+
if (!safeParse.success) {
|
|
80
|
+
errorFormatter(safeParse.error.issues).forEach((error) => {
|
|
81
|
+
errors.push(`${filePath}: ${error}`);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return errors;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function securityContextValidation(filesList: [string, string][]) {
|
|
90
|
+
const errors: string[] = [];
|
|
91
|
+
|
|
92
|
+
const nameSet = new Set<string>();
|
|
93
|
+
for (const [_, filePath] of filesList) {
|
|
94
|
+
const fileContentRaw = await fs.readFile(filePath, "utf8");
|
|
95
|
+
|
|
96
|
+
const cube = YAML.parse(fileContentRaw);
|
|
97
|
+
|
|
98
|
+
cube.forEach((item: { name: string }) => {
|
|
99
|
+
if (nameSet.has(item.name)) {
|
|
100
|
+
errors.push(
|
|
101
|
+
`${filePath}: security context with name "${item.name}" already exists`,
|
|
102
|
+
);
|
|
103
|
+
} else {
|
|
104
|
+
nameSet.add(item.name);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const safeParse = securityContextSchema.safeParse(cube);
|
|
109
|
+
if (!safeParse.success) {
|
|
110
|
+
errorFormatter(safeParse.error.issues).forEach((error) => {
|
|
111
|
+
errors.push(`${filePath}: ${error}`);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return errors;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
enum MeasureTypeEnum {
|
|
120
|
+
string = "string",
|
|
121
|
+
time = "time",
|
|
122
|
+
boolean = "boolean",
|
|
123
|
+
number = "number",
|
|
124
|
+
count = "count",
|
|
125
|
+
count_distinct = "count_distinct",
|
|
126
|
+
count_distinct_approx = "count_distinct_approx",
|
|
127
|
+
sum = "sum",
|
|
128
|
+
avg = "avg",
|
|
129
|
+
min = "min",
|
|
130
|
+
max = "max",
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
enum DimensionTypeEnum {
|
|
134
|
+
string = "string",
|
|
135
|
+
time = "time",
|
|
136
|
+
boolean = "boolean",
|
|
137
|
+
number = "number",
|
|
138
|
+
geo = "geo",
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const cubeModelSchema = z
|
|
142
|
+
.object({
|
|
143
|
+
cubes: z
|
|
144
|
+
.object({
|
|
145
|
+
name: z.string(),
|
|
146
|
+
dimensions: z
|
|
147
|
+
.object({
|
|
148
|
+
name: z.string(),
|
|
149
|
+
type: z.nativeEnum(DimensionTypeEnum),
|
|
150
|
+
})
|
|
151
|
+
.array()
|
|
152
|
+
.optional(),
|
|
153
|
+
measures: z
|
|
154
|
+
.object({
|
|
155
|
+
name: z.string(),
|
|
156
|
+
type: z.nativeEnum(MeasureTypeEnum),
|
|
157
|
+
})
|
|
158
|
+
.array()
|
|
159
|
+
.optional(),
|
|
160
|
+
})
|
|
161
|
+
.array()
|
|
162
|
+
.min(1),
|
|
163
|
+
})
|
|
164
|
+
.refine(
|
|
165
|
+
(data) =>
|
|
166
|
+
data.cubes.every(
|
|
167
|
+
(cube) => cube.dimensions?.length || cube.measures?.length,
|
|
168
|
+
),
|
|
169
|
+
{
|
|
170
|
+
message: "At least one measure or dimension must be defined",
|
|
171
|
+
path: ["cubes"],
|
|
172
|
+
},
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
const securityContextSchema = z.array(
|
|
176
|
+
z.object({
|
|
177
|
+
name: z.string(),
|
|
178
|
+
securityContext: z.object({}), // can be any object
|
|
179
|
+
}),
|
|
180
|
+
);
|