@superblocksteam/sdk 1.14.2 → 2.0.3-next.46
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/.mocharc.json +7 -0
- package/.prettierrc +18 -0
- package/dist/cli-replacement/dev.d.mts +19 -0
- package/dist/cli-replacement/dev.d.mts.map +1 -0
- package/dist/cli-replacement/dev.mjs +122 -0
- package/dist/cli-replacement/dev.mjs.map +1 -0
- package/dist/cli-replacement/init.d.ts +14 -0
- package/dist/cli-replacement/init.d.ts.map +1 -0
- package/dist/cli-replacement/init.js +26 -0
- package/dist/cli-replacement/init.js.map +1 -0
- package/dist/client.d.ts +31 -17
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +137 -155
- package/dist/client.js.map +1 -0
- package/dist/dbfs/client.d.ts +6 -0
- package/dist/dbfs/client.d.ts.map +1 -0
- package/dist/dbfs/client.js +117 -0
- package/dist/dbfs/client.js.map +1 -0
- package/dist/dbfs/local.d.ts +15 -0
- package/dist/dbfs/local.d.ts.map +1 -0
- package/dist/dbfs/local.js +126 -0
- package/dist/dbfs/local.js.map +1 -0
- package/dist/dev-utils/custom-build.d.mts +4 -0
- package/dist/dev-utils/custom-build.d.mts.map +1 -0
- package/dist/dev-utils/custom-build.mjs +99 -0
- package/dist/dev-utils/custom-build.mjs.map +1 -0
- package/dist/dev-utils/custom-config.d.mts +2 -0
- package/dist/dev-utils/custom-config.d.mts.map +1 -0
- package/dist/dev-utils/custom-config.mjs +57 -0
- package/dist/dev-utils/custom-config.mjs.map +1 -0
- package/dist/dev-utils/dev-logger.d.mts +8 -0
- package/dist/dev-utils/dev-logger.d.mts.map +1 -0
- package/dist/dev-utils/dev-logger.mjs +25 -0
- package/dist/dev-utils/dev-logger.mjs.map +1 -0
- package/dist/dev-utils/dev-server.d.mts +18 -0
- package/dist/dev-utils/dev-server.d.mts.map +1 -0
- package/dist/dev-utils/dev-server.mjs +265 -0
- package/dist/dev-utils/dev-server.mjs.map +1 -0
- package/dist/dev-utils/dev-tracer.d.ts +3 -0
- package/dist/dev-utils/dev-tracer.d.ts.map +1 -0
- package/dist/dev-utils/dev-tracer.js +28 -0
- package/dist/dev-utils/dev-tracer.js.map +1 -0
- package/dist/dev-utils/vite-plugin-dd-rum.d.mts +10 -0
- package/dist/dev-utils/vite-plugin-dd-rum.d.mts.map +1 -0
- package/dist/dev-utils/vite-plugin-dd-rum.mjs +34 -0
- package/dist/dev-utils/vite-plugin-dd-rum.mjs.map +1 -0
- package/dist/dev-utils/vite-plugin-react-transform.d.mts +7 -0
- package/dist/dev-utils/vite-plugin-react-transform.d.mts.map +1 -0
- package/dist/dev-utils/vite-plugin-react-transform.mjs +110 -0
- package/dist/dev-utils/vite-plugin-react-transform.mjs.map +1 -0
- package/dist/dev-utils/vite-plugin-sb-cdn.d.mts +34 -0
- package/dist/dev-utils/vite-plugin-sb-cdn.d.mts.map +1 -0
- package/dist/dev-utils/vite-plugin-sb-cdn.mjs +720 -0
- package/dist/dev-utils/vite-plugin-sb-cdn.mjs.map +1 -0
- package/dist/errors.d.ts +1 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +4 -9
- package/dist/errors.js.map +1 -0
- package/dist/flag.d.ts +2 -2
- package/dist/flag.d.ts.map +1 -0
- package/dist/flag.js +5 -9
- package/dist/flag.js.map +1 -0
- package/dist/index.d.ts +10 -4
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -20
- package/dist/index.js.map +1 -0
- package/dist/sdk.d.ts +42 -18
- package/dist/sdk.d.ts.map +1 -0
- package/dist/sdk.js +47 -43
- package/dist/sdk.js.map +1 -0
- package/dist/socket/handlers.d.ts +3 -128
- package/dist/socket/handlers.d.ts.map +1 -0
- package/dist/socket/handlers.js +7 -9
- package/dist/socket/handlers.js.map +1 -0
- package/dist/socket/index.d.ts +4 -3
- package/dist/socket/index.d.ts.map +1 -0
- package/dist/socket/index.js +12 -21
- package/dist/socket/index.js.map +1 -0
- package/dist/socket/signing.d.ts +3 -1
- package/dist/socket/signing.d.ts.map +1 -0
- package/dist/socket/signing.js +8 -17
- package/dist/socket/signing.js.map +1 -0
- package/dist/socket/socket.d.ts +3 -2
- package/dist/socket/socket.d.ts.map +1 -0
- package/dist/socket/socket.js +9 -19
- package/dist/socket/socket.js.map +1 -0
- package/dist/types/common.d.ts +2 -103
- package/dist/types/common.d.ts.map +1 -0
- package/dist/types/common.js +8 -24
- package/dist/types/common.js.map +1 -0
- package/dist/types/index.d.ts +5 -4
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +5 -20
- package/dist/types/index.js.map +1 -0
- package/dist/types/plugin.d.ts +1 -0
- package/dist/types/plugin.d.ts.map +1 -0
- package/dist/types/plugin.js +3 -5
- package/dist/types/plugin.js.map +1 -0
- package/dist/types/signing.d.ts +2 -1
- package/dist/types/signing.d.ts.map +1 -0
- package/dist/types/signing.js +2 -2
- package/dist/types/signing.js.map +1 -0
- package/dist/types/socket.d.ts +1 -0
- package/dist/types/socket.d.ts.map +1 -0
- package/dist/types/socket.js +2 -5
- package/dist/types/socket.js.map +1 -0
- package/dist/utils.d.ts +3 -1
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +14 -23
- package/dist/utils.js.map +1 -0
- package/dist/version-control.d.mts +59 -0
- package/dist/version-control.d.mts.map +1 -0
- package/dist/version-control.mjs +899 -0
- package/dist/version-control.mjs.map +1 -0
- package/eslint.config.js +85 -0
- package/package.json +72 -32
- package/src/cli-replacement/dev.mts +182 -0
- package/src/cli-replacement/init.ts +47 -0
- package/src/client.ts +114 -38
- package/src/dbfs/client.ts +162 -0
- package/src/dbfs/local.ts +163 -0
- package/src/dev-utils/custom-build.mts +113 -0
- package/src/dev-utils/custom-config.mts +66 -0
- package/src/dev-utils/dev-logger.mts +39 -0
- package/src/dev-utils/dev-server.mts +342 -0
- package/src/dev-utils/dev-tracer.ts +31 -0
- package/src/dev-utils/vite-plugin-dd-rum.mts +47 -0
- package/src/dev-utils/vite-plugin-react-transform.mts +130 -0
- package/src/dev-utils/vite-plugin-sb-cdn.mts +988 -0
- package/src/flag.ts +2 -3
- package/src/index.ts +119 -4
- package/src/sdk.ts +91 -17
- package/src/socket/handlers.ts +9 -147
- package/src/socket/index.ts +6 -9
- package/src/socket/signing.ts +7 -8
- package/src/socket/socket.ts +8 -9
- package/src/types/common.ts +2 -119
- package/src/types/index.ts +4 -4
- package/src/types/signing.ts +1 -1
- package/src/types/socket.ts +1 -1
- package/src/utils.ts +5 -6
- package/src/version-control.mts +1351 -0
- package/test/dev-utils/fixture/index.html +12 -0
- package/test/dev-utils/fixture/main.jsx +22 -0
- package/test/dev-utils/fixture/package-lock.json +25 -0
- package/test/dev-utils/fixture/package.json +9 -0
- package/test/dev-utils/vite-plugin-sb-cdn.test.mts +74 -0
- package/test/tsconfig.json +9 -0
- package/test/version-control.test.mts +1412 -0
- package/tsconfig.json +15 -4
- package/tsconfig.tsbuildinfo +1 -1
- package/.eslintrc.json +0 -55
|
@@ -0,0 +1,1351 @@
|
|
|
1
|
+
import * as https from "https";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { dirname } from "path";
|
|
4
|
+
import { ExportViewMode } from "@superblocksteam/shared";
|
|
5
|
+
import {
|
|
6
|
+
RESOURCE_CONFIG_PATH,
|
|
7
|
+
SUPERBLOCKS_HOME_FOLDER_NAME,
|
|
8
|
+
getSuperblocksApplicationConfigIfExists,
|
|
9
|
+
getSuperblocksApplicationConfigJson,
|
|
10
|
+
getSuperblocksBackendConfigIfExists,
|
|
11
|
+
getSuperblocksApplicationV2ConfigJson,
|
|
12
|
+
readAppApiYamlFile,
|
|
13
|
+
writeApiFiles,
|
|
14
|
+
DEFAULT_LINES_FOR_LARGE_STEPS,
|
|
15
|
+
} from "@superblocksteam/util";
|
|
16
|
+
import { bold } from "colorette";
|
|
17
|
+
import fs from "fs-extra";
|
|
18
|
+
import { isEmpty, isArray, isObject, get } from "lodash-es";
|
|
19
|
+
import * as semver from "semver";
|
|
20
|
+
import { simpleGit } from "simple-git";
|
|
21
|
+
import slugify from "slugify";
|
|
22
|
+
import { parse, stringify as ymlstringify } from "yaml";
|
|
23
|
+
import type {
|
|
24
|
+
ApplicationWrapper,
|
|
25
|
+
MultiPageApplicationWrapper,
|
|
26
|
+
MultiPageApplicationWrapperWithComponents,
|
|
27
|
+
CodeModeApplicationWrapper,
|
|
28
|
+
} from "./client.js";
|
|
29
|
+
import type { FeatureFlags } from "./flag.js";
|
|
30
|
+
import type { SuperblocksSdk } from "./sdk.js";
|
|
31
|
+
import type { ApiWithPb, Page } from "./types/index.js";
|
|
32
|
+
import type {
|
|
33
|
+
LocalGitRepoState,
|
|
34
|
+
SuperblocksBackendConfig,
|
|
35
|
+
SuperblocksMonorepoConfig,
|
|
36
|
+
SuperblocksApplicationConfig,
|
|
37
|
+
SuperblocksResourceConfig,
|
|
38
|
+
SuperblocksResourceConfigMetadata,
|
|
39
|
+
VersionedResourceConfig,
|
|
40
|
+
ApiInfo,
|
|
41
|
+
PageInfo,
|
|
42
|
+
SuperblocksApplicationV2Config,
|
|
43
|
+
ApiWrapper,
|
|
44
|
+
AdditionalStepFiles,
|
|
45
|
+
} from "@superblocksteam/util";
|
|
46
|
+
import type { SimpleGit, StatusResult } from "simple-git";
|
|
47
|
+
|
|
48
|
+
export const LATEST_EDITS_MODE = "latest-edits";
|
|
49
|
+
export const MOST_RECENT_COMMIT_MODE = "most-recent-commit";
|
|
50
|
+
export const DEPLOYED_MODE = "deployed";
|
|
51
|
+
|
|
52
|
+
export const DEFAULT_BRANCH = "main";
|
|
53
|
+
|
|
54
|
+
export const modeFlagValuesMap: Record<string, string> = {
|
|
55
|
+
[LATEST_EDITS_MODE]: "Latest edits",
|
|
56
|
+
[MOST_RECENT_COMMIT_MODE]: "Most recent commit",
|
|
57
|
+
[DEPLOYED_MODE]: "Deployed",
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export enum FileStructureType {
|
|
61
|
+
SINGLE_PAGE = "single-page",
|
|
62
|
+
MULTI_PAGE = "multi-page",
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function modeFlagToViewMode(modeFlag: ModeFlag): ExportViewMode {
|
|
66
|
+
switch (modeFlag) {
|
|
67
|
+
case LATEST_EDITS_MODE:
|
|
68
|
+
return ExportViewMode.EXPORT_LIVE;
|
|
69
|
+
case MOST_RECENT_COMMIT_MODE:
|
|
70
|
+
return ExportViewMode.EXPORT_LATEST;
|
|
71
|
+
case DEPLOYED_MODE:
|
|
72
|
+
return ExportViewMode.EXPORT_DEPLOYED;
|
|
73
|
+
default:
|
|
74
|
+
throw new Error(`Unsupported mode flag: ${modeFlag}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export type ModeFlag = (keyof typeof modeFlagValuesMap)[number];
|
|
79
|
+
|
|
80
|
+
export const SELECT_PROMPT_HELP = "Use ↑/↓ arrow keys, Enter to confirm";
|
|
81
|
+
export const MULTI_SELECT_PROMPT_HELP =
|
|
82
|
+
"Type to filter, Use ↑/↓ arrow keys, Space to select, Enter to confirm";
|
|
83
|
+
|
|
84
|
+
export const atLeastOneSelection = (value: string[]) => {
|
|
85
|
+
if (isEmpty(value)) {
|
|
86
|
+
return `Please select at least one item ${bold("by pressing space")}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return true;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export type ApiRepresentation = {
|
|
93
|
+
extractLargeSourceFiles: boolean;
|
|
94
|
+
minLinesForExtraction: number;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const DEFAULT_FILE_VERSION = "0.1.0";
|
|
98
|
+
const SPLIT_LARGE_API_STEPS_VERSION = "0.2.0";
|
|
99
|
+
const LATEST_FILE_VERSION = SPLIT_LARGE_API_STEPS_VERSION;
|
|
100
|
+
|
|
101
|
+
export function getApiRepresentation(
|
|
102
|
+
featureFlags: FeatureFlags,
|
|
103
|
+
resourceConfig: SuperblocksResourceConfig,
|
|
104
|
+
): ApiRepresentation {
|
|
105
|
+
const linesForLargeSteps =
|
|
106
|
+
featureFlags.linesForLargeSteps() ?? DEFAULT_LINES_FOR_LARGE_STEPS;
|
|
107
|
+
if (
|
|
108
|
+
featureFlags.splitLargeApiStepsEnabled() &&
|
|
109
|
+
isPostSplitLargeApiSteps(resourceConfig)
|
|
110
|
+
) {
|
|
111
|
+
return {
|
|
112
|
+
extractLargeSourceFiles: true,
|
|
113
|
+
minLinesForExtraction: linesForLargeSteps,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
// Return the default
|
|
117
|
+
return {
|
|
118
|
+
extractLargeSourceFiles: false,
|
|
119
|
+
minLinesForExtraction: linesForLargeSteps,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function isPostSplitLargeApiSteps(
|
|
124
|
+
resourceConfig: SuperblocksResourceConfig | undefined,
|
|
125
|
+
) {
|
|
126
|
+
if (!resourceConfig) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
const version = resourceConfig.metadata?.fileVersion ?? DEFAULT_FILE_VERSION;
|
|
130
|
+
return semver.compare(version, SPLIT_LARGE_API_STEPS_VERSION) >= 0;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function slugifyName(originalName: any): string {
|
|
134
|
+
return (slugify as any)(originalName, {
|
|
135
|
+
replacement: "_",
|
|
136
|
+
lower: true,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// resolves a true promise when download is complete. If there's an error, resolve false
|
|
141
|
+
async function downloadFile(
|
|
142
|
+
rootDirectory: string,
|
|
143
|
+
filepath: string,
|
|
144
|
+
url: string,
|
|
145
|
+
): Promise<string> {
|
|
146
|
+
const fullPath = `${rootDirectory}/${filepath}`;
|
|
147
|
+
// eslint-disable-next-line no-async-promise-executor
|
|
148
|
+
const result = await new Promise<boolean>(async (resolve) => {
|
|
149
|
+
try {
|
|
150
|
+
// create directory path if it doesn't exist yet
|
|
151
|
+
if (!(await fs.pathExists(fullPath))) {
|
|
152
|
+
await fs.mkdir(dirname(fullPath), { recursive: true });
|
|
153
|
+
}
|
|
154
|
+
const file = fs.createWriteStream(fullPath);
|
|
155
|
+
https.get(url, (resp) => {
|
|
156
|
+
resp.pipe(file);
|
|
157
|
+
file
|
|
158
|
+
.on("finish", () => {
|
|
159
|
+
file.end(() => resolve(true));
|
|
160
|
+
})
|
|
161
|
+
.on("error", () => {
|
|
162
|
+
fs.unlink(filepath);
|
|
163
|
+
resolve(false);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
} catch {
|
|
167
|
+
return resolve(false);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// failed to download correctly, attempt to clean up file
|
|
172
|
+
if (!result) {
|
|
173
|
+
try {
|
|
174
|
+
await fs.unlink(fullPath);
|
|
175
|
+
} catch {
|
|
176
|
+
console.log("Failed to delete file", fullPath);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return Promise.resolve(fullPath);
|
|
180
|
+
}
|
|
181
|
+
return Promise.resolve("");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function readYamlFile(path: string): Promise<any> {
|
|
185
|
+
return parse(await fs.readFile(path, "utf8"));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function getFileStructureTypeFromResourceConfig(
|
|
189
|
+
superblocksConfig: SuperblocksApplicationConfig,
|
|
190
|
+
) {
|
|
191
|
+
if (superblocksConfig.pages) {
|
|
192
|
+
return FileStructureType.MULTI_PAGE;
|
|
193
|
+
}
|
|
194
|
+
return FileStructureType.SINGLE_PAGE;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export async function getFileStructureType(
|
|
198
|
+
rootPath: string,
|
|
199
|
+
existingRelativeLocation: string,
|
|
200
|
+
): Promise<FileStructureType> {
|
|
201
|
+
try {
|
|
202
|
+
const superblocksConfig = await getSuperblocksApplicationConfigJson(
|
|
203
|
+
`${rootPath}/${existingRelativeLocation}`,
|
|
204
|
+
);
|
|
205
|
+
return getFileStructureTypeFromResourceConfig(superblocksConfig);
|
|
206
|
+
} catch {
|
|
207
|
+
await getSuperblocksApplicationV2ConfigJson(
|
|
208
|
+
`${rootPath}/${existingRelativeLocation}`,
|
|
209
|
+
);
|
|
210
|
+
return FileStructureType.MULTI_PAGE;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// NOTE: If a change is made to how applications are read from disk, please update
|
|
215
|
+
// logic to write applications to disk in the "writeResourceToDisk" function accordingly.
|
|
216
|
+
// @deprecated this can be removed once all customers move to multi page applications
|
|
217
|
+
export async function readApplicationFromDisk(
|
|
218
|
+
rootPath: string,
|
|
219
|
+
existingRelativeLocation: string,
|
|
220
|
+
): Promise<ApplicationWrapper> {
|
|
221
|
+
const application = await readYamlFile(
|
|
222
|
+
`${rootPath}/${existingRelativeLocation}/application.yaml`,
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
const page = await readYamlFile(
|
|
226
|
+
`${rootPath}/${existingRelativeLocation}/page.yaml`,
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
const apisDirName = `${rootPath}/${existingRelativeLocation}/apis`;
|
|
230
|
+
const apis: any[] = [];
|
|
231
|
+
|
|
232
|
+
if (await fs.pathExists(apisDirName)) {
|
|
233
|
+
const apiFiles = await fs.readdir(apisDirName);
|
|
234
|
+
for (const apiFile of apiFiles) {
|
|
235
|
+
const { api: apiContent } = await readAppApiYamlFile(
|
|
236
|
+
apisDirName,
|
|
237
|
+
apiFile,
|
|
238
|
+
);
|
|
239
|
+
// This mimics the shape of the ApiV3Dto object
|
|
240
|
+
apis.push({
|
|
241
|
+
id: apiContent.metadata.id,
|
|
242
|
+
apiPb: apiContent,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
application,
|
|
249
|
+
apis,
|
|
250
|
+
page,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// NOTE: If a change is made to how applications are read from disk, please update
|
|
255
|
+
// logic to write applications to disk in the "writeResourceToDisk" function accordingly.
|
|
256
|
+
export async function readMultiPageApplicationFromDisk(
|
|
257
|
+
rootPath: string,
|
|
258
|
+
existingRelativeLocation: string,
|
|
259
|
+
): Promise<MultiPageApplicationWrapper> {
|
|
260
|
+
const superblocksApplicationConfig =
|
|
261
|
+
await getSuperblocksApplicationConfigJson(
|
|
262
|
+
`${rootPath}/${existingRelativeLocation}`,
|
|
263
|
+
);
|
|
264
|
+
const application = await readYamlFile(
|
|
265
|
+
`${rootPath}/${existingRelativeLocation}/application.yaml`,
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
const pagesDirName = `${rootPath}/${existingRelativeLocation}/pages`;
|
|
269
|
+
const pages: Page[] = [];
|
|
270
|
+
|
|
271
|
+
if (await fs.pathExists(pagesDirName)) {
|
|
272
|
+
for (const page of Object.values(
|
|
273
|
+
superblocksApplicationConfig.pages ?? {},
|
|
274
|
+
)) {
|
|
275
|
+
// Read in the page definition
|
|
276
|
+
const pageContent = await readYamlFile(
|
|
277
|
+
`${pagesDirName}/${slugifyName(page.name)}/page.yaml`,
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
// Read in the API definitions for this page
|
|
281
|
+
const pageApisDirName = `${pagesDirName}/${slugifyName(page.name)}/apis`;
|
|
282
|
+
const apis: ApiWithPb[] = [];
|
|
283
|
+
|
|
284
|
+
if (await fs.pathExists(pageApisDirName)) {
|
|
285
|
+
// Get the API names from pageApis if it exists, or from apis if it doesn't
|
|
286
|
+
const pageApiNames = getApiNamesFromPageConfig(page);
|
|
287
|
+
for (const apiName of pageApiNames) {
|
|
288
|
+
const { api: apiContent } = await readAppApiYamlFile(
|
|
289
|
+
pageApisDirName,
|
|
290
|
+
apiName,
|
|
291
|
+
);
|
|
292
|
+
// This mimics the shape of the ApiV3Dto object
|
|
293
|
+
apis.push({
|
|
294
|
+
id: apiContent.metadata.id,
|
|
295
|
+
pageId: page.id, // Alternatively, pageId is also stored in apiPb.trigger.application.pageId
|
|
296
|
+
apiPb: apiContent,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
pages.push({
|
|
301
|
+
id: pageContent.id,
|
|
302
|
+
name: pageContent.name,
|
|
303
|
+
applicationId: pageContent.applicationId,
|
|
304
|
+
isHidden: pageContent.isHidden,
|
|
305
|
+
layouts: pageContent.layouts,
|
|
306
|
+
apis,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const appApisDirName = `${rootPath}/${existingRelativeLocation}/apis`;
|
|
312
|
+
const apis: any[] = [];
|
|
313
|
+
|
|
314
|
+
if (await fs.pathExists(appApisDirName)) {
|
|
315
|
+
// Get the API names from appApis if it exists, or from apis if it doesn't
|
|
316
|
+
const appConfig = superblocksApplicationConfig;
|
|
317
|
+
const apiNames = getApiNamesFromApplicationConfig(appConfig);
|
|
318
|
+
for (const apiName of apiNames) {
|
|
319
|
+
const { api: apiContent } = await readAppApiYamlFile(
|
|
320
|
+
appApisDirName,
|
|
321
|
+
apiName,
|
|
322
|
+
);
|
|
323
|
+
// This mimics the shape of the ApiV3Dto object
|
|
324
|
+
apis.push({
|
|
325
|
+
id: apiContent.metadata.id,
|
|
326
|
+
apiPb: apiContent,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return {
|
|
332
|
+
application,
|
|
333
|
+
apis,
|
|
334
|
+
pages,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function getApiNamesFromApplicationConfig(
|
|
339
|
+
applicationConfig: SuperblocksApplicationConfig,
|
|
340
|
+
): string[] {
|
|
341
|
+
return applicationConfig.appApis
|
|
342
|
+
? Object.values(applicationConfig.appApis).map((api) => api.name)
|
|
343
|
+
: Object.values(applicationConfig.apis ?? {});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function getApiNamesFromPageConfig(pageConfig: PageInfo): string[] {
|
|
347
|
+
return pageConfig.pageApis
|
|
348
|
+
? Object.values(pageConfig.pageApis).map((api) => api.name)
|
|
349
|
+
: Object.values(pageConfig.apis ?? {});
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export async function readApiFromDisk(
|
|
353
|
+
rootPath: string,
|
|
354
|
+
existingRelativeLocation: string,
|
|
355
|
+
): Promise<ApiWrapper> {
|
|
356
|
+
const path = `${rootPath}/${existingRelativeLocation}`;
|
|
357
|
+
const { api: apiContent } = await readAppApiYamlFile(path);
|
|
358
|
+
return {
|
|
359
|
+
apiPb: apiContent,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// NOTE: If a change is made to how applications are written to disk, please update
|
|
364
|
+
// logic to read applications from disk in the "readApplicationFromDisk" function accordingly.
|
|
365
|
+
export async function writeResourceToDisk(
|
|
366
|
+
resourceType: string,
|
|
367
|
+
resourceId: string,
|
|
368
|
+
resource: any,
|
|
369
|
+
rootPath: string,
|
|
370
|
+
featureFlags: FeatureFlags,
|
|
371
|
+
existingRelativeLocation?: string,
|
|
372
|
+
preferredApiRepresentation?: ApiRepresentation,
|
|
373
|
+
): Promise<VersionedResourceConfig> {
|
|
374
|
+
switch (resourceType) {
|
|
375
|
+
case "APPLICATION": {
|
|
376
|
+
const parentDirName = "apps";
|
|
377
|
+
const newRelativeLocation = `${parentDirName}/${slugifyName(
|
|
378
|
+
resource.application.name,
|
|
379
|
+
)}`;
|
|
380
|
+
const relativeLocation = existingRelativeLocation ?? newRelativeLocation;
|
|
381
|
+
const appDirName = path.resolve(rootPath, relativeLocation);
|
|
382
|
+
if (!(await fs.pathExists(appDirName))) {
|
|
383
|
+
await fs.mkdir(appDirName, { recursive: true });
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const applicationContent = ymlstringify(resource.application, {
|
|
387
|
+
sortMapEntries: true,
|
|
388
|
+
});
|
|
389
|
+
await fs.outputFile(`${appDirName}/application.yaml`, applicationContent);
|
|
390
|
+
if (resource.page) {
|
|
391
|
+
const pageContent = ymlstringify(resource.page, {
|
|
392
|
+
sortMapEntries: true,
|
|
393
|
+
blockQuote: "literal",
|
|
394
|
+
});
|
|
395
|
+
await fs.outputFile(`${appDirName}/page.yaml`, pageContent);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const apiPromises: Array<Promise<void>> = [];
|
|
399
|
+
const apisDirName = `${appDirName}/apis`;
|
|
400
|
+
await fs.ensureDir(apisDirName);
|
|
401
|
+
|
|
402
|
+
const existingApplicationConfig =
|
|
403
|
+
await getSuperblocksApplicationConfigIfExists(appDirName);
|
|
404
|
+
const existingFilePaths = getExistingFilePathsForApplicationApi(
|
|
405
|
+
existingApplicationConfig,
|
|
406
|
+
appDirName,
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
const newApplicationConfig: SuperblocksApplicationConfig = {
|
|
410
|
+
configType: "APPLICATION",
|
|
411
|
+
defaultPageId: resource.page?.id,
|
|
412
|
+
id: resource.application.id,
|
|
413
|
+
metadata: getResourceConfigMetadata(
|
|
414
|
+
featureFlags,
|
|
415
|
+
existingApplicationConfig,
|
|
416
|
+
),
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
const apiRepresentation =
|
|
420
|
+
preferredApiRepresentation ??
|
|
421
|
+
getApiRepresentation(featureFlags, newApplicationConfig);
|
|
422
|
+
|
|
423
|
+
if (resource.apis) {
|
|
424
|
+
for (const api of resource.apis as ApiWrapper[]) {
|
|
425
|
+
const apiInfo = await writeAppApi(
|
|
426
|
+
api,
|
|
427
|
+
appDirName,
|
|
428
|
+
existingFilePaths,
|
|
429
|
+
apiPromises,
|
|
430
|
+
apiRepresentation,
|
|
431
|
+
);
|
|
432
|
+
if (apiRepresentation.extractLargeSourceFiles) {
|
|
433
|
+
if (!newApplicationConfig.appApis) {
|
|
434
|
+
newApplicationConfig.appApis = {};
|
|
435
|
+
}
|
|
436
|
+
newApplicationConfig.appApis[api.apiPb.metadata.id] = apiInfo;
|
|
437
|
+
} else {
|
|
438
|
+
if (!newApplicationConfig.apis) {
|
|
439
|
+
newApplicationConfig.apis = {};
|
|
440
|
+
}
|
|
441
|
+
newApplicationConfig.apis[api.apiPb.metadata.id] = apiInfo.name;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
await Promise.all(apiPromises);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Delete any existing files that were not overwritten
|
|
449
|
+
for (const filePath of existingFilePaths) {
|
|
450
|
+
await fs.remove(filePath);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
await fs.ensureDir(`${appDirName}/${SUPERBLOCKS_HOME_FOLDER_NAME}`);
|
|
454
|
+
|
|
455
|
+
await fs.writeFile(
|
|
456
|
+
`${appDirName}/${RESOURCE_CONFIG_PATH}`,
|
|
457
|
+
JSON.stringify(sortByKey(newApplicationConfig), null, 2),
|
|
458
|
+
);
|
|
459
|
+
|
|
460
|
+
const createdFiles = await Promise.resolve<string[]>(
|
|
461
|
+
// Defensive check for when application settings are missing componentFiles
|
|
462
|
+
resource.componentFiles?.map(
|
|
463
|
+
(file: { filename: string; url: string }) =>
|
|
464
|
+
downloadFile(appDirName, file.filename, file.url),
|
|
465
|
+
) ?? [],
|
|
466
|
+
);
|
|
467
|
+
|
|
468
|
+
// print out failed downloads synchronously here
|
|
469
|
+
createdFiles
|
|
470
|
+
.filter((createdFiles) => createdFiles.length)
|
|
471
|
+
.forEach((createdFile) => {
|
|
472
|
+
console.log(`Unable to download ${createdFile}`);
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
return {
|
|
476
|
+
location: relativeLocation,
|
|
477
|
+
resourceType: "APPLICATION",
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
case "BACKEND": {
|
|
482
|
+
const parentDirName = "backends";
|
|
483
|
+
const apiName = slugifyName(extractApiName(resource));
|
|
484
|
+
const newRelativeLocation = `${parentDirName}/${apiName}`;
|
|
485
|
+
const relativeLocation = existingRelativeLocation ?? newRelativeLocation;
|
|
486
|
+
const backendDirName = path.resolve(rootPath, relativeLocation);
|
|
487
|
+
if (!(await fs.pathExists(backendDirName))) {
|
|
488
|
+
await fs.mkdir(backendDirName, { recursive: true });
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const existingBackendConfig =
|
|
492
|
+
await getSuperblocksBackendConfigIfExists(backendDirName);
|
|
493
|
+
const existingFilePaths = getExistingFilePathsForBackendApi(
|
|
494
|
+
existingBackendConfig,
|
|
495
|
+
backendDirName,
|
|
496
|
+
);
|
|
497
|
+
const backendConfig: SuperblocksBackendConfig = {
|
|
498
|
+
id: resourceId,
|
|
499
|
+
configType: "BACKEND",
|
|
500
|
+
metadata: getResourceConfigMetadata(
|
|
501
|
+
featureFlags,
|
|
502
|
+
existingBackendConfig,
|
|
503
|
+
),
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
const apiRepresentation =
|
|
507
|
+
preferredApiRepresentation ??
|
|
508
|
+
getApiRepresentation(featureFlags, backendConfig);
|
|
509
|
+
|
|
510
|
+
// Write the API file(s)
|
|
511
|
+
const apiInfo = await writeBackendApi(
|
|
512
|
+
resource,
|
|
513
|
+
backendDirName,
|
|
514
|
+
[],
|
|
515
|
+
apiRepresentation,
|
|
516
|
+
existingFilePaths,
|
|
517
|
+
);
|
|
518
|
+
if (apiRepresentation.extractLargeSourceFiles) {
|
|
519
|
+
backendConfig.sourceFiles = apiInfo.sourceFiles;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Write the backend config file
|
|
523
|
+
await fs.ensureDir(`${backendDirName}/${SUPERBLOCKS_HOME_FOLDER_NAME}`);
|
|
524
|
+
await fs.writeFile(
|
|
525
|
+
`${backendDirName}/${RESOURCE_CONFIG_PATH}`,
|
|
526
|
+
JSON.stringify(sortByKey(backendConfig), null, 2),
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
// Delete any existing files that were not overwritten
|
|
530
|
+
for (const filePath of existingFilePaths) {
|
|
531
|
+
await fs.remove(filePath);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
return {
|
|
535
|
+
location: relativeLocation,
|
|
536
|
+
resourceType: "BACKEND",
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
default: {
|
|
541
|
+
throw new Error(`Unsupported resource type: ${resourceType}`);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// NOTE: If a change is made to how applications are written to disk, please update
|
|
547
|
+
// logic to read applications from disk in the "readMultiPageApplicationFromDisk" function accordingly.
|
|
548
|
+
export async function writeApplicationToDisk({
|
|
549
|
+
sdk,
|
|
550
|
+
resource,
|
|
551
|
+
projectRootFolder,
|
|
552
|
+
appRelativePath,
|
|
553
|
+
featureFlags,
|
|
554
|
+
migrateFromSinglePage,
|
|
555
|
+
preferredApiRepresentation,
|
|
556
|
+
}: {
|
|
557
|
+
sdk: SuperblocksSdk;
|
|
558
|
+
resource:
|
|
559
|
+
| MultiPageApplicationWrapperWithComponents
|
|
560
|
+
| CodeModeApplicationWrapper;
|
|
561
|
+
projectRootFolder: string;
|
|
562
|
+
appRelativePath?: string;
|
|
563
|
+
featureFlags: FeatureFlags;
|
|
564
|
+
migrateFromSinglePage?: boolean;
|
|
565
|
+
preferredApiRepresentation?: ApiRepresentation;
|
|
566
|
+
}): Promise<VersionedResourceConfig> {
|
|
567
|
+
const parentDirName = "apps";
|
|
568
|
+
const newRelativeLocation = `${parentDirName}/${slugifyName(
|
|
569
|
+
resource.application.name,
|
|
570
|
+
)}`;
|
|
571
|
+
const relativeLocation = appRelativePath ?? newRelativeLocation;
|
|
572
|
+
const appDirName = path.resolve(projectRootFolder, relativeLocation);
|
|
573
|
+
if (!(await fs.pathExists(appDirName))) {
|
|
574
|
+
await fs.mkdir(appDirName, { recursive: true });
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (resource.type === "code-mode") {
|
|
578
|
+
await writeCodeModeApplicationToDisk(
|
|
579
|
+
sdk,
|
|
580
|
+
resource,
|
|
581
|
+
appDirName,
|
|
582
|
+
featureFlags,
|
|
583
|
+
);
|
|
584
|
+
return {
|
|
585
|
+
location: relativeLocation,
|
|
586
|
+
resourceType: "APPLICATION" as const,
|
|
587
|
+
};
|
|
588
|
+
} else {
|
|
589
|
+
return await writeV1ApplicationToDisk(
|
|
590
|
+
resource,
|
|
591
|
+
appDirName,
|
|
592
|
+
featureFlags,
|
|
593
|
+
relativeLocation,
|
|
594
|
+
migrateFromSinglePage,
|
|
595
|
+
preferredApiRepresentation,
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
async function writeV1ApplicationToDisk(
|
|
601
|
+
resource: MultiPageApplicationWrapperWithComponents,
|
|
602
|
+
appDirName: string,
|
|
603
|
+
featureFlags: FeatureFlags,
|
|
604
|
+
relativeLocation: string,
|
|
605
|
+
migrateFromSinglePage?: boolean,
|
|
606
|
+
preferredApiRepresentation?: ApiRepresentation,
|
|
607
|
+
) {
|
|
608
|
+
const applicationContent = ymlstringify(resource.application, {
|
|
609
|
+
sortMapEntries: true,
|
|
610
|
+
});
|
|
611
|
+
await fs.outputFile(`${appDirName}/application.yaml`, applicationContent);
|
|
612
|
+
|
|
613
|
+
// Find the existing application config and existing file paths
|
|
614
|
+
const existingApplicationConfig =
|
|
615
|
+
await getSuperblocksApplicationConfigIfExists(appDirName);
|
|
616
|
+
const existingFilePaths = getExistingFilePathsForApplicationApi(
|
|
617
|
+
existingApplicationConfig,
|
|
618
|
+
appDirName,
|
|
619
|
+
);
|
|
620
|
+
|
|
621
|
+
const newApplicationConfig: SuperblocksApplicationConfig = {
|
|
622
|
+
configType: "APPLICATION",
|
|
623
|
+
id: resource.application.id,
|
|
624
|
+
metadata: getResourceConfigMetadata(
|
|
625
|
+
featureFlags,
|
|
626
|
+
existingApplicationConfig,
|
|
627
|
+
),
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
const apiRepresentation =
|
|
631
|
+
preferredApiRepresentation ??
|
|
632
|
+
getApiRepresentation(featureFlags, newApplicationConfig);
|
|
633
|
+
|
|
634
|
+
newApplicationConfig.pages = {};
|
|
635
|
+
|
|
636
|
+
const apiPromises: Array<Promise<void>> = [];
|
|
637
|
+
|
|
638
|
+
for (const page of Object.values(resource.pages)) {
|
|
639
|
+
const pageId = page.id;
|
|
640
|
+
const pageDirName = `${appDirName}/pages/${slugifyName(page.name)}`;
|
|
641
|
+
if (!(await fs.pathExists(pageDirName))) {
|
|
642
|
+
await fs.mkdir(pageDirName, { recursive: true });
|
|
643
|
+
}
|
|
644
|
+
newApplicationConfig.pages[pageId] = {
|
|
645
|
+
id: page.id,
|
|
646
|
+
name: page.name,
|
|
647
|
+
};
|
|
648
|
+
const pageConfig = newApplicationConfig.pages[pageId];
|
|
649
|
+
|
|
650
|
+
const pageContent = ymlstringify(
|
|
651
|
+
{
|
|
652
|
+
id: page.id,
|
|
653
|
+
name: page.name,
|
|
654
|
+
applicationId: page.applicationId,
|
|
655
|
+
isHidden: page.isHidden,
|
|
656
|
+
layouts: page.layouts,
|
|
657
|
+
},
|
|
658
|
+
{
|
|
659
|
+
sortMapEntries: true,
|
|
660
|
+
blockQuote: "literal",
|
|
661
|
+
},
|
|
662
|
+
);
|
|
663
|
+
const pageFilePath = `${pageDirName}/page.yaml`;
|
|
664
|
+
await fs.outputFile(pageFilePath, pageContent);
|
|
665
|
+
existingFilePaths.delete(pageFilePath);
|
|
666
|
+
for (const api of page.apis as ApiWrapper[]) {
|
|
667
|
+
const apiInfo = await writeAppApi(
|
|
668
|
+
api,
|
|
669
|
+
pageDirName,
|
|
670
|
+
existingFilePaths,
|
|
671
|
+
apiPromises,
|
|
672
|
+
apiRepresentation,
|
|
673
|
+
);
|
|
674
|
+
// Update the page config with the new api info
|
|
675
|
+
const apiId = api.apiPb.metadata.id;
|
|
676
|
+
if (apiRepresentation.extractLargeSourceFiles) {
|
|
677
|
+
// Add the new pageApis field whenever large source files might be extracted
|
|
678
|
+
if (!pageConfig.pageApis) {
|
|
679
|
+
pageConfig.pageApis = {};
|
|
680
|
+
}
|
|
681
|
+
pageConfig.pageApis[apiId] = {
|
|
682
|
+
name: apiInfo.name,
|
|
683
|
+
sourceFiles: apiInfo.sourceFiles?.sort(),
|
|
684
|
+
};
|
|
685
|
+
} else {
|
|
686
|
+
if (!pageConfig.apis) {
|
|
687
|
+
pageConfig.apis = {};
|
|
688
|
+
}
|
|
689
|
+
pageConfig.apis[apiId] = apiInfo.name;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
const appApis: Record<string, ApiInfo> = {};
|
|
695
|
+
if (resource.apis && resource.apis.length) {
|
|
696
|
+
for (const api of resource.apis as ApiWrapper[]) {
|
|
697
|
+
const apiInfo = await writeAppApi(
|
|
698
|
+
api,
|
|
699
|
+
appDirName,
|
|
700
|
+
existingFilePaths,
|
|
701
|
+
apiPromises,
|
|
702
|
+
apiRepresentation,
|
|
703
|
+
);
|
|
704
|
+
const apiId = api.apiPb.metadata.id;
|
|
705
|
+
if (apiRepresentation.extractLargeSourceFiles) {
|
|
706
|
+
if (!newApplicationConfig.appApis) {
|
|
707
|
+
newApplicationConfig.appApis = {};
|
|
708
|
+
}
|
|
709
|
+
newApplicationConfig.appApis[apiId] = apiInfo;
|
|
710
|
+
} else {
|
|
711
|
+
if (!newApplicationConfig.apis) {
|
|
712
|
+
newApplicationConfig.apis = {};
|
|
713
|
+
}
|
|
714
|
+
newApplicationConfig.apis[apiId] = apiInfo.name;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
await Promise.all(apiPromises);
|
|
718
|
+
}
|
|
719
|
+
if (apiRepresentation.extractLargeSourceFiles) {
|
|
720
|
+
newApplicationConfig.appApis = appApis;
|
|
721
|
+
} else if (!newApplicationConfig.apis) {
|
|
722
|
+
// Make sure there is an empty object even if there are no APIs
|
|
723
|
+
newApplicationConfig.apis = {};
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Delete any existing files that were not overwritten
|
|
727
|
+
for (const filePath of existingFilePaths) {
|
|
728
|
+
await fs.remove(filePath);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
await fs.ensureDir(`${appDirName}/${SUPERBLOCKS_HOME_FOLDER_NAME}`);
|
|
732
|
+
|
|
733
|
+
await fs.writeFile(
|
|
734
|
+
`${appDirName}/${RESOURCE_CONFIG_PATH}`,
|
|
735
|
+
JSON.stringify(sortByKey(newApplicationConfig), null, 2),
|
|
736
|
+
);
|
|
737
|
+
|
|
738
|
+
const createdFiles = await Promise.resolve<string[]>(
|
|
739
|
+
// Defensive check for when application settings are missing componentFiles
|
|
740
|
+
resource.componentFiles?.map((file: { filename: string; url: string }) =>
|
|
741
|
+
downloadFile(appDirName, file.filename, file.url),
|
|
742
|
+
) ?? [],
|
|
743
|
+
);
|
|
744
|
+
|
|
745
|
+
// print out failed downloads synchronously here
|
|
746
|
+
createdFiles
|
|
747
|
+
.filter((createdFiles) => createdFiles.length)
|
|
748
|
+
.forEach((createdFile) => {
|
|
749
|
+
console.log(`Unable to download ${createdFile}`);
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
// do a post-migration cleanup if necessary
|
|
753
|
+
if (migrateFromSinglePage) {
|
|
754
|
+
await fs.remove(`${appDirName}/page.yaml`);
|
|
755
|
+
// delete app-level apis if they are not present in the new application config
|
|
756
|
+
if (!resource.apis || !resource.apis.length) {
|
|
757
|
+
await fs.remove(`${appDirName}/apis`);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
return {
|
|
762
|
+
location: relativeLocation,
|
|
763
|
+
resourceType: "APPLICATION" as const,
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
async function writeCodeModeApplicationToDisk(
|
|
768
|
+
sdk: SuperblocksSdk,
|
|
769
|
+
resource: CodeModeApplicationWrapper,
|
|
770
|
+
appDirName: string,
|
|
771
|
+
featureFlags: FeatureFlags,
|
|
772
|
+
): Promise<void> {
|
|
773
|
+
// Find the existing application config and existing file paths
|
|
774
|
+
const existingApplicationConfig =
|
|
775
|
+
await getSuperblocksApplicationConfigIfExists(appDirName);
|
|
776
|
+
|
|
777
|
+
const newApplicationConfig: SuperblocksApplicationV2Config = {
|
|
778
|
+
configType: "APPLICATION_V2",
|
|
779
|
+
id: resource.application.id,
|
|
780
|
+
metadata: getResourceConfigMetadata(
|
|
781
|
+
featureFlags,
|
|
782
|
+
existingApplicationConfig,
|
|
783
|
+
),
|
|
784
|
+
};
|
|
785
|
+
|
|
786
|
+
await fs.ensureDir(`${appDirName}/${SUPERBLOCKS_HOME_FOLDER_NAME}`);
|
|
787
|
+
|
|
788
|
+
await fs.writeFile(
|
|
789
|
+
`${appDirName}/${RESOURCE_CONFIG_PATH}`,
|
|
790
|
+
JSON.stringify(sortByKey(newApplicationConfig), null, 2),
|
|
791
|
+
);
|
|
792
|
+
|
|
793
|
+
// This writes to DBFS
|
|
794
|
+
try {
|
|
795
|
+
await sdk.dbfsGetApplication({
|
|
796
|
+
applicationId: resource.application.id,
|
|
797
|
+
branch: DEFAULT_BRANCH,
|
|
798
|
+
localDirectoryPath: appDirName,
|
|
799
|
+
});
|
|
800
|
+
} catch (e) {
|
|
801
|
+
// This is most likely to happen if the application is not initialized
|
|
802
|
+
console.log(e);
|
|
803
|
+
throw e;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function getExistingFilePathsForApplicationApi(
|
|
808
|
+
superblocksApplicationConfig: SuperblocksApplicationConfig | undefined,
|
|
809
|
+
location: string,
|
|
810
|
+
): Set<string> {
|
|
811
|
+
const paths = new Set<string>();
|
|
812
|
+
if (!superblocksApplicationConfig) {
|
|
813
|
+
return paths;
|
|
814
|
+
}
|
|
815
|
+
// Pages
|
|
816
|
+
if (superblocksApplicationConfig.pages) {
|
|
817
|
+
for (const page of Object.values(superblocksApplicationConfig.pages)) {
|
|
818
|
+
// Page YAML
|
|
819
|
+
const pageNameSlug = slugifyName(page.name);
|
|
820
|
+
const pagePath = `${location}/pages/${pageNameSlug}`;
|
|
821
|
+
paths.add(`${pagePath}/page.yaml`);
|
|
822
|
+
|
|
823
|
+
// Page APIs
|
|
824
|
+
if (page.pageApis) {
|
|
825
|
+
for (const api of Object.values(page.pageApis)) {
|
|
826
|
+
addExistingFilePathsForApi(api, `${pagePath}/apis`, paths, true);
|
|
827
|
+
}
|
|
828
|
+
} else if (page.apis) {
|
|
829
|
+
for (const apiName of Object.values(page.apis)) {
|
|
830
|
+
addExistingFilePathsForApi(
|
|
831
|
+
{ name: apiName },
|
|
832
|
+
`${pagePath}/apis`,
|
|
833
|
+
paths,
|
|
834
|
+
true,
|
|
835
|
+
);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
// APIs
|
|
841
|
+
if (superblocksApplicationConfig.appApis) {
|
|
842
|
+
// For file version >= 0.2.0, there are API files either the 'apis' folder or in an API-specific folder
|
|
843
|
+
for (const api of Object.values(superblocksApplicationConfig.appApis)) {
|
|
844
|
+
addExistingFilePathsForApi(api, `${location}/apis`, paths, true);
|
|
845
|
+
}
|
|
846
|
+
} else if (superblocksApplicationConfig.apis) {
|
|
847
|
+
// For file version < 0.2.0, there are only API file
|
|
848
|
+
for (const apiName of Object.values(superblocksApplicationConfig.apis)) {
|
|
849
|
+
addExistingFilePathsForApi(
|
|
850
|
+
{ name: apiName },
|
|
851
|
+
`${location}/apis`,
|
|
852
|
+
paths,
|
|
853
|
+
true,
|
|
854
|
+
);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
return paths;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
function getExistingFilePathsForBackendApi(
|
|
861
|
+
superblocksBackendConfig: SuperblocksBackendConfig | undefined,
|
|
862
|
+
location: string,
|
|
863
|
+
): Set<string> {
|
|
864
|
+
const paths = new Set<string>();
|
|
865
|
+
if (!superblocksBackendConfig) {
|
|
866
|
+
return paths;
|
|
867
|
+
}
|
|
868
|
+
// Pages
|
|
869
|
+
if (superblocksBackendConfig.sourceFiles) {
|
|
870
|
+
addExistingFilePathsForApi(
|
|
871
|
+
{ name: "api", sourceFiles: superblocksBackendConfig.sourceFiles },
|
|
872
|
+
location,
|
|
873
|
+
paths,
|
|
874
|
+
false,
|
|
875
|
+
);
|
|
876
|
+
} else {
|
|
877
|
+
addExistingFilePathsForApi({ name: "api" }, location, paths, false);
|
|
878
|
+
}
|
|
879
|
+
return paths;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
export function addExistingFilePathsForApi(
|
|
883
|
+
api: ApiInfo,
|
|
884
|
+
location: string,
|
|
885
|
+
paths: Set<string>,
|
|
886
|
+
useNestedFolder: boolean,
|
|
887
|
+
) {
|
|
888
|
+
const apiNameSlug = slugifyName(api.name);
|
|
889
|
+
if (api.sourceFiles) {
|
|
890
|
+
// File version 0.2.0 and later
|
|
891
|
+
// API files are in an API-specific folder
|
|
892
|
+
const apiDirPath = useNestedFolder
|
|
893
|
+
? `${location}/${apiNameSlug}`
|
|
894
|
+
: location;
|
|
895
|
+
paths.add(`${apiDirPath}/api.yaml`);
|
|
896
|
+
// And there are source files
|
|
897
|
+
for (const sourceFile of api.sourceFiles ?? []) {
|
|
898
|
+
paths.add(`${apiDirPath}/${sourceFile}`);
|
|
899
|
+
}
|
|
900
|
+
} else {
|
|
901
|
+
// Pre-file version 0.2.0
|
|
902
|
+
// API file is in the 'apis' folder with no separate source files
|
|
903
|
+
paths.add(`${location}/${apiNameSlug}.yaml`);
|
|
904
|
+
}
|
|
905
|
+
return paths;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
function getResourceConfigMetadata(
|
|
909
|
+
featureFlags: FeatureFlags,
|
|
910
|
+
existingResourceConfig: SuperblocksResourceConfig | undefined,
|
|
911
|
+
): SuperblocksResourceConfigMetadata | undefined {
|
|
912
|
+
if (!existingResourceConfig) {
|
|
913
|
+
// This is a new application, and we may need to add a metadata field to the application config
|
|
914
|
+
if (featureFlags.splitLargeApiStepsInNewEnabled()) {
|
|
915
|
+
return {
|
|
916
|
+
fileVersion: LATEST_FILE_VERSION,
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
} else if (existingResourceConfig.metadata) {
|
|
920
|
+
return existingResourceConfig.metadata;
|
|
921
|
+
}
|
|
922
|
+
return undefined;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
export async function removeResourceFromDisk(
|
|
926
|
+
rootPath: string,
|
|
927
|
+
resourceRelativeLocation: string,
|
|
928
|
+
) {
|
|
929
|
+
const resourceLocation = path.resolve(rootPath, resourceRelativeLocation);
|
|
930
|
+
await fs.remove(resourceLocation);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
export async function getMode(
|
|
934
|
+
task: any,
|
|
935
|
+
mode: ModeFlag,
|
|
936
|
+
): Promise<ExportViewMode> {
|
|
937
|
+
if (mode) {
|
|
938
|
+
return modeFlagToViewMode(mode);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
const selectedMode = await task.prompt([
|
|
942
|
+
{
|
|
943
|
+
type: "Select",
|
|
944
|
+
name: "mode",
|
|
945
|
+
message: `Select which version of a resource you would like to fetch (${SELECT_PROMPT_HELP})`,
|
|
946
|
+
choices: Object.keys(modeFlagValuesMap).map((mode: string) => ({
|
|
947
|
+
name: mode,
|
|
948
|
+
message: modeFlagValuesMap[mode],
|
|
949
|
+
})),
|
|
950
|
+
initial: 0,
|
|
951
|
+
multiple: false,
|
|
952
|
+
},
|
|
953
|
+
]);
|
|
954
|
+
|
|
955
|
+
return modeFlagToViewMode(selectedMode);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
export function sortByKey(obj: unknown): unknown {
|
|
959
|
+
if (isArray(obj)) {
|
|
960
|
+
return obj.map((item) => sortByKey(item));
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
if (isObject(obj)) {
|
|
964
|
+
const sortedKeys = Object.keys(obj).sort();
|
|
965
|
+
const sortedObj: Record<string, any> = {};
|
|
966
|
+
for (const key of sortedKeys) {
|
|
967
|
+
sortedObj[key] = sortByKey(get(obj, key));
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
return sortedObj;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
return obj;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
export async function getLocalGitRepoState(
|
|
977
|
+
overrideLocalBranch?: string,
|
|
978
|
+
): Promise<LocalGitRepoState> {
|
|
979
|
+
const git = simpleGit();
|
|
980
|
+
|
|
981
|
+
let status: StatusResult;
|
|
982
|
+
try {
|
|
983
|
+
// do not return untracked files which can be slow, we only care about the branch info
|
|
984
|
+
status = await git.status(["--untracked-files=no"]);
|
|
985
|
+
} catch {
|
|
986
|
+
return { status: "NO_GIT" };
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
let localBranchName: string;
|
|
990
|
+
if (overrideLocalBranch) {
|
|
991
|
+
const branches = await git.branch();
|
|
992
|
+
if (!branches.all.includes(overrideLocalBranch)) {
|
|
993
|
+
throw new Error(`There is no branch named ${overrideLocalBranch}`);
|
|
994
|
+
}
|
|
995
|
+
localBranchName = overrideLocalBranch;
|
|
996
|
+
} else {
|
|
997
|
+
if (status.detached || !status.current) {
|
|
998
|
+
return { status: "DETACHED" };
|
|
999
|
+
}
|
|
1000
|
+
localBranchName = status.current;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
const remoteName = (await git.getConfig(`branch.${localBranchName}.remote`))
|
|
1004
|
+
?.value;
|
|
1005
|
+
if (!remoteName) {
|
|
1006
|
+
return { status: "IN_A_BRANCH", localBranchName };
|
|
1007
|
+
}
|
|
1008
|
+
const remoteFetchUrl = (await git.getConfig(`remote.${remoteName}.url`))
|
|
1009
|
+
?.value;
|
|
1010
|
+
if (!remoteFetchUrl) {
|
|
1011
|
+
return { status: "IN_A_BRANCH", localBranchName };
|
|
1012
|
+
}
|
|
1013
|
+
const remotePushUrl = (await git.getConfig(`remote.${remoteName}.pushurl`))
|
|
1014
|
+
?.value;
|
|
1015
|
+
let upstreamBranchName = (
|
|
1016
|
+
await git.revparse(["--abbrev-ref", "--symbolic-full-name", "@{u}"])
|
|
1017
|
+
).split("\n", 1)[0];
|
|
1018
|
+
if (upstreamBranchName.startsWith(`${remoteName}/`)) {
|
|
1019
|
+
// `upstreamBranchName` is in the form `$remoteName/$branchName`, so we need to remove the `$remoteName/` part
|
|
1020
|
+
upstreamBranchName = upstreamBranchName.substring(remoteName.length + 1);
|
|
1021
|
+
}
|
|
1022
|
+
return {
|
|
1023
|
+
status: "IN_A_BRANCH",
|
|
1024
|
+
localBranchName,
|
|
1025
|
+
upstream: {
|
|
1026
|
+
branchName: upstreamBranchName,
|
|
1027
|
+
remoteName,
|
|
1028
|
+
url: remoteFetchUrl,
|
|
1029
|
+
pushUrl: remotePushUrl,
|
|
1030
|
+
},
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
/**
|
|
1035
|
+
* Returns the current git branch, or undefined if not in a git repo
|
|
1036
|
+
*/
|
|
1037
|
+
export async function getCurrentGitBranchIfGit(): Promise<string | null> {
|
|
1038
|
+
const git: SimpleGit = simpleGit();
|
|
1039
|
+
try {
|
|
1040
|
+
// do not return untracked files which can be slow, we only care about the branch info
|
|
1041
|
+
const status = await git.status(["--untracked-files=no"]);
|
|
1042
|
+
const currentBranch = status.current;
|
|
1043
|
+
return currentBranch;
|
|
1044
|
+
} catch {
|
|
1045
|
+
// ignore
|
|
1046
|
+
}
|
|
1047
|
+
return null;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
/**
|
|
1051
|
+
* Returns the current git branch, or throws if not in a git repo
|
|
1052
|
+
*/
|
|
1053
|
+
export async function getCurrentGitBranch(): Promise<string> {
|
|
1054
|
+
const currentBranch = await getCurrentGitBranchIfGit();
|
|
1055
|
+
if (!currentBranch) {
|
|
1056
|
+
throw new Error("No git repository found");
|
|
1057
|
+
}
|
|
1058
|
+
return currentBranch;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
export async function getHeadCommit(branch: string): Promise<[string, string]> {
|
|
1062
|
+
const git: SimpleGit = simpleGit();
|
|
1063
|
+
let headCommitId;
|
|
1064
|
+
let headCommitMessage;
|
|
1065
|
+
const logResponse = await git.show(["--no-patch", "--format=%H%n%s", branch]);
|
|
1066
|
+
const logLines = logResponse.split("\n");
|
|
1067
|
+
if (logLines.length > 1) {
|
|
1068
|
+
headCommitId = logLines[0];
|
|
1069
|
+
headCommitMessage = logLines[1];
|
|
1070
|
+
}
|
|
1071
|
+
if (!headCommitId || !headCommitMessage) {
|
|
1072
|
+
throw new Error(`Failed to get head commit for branch '${branch}'`);
|
|
1073
|
+
}
|
|
1074
|
+
return [headCommitId, headCommitMessage];
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
export function isCI(): boolean {
|
|
1078
|
+
return process.env.CI === "true";
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
export async function isGitRepoDirty(): Promise<boolean> {
|
|
1082
|
+
if (isCI()) {
|
|
1083
|
+
// Skip dirtiness check in CI environments
|
|
1084
|
+
return false;
|
|
1085
|
+
}
|
|
1086
|
+
const git: SimpleGit = simpleGit();
|
|
1087
|
+
const status = await git.status();
|
|
1088
|
+
return !status.isClean();
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
export function extractApiName(api: ApiWrapper): string {
|
|
1092
|
+
return api.apiPb?.metadata?.name ?? api?.name ?? "Unknown";
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
export async function writeAppApi(
|
|
1096
|
+
api: ApiWrapper,
|
|
1097
|
+
directoryPath: string,
|
|
1098
|
+
existingFilePaths: Set<string>,
|
|
1099
|
+
apiPromises: Array<Promise<void>>,
|
|
1100
|
+
apiRepresentation?: ApiRepresentation,
|
|
1101
|
+
): Promise<ApiInfo> {
|
|
1102
|
+
const originalApiName = extractApiName(api);
|
|
1103
|
+
const additionalStepFiles: AdditionalStepFiles[] = [];
|
|
1104
|
+
await writeApiFiles(
|
|
1105
|
+
api,
|
|
1106
|
+
slugifyName(originalApiName),
|
|
1107
|
+
`${directoryPath}/apis`,
|
|
1108
|
+
true,
|
|
1109
|
+
apiPromises,
|
|
1110
|
+
additionalStepFiles,
|
|
1111
|
+
apiRepresentation,
|
|
1112
|
+
existingFilePaths,
|
|
1113
|
+
);
|
|
1114
|
+
return {
|
|
1115
|
+
name: originalApiName,
|
|
1116
|
+
sourceFiles: additionalStepFiles.map((file) => file.relativePath).sort(),
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
async function writeBackendApi(
|
|
1121
|
+
api: ApiWrapper,
|
|
1122
|
+
directoryPath: string,
|
|
1123
|
+
apiPromises: Array<Promise<void>>,
|
|
1124
|
+
apiRepresentation?: ApiRepresentation,
|
|
1125
|
+
existingFilePaths?: Set<string>,
|
|
1126
|
+
): Promise<ApiInfo> {
|
|
1127
|
+
const originalApiName = extractApiName(api);
|
|
1128
|
+
const additionalStepFiles: AdditionalStepFiles[] = [];
|
|
1129
|
+
await writeApiFiles(
|
|
1130
|
+
api,
|
|
1131
|
+
"api",
|
|
1132
|
+
directoryPath,
|
|
1133
|
+
false,
|
|
1134
|
+
apiPromises,
|
|
1135
|
+
additionalStepFiles,
|
|
1136
|
+
apiRepresentation,
|
|
1137
|
+
existingFilePaths,
|
|
1138
|
+
);
|
|
1139
|
+
return {
|
|
1140
|
+
name: originalApiName,
|
|
1141
|
+
sourceFiles: additionalStepFiles.map((file) => file.relativePath).sort(),
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
async function validateMultiPageApplication(
|
|
1146
|
+
applicationConfig: SuperblocksApplicationConfig,
|
|
1147
|
+
superblocksRootPath: string,
|
|
1148
|
+
location: string,
|
|
1149
|
+
): Promise<string | undefined> {
|
|
1150
|
+
// validate app level APIs
|
|
1151
|
+
const apiNames = getApiNamesFromApplicationConfig(applicationConfig);
|
|
1152
|
+
for (const apiName of apiNames) {
|
|
1153
|
+
const apiPath = path.resolve(
|
|
1154
|
+
superblocksRootPath,
|
|
1155
|
+
location,
|
|
1156
|
+
"apis",
|
|
1157
|
+
`${slugifyName(apiName)}.yaml`,
|
|
1158
|
+
);
|
|
1159
|
+
const validateApiError = await validateYamlFile(apiPath);
|
|
1160
|
+
if (validateApiError) {
|
|
1161
|
+
return validateApiError;
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
// validate pages
|
|
1165
|
+
for (const page of Object.values(applicationConfig.pages ?? {})) {
|
|
1166
|
+
const pagePath = path.resolve(
|
|
1167
|
+
superblocksRootPath,
|
|
1168
|
+
location,
|
|
1169
|
+
"pages",
|
|
1170
|
+
slugifyName(page.name),
|
|
1171
|
+
"page.yaml",
|
|
1172
|
+
);
|
|
1173
|
+
const validatePageError = await validateYamlFile(pagePath);
|
|
1174
|
+
if (validatePageError) {
|
|
1175
|
+
return validatePageError;
|
|
1176
|
+
}
|
|
1177
|
+
// validate page level APIs
|
|
1178
|
+
const pageApiNames = getApiNamesFromPageConfig(page);
|
|
1179
|
+
for (const apiName of pageApiNames) {
|
|
1180
|
+
const apisPath = path.resolve(
|
|
1181
|
+
superblocksRootPath,
|
|
1182
|
+
location,
|
|
1183
|
+
"pages",
|
|
1184
|
+
slugifyName(page.name),
|
|
1185
|
+
"apis",
|
|
1186
|
+
);
|
|
1187
|
+
// Try the API path not in a nested directory
|
|
1188
|
+
const apiPath = path.resolve(apisPath, `${slugifyName(apiName)}.yaml`);
|
|
1189
|
+
if (fs.pathExistsSync(apiPath)) {
|
|
1190
|
+
const validateApiError = await validateYamlFile(apiPath);
|
|
1191
|
+
if (validateApiError) {
|
|
1192
|
+
return validateApiError;
|
|
1193
|
+
}
|
|
1194
|
+
} else {
|
|
1195
|
+
// Try the API path in a nested directory
|
|
1196
|
+
const nestedApiPath = path.resolve(
|
|
1197
|
+
apisPath,
|
|
1198
|
+
slugifyName(apiName),
|
|
1199
|
+
"api.yaml",
|
|
1200
|
+
);
|
|
1201
|
+
if (fs.pathExistsSync(nestedApiPath)) {
|
|
1202
|
+
const validateApiError = await validateYamlFile(nestedApiPath);
|
|
1203
|
+
if (validateApiError) {
|
|
1204
|
+
return validateApiError;
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
return undefined;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
export async function validateLocalResource(
|
|
1214
|
+
superblocksRootPath: string,
|
|
1215
|
+
resource: VersionedResourceConfig,
|
|
1216
|
+
): Promise<string | undefined> {
|
|
1217
|
+
switch (resource.resourceType) {
|
|
1218
|
+
case "APPLICATION": {
|
|
1219
|
+
// make sure application config exists
|
|
1220
|
+
const applicationConfigPath = path.resolve(
|
|
1221
|
+
superblocksRootPath,
|
|
1222
|
+
resource.location,
|
|
1223
|
+
RESOURCE_CONFIG_PATH,
|
|
1224
|
+
);
|
|
1225
|
+
if (!(await fs.pathExists(applicationConfigPath))) {
|
|
1226
|
+
return `File ${relativeToCurrentDir(
|
|
1227
|
+
applicationConfigPath,
|
|
1228
|
+
)} not found. Superblocks CLI commands cannot function without it.`;
|
|
1229
|
+
}
|
|
1230
|
+
let applicationConfig:
|
|
1231
|
+
| SuperblocksApplicationConfig
|
|
1232
|
+
| SuperblocksApplicationV2Config
|
|
1233
|
+
| undefined = undefined;
|
|
1234
|
+
try {
|
|
1235
|
+
applicationConfig = await getSuperblocksApplicationConfigJson(
|
|
1236
|
+
path.join(superblocksRootPath, resource.location),
|
|
1237
|
+
);
|
|
1238
|
+
} catch {
|
|
1239
|
+
// Noop
|
|
1240
|
+
}
|
|
1241
|
+
if (!applicationConfig) {
|
|
1242
|
+
try {
|
|
1243
|
+
applicationConfig = await getSuperblocksApplicationV2ConfigJson(
|
|
1244
|
+
path.join(superblocksRootPath, resource.location),
|
|
1245
|
+
);
|
|
1246
|
+
if (!applicationConfig) {
|
|
1247
|
+
throw new Error();
|
|
1248
|
+
}
|
|
1249
|
+
} catch {
|
|
1250
|
+
return `File ${relativeToCurrentDir(
|
|
1251
|
+
applicationConfigPath,
|
|
1252
|
+
)} is not a valid JSON file. Please be sure it's valid JSON and rerun the command.`;
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
if (applicationConfig.configType === "APPLICATION_V2") {
|
|
1256
|
+
// TODO(wylie): Implement validation for pushing V2 applications
|
|
1257
|
+
break;
|
|
1258
|
+
}
|
|
1259
|
+
const applicationYamlPath = path.resolve(
|
|
1260
|
+
superblocksRootPath,
|
|
1261
|
+
resource.location,
|
|
1262
|
+
"application.yaml",
|
|
1263
|
+
);
|
|
1264
|
+
// make sure application.yaml is a well-formed yaml file
|
|
1265
|
+
try {
|
|
1266
|
+
await readYamlFile(applicationYamlPath);
|
|
1267
|
+
} catch {
|
|
1268
|
+
return `File ${relativeToCurrentDir(
|
|
1269
|
+
applicationYamlPath,
|
|
1270
|
+
)} is not a valid YAML file. Please be sure it's valid YAML and rerun the command.`;
|
|
1271
|
+
}
|
|
1272
|
+
const validationError = validateMultiPageApplication(
|
|
1273
|
+
applicationConfig,
|
|
1274
|
+
superblocksRootPath,
|
|
1275
|
+
resource.location,
|
|
1276
|
+
);
|
|
1277
|
+
if (validationError) {
|
|
1278
|
+
return validationError;
|
|
1279
|
+
}
|
|
1280
|
+
break;
|
|
1281
|
+
}
|
|
1282
|
+
case "BACKEND": {
|
|
1283
|
+
// make sure the backend config exists
|
|
1284
|
+
const backendConfigPath = path.resolve(
|
|
1285
|
+
superblocksRootPath,
|
|
1286
|
+
resource.location,
|
|
1287
|
+
RESOURCE_CONFIG_PATH,
|
|
1288
|
+
);
|
|
1289
|
+
if (!(await fs.pathExists(backendConfigPath))) {
|
|
1290
|
+
return `File ${relativeToCurrentDir(
|
|
1291
|
+
backendConfigPath,
|
|
1292
|
+
)} not found. Superblocks CLI commands cannot function without it.`;
|
|
1293
|
+
}
|
|
1294
|
+
// make sure it's a well-formed backend config
|
|
1295
|
+
try {
|
|
1296
|
+
await fs.readJSON(backendConfigPath);
|
|
1297
|
+
} catch {
|
|
1298
|
+
return `File ${relativeToCurrentDir(
|
|
1299
|
+
backendConfigPath,
|
|
1300
|
+
)} is not a valid JSON file. Please be sure it's valid JSON and rerun the command.`;
|
|
1301
|
+
}
|
|
1302
|
+
// make sure that api.yaml exists
|
|
1303
|
+
const apiYamlPath = path.resolve(
|
|
1304
|
+
superblocksRootPath,
|
|
1305
|
+
resource.location,
|
|
1306
|
+
"api.yaml",
|
|
1307
|
+
);
|
|
1308
|
+
const validateYamlFileError = await validateYamlFile(apiYamlPath);
|
|
1309
|
+
if (validateYamlFileError) {
|
|
1310
|
+
return validateYamlFileError;
|
|
1311
|
+
}
|
|
1312
|
+
break;
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
return undefined;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
export async function deleteResourcesAndUpdateRootConfig(
|
|
1319
|
+
removedResourceIds: string[],
|
|
1320
|
+
existingSuperblocksRootConfig: SuperblocksMonorepoConfig,
|
|
1321
|
+
superblocksRootPath: string,
|
|
1322
|
+
superblocksRootConfigPath: string,
|
|
1323
|
+
): Promise<void> {
|
|
1324
|
+
for (const resourceId of removedResourceIds) {
|
|
1325
|
+
const resource = existingSuperblocksRootConfig?.resources[resourceId];
|
|
1326
|
+
await removeResourceFromDisk(superblocksRootPath, resource.location);
|
|
1327
|
+
}
|
|
1328
|
+
for (const removedResourceId of removedResourceIds) {
|
|
1329
|
+
delete existingSuperblocksRootConfig.resources[removedResourceId];
|
|
1330
|
+
}
|
|
1331
|
+
// update superblocks.json file with removed resources
|
|
1332
|
+
await fs.writeFile(
|
|
1333
|
+
superblocksRootConfigPath,
|
|
1334
|
+
JSON.stringify(sortByKey(existingSuperblocksRootConfig), null, 2),
|
|
1335
|
+
);
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
async function validateYamlFile(yamlPath: string): Promise<string | undefined> {
|
|
1339
|
+
// make sure a yaml file is well-formed
|
|
1340
|
+
try {
|
|
1341
|
+
await readYamlFile(yamlPath);
|
|
1342
|
+
} catch {
|
|
1343
|
+
return `File ${relativeToCurrentDir(
|
|
1344
|
+
yamlPath,
|
|
1345
|
+
)} is not a valid YAML file. Please be sure it's valid YAML and rerun the command.`;
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
function relativeToCurrentDir(applicationConfigPath: string) {
|
|
1350
|
+
return `./${path.relative(process.cwd(), applicationConfigPath)}`;
|
|
1351
|
+
}
|