@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.
Files changed (152) hide show
  1. package/.mocharc.json +7 -0
  2. package/.prettierrc +18 -0
  3. package/dist/cli-replacement/dev.d.mts +19 -0
  4. package/dist/cli-replacement/dev.d.mts.map +1 -0
  5. package/dist/cli-replacement/dev.mjs +122 -0
  6. package/dist/cli-replacement/dev.mjs.map +1 -0
  7. package/dist/cli-replacement/init.d.ts +14 -0
  8. package/dist/cli-replacement/init.d.ts.map +1 -0
  9. package/dist/cli-replacement/init.js +26 -0
  10. package/dist/cli-replacement/init.js.map +1 -0
  11. package/dist/client.d.ts +31 -17
  12. package/dist/client.d.ts.map +1 -0
  13. package/dist/client.js +137 -155
  14. package/dist/client.js.map +1 -0
  15. package/dist/dbfs/client.d.ts +6 -0
  16. package/dist/dbfs/client.d.ts.map +1 -0
  17. package/dist/dbfs/client.js +117 -0
  18. package/dist/dbfs/client.js.map +1 -0
  19. package/dist/dbfs/local.d.ts +15 -0
  20. package/dist/dbfs/local.d.ts.map +1 -0
  21. package/dist/dbfs/local.js +126 -0
  22. package/dist/dbfs/local.js.map +1 -0
  23. package/dist/dev-utils/custom-build.d.mts +4 -0
  24. package/dist/dev-utils/custom-build.d.mts.map +1 -0
  25. package/dist/dev-utils/custom-build.mjs +99 -0
  26. package/dist/dev-utils/custom-build.mjs.map +1 -0
  27. package/dist/dev-utils/custom-config.d.mts +2 -0
  28. package/dist/dev-utils/custom-config.d.mts.map +1 -0
  29. package/dist/dev-utils/custom-config.mjs +57 -0
  30. package/dist/dev-utils/custom-config.mjs.map +1 -0
  31. package/dist/dev-utils/dev-logger.d.mts +8 -0
  32. package/dist/dev-utils/dev-logger.d.mts.map +1 -0
  33. package/dist/dev-utils/dev-logger.mjs +25 -0
  34. package/dist/dev-utils/dev-logger.mjs.map +1 -0
  35. package/dist/dev-utils/dev-server.d.mts +18 -0
  36. package/dist/dev-utils/dev-server.d.mts.map +1 -0
  37. package/dist/dev-utils/dev-server.mjs +265 -0
  38. package/dist/dev-utils/dev-server.mjs.map +1 -0
  39. package/dist/dev-utils/dev-tracer.d.ts +3 -0
  40. package/dist/dev-utils/dev-tracer.d.ts.map +1 -0
  41. package/dist/dev-utils/dev-tracer.js +28 -0
  42. package/dist/dev-utils/dev-tracer.js.map +1 -0
  43. package/dist/dev-utils/vite-plugin-dd-rum.d.mts +10 -0
  44. package/dist/dev-utils/vite-plugin-dd-rum.d.mts.map +1 -0
  45. package/dist/dev-utils/vite-plugin-dd-rum.mjs +34 -0
  46. package/dist/dev-utils/vite-plugin-dd-rum.mjs.map +1 -0
  47. package/dist/dev-utils/vite-plugin-react-transform.d.mts +7 -0
  48. package/dist/dev-utils/vite-plugin-react-transform.d.mts.map +1 -0
  49. package/dist/dev-utils/vite-plugin-react-transform.mjs +110 -0
  50. package/dist/dev-utils/vite-plugin-react-transform.mjs.map +1 -0
  51. package/dist/dev-utils/vite-plugin-sb-cdn.d.mts +34 -0
  52. package/dist/dev-utils/vite-plugin-sb-cdn.d.mts.map +1 -0
  53. package/dist/dev-utils/vite-plugin-sb-cdn.mjs +720 -0
  54. package/dist/dev-utils/vite-plugin-sb-cdn.mjs.map +1 -0
  55. package/dist/errors.d.ts +1 -0
  56. package/dist/errors.d.ts.map +1 -0
  57. package/dist/errors.js +4 -9
  58. package/dist/errors.js.map +1 -0
  59. package/dist/flag.d.ts +2 -2
  60. package/dist/flag.d.ts.map +1 -0
  61. package/dist/flag.js +5 -9
  62. package/dist/flag.js.map +1 -0
  63. package/dist/index.d.ts +10 -4
  64. package/dist/index.d.ts.map +1 -0
  65. package/dist/index.js +10 -20
  66. package/dist/index.js.map +1 -0
  67. package/dist/sdk.d.ts +42 -18
  68. package/dist/sdk.d.ts.map +1 -0
  69. package/dist/sdk.js +47 -43
  70. package/dist/sdk.js.map +1 -0
  71. package/dist/socket/handlers.d.ts +3 -128
  72. package/dist/socket/handlers.d.ts.map +1 -0
  73. package/dist/socket/handlers.js +7 -9
  74. package/dist/socket/handlers.js.map +1 -0
  75. package/dist/socket/index.d.ts +4 -3
  76. package/dist/socket/index.d.ts.map +1 -0
  77. package/dist/socket/index.js +12 -21
  78. package/dist/socket/index.js.map +1 -0
  79. package/dist/socket/signing.d.ts +3 -1
  80. package/dist/socket/signing.d.ts.map +1 -0
  81. package/dist/socket/signing.js +8 -17
  82. package/dist/socket/signing.js.map +1 -0
  83. package/dist/socket/socket.d.ts +3 -2
  84. package/dist/socket/socket.d.ts.map +1 -0
  85. package/dist/socket/socket.js +9 -19
  86. package/dist/socket/socket.js.map +1 -0
  87. package/dist/types/common.d.ts +2 -103
  88. package/dist/types/common.d.ts.map +1 -0
  89. package/dist/types/common.js +8 -24
  90. package/dist/types/common.js.map +1 -0
  91. package/dist/types/index.d.ts +5 -4
  92. package/dist/types/index.d.ts.map +1 -0
  93. package/dist/types/index.js +5 -20
  94. package/dist/types/index.js.map +1 -0
  95. package/dist/types/plugin.d.ts +1 -0
  96. package/dist/types/plugin.d.ts.map +1 -0
  97. package/dist/types/plugin.js +3 -5
  98. package/dist/types/plugin.js.map +1 -0
  99. package/dist/types/signing.d.ts +2 -1
  100. package/dist/types/signing.d.ts.map +1 -0
  101. package/dist/types/signing.js +2 -2
  102. package/dist/types/signing.js.map +1 -0
  103. package/dist/types/socket.d.ts +1 -0
  104. package/dist/types/socket.d.ts.map +1 -0
  105. package/dist/types/socket.js +2 -5
  106. package/dist/types/socket.js.map +1 -0
  107. package/dist/utils.d.ts +3 -1
  108. package/dist/utils.d.ts.map +1 -0
  109. package/dist/utils.js +14 -23
  110. package/dist/utils.js.map +1 -0
  111. package/dist/version-control.d.mts +59 -0
  112. package/dist/version-control.d.mts.map +1 -0
  113. package/dist/version-control.mjs +899 -0
  114. package/dist/version-control.mjs.map +1 -0
  115. package/eslint.config.js +85 -0
  116. package/package.json +72 -32
  117. package/src/cli-replacement/dev.mts +182 -0
  118. package/src/cli-replacement/init.ts +47 -0
  119. package/src/client.ts +114 -38
  120. package/src/dbfs/client.ts +162 -0
  121. package/src/dbfs/local.ts +163 -0
  122. package/src/dev-utils/custom-build.mts +113 -0
  123. package/src/dev-utils/custom-config.mts +66 -0
  124. package/src/dev-utils/dev-logger.mts +39 -0
  125. package/src/dev-utils/dev-server.mts +342 -0
  126. package/src/dev-utils/dev-tracer.ts +31 -0
  127. package/src/dev-utils/vite-plugin-dd-rum.mts +47 -0
  128. package/src/dev-utils/vite-plugin-react-transform.mts +130 -0
  129. package/src/dev-utils/vite-plugin-sb-cdn.mts +988 -0
  130. package/src/flag.ts +2 -3
  131. package/src/index.ts +119 -4
  132. package/src/sdk.ts +91 -17
  133. package/src/socket/handlers.ts +9 -147
  134. package/src/socket/index.ts +6 -9
  135. package/src/socket/signing.ts +7 -8
  136. package/src/socket/socket.ts +8 -9
  137. package/src/types/common.ts +2 -119
  138. package/src/types/index.ts +4 -4
  139. package/src/types/signing.ts +1 -1
  140. package/src/types/socket.ts +1 -1
  141. package/src/utils.ts +5 -6
  142. package/src/version-control.mts +1351 -0
  143. package/test/dev-utils/fixture/index.html +12 -0
  144. package/test/dev-utils/fixture/main.jsx +22 -0
  145. package/test/dev-utils/fixture/package-lock.json +25 -0
  146. package/test/dev-utils/fixture/package.json +9 -0
  147. package/test/dev-utils/vite-plugin-sb-cdn.test.mts +74 -0
  148. package/test/tsconfig.json +9 -0
  149. package/test/version-control.test.mts +1412 -0
  150. package/tsconfig.json +15 -4
  151. package/tsconfig.tsbuildinfo +1 -1
  152. package/.eslintrc.json +0 -55
package/src/client.ts CHANGED
@@ -1,38 +1,42 @@
1
1
  import * as fs from "fs";
2
+ import { Bucketeer, FileDescriptor } from "@superblocksteam/bucketeer-sdk";
3
+ import { ExportViewMode } from "@superblocksteam/shared";
2
4
  import {
3
5
  COMPONENT_EVENT_HEADER,
4
6
  ComponentEvent,
5
7
  ForbiddenError,
6
8
  getBucketeerUrlFromSuperblocksUrl,
7
9
  getContentType,
8
- LocalGitRepoState,
9
10
  NotFoundError,
10
- SuperblocksResourceType,
11
11
  BadRequestError,
12
12
  unreachable,
13
- ValidateGitSetupRequestBody,
14
13
  } from "@superblocksteam/util";
15
- import axios, { AxiosError, AxiosRequestConfig } from "axios";
14
+ import axios, { AxiosError } from "axios";
16
15
  import FormData from "form-data";
17
- import { isEqual, isEmpty } from "lodash";
16
+ import { isEqual, isEmpty } from "lodash-es";
18
17
  import {
19
18
  BranchNotCheckedOutError,
20
19
  CommitAlreadyExistsError,
21
20
  ValidateGitSetupError,
22
- } from "./errors";
23
- import { signingEnabled } from "./flag";
24
- import { connectToISocketRPCServer, StdISocketRPCClient } from "./socket";
25
- import {
26
- AgentType,
27
- Api,
21
+ } from "./errors.js";
22
+ import { signingEnabled } from "./flag.js";
23
+ import { connectToISocketRPCServer } from "./socket/index.js";
24
+ import { AgentType } from "./types/index.js";
25
+ import { getAgentUrl } from "./utils.js";
26
+ import type { StdISocketRPCClient } from "./socket/index.js";
27
+ import type {
28
28
  ApiWithPb,
29
29
  Page,
30
30
  RemoteCommitDto,
31
- UserMeDto,
32
- ViewMode,
33
31
  DeploymentDto,
34
- } from "./types";
35
- import { getAgentUrl } from "./utils";
32
+ UserMeDto,
33
+ } from "./types/index.js";
34
+ import type {
35
+ LocalGitRepoState,
36
+ SuperblocksResourceType,
37
+ ValidateGitSetupRequestBody,
38
+ } from "@superblocksteam/util";
39
+ import type { AxiosRequestConfig } from "axios";
36
40
 
37
41
  const BASE_BUCKETEER_URL = "api";
38
42
  const BASE_SERVER_PUBLIC_API_URL_V1 = "api/v1/public";
@@ -63,23 +67,31 @@ export interface MultiPageApplicationWrapper {
63
67
  apis: Record<string, any>[];
64
68
  }
65
69
 
70
+ export interface MultiPageApplicationWrapperWithComponents {
71
+ type: "multi-page";
72
+ application: Record<string, any>;
73
+ pages: Page[];
74
+ apis: Record<string, any>[];
75
+ componentFiles: any;
76
+ }
77
+
78
+ export interface CodeModeApplicationWrapper {
79
+ type: "code-mode";
80
+ application: Record<string, any>;
81
+ }
82
+
66
83
  export type PushMultiPageApplicationWithCommitConfig =
67
84
  MultiPageApplicationWrapper & {
68
- commitId?: string;
69
- commitMessage?: string;
85
+ commitId: string;
86
+ commitMessage: string;
70
87
  gitState: LocalGitRepoState;
71
88
  skipCommit: boolean;
72
89
  };
73
90
 
74
- export interface ApiWrapper {
75
- apiPb: Api;
76
- name?: string; //@deprecated this attribute is used for getting a name for backends(jobs and workflows) that were never migrated to the new API version.
77
- }
78
-
79
91
  export type PushApiWithCommitConfig = {
80
92
  apiPb: Record<string, any>;
81
- commitId?: string;
82
- commitMessage?: string;
93
+ commitId: string;
94
+ commitMessage: string;
83
95
  gitState: LocalGitRepoState;
84
96
  skipCommit: boolean;
85
97
  };
@@ -136,12 +148,12 @@ export async function fetchApplication({
136
148
  branch?: string;
137
149
  token: string;
138
150
  superblocksBaseUrl: string;
139
- viewMode: ViewMode;
151
+ viewMode: ExportViewMode;
140
152
  commitId?: string;
141
153
  skipSigningVerification?: boolean;
142
154
  injectedHeaders: Record<string, string>;
143
155
  }): Promise<MultiPageApplicationWrapper | undefined> {
144
- if (commitId && viewMode !== "export-commit") {
156
+ if (commitId && viewMode !== ExportViewMode.EXPORT_COMMIT) {
145
157
  throw new Error(
146
158
  `If commitId ${commitId} is provided, viewMode cannot be ${viewMode}`,
147
159
  );
@@ -177,7 +189,7 @@ export async function fetchApplication({
177
189
  branchName: branch,
178
190
  commitId,
179
191
  });
180
- return resp.data;
192
+ return resp.data as unknown as MultiPageApplicationWrapper;
181
193
  } finally {
182
194
  socket.close();
183
195
  }
@@ -263,14 +275,13 @@ export async function fetchApplicationWithComponents({
263
275
  branch: string;
264
276
  token: string;
265
277
  superblocksBaseUrl: string;
266
- viewMode: ViewMode;
278
+ viewMode: ExportViewMode;
267
279
  commitId?: string;
268
280
  skipSigningVerification?: boolean;
269
281
  injectedHeaders: Record<string, string>;
270
282
  }): Promise<
271
- | (MultiPageApplicationWrapper & {
272
- componentFiles: any;
273
- })
283
+ | MultiPageApplicationWrapperWithComponents
284
+ | CodeModeApplicationWrapper
274
285
  | undefined
275
286
  > {
276
287
  const applicationWrapper = await fetchApplication({
@@ -289,9 +300,48 @@ export async function fetchApplicationWithComponents({
289
300
  return;
290
301
  }
291
302
 
303
+ if (applicationWrapper.application.devEnvEnabled) {
304
+ return {
305
+ type: "code-mode",
306
+ ...applicationWrapper,
307
+ } satisfies CodeModeApplicationWrapper;
308
+ }
309
+
310
+ return await fetchApplicationWithComponentsFromBucketeer({
311
+ applicationWrapper,
312
+ superblocksBaseUrl,
313
+ applicationId,
314
+ branch,
315
+ commitId,
316
+ viewMode,
317
+ token,
318
+ injectedHeaders,
319
+ });
320
+ }
321
+
322
+ async function fetchApplicationWithComponentsFromBucketeer({
323
+ applicationWrapper,
324
+ superblocksBaseUrl,
325
+ applicationId,
326
+ branch,
327
+ commitId,
328
+ viewMode,
329
+ token,
330
+ injectedHeaders,
331
+ }: {
332
+ applicationWrapper: MultiPageApplicationWrapper;
333
+ superblocksBaseUrl: string;
334
+ applicationId: string;
335
+ branch: string;
336
+ commitId?: string;
337
+ viewMode: ExportViewMode;
338
+ token: string;
339
+ injectedHeaders: Record<string, string>;
340
+ }): Promise<MultiPageApplicationWrapperWithComponents> {
292
341
  // if there are no custom components, just return here without trying to initialize them using bucketeer
293
342
  if (isEmpty(applicationWrapper.application?.settings?.registeredComponents)) {
294
343
  return {
344
+ type: "multi-page",
295
345
  ...applicationWrapper,
296
346
  componentFiles: null,
297
347
  };
@@ -318,9 +368,7 @@ export async function fetchApplicationWithComponents({
318
368
  },
319
369
  };
320
370
  const bucketeerApp = (await axios(config))
321
- .data as MultiPageApplicationWrapper & {
322
- componentFiles: any;
323
- };
371
+ .data as MultiPageApplicationWrapperWithComponents;
324
372
 
325
373
  if (
326
374
  !isEqual(
@@ -383,7 +431,7 @@ export async function fetchApi(
383
431
  apiId: string,
384
432
  token: string,
385
433
  superblocksBaseUrl: string,
386
- viewMode: ViewMode,
434
+ viewMode: ExportViewMode,
387
435
  branch?: string,
388
436
  commitId?: string,
389
437
  skipSigningVerification = false,
@@ -716,7 +764,7 @@ You can reduce your component bundle size by uploading static assets to a separa
716
764
  branchName: branch ?? undefined,
717
765
  srcFiles: srcFiles.map((file) => file.filename),
718
766
  buildFiles: buildFiles.map((file) => file.filename),
719
- registeredComponents: componentConfigs,
767
+ registeredComponents: componentConfigs as Record<string, never>,
720
768
  cliVersion,
721
769
  componentBaseUrl: uploadResponse.data.componentBaseUrl,
722
770
  signingRequired: !isEmpty(initialSocket),
@@ -858,7 +906,7 @@ export async function pushApplication({
858
906
  if (resp.responseMeta.status !== 200) {
859
907
  // Get the raw error message from the server and throw it. The outer try-catch block will wrap it in a nicer error message
860
908
  const message: string =
861
- (resp?.responseMeta?.message as string) ?? JSON.stringify(resp?.data);
909
+ resp?.responseMeta?.message ?? JSON.stringify(resp?.data);
862
910
  throw new Error(message);
863
911
  }
864
912
  return resp.data;
@@ -968,7 +1016,7 @@ export async function pushApi({
968
1016
  if (resp.responseMeta.status !== 200) {
969
1017
  // Get the raw error message from the server and throw it. The outer try-catch block will wrap it in a nicer error message
970
1018
  const message: string =
971
- (resp?.responseMeta?.message as string) ?? JSON.stringify(resp?.data);
1019
+ resp?.responseMeta?.message ?? JSON.stringify(resp?.data);
972
1020
  throw new Error(message);
973
1021
  }
974
1022
  return resp.data;
@@ -1215,3 +1263,31 @@ async function deployResource(
1215
1263
  throw new Error(`${e.message}`);
1216
1264
  }
1217
1265
  }
1266
+
1267
+ export async function uploadApplication({
1268
+ files,
1269
+ scopedJwt,
1270
+ url,
1271
+ cliVersion,
1272
+ }: {
1273
+ files: string[];
1274
+ scopedJwt: string;
1275
+ url: string;
1276
+ cliVersion: string;
1277
+ }) {
1278
+ const fds = filesToFileDescriptors(files);
1279
+ const bucketeer = new Bucketeer({
1280
+ token: scopedJwt,
1281
+ baseUrl: url,
1282
+ cliVersion,
1283
+ maxTotalFileSizeMB: SUPERBLOCKS_MAX_FILE_SIZE_MB,
1284
+ });
1285
+ await bucketeer.uploadApplication(fds);
1286
+ }
1287
+
1288
+ function filesToFileDescriptors(files: string[]) {
1289
+ const fds = files.map((file) => {
1290
+ return new FileDescriptor(file, fs.createReadStream(file));
1291
+ });
1292
+ return fds;
1293
+ }
@@ -0,0 +1,162 @@
1
+ import { unreachable } from "@superblocksteam/util";
2
+ import { connectToISocketRPCServer } from "../socket/index.js";
3
+ import { doDownloadDirectoryToLocal, doUploadLocalDirectory } from "./local.js";
4
+
5
+ // TODO(code-mode): this is re-implemented in the vite-plugin-file-sync package
6
+ // because SDK depends on vite-plugin-file-sync, but can't be circular
7
+ export async function getApplicationDirectoryHash(
8
+ token: string,
9
+ superblocksBaseUrl: string,
10
+ applicationId: string,
11
+ branch: string | undefined,
12
+ ) {
13
+ const rpcClient = await connectToISocketRPCServer({
14
+ token,
15
+ superblocksBaseUrl,
16
+ });
17
+ try {
18
+ const response =
19
+ await rpcClient.call.v3.application.liveEditDirectoryContents.get({
20
+ applicationId,
21
+ branchName: branch,
22
+ });
23
+ const liveEditHash = response.data.hash;
24
+ if (!liveEditHash) {
25
+ throw new Error("No live edit hash found");
26
+ }
27
+ return liveEditHash;
28
+ } finally {
29
+ rpcClient.close();
30
+ }
31
+ }
32
+
33
+ export async function downloadApplicationDirectory(
34
+ token: string,
35
+ superblocksBaseUrl: string,
36
+ applicationId: string,
37
+ branch: string | undefined,
38
+ localDirectoryPath: string,
39
+ ): Promise<void> {
40
+ const rpcClient = await connectToISocketRPCServer({
41
+ token,
42
+ superblocksBaseUrl,
43
+ });
44
+ try {
45
+ const liveEditHash = await getApplicationDirectoryHash(
46
+ token,
47
+ superblocksBaseUrl,
48
+ applicationId,
49
+ branch,
50
+ );
51
+ await doDownloadDirectoryToLocal(
52
+ rpcClient,
53
+ liveEditHash,
54
+ localDirectoryPath,
55
+ );
56
+ } finally {
57
+ rpcClient.close();
58
+ }
59
+ }
60
+
61
+ export async function uploadLocalApplication(
62
+ token: string,
63
+ superblocksBaseUrl: string,
64
+ applicationId: string,
65
+ branch: string | undefined,
66
+ localDirectoryPath: string,
67
+ ): Promise<void> {
68
+ const rpcClient = await connectToISocketRPCServer({
69
+ token,
70
+ superblocksBaseUrl,
71
+ });
72
+ try {
73
+ const directoryHash = await doUploadLocalDirectory(
74
+ rpcClient,
75
+ localDirectoryPath,
76
+ );
77
+ console.log(`New application directory hash: ${directoryHash}`);
78
+ await rpcClient.call.v3.application.liveEditDirectoryContents.set({
79
+ applicationId,
80
+ branchName: branch,
81
+ hash: directoryHash,
82
+ });
83
+ } finally {
84
+ rpcClient.close();
85
+ }
86
+ }
87
+
88
+ export async function printDirectoryEntries(
89
+ token: string,
90
+ superblocksBaseUrl: string,
91
+ directoryHash: string,
92
+ ): Promise<void> {
93
+ const rpcClient = await connectToISocketRPCServer({
94
+ token,
95
+ superblocksBaseUrl,
96
+ });
97
+ try {
98
+ const directoryContentsResponse =
99
+ await rpcClient.call.v1.dbfs.directoryContents.get({
100
+ hash: directoryHash,
101
+ });
102
+ for (const entry of directoryContentsResponse.data.contents) {
103
+ let executable: boolean;
104
+ let hash: string;
105
+ let target: string | undefined;
106
+ switch (entry.type) {
107
+ case "-": {
108
+ executable = entry.executable;
109
+ hash = entry.hash;
110
+ break;
111
+ }
112
+ case "l": {
113
+ executable = false;
114
+ target = entry.target;
115
+ hash = " ".repeat(Math.ceil(256 / 24) * 4);
116
+ break;
117
+ }
118
+ case "d": {
119
+ executable = false;
120
+ hash = entry.hash;
121
+ break;
122
+ }
123
+ default: {
124
+ unreachable(entry);
125
+ }
126
+ }
127
+ console.log(
128
+ `${entry.type}${executable ? "x" : " "} ${hash} ${entry.name}${
129
+ target ? ` -> ${target}` : ""
130
+ }`,
131
+ );
132
+ }
133
+ } finally {
134
+ rpcClient.close();
135
+ }
136
+ }
137
+
138
+ export async function printFileContents(
139
+ token: string,
140
+ superblocksBaseUrl: string,
141
+ fileHash: string,
142
+ ): Promise<void> {
143
+ const rpcClient = await connectToISocketRPCServer({
144
+ token,
145
+ superblocksBaseUrl,
146
+ });
147
+ try {
148
+ const fileContentsResponse = await rpcClient.call.v1.dbfs.fileContents.get({
149
+ hash: fileHash,
150
+ });
151
+ // The file can contain binary data, do not interpret it as text
152
+ // Note: `Buffer.from` is a Node.js specific API
153
+ const fileContents = Buffer.from(
154
+ fileContentsResponse.data.contents,
155
+ "base64",
156
+ );
157
+ // Note: `process.stdout.write` is a Node.js specific API
158
+ process.stdout.write(fileContents);
159
+ } finally {
160
+ rpcClient.close();
161
+ }
162
+ }
@@ -0,0 +1,163 @@
1
+ import * as fsp from "node:fs/promises";
2
+ import {
3
+ hashDirectoryContents,
4
+ hashFileContents,
5
+ type DirectoryEntry,
6
+ } from "@superblocksteam/shared";
7
+ import { unreachable } from "@superblocksteam/util";
8
+ import { listLocalDirectory } from "@superblocksteam/vite-plugin-file-sync/list-dir";
9
+ import type { StdISocketRPCClient } from "../socket/index.js";
10
+
11
+ export async function doDownloadDirectoryToLocal(
12
+ rpcClient: StdISocketRPCClient,
13
+ directoryHash: string,
14
+ localDirectoryPath: string,
15
+ ): Promise<void> {
16
+ // ensure that the top-level local directory exists
17
+ await fsp.mkdir(localDirectoryPath, { recursive: true });
18
+ await doDownloadDirectoryToLocalRec(
19
+ rpcClient,
20
+ directoryHash,
21
+ localDirectoryPath,
22
+ );
23
+ }
24
+
25
+ async function doDownloadDirectoryToLocalRec(
26
+ rpcClient: StdISocketRPCClient,
27
+ directoryHash: string,
28
+ localDirectoryPath: string,
29
+ ): Promise<void> {
30
+ const directoryContentsResponse =
31
+ await rpcClient.call.v1.dbfs.directoryContents.get({ hash: directoryHash });
32
+ if (!directoryContentsResponse.data) {
33
+ throw new Error("Failed to get directory contents");
34
+ }
35
+
36
+ for (const entry of directoryContentsResponse.data.contents) {
37
+ const localFilePath = `${localDirectoryPath}/${entry.name}`;
38
+ switch (entry.type) {
39
+ case "-": {
40
+ const fileContentsResponse =
41
+ await rpcClient.call.v1.dbfs.fileContents.get({ hash: entry.hash });
42
+ const fileContents = Buffer.from(
43
+ fileContentsResponse.data.contents,
44
+ "base64",
45
+ );
46
+ await fsp.writeFile(localFilePath, fileContents);
47
+ break;
48
+ }
49
+ case "l": {
50
+ await fsp.symlink(entry.target, localFilePath);
51
+ break;
52
+ }
53
+ case "d": {
54
+ await fsp.mkdir(localFilePath, {
55
+ recursive: true /* do not reject if exists */,
56
+ });
57
+ await doDownloadDirectoryToLocalRec(
58
+ rpcClient,
59
+ entry.hash,
60
+ localFilePath,
61
+ );
62
+ break;
63
+ }
64
+ default: {
65
+ unreachable(entry);
66
+ }
67
+ }
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Computes the hash of a local directory, including the hashes of all its contents recursively.
73
+ *
74
+ * @param localDirectoryPath - The path to the local directory to hash.
75
+ * @returns A promise that resolves to the hash of the directory and its contents.
76
+ */
77
+ export async function hashLocalDirectory(localDirectoryPath: string): Promise<{
78
+ hash: string;
79
+ contents: DirectoryEntry[];
80
+ }> {
81
+ const directoryContents: any[] = [];
82
+ const localDirListing = listLocalDirectory(localDirectoryPath);
83
+ for await (const localDirEntry of localDirListing) {
84
+ let entry: any;
85
+ switch (localDirEntry.type) {
86
+ case "-": {
87
+ const hash = await hashFileContents(localDirEntry.contents);
88
+ entry = {
89
+ type: "-",
90
+ executable: localDirEntry.executable,
91
+ name: localDirEntry.name,
92
+ hash,
93
+ };
94
+ break;
95
+ }
96
+ case "d": {
97
+ const { hash, contents: subdirectoryContents } =
98
+ await hashLocalDirectory(localDirEntry.localPath);
99
+ entry = {
100
+ type: "d",
101
+ name: localDirEntry.name,
102
+ hash,
103
+ contents: subdirectoryContents,
104
+ };
105
+ break;
106
+ }
107
+ case "l": {
108
+ entry = {
109
+ type: "l",
110
+ target: localDirEntry.target,
111
+ name: localDirEntry.name,
112
+ };
113
+ break;
114
+ }
115
+ default:
116
+ unreachable(localDirEntry);
117
+ }
118
+ directoryContents.push(entry);
119
+ }
120
+ const hash = await hashDirectoryContents(directoryContents);
121
+ return { contents: directoryContents, hash };
122
+ }
123
+
124
+ export async function doUploadLocalDirectory(
125
+ rpcClient: StdISocketRPCClient,
126
+ localDirectoryPath: string,
127
+ ): Promise<string> {
128
+ const directoryContents: DirectoryEntry[] = [];
129
+ const localDirListing = listLocalDirectory(localDirectoryPath);
130
+ for await (const localDirEntry of localDirListing) {
131
+ let entry: DirectoryEntry;
132
+ if (localDirEntry.type === "-") {
133
+ const putFileResponse = await rpcClient.call.v1.dbfs.fileContents.put({
134
+ contents: localDirEntry.contents.toString("base64"),
135
+ });
136
+ entry = {
137
+ type: "-",
138
+ hash: putFileResponse.data.hash,
139
+ executable: localDirEntry.executable,
140
+ name: localDirEntry.name,
141
+ };
142
+ } else if (localDirEntry.type === "d") {
143
+ const childHash = await doUploadLocalDirectory(
144
+ rpcClient,
145
+ localDirEntry.localPath,
146
+ );
147
+ entry = { type: "d", name: localDirEntry.name, hash: childHash };
148
+ } else if (localDirEntry.type === "l") {
149
+ entry = {
150
+ type: "l",
151
+ target: localDirEntry.target,
152
+ name: localDirEntry.name,
153
+ };
154
+ } else {
155
+ unreachable(localDirEntry);
156
+ }
157
+ directoryContents.push(entry);
158
+ }
159
+ const putDirResponse = await rpcClient.call.v1.dbfs.directoryContents.put({
160
+ contents: directoryContents,
161
+ });
162
+ return putDirResponse.data.hash;
163
+ }
@@ -0,0 +1,113 @@
1
+ import path from "node:path";
2
+ import watcher, { type FSWatcher } from "chokidar";
3
+ import { red } from "colorette";
4
+ import fs from "fs-extra";
5
+ import semver from "semver";
6
+ import { build } from "vite";
7
+ import { getLegacyComponentsConfig } from "./custom-config.mjs";
8
+ import type { Plugin, UserConfig, ViteDevServer } from "vite";
9
+
10
+ // we only need to build if not on React 18
11
+ export async function isCustomComponentsEnabled() {
12
+ try {
13
+ const packageJson = await fs.readJson(
14
+ path.join(process.cwd(), "custom", "package.json"),
15
+ );
16
+ const reactVersion =
17
+ packageJson.dependencies?.["react"] ||
18
+ packageJson.devDependencies?.["react"];
19
+ if (!reactVersion) {
20
+ return false;
21
+ }
22
+ return semver.lt(semver.coerce(reactVersion)!, "18.0.0");
23
+ } catch {
24
+ return false;
25
+ }
26
+ }
27
+
28
+ async function buildCustomComponents(config: UserConfig) {
29
+ try {
30
+ console.log("Building custom components...");
31
+ await build(config);
32
+ console.log("Custom components build complete");
33
+ return true;
34
+ } catch (error) {
35
+ console.error(red("Custom components build failed:"), "\n", error);
36
+ return false;
37
+ }
38
+ }
39
+
40
+ let __watcher: FSWatcher | undefined;
41
+
42
+ async function watchCustomComponents(folder: string) {
43
+ if (__watcher) {
44
+ return __watcher;
45
+ }
46
+ __watcher = watcher.watch(folder, {
47
+ ignored: [/dist/, /node_modules/],
48
+ persistent: true,
49
+ ignoreInitial: true,
50
+ });
51
+ return __watcher;
52
+ }
53
+
54
+ async function buildCustomComponentsProject() {
55
+ const customComponentsFolder = path.join(process.cwd(), "custom");
56
+ const config = getLegacyComponentsConfig(customComponentsFolder);
57
+ await buildCustomComponents(config);
58
+ }
59
+
60
+ // on file changes, we want to rebuild the custom components library
61
+ // and let our vite server process that the file as changed, which triggers HMR
62
+ // Related reading: https://vite.dev/changes/hotupdate-hook.html
63
+ // TODO: can we run a vite dev server instead of a vite build in order to use hotUpdate to get changes?
64
+ async function watchForChanges(server: ViteDevServer) {
65
+ const customComponentsFolder = path.join(process.cwd(), "custom");
66
+ const watcher = await watchCustomComponents(customComponentsFolder);
67
+
68
+ const handleChange = async (filePath: string) => {
69
+ // We need to rebuild the configuration to pick up on any file system changes
70
+ const config = getLegacyComponentsConfig(customComponentsFolder);
71
+
72
+ console.log(`Custom component file changed: ${filePath}`);
73
+ const url = path.relative(customComponentsFolder, filePath);
74
+ await buildCustomComponents(config);
75
+
76
+ const module = await server.moduleGraph.getModuleByUrl(
77
+ `/custom/dist/${url}`.replace(/\.tsx?$/, ".js"),
78
+ );
79
+ if (module) {
80
+ server.reloadModule(module);
81
+ }
82
+ };
83
+
84
+ watcher.on("change", handleChange);
85
+ watcher.on("add", handleChange);
86
+ watcher.on("unlink", handleChange);
87
+ }
88
+
89
+ export const customComponentsPlugin = (): Plugin => {
90
+ return {
91
+ enforce: "pre",
92
+ name: "custom-components",
93
+ // vite is calling this twice during the initial dev server start due to how remix loads
94
+ // configs via resolveConfig
95
+ async buildStart() {
96
+ const enabled = await isCustomComponentsEnabled();
97
+ if (!enabled) {
98
+ return;
99
+ }
100
+ await buildCustomComponentsProject();
101
+ },
102
+ async configureServer(server) {
103
+ const enabled = await isCustomComponentsEnabled();
104
+ if (!enabled) {
105
+ return;
106
+ }
107
+ await watchForChanges(server);
108
+ },
109
+ buildEnd() {
110
+ __watcher?.close();
111
+ },
112
+ };
113
+ };