@hubspot/local-dev-lib 0.7.12-experimental.1 → 0.7.13-experimental.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import { AxiosResponse } from 'axios';
2
- import { FileMapperNode, FileMapperOptions, FileTree } from '../types/Files.js';
2
+ import { DirectoryMetaNode, FileMapperNode, FileMapperOptions, FileTree } from '../types/Files.js';
3
3
  import { HubSpotPromise } from '../types/Http.js';
4
4
  export declare function createFileMapperNodeFromStreamResponse(filePath: string, response: Partial<AxiosResponse>): FileMapperNode;
5
5
  export declare function upload(accountId: number, src: string, dest: string, options?: FileMapperOptions): HubSpotPromise<void>;
@@ -10,3 +10,4 @@ export declare function downloadDefault(accountId: number, filepath: string, opt
10
10
  export declare function deleteFile(accountId: number, filePath: string): HubSpotPromise<void>;
11
11
  export declare function moveFile(accountId: number, srcPath: string, destPath: string): HubSpotPromise<void>;
12
12
  export declare function getDirectoryContentsByPath(accountId: number, path: string): HubSpotPromise<FileMapperNode>;
13
+ export declare function getDirectoryMetaByPath(accountId: number, path: string, options?: FileMapperOptions): HubSpotPromise<DirectoryMetaNode>;
package/api/fileMapper.js CHANGED
@@ -89,3 +89,10 @@ export function getDirectoryContentsByPath(accountId, path) {
89
89
  url: `${FILE_MAPPER_API_PATH}/meta/${path}`,
90
90
  });
91
91
  }
92
+ // getDirectoryContentsByPath has the wrong return type for /meta/ — children are strings, not FileMapperNodes
93
+ export function getDirectoryMetaByPath(accountId, path, options = {}) {
94
+ return http.get(accountId, {
95
+ url: `${FILE_MAPPER_API_PATH}/meta/${path.split('/').map(encodeURIComponent).join('/')}`,
96
+ ...options,
97
+ });
98
+ }
package/api/projects.d.ts CHANGED
@@ -6,7 +6,7 @@ import { Deploy, ProjectDeployResponse, ProjectDeployResponseV1, ProjectDeletion
6
6
  import { MigrateAppResponse, CloneAppResponse, PollAppResponse } from '../types/Migration.js';
7
7
  export declare function fetchProjects(accountId: number): HubSpotPromise<FetchProjectResponse>;
8
8
  export declare function createProject(accountId: number, name: string): HubSpotPromise<Project>;
9
- export declare function uploadProject(accountId: number, projectName: string, projectFile: string, uploadMessage: string, platformVersion?: string, intermediateRepresentation?: unknown): HubSpotPromise<UploadProjectResponse | UploadIRResponse>;
9
+ export declare function uploadProject(accountId: number, projectName: string, projectFile: string, uploadMessage: string, platformVersion?: string, intermediateRepresentation?: unknown, skipAutoDeploy?: boolean): HubSpotPromise<UploadProjectResponse | UploadIRResponse>;
10
10
  export declare function fetchProject(accountId: number, projectName: string): HubSpotPromise<Project>;
11
11
  export declare function fetchProjectComponentsMetadata(accountId: number, projectId: number): HubSpotPromise<ProjectComponentsMetadata>;
12
12
  export declare function downloadProject(accountId: number, projectName: string, buildId: number): HubSpotPromise<Buffer>;
package/api/projects.js CHANGED
@@ -21,7 +21,7 @@ export function createProject(accountId, name) {
21
21
  },
22
22
  });
23
23
  }
24
- export async function uploadProject(accountId, projectName, projectFile, uploadMessage, platformVersion, intermediateRepresentation) {
24
+ export async function uploadProject(accountId, projectName, projectFile, uploadMessage, platformVersion, intermediateRepresentation, skipAutoDeploy) {
25
25
  if (intermediateRepresentation) {
26
26
  const formData = {
27
27
  projectFilesZip: fs.createReadStream(projectFile),
@@ -30,6 +30,7 @@ export async function uploadProject(accountId, projectName, projectFile, uploadM
30
30
  ...intermediateRepresentation,
31
31
  projectName,
32
32
  buildMessage: uploadMessage,
33
+ skipAutoDeploy: skipAutoDeploy || false,
33
34
  }),
34
35
  };
35
36
  const response = await http.post(accountId, {
@@ -49,6 +50,9 @@ export async function uploadProject(accountId, projectName, projectFile, uploadM
49
50
  if (platformVersion) {
50
51
  formData.platformVersion = platformVersion;
51
52
  }
53
+ if (skipAutoDeploy) {
54
+ formData.skipAutoDeploy = String(skipAutoDeploy);
55
+ }
52
56
  return http.post(accountId, {
53
57
  url: `${PROJECTS_API_PATH}/upload/${encodeURIComponent(projectName)}`,
54
58
  timeout: 60_000,
package/lib/cms/themes.js CHANGED
@@ -1,7 +1,5 @@
1
1
  import findup from 'findup-sync';
2
- import { getHubSpotWebsiteOrigin } from '../urls.js';
3
- import { ENVIRONMENTS } from '../../constants/environments.js';
4
- import { getConfigAccountEnvironment } from '../../config/index.js';
2
+ import { getHubSpotWebsiteOriginByAccountId } from '../urls.js';
5
3
  export function getThemeJSONPath(path) {
6
4
  return findup('theme.json', {
7
5
  cwd: path,
@@ -21,8 +19,6 @@ export function getThemePreviewUrl(filePath, accountId) {
21
19
  const themeName = getThemeNameFromPath(filePath);
22
20
  if (!themeName)
23
21
  return;
24
- const baseUrl = getHubSpotWebsiteOrigin(getConfigAccountEnvironment(accountId) === 'qa'
25
- ? ENVIRONMENTS.QA
26
- : ENVIRONMENTS.PROD);
22
+ const baseUrl = getHubSpotWebsiteOriginByAccountId(accountId);
27
23
  return `${baseUrl}/theme-previewer/${accountId}/edit/${encodeURIComponent(themeName)}`;
28
24
  }
@@ -3,10 +3,15 @@ export declare function isPathToFile(filepath: string): boolean;
3
3
  export declare function isPathToModule(filepath: string): boolean;
4
4
  export declare function isPathToRoot(filepath: string): boolean;
5
5
  export declare function isPathToHubspot(filepath: string): boolean;
6
- export declare function getFileMapperQueryValues(cmsPublishMode?: CmsPublishMode | null, { staging, assetVersion }?: FileMapperInputOptions): FileMapperOptions;
6
+ export declare function getFileMapperQueryValues(cmsPublishMode?: CmsPublishMode | null, { staging, assetVersion, timeout }?: FileMapperInputOptions): FileMapperOptions;
7
7
  export declare function getTypeDataFromPath(src: string): PathTypeData;
8
8
  export declare function recurseFolder(node: FileMapperNode, callback: RecursiveFileMapperCallback, filepath?: string, depth?: number): boolean;
9
9
  export declare function writeUtimes(accountId: number, filepath: string, node: FileMapperNode): Promise<void>;
10
+ /**
11
+ * @deprecated Use downloadFileOrFolder instead. This function fetches the
12
+ * entire directory tree in a single request, which times out for large
13
+ * directories.
14
+ */
10
15
  export declare function fetchFolderFromApi(accountId: number, src: string, cmsPublishMode?: CmsPublishMode, options?: FileMapperInputOptions): Promise<FileMapperNode>;
11
16
  /**
12
17
  * Fetch a file/folder and write to local file system.
package/lib/fileMapper.js CHANGED
@@ -4,16 +4,13 @@ import path from 'path';
4
4
  import PQueue from 'p-queue';
5
5
  import { getCwd, getExt, convertToLocalFileSystemPath, isAllowedExtension, } from './path.js';
6
6
  import { logger } from './logger.js';
7
- import { fetchFileStream, download, downloadDefault, } from '../api/fileMapper.js';
7
+ import { fetchFileStream, download, downloadDefault, getDirectoryMetaByPath, } from '../api/fileMapper.js';
8
8
  import { MODULE_EXTENSION, FUNCTIONS_EXTENSION, JSR_ALLOWED_EXTENSIONS, } from '../constants/extensions.js';
9
9
  import { CMS_PUBLISH_MODE } from '../constants/files.js';
10
10
  import { isTimeoutError } from '../errors/index.js';
11
11
  import { i18n } from '../utils/lang.js';
12
12
  import { FileSystemError } from '../models/FileSystemError.js';
13
13
  const i18nKey = 'lib.fileMapper';
14
- const queue = new PQueue({
15
- concurrency: 10,
16
- });
17
14
  export function isPathToFile(filepath) {
18
15
  const ext = getExt(filepath);
19
16
  return !!ext && ext !== MODULE_EXTENSION && ext !== FUNCTIONS_EXTENSION;
@@ -37,13 +34,14 @@ function useApiBuffer(cmsPublishMode) {
37
34
  return cmsPublishMode === CMS_PUBLISH_MODE.draft;
38
35
  }
39
36
  // Determines API param based on publish mode and options
40
- export function getFileMapperQueryValues(cmsPublishMode, { staging, assetVersion } = {}) {
37
+ export function getFileMapperQueryValues(cmsPublishMode, { staging, assetVersion, timeout } = {}) {
41
38
  return {
42
39
  params: {
43
40
  buffer: useApiBuffer(cmsPublishMode),
44
41
  environmentId: staging ? 2 : 1,
45
42
  version: assetVersion,
46
43
  },
44
+ ...(timeout !== undefined && { timeout }),
47
45
  };
48
46
  }
49
47
  // Determines version number to log based on input.options
@@ -141,40 +139,6 @@ async function fetchAndWriteFileStream(accountId, srcPath, filepath, cmsPublishM
141
139
  const node = await fetchFileStream(accountId, srcPath, filepath, getFileMapperQueryValues(cmsPublishMode, options));
142
140
  await writeUtimes(accountId, filepath, node);
143
141
  }
144
- // Writes an individual file or folder (not recursive). If file source is missing, the
145
- //file is fetched.
146
- async function writeFileMapperNode(accountId, filepath, node, cmsPublishMode, options = {}) {
147
- const localFilepath = convertToLocalFileSystemPath(path.resolve(filepath));
148
- if (await skipExisting(localFilepath, options.overwrite)) {
149
- logger.log(i18n(`${i18nKey}.skippedExisting`, {
150
- filepath: localFilepath,
151
- }));
152
- return true;
153
- }
154
- if (!node.folder) {
155
- try {
156
- await fetchAndWriteFileStream(accountId, node.path, localFilepath, cmsPublishMode, options);
157
- return true;
158
- }
159
- catch (err) {
160
- return false;
161
- }
162
- }
163
- try {
164
- await fs.ensureDir(localFilepath);
165
- logger.log(i18n(`${i18nKey}.wroteFolder`, {
166
- filepath: localFilepath,
167
- }));
168
- }
169
- catch (err) {
170
- throw new FileSystemError({ cause: err }, {
171
- filepath: localFilepath,
172
- accountId,
173
- operation: 'write',
174
- });
175
- }
176
- return true;
177
- }
178
142
  async function downloadFile(accountId, src, destPath, cmsPublishMode, options = {}) {
179
143
  const { isFile, isHubspot } = getTypeDataFromPath(src);
180
144
  if (!isFile) {
@@ -201,7 +165,6 @@ async function downloadFile(accountId, src, destPath, cmsPublishMode, options =
201
165
  }
202
166
  const localFsPath = convertToLocalFileSystemPath(filepath);
203
167
  await fetchAndWriteFileStream(accountId, src, localFsPath, cmsPublishMode, options);
204
- await queue.onIdle();
205
168
  logger.success(i18n(`${i18nKey}.completedFetch`, {
206
169
  src,
207
170
  version: getAssetVersionIdentifier(options.assetVersion, src),
@@ -218,6 +181,11 @@ async function downloadFile(accountId, src, destPath, cmsPublishMode, options =
218
181
  }
219
182
  }
220
183
  }
184
+ /**
185
+ * @deprecated Use downloadFileOrFolder instead. This function fetches the
186
+ * entire directory tree in a single request, which times out for large
187
+ * directories.
188
+ */
221
189
  export async function fetchFolderFromApi(accountId, src, cmsPublishMode, options = {}) {
222
190
  const { isRoot, isFolder, isHubspot } = getTypeDataFromPath(src);
223
191
  if (!isFolder) {
@@ -233,44 +201,82 @@ export async function fetchFolderFromApi(accountId, src, cmsPublishMode, options
233
201
  logger.log(i18n(`${i18nKey}.folderFetch`, { src, accountId }));
234
202
  return node;
235
203
  }
204
+ async function queueFolderTree(accountId, src, localPath, directoryNode, ctx) {
205
+ const { cmsPublishMode, options, failedPaths, queue } = ctx;
206
+ if (!directoryNode.folder)
207
+ return;
208
+ const { isRoot } = getTypeDataFromPath(src);
209
+ let children = directoryNode.children || [];
210
+ if (isRoot) {
211
+ children = ['@hubspot', ...children];
212
+ }
213
+ try {
214
+ await fs.ensureDir(localPath);
215
+ }
216
+ catch (err) {
217
+ throw new FileSystemError({ cause: err }, { filepath: localPath, accountId, operation: 'write' });
218
+ }
219
+ logger.log(i18n(`${i18nKey}.wroteFolder`, { filepath: localPath }));
220
+ const queryValues = getFileMapperQueryValues(cmsPublishMode, options);
221
+ const queueFileDownload = (remotePath, destPath) => {
222
+ queue.add(async () => {
223
+ try {
224
+ await fetchAndWriteFileStream(accountId, remotePath, destPath, cmsPublishMode, options);
225
+ }
226
+ catch (err) {
227
+ failedPaths.add(remotePath);
228
+ logger.debug(i18n(`${i18nKey}.errors.failedToFetchFile`, {
229
+ src: remotePath,
230
+ dest: destPath,
231
+ }));
232
+ }
233
+ });
234
+ };
235
+ for (const childName of children) {
236
+ const childRemotePath = isRoot ? childName : `${src}/${childName}`;
237
+ const childLocalPath = convertToLocalFileSystemPath(path.join(localPath, childName));
238
+ if (isAllowedExtension(childRemotePath, [...JSR_ALLOWED_EXTENSIONS])) {
239
+ queueFileDownload(childRemotePath, childLocalPath);
240
+ }
241
+ else {
242
+ const { data: childNode } = await getDirectoryMetaByPath(accountId, childRemotePath, queryValues);
243
+ if (childNode) {
244
+ if (childNode.folder) {
245
+ await queueFolderTree(accountId, childRemotePath, childLocalPath, childNode, ctx);
246
+ }
247
+ else {
248
+ queueFileDownload(childRemotePath, childLocalPath);
249
+ }
250
+ }
251
+ }
252
+ }
253
+ }
236
254
  async function downloadFolder(accountId, src, destPath, cmsPublishMode, options = {}) {
255
+ src = src.length > 1 ? src.replace(/\/+$/, '') : src;
256
+ const dest = path.resolve(destPath);
257
+ const { isRoot } = getTypeDataFromPath(src);
258
+ const metaPath = isRoot ? '/' : src;
259
+ const queryValues = getFileMapperQueryValues(cmsPublishMode, options);
260
+ const failedPaths = new Set();
261
+ const queue = new PQueue({ concurrency: 10 });
237
262
  try {
238
- const node = await fetchFolderFromApi(accountId, src, cmsPublishMode, options);
239
- if (!node) {
263
+ const { data: rootMeta } = await getDirectoryMetaByPath(accountId, metaPath, queryValues);
264
+ if (!rootMeta)
240
265
  return;
241
- }
242
- const dest = path.resolve(destPath);
266
+ logger.log(i18n(`${i18nKey}.folderFetch`, { src, accountId }));
243
267
  const rootPath = dest === getCwd()
244
- ? convertToLocalFileSystemPath(path.resolve(dest, node.name))
268
+ ? convertToLocalFileSystemPath(path.resolve(dest, rootMeta.name || path.basename(src)))
245
269
  : dest;
246
- let success = true;
247
- recurseFolder(node, (childNode, filepath) => {
248
- queue.add(async () => {
249
- const succeeded = await writeFileMapperNode(accountId, filepath || '', childNode, cmsPublishMode, options);
250
- if (succeeded === false) {
251
- success = false;
252
- logger.debug(i18n(`${i18nKey}.errors.failedToFetchFile`, {
253
- src: childNode.path,
254
- dest: filepath || '',
255
- }));
256
- }
257
- });
258
- return success;
259
- }, rootPath);
270
+ await queueFolderTree(accountId, src, rootPath, rootMeta, {
271
+ cmsPublishMode,
272
+ options,
273
+ failedPaths,
274
+ queue,
275
+ });
260
276
  await queue.onIdle();
261
- if (success) {
262
- logger.success(i18n(`${i18nKey}.completedFolderFetch`, {
263
- src,
264
- version: getAssetVersionIdentifier(options.assetVersion, src),
265
- dest,
266
- }));
267
- }
268
- else {
269
- // TODO: Fix this exception. It is triggering the catch block so this error is being rewritten
270
- throw new Error(i18n(`${i18nKey}.errors.incompleteFetch`, { src }));
271
- }
272
277
  }
273
278
  catch (err) {
279
+ queue.clear();
274
280
  const error = err;
275
281
  if (isTimeoutError(error)) {
276
282
  throw new Error(i18n(`${i18nKey}.errors.assetTimeout`), { cause: error });
@@ -279,6 +285,14 @@ async function downloadFolder(accountId, src, destPath, cmsPublishMode, options
279
285
  throw new Error(i18n(`${i18nKey}.errors.failedToFetchFolder`, { src, dest: destPath }), { cause: err });
280
286
  }
281
287
  }
288
+ if (failedPaths.size > 0) {
289
+ throw new Error(i18n(`${i18nKey}.errors.incompleteFetch`, { src }));
290
+ }
291
+ logger.success(i18n(`${i18nKey}.completedFolderFetch`, {
292
+ src,
293
+ version: getAssetVersionIdentifier(options.assetVersion, src),
294
+ dest,
295
+ }));
282
296
  }
283
297
  /**
284
298
  * Fetch a file/folder and write to local file system.
package/lib/urls.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export declare const getHubSpotWebsiteOrigin: (env: string) => string;
2
+ export declare function getHubSpotWebsiteOriginByAccountId(accountId: number): string;
2
3
  export declare function getHubSpotApiOrigin(env?: string, useLocalHost?: boolean): string;
package/lib/urls.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { ENVIRONMENTS } from '../constants/environments.js';
2
+ import { getConfigAccountEnvironment } from '../config/index.js';
2
3
  function getEnvUrlString(env) {
3
4
  if (typeof env !== 'string') {
4
5
  return '';
@@ -6,6 +7,11 @@ function getEnvUrlString(env) {
6
7
  return env.toLowerCase() === ENVIRONMENTS.QA ? ENVIRONMENTS.QA : '';
7
8
  }
8
9
  export const getHubSpotWebsiteOrigin = (env) => `https://app.hubspot${getEnvUrlString(env)}.com`;
10
+ export function getHubSpotWebsiteOriginByAccountId(accountId) {
11
+ return getHubSpotWebsiteOrigin(getConfigAccountEnvironment(accountId) === ENVIRONMENTS.QA
12
+ ? ENVIRONMENTS.QA
13
+ : ENVIRONMENTS.PROD);
14
+ }
9
15
  export function getHubSpotApiOrigin(env, useLocalHost) {
10
16
  let domain;
11
17
  const domainOverride = process.env.HUBAPI_DOMAIN_OVERRIDE;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hubspot/local-dev-lib",
3
- "version": "0.7.12-experimental.1",
3
+ "version": "0.7.13-experimental.0",
4
4
  "type": "module",
5
5
  "description": "Provides library functionality for HubSpot local development tooling, including the HubSpot CLI",
6
6
  "files": [
@@ -9,7 +9,7 @@
9
9
  ],
10
10
  "repository": {
11
11
  "type": "git",
12
- "url": "https://github.com/HubSpot/hubspot-local-dev-lib"
12
+ "url": "git+https://github.com/HubSpot/hubspot-local-dev-lib.git"
13
13
  },
14
14
  "publishConfig": {
15
15
  "registry": "https://registry.npmjs.org/",
@@ -47,7 +47,7 @@
47
47
  "open": "^8.4.2",
48
48
  "tsx": "^4.20.6",
49
49
  "typescript": "^5.9.3",
50
- "vitest": "^2.1.9",
50
+ "vitest": "^3.2.6",
51
51
  "yargs": "^17.7.2"
52
52
  },
53
53
  "exports": {
@@ -72,7 +72,7 @@
72
72
  },
73
73
  "dependencies": {
74
74
  "address": "2.0.2",
75
- "axios": "1.15.2",
75
+ "axios": "1.16.0",
76
76
  "chalk": "5.6.2",
77
77
  "chokidar": "3.6.0",
78
78
  "content-disposition": "0.5.4",
@@ -85,7 +85,7 @@
85
85
  "http-proxy-agent": "7.0.2",
86
86
  "https-proxy-agent": "7.0.6",
87
87
  "ignore": "^7.0.0",
88
- "js-yaml": "4.1.1",
88
+ "js-yaml": "4.2.0",
89
89
  "moment": "2.30.1",
90
90
  "p-queue": "^7.0.0",
91
91
  "prettier": "^3.6.2",
package/types/Files.d.ts CHANGED
@@ -18,6 +18,14 @@ export type FileMapperNode = {
18
18
  folder: boolean;
19
19
  children: Array<FileMapperNode>;
20
20
  };
21
+ export type DirectoryMetaNode = {
22
+ name: string;
23
+ path: string;
24
+ folder: boolean;
25
+ children: Array<string>;
26
+ createdAt?: number;
27
+ updatedAt?: number;
28
+ };
21
29
  export type CmsPublishMode = ValueOf<typeof CMS_PUBLISH_MODE>;
22
30
  export type FileMapperOptions = Omit<HttpOptions, 'url'>;
23
31
  export type FileMapperInputOptions = {