@hubspot/project-parsing-lib 0.2.0 → 0.2.1-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.
Files changed (66) hide show
  1. package/README.md +4 -35
  2. package/package.json +124 -22
  3. package/src/exports/constants.d.ts +1 -0
  4. package/src/exports/constants.js +1 -0
  5. package/src/exports/lang.d.ts +1 -0
  6. package/src/exports/lang.js +1 -0
  7. package/src/exports/migrate.d.ts +1 -0
  8. package/src/exports/migrate.js +1 -0
  9. package/src/exports/profiles.d.ts +3 -0
  10. package/src/exports/profiles.js +2 -0
  11. package/src/exports/projects.d.ts +4 -0
  12. package/src/exports/projects.js +3 -0
  13. package/src/exports/schema.d.ts +2 -0
  14. package/src/exports/schema.js +1 -0
  15. package/src/exports/themes.d.ts +2 -0
  16. package/src/exports/themes.js +1 -0
  17. package/src/exports/transform.d.ts +2 -0
  18. package/src/exports/transform.js +1 -0
  19. package/src/exports/translate.d.ts +3 -0
  20. package/src/exports/translate.js +2 -0
  21. package/src/exports/uid.d.ts +1 -0
  22. package/src/exports/uid.js +1 -0
  23. package/src/exports/validation.d.ts +3 -0
  24. package/src/exports/validation.js +2 -0
  25. package/src/exports/workspaces.d.ts +2 -0
  26. package/src/exports/workspaces.js +1 -0
  27. package/src/lang/copy.d.ts +8 -1
  28. package/src/lang/copy.js +30 -33
  29. package/src/lib/constants.d.ts +55 -28
  30. package/src/lib/constants.js +172 -121
  31. package/src/lib/errors.d.ts +4 -3
  32. package/src/lib/errors.js +62 -38
  33. package/src/lib/files.d.ts +10 -1
  34. package/src/lib/files.js +51 -40
  35. package/src/lib/localDev.d.ts +4 -0
  36. package/src/lib/localDev.js +72 -0
  37. package/src/lib/migrate.d.ts +1 -0
  38. package/src/lib/migrate.js +43 -45
  39. package/src/lib/migrateThemes.d.ts +25 -0
  40. package/src/lib/migrateThemes.js +120 -0
  41. package/src/lib/minimalArboristTree.d.ts +118 -0
  42. package/src/lib/minimalArboristTree.js +32 -0
  43. package/src/lib/platformVersion.d.ts +3 -0
  44. package/src/lib/platformVersion.js +16 -0
  45. package/src/lib/profiles.d.ts +6 -1
  46. package/src/lib/profiles.js +95 -40
  47. package/src/lib/project.d.ts +13 -0
  48. package/src/lib/project.js +36 -0
  49. package/src/lib/schemas.d.ts +2 -2
  50. package/src/lib/schemas.js +11 -11
  51. package/src/lib/transform.d.ts +4 -2
  52. package/src/lib/transform.js +100 -53
  53. package/src/lib/translate.d.ts +3 -0
  54. package/src/lib/translate.js +62 -0
  55. package/src/lib/types.d.ts +30 -6
  56. package/src/lib/types.js +1 -2
  57. package/src/lib/uid.d.ts +2 -0
  58. package/src/lib/uid.js +14 -9
  59. package/src/lib/utils.d.ts +3 -0
  60. package/src/lib/utils.js +16 -0
  61. package/src/lib/validation.d.ts +4 -4
  62. package/src/lib/validation.js +61 -53
  63. package/src/lib/workspaces.d.ts +68 -0
  64. package/src/lib/workspaces.js +290 -0
  65. package/src/index.d.ts +0 -18
  66. package/src/index.js +0 -86
package/src/lib/files.js CHANGED
@@ -1,66 +1,59 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.loadHsProfileFile = loadHsProfileFile;
7
- exports.getAllHsProfiles = getAllHsProfiles;
8
- exports.loadHsMetaFiles = loadHsMetaFiles;
9
- const fs_1 = __importDefault(require("fs"));
10
- const path_1 = __importDefault(require("path"));
11
- const fs_2 = require("@hubspot/local-dev-lib/fs");
12
- const constants_1 = require("./constants");
13
- const copy_1 = require("../lang/copy");
14
- const logger_1 = require("@hubspot/local-dev-lib/logger");
15
- const profiles_1 = require("./profiles");
16
- function loadHsProfileFile(projectSourceDir, profile) {
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { walk } from '@hubspot/local-dev-lib/fs';
4
+ import { ALLOWED_COMPONENT_DIRECTORIES, ALLOWED_SUB_COMPONENT_DIRECTORIES, METAFILE_EXTENSION, ALLOWED_TOP_LEVEL_FIELDS, } from './constants.js';
5
+ import { errorMessages, logMessages } from '../lang/copy.js';
6
+ import { logger } from '@hubspot/local-dev-lib/logger';
7
+ import { getHsProfileFilename, getHsProfileName, getIsProfileFile, } from './profiles.js';
8
+ export function loadHsProfileFile(projectSourceDir, profile) {
17
9
  if (!profile) {
18
- throw new Error(copy_1.errorMessages.profile.noProfileSpecified);
10
+ throw new Error(errorMessages.profile.noProfileSpecified);
19
11
  }
20
- const profileFile = (0, profiles_1.getHsProfileFilename)(profile);
21
- const profileFilepath = path_1.default.join(projectSourceDir, profileFile);
12
+ const profileFile = getHsProfileFilename(profile);
13
+ const profileFilepath = path.join(projectSourceDir, profileFile);
22
14
  let hsProfile;
23
15
  try {
24
- hsProfile = JSON.parse(fs_1.default.readFileSync(profileFilepath, 'utf8'));
16
+ hsProfile = JSON.parse(fs.readFileSync(profileFilepath, 'utf8'));
25
17
  }
26
18
  catch (_e) {
27
- throw new Error(copy_1.errorMessages.profile.failedToLoadHsProfile(profileFile));
19
+ throw new Error(errorMessages.profile.failedToLoadHsProfile(profileFile));
28
20
  }
29
21
  return hsProfile;
30
22
  }
31
- function getAllHsProfiles(projectSourceDir) {
23
+ export function getAllHsProfiles(projectSourceDir) {
32
24
  return new Promise((resolve, _reject) => {
33
- fs_1.default.readdir(projectSourceDir, { recursive: false, withFileTypes: true }, (err, files) => {
25
+ fs.readdir(projectSourceDir, { recursive: false, withFileTypes: true }, (err, files) => {
34
26
  if (err) {
35
27
  return resolve([]);
36
28
  }
37
29
  return resolve(files
38
- .filter(file => file.isFile() && (0, profiles_1.getIsProfileFile)(file.name))
39
- .map(file => (0, profiles_1.getHsProfileName)(file.name)));
30
+ .filter(file => file.isFile() && getIsProfileFile(file.name))
31
+ .map(file => getHsProfileName(file.name)));
40
32
  });
41
33
  });
42
34
  }
43
- async function loadHsMetaFiles(translationContext) {
44
- const metaFiles = await locateHsMetaFiles(translationContext);
35
+ export async function loadHsMetaFiles(translationContext) {
36
+ const metaFiles = await locateHsMetaFiles(translationContext.projectSourceDir);
45
37
  return parseHsMetaFiles(metaFiles, translationContext);
46
38
  }
47
- async function locateHsMetaFiles(translationContext) {
48
- const { projectSourceDir } = translationContext;
49
- return (await (0, fs_2.walk)(projectSourceDir, ['node_modules'])).reduce((metaFiles, file) => {
50
- if (!file.endsWith(constants_1.metafileExtension)) {
39
+ export async function locateHsMetaFiles(projectSourceDir, options = { silent: false }) {
40
+ return (await walk(projectSourceDir, ['node_modules'])).reduce((metaFiles, file) => {
41
+ if (!file.endsWith(METAFILE_EXTENSION)) {
51
42
  return metaFiles;
52
43
  }
53
- const pathRelativeToProjectSrcDir = path_1.default.relative(projectSourceDir, file);
54
- const { dir: metaFileDir } = path_1.default.parse(pathRelativeToProjectSrcDir);
55
- const parentDirectory = metaFileDir.split(path_1.default.sep)[0];
56
- if (constants_1.allowedComponentDirectories.includes(metaFileDir)) {
44
+ const pathRelativeToProjectSrcDir = path.relative(projectSourceDir, file);
45
+ const { dir: metaFileDir } = path.parse(pathRelativeToProjectSrcDir);
46
+ const parentDirectory = metaFileDir.split(path.sep)[0];
47
+ if (ALLOWED_COMPONENT_DIRECTORIES.includes(metaFileDir)) {
57
48
  metaFiles.push({ file });
58
49
  }
59
- else if (constants_1.allowedSubComponentDirectories.includes(metaFileDir)) {
50
+ else if (ALLOWED_SUB_COMPONENT_DIRECTORIES.includes(metaFileDir)) {
60
51
  metaFiles.push({ file, parentDirectory });
61
52
  }
62
53
  else {
63
- logger_1.logger.warn(copy_1.logMessages.files.skippingPath(pathRelativeToProjectSrcDir));
54
+ if (!options.silent) {
55
+ logger.warn(logMessages.files.skippingPath(pathRelativeToProjectSrcDir));
56
+ }
64
57
  }
65
58
  return metaFiles;
66
59
  }, []);
@@ -75,7 +68,7 @@ function loadFile(metaFileLocation, translationContext) {
75
68
  const { projectSourceDir } = translationContext;
76
69
  return new Promise(async (resolve) => {
77
70
  const { file, parentDirectory } = metaFileLocation;
78
- fs_1.default.readFile(file, 'utf-8', (err, content) => {
71
+ fs.readFile(file, 'utf-8', (err, content) => {
79
72
  if (err) {
80
73
  return resolve({
81
74
  file,
@@ -83,7 +76,7 @@ function loadFile(metaFileLocation, translationContext) {
83
76
  });
84
77
  }
85
78
  resolve({
86
- file: path_1.default.relative(projectSourceDir, file),
79
+ file: path.relative(projectSourceDir, file),
87
80
  content,
88
81
  parentDirectory,
89
82
  errors: [],
@@ -101,7 +94,25 @@ function parseFile(fileLoadResult) {
101
94
  parsedFileContents = JSON.parse(fileLoadResult.content);
102
95
  }
103
96
  catch (_e) {
104
- fileLoadResult.errors?.push(copy_1.errorMessages.validation.invalidJson);
97
+ fileLoadResult.errors?.push(errorMessages.validation.invalidJson);
98
+ }
99
+ // Validate top-level fields (only uid, type, and config are expected)
100
+ if (parsedFileContents &&
101
+ typeof parsedFileContents === 'object' &&
102
+ !Array.isArray(parsedFileContents)) {
103
+ const actualFields = Object.keys(parsedFileContents);
104
+ const unexpectedFields = actualFields.filter(field => !ALLOWED_TOP_LEVEL_FIELDS.has(field));
105
+ if (unexpectedFields.length > 0) {
106
+ const unexpectedFieldsList = unexpectedFields.join(', ');
107
+ fileLoadResult.errors?.push(errorMessages.validation.unexpectedToplevelFields(unexpectedFieldsList));
108
+ }
105
109
  }
106
110
  return { ...fileLoadResult, content: parsedFileContents };
107
111
  }
112
+ export async function projectContainsHsMetaFiles(projectSourceDir) {
113
+ const hsMetaFiles = (await walk(projectSourceDir, ['node_modules'])).filter(file => file.endsWith(METAFILE_EXTENSION));
114
+ return hsMetaFiles.length > 0;
115
+ }
116
+ export function convertPathToPosixPath(filepath) {
117
+ return filepath.replaceAll(path.win32.sep, path.posix.sep);
118
+ }
@@ -0,0 +1,4 @@
1
+ import { BEProfileVariables, HSProfileVariables, IntermediateRepresentationNode, IntermediateRepresentationNodeLocalDev, FileParseResult } from './types.js';
2
+ export declare function getLocalDevProjectNodes(intermediateNodesIndexedByUid: Record<string, IntermediateRepresentationNode>, projectSourceDir: string, projectNodesAtLastUpload?: Record<string, IntermediateRepresentationNodeLocalDev>): Record<string, IntermediateRepresentationNodeLocalDev>;
3
+ export declare function getRemovedNodesAndNodesWithErrors(intermediateNodesIndexedByUid: Record<string, IntermediateRepresentationNode>, projectNodesAtLastUpload: Record<string, IntermediateRepresentationNodeLocalDev>, parsingErrors: FileParseResult[]): Record<string, IntermediateRepresentationNodeLocalDev>;
4
+ export declare function getLocalDevProfileData(profileVariables?: BEProfileVariables): HSProfileVariables;
@@ -0,0 +1,72 @@
1
+ import path from 'path';
2
+ import { isDeepEqual } from '@hubspot/local-dev-lib/isDeepEqual';
3
+ export function getLocalDevProjectNodes(intermediateNodesIndexedByUid, projectSourceDir, projectNodesAtLastUpload) {
4
+ const localDevProjectNodes = {};
5
+ Object.entries(intermediateNodesIndexedByUid).forEach(([uid, node]) => {
6
+ const component = intermediateNodesIndexedByUid[uid];
7
+ const componentConfigPath = path.join(projectSourceDir, component.metaFilePath);
8
+ let configUpdatedSinceLastUpload = false;
9
+ if (projectNodesAtLastUpload) {
10
+ const componentAtLastUpload = projectNodesAtLastUpload[uid];
11
+ if (componentAtLastUpload) {
12
+ configUpdatedSinceLastUpload = !isDeepEqual(component.config, componentAtLastUpload.config);
13
+ }
14
+ else {
15
+ // Component is net new
16
+ configUpdatedSinceLastUpload = true;
17
+ }
18
+ }
19
+ localDevProjectNodes[uid] = {
20
+ ...node,
21
+ localDev: {
22
+ componentRoot: path.dirname(componentConfigPath),
23
+ componentConfigPath,
24
+ configUpdatedSinceLastUpload,
25
+ removed: false,
26
+ parsingErrors: [],
27
+ },
28
+ };
29
+ });
30
+ return localDevProjectNodes;
31
+ }
32
+ export function getRemovedNodesAndNodesWithErrors(intermediateNodesIndexedByUid, projectNodesAtLastUpload, parsingErrors) {
33
+ const nodesWithErrors = {};
34
+ Object.entries(projectNodesAtLastUpload).forEach(([uid, node]) => {
35
+ // Node is still in the project
36
+ if (intermediateNodesIndexedByUid[uid]) {
37
+ return;
38
+ // Node has parsing errors but was not removed
39
+ }
40
+ const errorForNode = parsingErrors.find(error => error.file === node.metaFilePath);
41
+ if (errorForNode) {
42
+ nodesWithErrors[uid] = {
43
+ ...node,
44
+ localDev: {
45
+ ...node.localDev,
46
+ parsingErrors: errorForNode.errors,
47
+ },
48
+ };
49
+ // Node was removed since the last upload
50
+ }
51
+ else {
52
+ nodesWithErrors[uid] = {
53
+ ...node,
54
+ localDev: {
55
+ ...node.localDev,
56
+ removed: true,
57
+ },
58
+ };
59
+ }
60
+ });
61
+ return nodesWithErrors;
62
+ }
63
+ export function getLocalDevProfileData(profileVariables) {
64
+ if (!profileVariables) {
65
+ return {};
66
+ }
67
+ const localDevProfileData = {};
68
+ Object.entries(profileVariables).forEach(([key, value]) => {
69
+ localDevProfileData[key] = value.value;
70
+ });
71
+ return localDevProfileData;
72
+ }
@@ -1 +1,2 @@
1
+ export declare const OUTPUT_IR_FILE = "FULL_IR.json";
1
2
  export declare function migrate(inputDir: string, outputDir: string): Promise<void>;
@@ -1,35 +1,30 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.migrate = migrate;
7
- const fs_1 = require("@hubspot/local-dev-lib/fs");
8
- const path_1 = __importDefault(require("path"));
9
- const fs_2 = __importDefault(require("fs"));
10
- const transform_1 = require("./transform");
11
- const constants_1 = require("./constants");
12
- const index_1 = require("../index");
1
+ import { walk } from '@hubspot/local-dev-lib/fs';
2
+ import path from 'path';
3
+ import fs from 'fs';
4
+ import { mapToUserFacingType } from './transform.js';
5
+ import { METAFILE_EXTENSION, Components, HS_PROJECT_JSON_FILENAME, APP_FUNCTIONS_PACKAGE_KEY, APP_FUNCTIONS_KEY, } from './constants.js';
6
+ import { translate } from './translate.js';
7
+ import { loadJsonFile } from './utils.js';
13
8
  const IR_FILENAME = 'ir.json';
14
9
  const filesDirectory = 'files';
15
- const OUTPUT_IR_FILE = 'FULL_IR.json';
16
- async function migrate(inputDir, outputDir) {
17
- if (!fs_2.default.existsSync(inputDir)) {
10
+ export const OUTPUT_IR_FILE = 'FULL_IR.json';
11
+ export async function migrate(inputDir, outputDir) {
12
+ if (!fs.existsSync(inputDir)) {
18
13
  throw new Error(`Input directory ${inputDir} does not exist`);
19
14
  }
20
- const hsProjectJsonPath = path_1.default.join(inputDir, constants_1.hsProjectJsonFilename);
21
- if (!fs_2.default.existsSync(hsProjectJsonPath)) {
22
- throw new Error(`${constants_1.hsProjectJsonFilename} file does not exist in ${inputDir}`);
15
+ const hsProjectJsonPath = path.join(inputDir, HS_PROJECT_JSON_FILENAME);
16
+ if (!fs.existsSync(hsProjectJsonPath)) {
17
+ throw new Error(`${HS_PROJECT_JSON_FILENAME} file does not exist in ${inputDir}`);
23
18
  }
24
19
  let hsProjectJson;
25
20
  try {
26
21
  hsProjectJson = loadJsonFile(hsProjectJsonPath);
27
22
  }
28
23
  catch (e) {
29
- throw new Error(`Error parsing ${constants_1.hsProjectJsonFilename}: ${e}`);
24
+ throw new Error(`Error parsing ${HS_PROJECT_JSON_FILENAME}: ${e}`);
30
25
  }
31
- const files = await (0, fs_1.walk)(inputDir);
32
- const sourceCodeOutputDir = path_1.default.join(outputDir, 'src');
26
+ const files = await walk(inputDir);
27
+ const sourceCodeOutputDir = path.join(outputDir, 'src');
33
28
  // Create the output directory if it doesn't exist
34
29
  ensureDirExists(sourceCodeOutputDir);
35
30
  files.forEach((filename) => {
@@ -37,63 +32,66 @@ async function migrate(inputDir, outputDir) {
37
32
  if (!isIRFile(filename)) {
38
33
  return;
39
34
  }
40
- const irDirName = path_1.default.dirname(filename);
35
+ const irDirName = path.dirname(filename);
41
36
  const IR = loadJsonFile(filename);
42
37
  const { metaFilePath } = IR;
43
- const projectConfig = convertIRToProjectConfig(IR);
44
- const fullOutputPath = path_1.default.join(sourceCodeOutputDir, getTargetDirectoryFromComponentType(projectConfig.type));
38
+ const component = convertIRToProjectConfig(IR);
39
+ const fullOutputPath = path.join(sourceCodeOutputDir, getTargetDirectoryFromComponentType(component.type));
45
40
  // Ensure the output directory exists
46
- const currentFilesDirectory = path_1.default.join(irDirName, filesDirectory);
41
+ const currentFilesDirectory = path.join(irDirName, filesDirectory);
47
42
  ensureDirExists(currentFilesDirectory);
48
- fs_2.default.cpSync(currentFilesDirectory, fullOutputPath, {
43
+ fs.cpSync(currentFilesDirectory, fullOutputPath, {
49
44
  recursive: true,
50
45
  });
46
+ // In the current component is a APP_FUNCTIONS_PACKAGE we don't want to write a hsmeta.json file
47
+ // since it is an auto generate component
48
+ if (component.type === APP_FUNCTIONS_PACKAGE_KEY) {
49
+ return;
50
+ }
51
51
  // Use the metaFilePath if provided, otherwise use the project UID
52
- const hsmetaFilePath = path_1.default.join(fullOutputPath, metaFilePath
53
- ? path_1.default.basename(metaFilePath)
54
- : `${projectConfig.uid}${constants_1.metafileExtension}`);
52
+ const hsmetaFilePath = path.join(fullOutputPath, metaFilePath
53
+ ? path.basename(metaFilePath)
54
+ : `${component.uid}${METAFILE_EXTENSION}`);
55
55
  // Write the hsmeta file to the output directory
56
- fs_2.default.writeFileSync(hsmetaFilePath, JSON.stringify(projectConfig, null, 2));
56
+ fs.writeFileSync(hsmetaFilePath, JSON.stringify(component, null, 2));
57
57
  });
58
- const IR = await (0, index_1.translate)({
58
+ const IR = await translate({
59
59
  projectSourceDir: sourceCodeOutputDir,
60
60
  platformVersion: hsProjectJson.platformVersion,
61
61
  }, { skipValidation: true });
62
62
  // Write the IR file to the output directory
63
- fs_2.default.writeFileSync(path_1.default.join(outputDir, OUTPUT_IR_FILE), JSON.stringify(IR, null, 2));
63
+ fs.writeFileSync(path.join(outputDir, OUTPUT_IR_FILE), JSON.stringify(IR, null, 2));
64
64
  }
65
65
  function ensureDirExists(dir) {
66
- if (!fs_2.default.existsSync(dir)) {
67
- fs_2.default.mkdirSync(dir, { recursive: true });
66
+ if (!fs.existsSync(dir)) {
67
+ fs.mkdirSync(dir, { recursive: true });
68
68
  }
69
69
  }
70
70
  function isIRFile(filename) {
71
- const { base } = path_1.default.parse(filename);
71
+ const { base } = path.parse(filename);
72
72
  return base.toLowerCase() === IR_FILENAME;
73
73
  }
74
- function loadJsonFile(filename) {
75
- return JSON.parse(fs_2.default.readFileSync(filename, {
76
- encoding: 'utf-8',
77
- }));
78
- }
79
74
  function convertIRToProjectConfig(IR) {
80
75
  const { config, uid, componentType } = IR;
81
76
  return {
82
77
  config,
83
78
  uid,
84
- type: (0, transform_1.mapToUserFacingType)(componentType),
79
+ type: mapToUserFacingType(componentType),
85
80
  // We are assuming no dependencies for now since this will be a freshly migrated
86
81
  // project and the current supported components don't have non-hierarchical dependencies
87
82
  dependencies: undefined,
88
83
  };
89
84
  }
90
85
  function getTargetDirectoryFromComponentType(componentType) {
91
- const component = constants_1.Components[componentType];
86
+ let component = Components[componentType];
87
+ if (componentType === APP_FUNCTIONS_PACKAGE_KEY) {
88
+ component = Components[APP_FUNCTIONS_KEY];
89
+ }
92
90
  if (!component) {
93
- throw Error('Unsupported component type');
91
+ throw Error(`Unsupported component type: "${componentType}"`);
94
92
  }
95
93
  const parentDirectory = component.parentComponent
96
- ? constants_1.Components[component.parentComponent].dir
94
+ ? Components[component.parentComponent].dir
97
95
  : '';
98
- return path_1.default.join(parentDirectory, component.dir);
96
+ return path.join(parentDirectory, component.dir);
99
97
  }
@@ -0,0 +1,25 @@
1
+ export type ThemeDetails = {
2
+ configFilepath: string;
3
+ themePath: string;
4
+ themeConfig: {
5
+ secret_names?: string[];
6
+ };
7
+ };
8
+ export type ReactThemeDetails = {
9
+ configFilepath: string;
10
+ themePath: string;
11
+ themeConfig: {
12
+ secretNames?: string[];
13
+ };
14
+ };
15
+ export type ProjectThemeDetails = {
16
+ legacyThemeDetails: ThemeDetails[];
17
+ legacyReactThemeDetails: ReactThemeDetails[];
18
+ };
19
+ export declare function getProjectThemeDetails(projectSourceDir: string): Promise<ProjectThemeDetails>;
20
+ export declare function migrateThemes(projectDir: string, projectSourceDir: string): Promise<{
21
+ legacyThemeDetails: ThemeDetails[];
22
+ legacyReactThemeDetails: ReactThemeDetails[];
23
+ migrated: boolean;
24
+ failureReason?: string;
25
+ }>;
@@ -0,0 +1,120 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import { promisify } from 'util';
5
+ import { walk } from '@hubspot/local-dev-lib/fs';
6
+ import { THEME_KEY, CMS_ASSETS_KEY, METAFILE_EXTENSION } from './constants.js';
7
+ import { loadJsonFile } from './utils.js';
8
+ import { errorMessages } from '../lang/copy.js';
9
+ const mkdtempAsync = promisify(fs.mkdtemp);
10
+ const LEGACY_THEME_CONFIG = 'theme.json';
11
+ const LEGACY_REACT_THEME_CONFIG = 'cms-assets.json';
12
+ export async function getProjectThemeDetails(projectSourceDir) {
13
+ const files = await walk(projectSourceDir, ['node_modules']);
14
+ const legacyThemeDetails = [];
15
+ const legacyReactThemeDetails = [];
16
+ files.forEach((filename) => {
17
+ const isLegacyThemeConfig = filename.endsWith(LEGACY_THEME_CONFIG);
18
+ const isLegacyReactThemeConfig = filename.endsWith(LEGACY_REACT_THEME_CONFIG);
19
+ if (isLegacyThemeConfig || isLegacyReactThemeConfig) {
20
+ const parsedConfig = loadJsonFile(filename);
21
+ const themeDetails = {
22
+ configFilepath: filename,
23
+ themePath: path.dirname(filename),
24
+ themeConfig: parsedConfig,
25
+ };
26
+ if (isLegacyThemeConfig) {
27
+ legacyThemeDetails.push(themeDetails);
28
+ }
29
+ else {
30
+ legacyReactThemeDetails.push(themeDetails);
31
+ }
32
+ }
33
+ });
34
+ return {
35
+ legacyThemeDetails,
36
+ legacyReactThemeDetails,
37
+ };
38
+ }
39
+ function buildNewTheme(componentKey, migratedProjectTempDir, themeDetails) {
40
+ const themeName = path.basename(themeDetails.themePath);
41
+ const componentDir = path.join(migratedProjectTempDir, componentKey);
42
+ const newThemeDir = path.join(componentDir, themeName);
43
+ fs.cpSync(themeDetails.themePath, newThemeDir, {
44
+ recursive: true,
45
+ });
46
+ const newThemeConfig = {
47
+ uid: themeName,
48
+ type: componentKey,
49
+ config: {
50
+ themePath: themeName,
51
+ secretNames: [],
52
+ },
53
+ };
54
+ // Remove secrets from the legacy theme config if they exist & move them to the new theme config
55
+ let updatedLegacyThemeConfig;
56
+ if ('secret_names' in themeDetails.themeConfig) {
57
+ const { secret_names, ...configWithoutSecrets } = themeDetails.themeConfig;
58
+ if (secret_names) {
59
+ newThemeConfig.config.secretNames = secret_names;
60
+ updatedLegacyThemeConfig = configWithoutSecrets;
61
+ }
62
+ }
63
+ else if ('secretNames' in themeDetails.themeConfig) {
64
+ const { secretNames, ...configWithoutSecrets } = themeDetails.themeConfig;
65
+ if (secretNames) {
66
+ newThemeConfig.config.secretNames = secretNames;
67
+ updatedLegacyThemeConfig = configWithoutSecrets;
68
+ }
69
+ }
70
+ if (updatedLegacyThemeConfig) {
71
+ const newLegacyConfigFilepath = path.join(newThemeDir, path.basename(themeDetails.configFilepath));
72
+ fs.writeFileSync(newLegacyConfigFilepath, JSON.stringify(updatedLegacyThemeConfig, null, 2));
73
+ }
74
+ fs.writeFileSync(path.join(componentDir, `${themeName}${METAFILE_EXTENSION}`), JSON.stringify(newThemeConfig, null, 2));
75
+ }
76
+ export async function migrateThemes(projectDir, projectSourceDir) {
77
+ const { legacyThemeDetails, legacyReactThemeDetails } = await getProjectThemeDetails(projectSourceDir);
78
+ // Migrate the project to a temporary directory to avoid file conflicts in the original project
79
+ const migratedProjectTempDir = await mkdtempAsync(path.join(os.tmpdir(), 'hubspot-migrated-project-'));
80
+ let isRootReactThemeProject = false;
81
+ legacyReactThemeDetails.forEach(themeDetails => {
82
+ isRootReactThemeProject = themeDetails.themePath === projectSourceDir;
83
+ if (!isRootReactThemeProject) {
84
+ buildNewTheme(CMS_ASSETS_KEY, migratedProjectTempDir, themeDetails);
85
+ }
86
+ });
87
+ // Do not support migrating themes that live at the root of the project src directory
88
+ if (isRootReactThemeProject) {
89
+ return {
90
+ legacyThemeDetails,
91
+ legacyReactThemeDetails,
92
+ migrated: false,
93
+ failureReason: errorMessages.migrateThemes.rootReactThemeNotSupported,
94
+ };
95
+ }
96
+ legacyThemeDetails.forEach(themeDetails => {
97
+ buildNewTheme(THEME_KEY, migratedProjectTempDir, themeDetails);
98
+ });
99
+ // Copy the rest of the project files to the temp directory
100
+ fs.cpSync(projectSourceDir, migratedProjectTempDir, {
101
+ recursive: true,
102
+ filter: src => {
103
+ return (!legacyThemeDetails.some(file => src.includes(file.themePath)) &&
104
+ !legacyReactThemeDetails.some(file => src.includes(file.themePath)));
105
+ },
106
+ });
107
+ // Archive the legacy theme files
108
+ const archiveDest = path.join(projectDir, 'archive');
109
+ fs.renameSync(projectSourceDir, archiveDest);
110
+ // Copy the new theme config files to the project source directory
111
+ fs.cpSync(migratedProjectTempDir, projectSourceDir, {
112
+ recursive: true,
113
+ });
114
+ fs.rmSync(migratedProjectTempDir, { recursive: true });
115
+ return {
116
+ legacyThemeDetails,
117
+ legacyReactThemeDetails,
118
+ migrated: true,
119
+ };
120
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * MINIMAL ARBORIST TREE
3
+ *
4
+ * This module provides a lightweight alternative to @npmcli/arborist for use
5
+ * with npm-packlist. Instead of importing the full arborist package (~3.4MB
6
+ * with 35+ transitive dependencies), we create a minimal tree object that
7
+ * satisfies npm-packlist's runtime requirements.
8
+ *
9
+ * WHY THIS EXISTS:
10
+ * - @npmcli/arborist adds ~3.4MB to the bundle
11
+ * - npm-packlist only uses a small subset of the Arborist Node interface
12
+ * - For our use case (determining packable files in workspace directories),
13
+ * we don't need the full dependency resolution capabilities of arborist
14
+ *
15
+ * CONTEXT: HUBSPOT DEVELOPER PROJECTS UPLOAD FLOW
16
+ *
17
+ * This library is used to collect files from npm workspace packages for upload
18
+ * to HubSpot's build system. Important constraints:
19
+ *
20
+ * - node_modules/ directories are EXCLUDED from uploads
21
+ * - The build system runs `npm install` to fetch dependencies
22
+ * - Source files, package.json, and config files are included
23
+ *
24
+ * This means bundleDependencies (which includes files from node_modules/) would
25
+ * not work even if we used full arborist, because node_modules/ is excluded
26
+ * from the upload archive. Developers with private registry packages should use
27
+ * file: dependencies instead, which ARE supported.
28
+ *
29
+ * HOW NPM-PACKLIST USES THE TREE (as of npm-packlist@8.0.1):
30
+ *
31
+ * tree.path - Directory path (REQUIRED)
32
+ * tree.package - Parsed package.json content (REQUIRED)
33
+ * .files - Files array for inclusion
34
+ * .main - Main entry point (always included)
35
+ * .bin - Binary files (always included)
36
+ * .browser - Browser field (always included)
37
+ * .bundleDependencies - Only used when isProjectRoot=true
38
+ * .dependencies - Only used when isProjectRoot=true
39
+ * .optionalDependencies - Only used when isProjectRoot=true
40
+ * tree.workspaces - Map of workspace paths (OPTIONAL, guarded by if-check)
41
+ * tree.isProjectRoot - Boolean for bundled deps handling (OPTIONAL)
42
+ * tree.edgesOut - Dependency edges (only accessed when isProjectRoot=true)
43
+ *
44
+ * KNOWN LIMITATIONS:
45
+ *
46
+ * 1. BUNDLED DEPENDENCIES NOT SUPPORTED
47
+ * We set isProjectRoot=false, which skips bundled dependency handling.
48
+ * If a workspace package has "bundleDependencies" in package.json, those
49
+ * files will NOT be included in the packlist result.
50
+ *
51
+ * NOTE: This would not work anyway because bundleDependencies includes files
52
+ * from node_modules/, and node_modules/ is excluded from project uploads.
53
+ * Even with full arborist, bundled dependencies would be filtered out.
54
+ * Developers needing to include private/local packages should use file:
55
+ * dependencies instead (e.g., "my-pkg": "file:../my-pkg").
56
+ *
57
+ * 2. WORKSPACE-AWARE FILTERING DISABLED
58
+ * We set workspaces=null, so npm-packlist won't exclude workspace
59
+ * directories. This is fine for our use case since we process each
60
+ * workspace independently.
61
+ *
62
+ * 3. VERSION COUPLING
63
+ * This minimal interface is based on npm-packlist@8.0.1. Future versions
64
+ * may access additional properties, causing runtime errors. If packlist
65
+ * behavior changes unexpectedly, check for new property accesses:
66
+ *
67
+ * grep -E "this\.tree\." node_modules/npm-packlist/lib/index.js
68
+ *
69
+ * DEBUGGING:
70
+ *
71
+ * If getPackableFiles() returns unexpected results:
72
+ *
73
+ * 1. Check if npm-packlist version changed (package.json)
74
+ * 2. Compare tree property accesses (grep command above)
75
+ * 3. Test with full arborist to isolate the issue:
76
+ *
77
+ * import Arborist from '@npmcli/arborist';
78
+ * const arb = new Arborist({ path: dir });
79
+ * const tree = await arb.loadActual();
80
+ * const files = await packlist(tree);
81
+ *
82
+ * 4. If full arborist works but minimal tree doesn't, add the missing
83
+ * properties to MinimalArboristTree interface and createMinimalTree()
84
+ */
85
+ /**
86
+ * Minimal subset of package.json fields used by npm-packlist
87
+ */
88
+ interface PackageJsonForPacklist {
89
+ name?: string;
90
+ files?: string[];
91
+ main?: string;
92
+ bin?: string | Record<string, string>;
93
+ browser?: string | Record<string, string>;
94
+ bundleDependencies?: string[];
95
+ dependencies?: Record<string, string>;
96
+ optionalDependencies?: Record<string, string>;
97
+ }
98
+ /**
99
+ * Minimal tree interface that satisfies npm-packlist's runtime requirements.
100
+ * This is NOT the full Arborist Node interface - only the properties that
101
+ * npm-packlist actually accesses at runtime.
102
+ */
103
+ export interface MinimalArboristTree {
104
+ path: string;
105
+ package: PackageJsonForPacklist;
106
+ workspaces: null;
107
+ isProjectRoot: false;
108
+ edgesOut: Map<string, unknown>;
109
+ }
110
+ /**
111
+ * Creates a minimal tree object that satisfies npm-packlist's requirements
112
+ * without importing the heavy @npmcli/arborist package.
113
+ *
114
+ * @param dir - Directory containing the package
115
+ * @returns Minimal tree object compatible with npm-packlist
116
+ */
117
+ export declare function createMinimalTree(dir: string): MinimalArboristTree;
118
+ export {};
@@ -0,0 +1,32 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ /**
4
+ * Creates a minimal tree object that satisfies npm-packlist's requirements
5
+ * without importing the heavy @npmcli/arborist package.
6
+ *
7
+ * @param dir - Directory containing the package
8
+ * @returns Minimal tree object compatible with npm-packlist
9
+ */
10
+ export function createMinimalTree(dir) {
11
+ let pkgJson = {};
12
+ try {
13
+ const content = fs.readFileSync(path.join(dir, 'package.json'), 'utf8');
14
+ pkgJson = JSON.parse(content);
15
+ }
16
+ catch {
17
+ // Use empty package.json if not found or invalid
18
+ // npm-packlist handles this gracefully
19
+ }
20
+ return {
21
+ path: dir,
22
+ package: pkgJson,
23
+ // Set to null to skip workspace-aware filtering
24
+ // (we process each workspace independently)
25
+ workspaces: null,
26
+ // Set to false to skip bundled dependency handling
27
+ // (not needed for workspace file collection)
28
+ isProjectRoot: false,
29
+ // Empty map - only accessed when isProjectRoot is true
30
+ edgesOut: new Map(),
31
+ };
32
+ }