@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.
- package/README.md +4 -35
- package/package.json +124 -22
- package/src/exports/constants.d.ts +1 -0
- package/src/exports/constants.js +1 -0
- package/src/exports/lang.d.ts +1 -0
- package/src/exports/lang.js +1 -0
- package/src/exports/migrate.d.ts +1 -0
- package/src/exports/migrate.js +1 -0
- package/src/exports/profiles.d.ts +3 -0
- package/src/exports/profiles.js +2 -0
- package/src/exports/projects.d.ts +4 -0
- package/src/exports/projects.js +3 -0
- package/src/exports/schema.d.ts +2 -0
- package/src/exports/schema.js +1 -0
- package/src/exports/themes.d.ts +2 -0
- package/src/exports/themes.js +1 -0
- package/src/exports/transform.d.ts +2 -0
- package/src/exports/transform.js +1 -0
- package/src/exports/translate.d.ts +3 -0
- package/src/exports/translate.js +2 -0
- package/src/exports/uid.d.ts +1 -0
- package/src/exports/uid.js +1 -0
- package/src/exports/validation.d.ts +3 -0
- package/src/exports/validation.js +2 -0
- package/src/exports/workspaces.d.ts +2 -0
- package/src/exports/workspaces.js +1 -0
- package/src/lang/copy.d.ts +8 -1
- package/src/lang/copy.js +30 -33
- package/src/lib/constants.d.ts +55 -28
- package/src/lib/constants.js +172 -121
- package/src/lib/errors.d.ts +4 -3
- package/src/lib/errors.js +62 -38
- package/src/lib/files.d.ts +10 -1
- package/src/lib/files.js +51 -40
- package/src/lib/localDev.d.ts +4 -0
- package/src/lib/localDev.js +72 -0
- package/src/lib/migrate.d.ts +1 -0
- package/src/lib/migrate.js +43 -45
- package/src/lib/migrateThemes.d.ts +25 -0
- package/src/lib/migrateThemes.js +120 -0
- package/src/lib/minimalArboristTree.d.ts +118 -0
- package/src/lib/minimalArboristTree.js +32 -0
- package/src/lib/platformVersion.d.ts +3 -0
- package/src/lib/platformVersion.js +16 -0
- package/src/lib/profiles.d.ts +6 -1
- package/src/lib/profiles.js +95 -40
- package/src/lib/project.d.ts +13 -0
- package/src/lib/project.js +36 -0
- package/src/lib/schemas.d.ts +2 -2
- package/src/lib/schemas.js +11 -11
- package/src/lib/transform.d.ts +4 -2
- package/src/lib/transform.js +100 -53
- package/src/lib/translate.d.ts +3 -0
- package/src/lib/translate.js +62 -0
- package/src/lib/types.d.ts +30 -6
- package/src/lib/types.js +1 -2
- package/src/lib/uid.d.ts +2 -0
- package/src/lib/uid.js +14 -9
- package/src/lib/utils.d.ts +3 -0
- package/src/lib/utils.js +16 -0
- package/src/lib/validation.d.ts +4 -4
- package/src/lib/validation.js +61 -53
- package/src/lib/workspaces.d.ts +68 -0
- package/src/lib/workspaces.js +290 -0
- package/src/index.d.ts +0 -18
- package/src/index.js +0 -86
package/src/lib/files.js
CHANGED
|
@@ -1,66 +1,59 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
};
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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(
|
|
10
|
+
throw new Error(errorMessages.profile.noProfileSpecified);
|
|
19
11
|
}
|
|
20
|
-
const profileFile =
|
|
21
|
-
const profileFilepath =
|
|
12
|
+
const profileFile = getHsProfileFilename(profile);
|
|
13
|
+
const profileFilepath = path.join(projectSourceDir, profileFile);
|
|
22
14
|
let hsProfile;
|
|
23
15
|
try {
|
|
24
|
-
hsProfile = JSON.parse(
|
|
16
|
+
hsProfile = JSON.parse(fs.readFileSync(profileFilepath, 'utf8'));
|
|
25
17
|
}
|
|
26
18
|
catch (_e) {
|
|
27
|
-
throw new Error(
|
|
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
|
-
|
|
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() &&
|
|
39
|
-
.map(file =>
|
|
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(
|
|
48
|
-
|
|
49
|
-
|
|
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 =
|
|
54
|
-
const { dir: metaFileDir } =
|
|
55
|
-
const parentDirectory = metaFileDir.split(
|
|
56
|
-
if (
|
|
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 (
|
|
50
|
+
else if (ALLOWED_SUB_COMPONENT_DIRECTORIES.includes(metaFileDir)) {
|
|
60
51
|
metaFiles.push({ file, parentDirectory });
|
|
61
52
|
}
|
|
62
53
|
else {
|
|
63
|
-
|
|
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
|
-
|
|
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:
|
|
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(
|
|
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
|
+
}
|
package/src/lib/migrate.d.ts
CHANGED
package/src/lib/migrate.js
CHANGED
|
@@ -1,35 +1,30 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
};
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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 (!
|
|
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 =
|
|
21
|
-
if (!
|
|
22
|
-
throw new Error(`${
|
|
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 ${
|
|
24
|
+
throw new Error(`Error parsing ${HS_PROJECT_JSON_FILENAME}: ${e}`);
|
|
30
25
|
}
|
|
31
|
-
const files = await
|
|
32
|
-
const sourceCodeOutputDir =
|
|
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 =
|
|
35
|
+
const irDirName = path.dirname(filename);
|
|
41
36
|
const IR = loadJsonFile(filename);
|
|
42
37
|
const { metaFilePath } = IR;
|
|
43
|
-
const
|
|
44
|
-
const fullOutputPath =
|
|
38
|
+
const component = convertIRToProjectConfig(IR);
|
|
39
|
+
const fullOutputPath = path.join(sourceCodeOutputDir, getTargetDirectoryFromComponentType(component.type));
|
|
45
40
|
// Ensure the output directory exists
|
|
46
|
-
const currentFilesDirectory =
|
|
41
|
+
const currentFilesDirectory = path.join(irDirName, filesDirectory);
|
|
47
42
|
ensureDirExists(currentFilesDirectory);
|
|
48
|
-
|
|
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 =
|
|
53
|
-
?
|
|
54
|
-
: `${
|
|
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
|
-
|
|
56
|
+
fs.writeFileSync(hsmetaFilePath, JSON.stringify(component, null, 2));
|
|
57
57
|
});
|
|
58
|
-
const IR = await
|
|
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
|
-
|
|
63
|
+
fs.writeFileSync(path.join(outputDir, OUTPUT_IR_FILE), JSON.stringify(IR, null, 2));
|
|
64
64
|
}
|
|
65
65
|
function ensureDirExists(dir) {
|
|
66
|
-
if (!
|
|
67
|
-
|
|
66
|
+
if (!fs.existsSync(dir)) {
|
|
67
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
function isIRFile(filename) {
|
|
71
|
-
const { base } =
|
|
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:
|
|
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
|
-
|
|
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(
|
|
91
|
+
throw Error(`Unsupported component type: "${componentType}"`);
|
|
94
92
|
}
|
|
95
93
|
const parentDirectory = component.parentComponent
|
|
96
|
-
?
|
|
94
|
+
? Components[component.parentComponent].dir
|
|
97
95
|
: '';
|
|
98
|
-
return
|
|
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
|
+
}
|