@hubspot/project-parsing-lib 0.2.0-beta.1 → 0.2.0-experimental.1
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 +54 -19
- package/src/exports/constants.d.ts +1 -0
- package/src/exports/constants.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 +3 -0
- package/src/exports/projects.js +2 -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/lang/copy.d.ts +7 -1
- package/src/lang/copy.js +29 -33
- package/src/lib/constants.d.ts +45 -28
- package/src/lib/constants.js +160 -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.js +32 -42
- package/src/lib/migrateThemes.d.ts +25 -0
- package/src/lib/migrateThemes.js +120 -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 +29 -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 +52 -0
- package/src/lib/types.d.ts +28 -4
- 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/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.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, } 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
10
|
const OUTPUT_IR_FILE = 'FULL_IR.json';
|
|
16
|
-
async function migrate(inputDir, outputDir) {
|
|
17
|
-
if (!
|
|
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,58 @@ 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
38
|
const projectConfig = convertIRToProjectConfig(IR);
|
|
44
|
-
const fullOutputPath =
|
|
39
|
+
const fullOutputPath = path.join(sourceCodeOutputDir, getTargetDirectoryFromComponentType(projectConfig.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
|
});
|
|
51
46
|
// Use the metaFilePath if provided, otherwise use the project UID
|
|
52
|
-
const hsmetaFilePath =
|
|
53
|
-
?
|
|
54
|
-
: `${projectConfig.uid}${
|
|
47
|
+
const hsmetaFilePath = path.join(fullOutputPath, metaFilePath
|
|
48
|
+
? path.basename(metaFilePath)
|
|
49
|
+
: `${projectConfig.uid}${METAFILE_EXTENSION}`);
|
|
55
50
|
// Write the hsmeta file to the output directory
|
|
56
|
-
|
|
51
|
+
fs.writeFileSync(hsmetaFilePath, JSON.stringify(projectConfig, null, 2));
|
|
57
52
|
});
|
|
58
|
-
const IR = await
|
|
53
|
+
const IR = await translate({
|
|
59
54
|
projectSourceDir: sourceCodeOutputDir,
|
|
60
55
|
platformVersion: hsProjectJson.platformVersion,
|
|
61
56
|
}, { skipValidation: true });
|
|
62
57
|
// Write the IR file to the output directory
|
|
63
|
-
|
|
58
|
+
fs.writeFileSync(path.join(outputDir, OUTPUT_IR_FILE), JSON.stringify(IR, null, 2));
|
|
64
59
|
}
|
|
65
60
|
function ensureDirExists(dir) {
|
|
66
|
-
if (!
|
|
67
|
-
|
|
61
|
+
if (!fs.existsSync(dir)) {
|
|
62
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
68
63
|
}
|
|
69
64
|
}
|
|
70
65
|
function isIRFile(filename) {
|
|
71
|
-
const { base } =
|
|
66
|
+
const { base } = path.parse(filename);
|
|
72
67
|
return base.toLowerCase() === IR_FILENAME;
|
|
73
68
|
}
|
|
74
|
-
function loadJsonFile(filename) {
|
|
75
|
-
return JSON.parse(fs_2.default.readFileSync(filename, {
|
|
76
|
-
encoding: 'utf-8',
|
|
77
|
-
}));
|
|
78
|
-
}
|
|
79
69
|
function convertIRToProjectConfig(IR) {
|
|
80
70
|
const { config, uid, componentType } = IR;
|
|
81
71
|
return {
|
|
82
72
|
config,
|
|
83
73
|
uid,
|
|
84
|
-
type:
|
|
74
|
+
type: mapToUserFacingType(componentType),
|
|
85
75
|
// We are assuming no dependencies for now since this will be a freshly migrated
|
|
86
76
|
// project and the current supported components don't have non-hierarchical dependencies
|
|
87
77
|
dependencies: undefined,
|
|
88
78
|
};
|
|
89
79
|
}
|
|
90
80
|
function getTargetDirectoryFromComponentType(componentType) {
|
|
91
|
-
const component =
|
|
81
|
+
const component = Components[componentType];
|
|
92
82
|
if (!component) {
|
|
93
83
|
throw Error('Unsupported component type');
|
|
94
84
|
}
|
|
95
85
|
const parentDirectory = component.parentComponent
|
|
96
|
-
?
|
|
86
|
+
? Components[component.parentComponent].dir
|
|
97
87
|
: '';
|
|
98
|
-
return
|
|
88
|
+
return path.join(parentDirectory, component.dir);
|
|
99
89
|
}
|
|
@@ -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
|
+
}
|
package/src/lib/profiles.d.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
import { FileParseResult, HsProfileFile } from './types';
|
|
1
|
+
import { FileParseResult, HsProfileFile, HSProfileVariables } from './types.js';
|
|
2
2
|
export declare function getIsProfileFile(filename: string): boolean;
|
|
3
3
|
export declare function getHsProfileFilename(profileName: string): string;
|
|
4
4
|
export declare function getHsProfileName(profileFilename: string): string;
|
|
5
|
+
export declare function getHsProfileVariables(hsProfileContents: HsProfileFile): HSProfileVariables;
|
|
6
|
+
export declare function validateProfileVariables(profileVariables: HSProfileVariables, filename: string): {
|
|
7
|
+
success: boolean;
|
|
8
|
+
errors: string[];
|
|
9
|
+
};
|
|
5
10
|
export declare function applyHsProfileVariables(fileParseResults: FileParseResult[], hsProfileContents: HsProfileFile): FileParseResult[];
|