@hubspot/project-parsing-lib 0.0.4 → 0.0.5-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/package.json +1 -1
- package/src/index.d.ts +4 -2
- package/src/index.js +35 -8
- package/src/lang/copy.d.ts +27 -0
- package/src/lang/copy.js +31 -0
- package/src/lib/constants.d.ts +7 -4
- package/src/lib/constants.js +54 -12
- package/src/lib/errors.d.ts +6 -4
- package/src/lib/errors.js +135 -11
- package/src/lib/files.js +4 -2
- package/src/lib/schemas.d.ts +1 -1
- package/src/lib/schemas.js +4 -4
- package/src/lib/transform.d.ts +2 -0
- package/src/lib/transform.js +9 -2
- package/src/lib/types.d.ts +17 -1
- package/src/lib/validation.d.ts +9 -5
- package/src/lib/validation.js +68 -70
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hubspot/project-parsing-lib",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5-experimental.0",
|
|
4
4
|
"description": "Parsing library for converting projects directory structures to their intermediate representation",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"main": "src/index.js",
|
package/src/index.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
import { IntermediateRepresentation, TranslationContext } from './lib/types';
|
|
2
|
-
export declare function translate(translationContext: TranslationContext): Promise<IntermediateRepresentation>;
|
|
1
|
+
import { IntermediateRepresentation, IntermediateRepresentationNode, IntermediateRepresentationLocalDev, TranslationContext, TranslationOptions, IntermediateRepresentationNodeLocalDev } from './lib/types';
|
|
2
|
+
export declare function translate(translationContext: TranslationContext, translationOptions?: TranslationOptions): Promise<IntermediateRepresentation>;
|
|
3
|
+
export declare function translateForLocalDev(translationContext: TranslationContext): Promise<IntermediateRepresentationLocalDev>;
|
|
3
4
|
export { isTranslationError } from './lib/errors';
|
|
5
|
+
export { IntermediateRepresentation, IntermediateRepresentationNode, IntermediateRepresentationLocalDev, IntermediateRepresentationNodeLocalDev, TranslationContext, };
|
package/src/index.js
CHANGED
|
@@ -1,23 +1,50 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.isTranslationError = void 0;
|
|
4
7
|
exports.translate = translate;
|
|
8
|
+
exports.translateForLocalDev = translateForLocalDev;
|
|
5
9
|
const files_1 = require("./lib/files");
|
|
6
10
|
const validation_1 = require("./lib/validation");
|
|
7
11
|
const transform_1 = require("./lib/transform");
|
|
8
|
-
const
|
|
9
|
-
|
|
12
|
+
const copy_1 = require("./lang/copy");
|
|
13
|
+
const path_1 = __importDefault(require("path"));
|
|
14
|
+
const defaultOptions = {
|
|
15
|
+
skipValidation: false,
|
|
16
|
+
};
|
|
17
|
+
async function translate(translationContext, translationOptions = defaultOptions) {
|
|
18
|
+
const { skipValidation } = translationOptions;
|
|
10
19
|
const metafileContents = await (0, files_1.loadHsMetaFiles)(translationContext);
|
|
11
20
|
if (metafileContents.length === 0) {
|
|
12
|
-
throw new Error(
|
|
21
|
+
throw new Error(copy_1.errorMessages.project.noHsMetaFiles);
|
|
13
22
|
}
|
|
14
23
|
const transformation = (0, transform_1.transform)(metafileContents);
|
|
15
24
|
const intermediateRepresentation = (0, transform_1.getIntermediateRepresentation)(transformation);
|
|
16
|
-
|
|
17
|
-
if (!
|
|
18
|
-
|
|
25
|
+
// Remove once extensions and serverless functions are supported
|
|
26
|
+
if (!skipValidation) {
|
|
27
|
+
await (0, validation_1.validateIntermediateRepresentation)(intermediateRepresentation, transformation, translationContext);
|
|
19
28
|
}
|
|
20
29
|
return intermediateRepresentation;
|
|
21
30
|
}
|
|
22
|
-
|
|
23
|
-
|
|
31
|
+
async function translateForLocalDev(translationContext) {
|
|
32
|
+
const IR = await translate(translationContext, { skipValidation: true });
|
|
33
|
+
const localDevIr = {
|
|
34
|
+
intermediateNodesIndexedByUid: {},
|
|
35
|
+
};
|
|
36
|
+
Object.entries(IR.intermediateNodesIndexedByUid).forEach(([uid, node]) => {
|
|
37
|
+
const component = IR.intermediateNodesIndexedByUid[uid];
|
|
38
|
+
const componentConfigPath = path_1.default.join(translationContext.projectSourceDir, component.metaFilePath);
|
|
39
|
+
localDevIr.intermediateNodesIndexedByUid[uid] = {
|
|
40
|
+
...node,
|
|
41
|
+
localDev: {
|
|
42
|
+
componentRoot: path_1.default.dirname(componentConfigPath),
|
|
43
|
+
componentConfigPath,
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
return localDevIr;
|
|
48
|
+
}
|
|
49
|
+
var errors_1 = require("./lib/errors");
|
|
50
|
+
Object.defineProperty(exports, "isTranslationError", { enumerable: true, get: function () { return errors_1.isTranslationError; } });
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { ComponentMetadata } from '../lib/constants';
|
|
2
|
+
export declare const errorMessages: {
|
|
3
|
+
api: {
|
|
4
|
+
failedToFetchSchemas: string;
|
|
5
|
+
};
|
|
6
|
+
project: {
|
|
7
|
+
noHsMetaFiles: string;
|
|
8
|
+
failedToTranslateProject: string;
|
|
9
|
+
duplicateUid: (uid: string, files: string[]) => string;
|
|
10
|
+
};
|
|
11
|
+
validation: {
|
|
12
|
+
errorWithFileHeader: (file: string, errors: string[]) => string;
|
|
13
|
+
missingRequiredField: (field: string) => string;
|
|
14
|
+
missingUid: string;
|
|
15
|
+
missingType: string;
|
|
16
|
+
missingConfig: string;
|
|
17
|
+
unsupportedType: (type: string) => string;
|
|
18
|
+
errorWithField: (field: string, error: string | undefined) => string;
|
|
19
|
+
invalidJson: string;
|
|
20
|
+
wrongDirectoryForComponent: (directory: string, componentType: string, componentMetadata: ComponentMetadata, correctDir: string) => string;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
export declare const logMessages: {
|
|
24
|
+
files: {
|
|
25
|
+
skippingPath: (path: string) => string;
|
|
26
|
+
};
|
|
27
|
+
};
|
package/src/lang/copy.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.logMessages = exports.errorMessages = void 0;
|
|
4
|
+
exports.errorMessages = {
|
|
5
|
+
api: {
|
|
6
|
+
failedToFetchSchemas: 'Failed to fetch schemas',
|
|
7
|
+
},
|
|
8
|
+
project: {
|
|
9
|
+
noHsMetaFiles: 'No *-hsmeta.json files found in the current directory. Please make sure you are inside the correct project directory.',
|
|
10
|
+
failedToTranslateProject: 'Project validation failed',
|
|
11
|
+
duplicateUid: (uid, files) => `Duplicate uid '${uid}' found in:\n- ${files.join('\n- ')}`,
|
|
12
|
+
},
|
|
13
|
+
validation: {
|
|
14
|
+
errorWithFileHeader: (file, errors) => `\n\nEncountered the following errors for ${file}:\n\t- ${errors.join('\n\t- ')}`,
|
|
15
|
+
missingRequiredField: (field) => `Missing required field: '${field}'`,
|
|
16
|
+
missingUid: `Missing required field: 'uid'`,
|
|
17
|
+
missingType: `Missing required field: 'type'`,
|
|
18
|
+
missingConfig: `Missing required field: 'config'`,
|
|
19
|
+
unsupportedType: (type) => `Unsupported type: ${type.toLowerCase()}`,
|
|
20
|
+
errorWithField: (field, error) => `Error with ${field}: ${error || 'Unknown error'}`,
|
|
21
|
+
invalidJson: 'Invalid JSON',
|
|
22
|
+
wrongDirectoryForComponent: (directory, componentType, componentMetadata, correctDir) =>
|
|
23
|
+
// Double check this string with Jono
|
|
24
|
+
`The directory '${directory}' is incorrect for type '${componentType}'. ${componentMetadata.userFriendlyName} ${componentMetadata.userFriendlyTypePlural} should only be placed in the '${correctDir}' directory`,
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
exports.logMessages = {
|
|
28
|
+
files: {
|
|
29
|
+
skippingPath: (path) => `Skipping ${path} as it is not in a valid directory`,
|
|
30
|
+
},
|
|
31
|
+
};
|
package/src/lib/constants.d.ts
CHANGED
|
@@ -9,13 +9,17 @@ export declare const TimelineEventsKey = "timeline-event";
|
|
|
9
9
|
export declare const VideoConferencingKey = "video-conferencing";
|
|
10
10
|
export declare const WebhooksKey = "webhooks";
|
|
11
11
|
export declare const WorkflowActionsKey = "workflow-action";
|
|
12
|
-
interface ComponentMetadata {
|
|
12
|
+
export interface ComponentMetadata {
|
|
13
13
|
dir: string;
|
|
14
|
-
isToplevel
|
|
14
|
+
isToplevel: boolean;
|
|
15
15
|
parentComponent?: string;
|
|
16
|
+
userFriendlyName: string;
|
|
17
|
+
userFriendlyType: string;
|
|
18
|
+
userFriendlyTypePlural: string;
|
|
16
19
|
}
|
|
17
20
|
export declare const Components: Record<string, ComponentMetadata>;
|
|
18
|
-
export declare const
|
|
21
|
+
export declare const userFacingToInternalType: Record<string, string>;
|
|
22
|
+
export declare const internalTypeToUserFacing: Record<string, string>;
|
|
19
23
|
export declare const metafileExtension = "-hsmeta.json";
|
|
20
24
|
export declare const allowedAppSubComponentsDirs: string[];
|
|
21
25
|
export declare const allowedThemeSubComponentsDirs: string[];
|
|
@@ -25,4 +29,3 @@ export declare const ProjectStructure: {
|
|
|
25
29
|
};
|
|
26
30
|
export declare const allowedComponentDirectories: string[];
|
|
27
31
|
export declare const allowedSubComponentDirectories: string[];
|
|
28
|
-
export {};
|
package/src/lib/constants.js
CHANGED
|
@@ -15,15 +15,25 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (
|
|
|
15
15
|
}) : function(o, v) {
|
|
16
16
|
o["default"] = v;
|
|
17
17
|
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || function (
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
};
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
25
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
-
exports.allowedSubComponentDirectories = exports.allowedComponentDirectories = exports.ProjectStructure = exports.allowedThemeSubComponentsDirs = exports.allowedAppSubComponentsDirs = exports.metafileExtension = exports.
|
|
36
|
+
exports.allowedSubComponentDirectories = exports.allowedComponentDirectories = exports.ProjectStructure = exports.allowedThemeSubComponentsDirs = exports.allowedAppSubComponentsDirs = exports.metafileExtension = exports.internalTypeToUserFacing = exports.userFacingToInternalType = exports.Components = exports.WorkflowActionsKey = exports.WebhooksKey = exports.VideoConferencingKey = exports.TimelineEventsKey = exports.MediaBridgeKey = exports.MarketingEventsKey = exports.FunctionsKey = exports.CardsKey = exports.CallingKey = exports.ThemeKey = exports.AppKey = void 0;
|
|
27
37
|
// Top Level Component types
|
|
28
38
|
const path = __importStar(require("path"));
|
|
29
39
|
// Component types
|
|
@@ -39,55 +49,87 @@ exports.TimelineEventsKey = 'timeline-event';
|
|
|
39
49
|
exports.VideoConferencingKey = 'video-conferencing';
|
|
40
50
|
exports.WebhooksKey = 'webhooks';
|
|
41
51
|
exports.WorkflowActionsKey = 'workflow-action';
|
|
52
|
+
const TopLevelComponentFields = {
|
|
53
|
+
isToplevel: true,
|
|
54
|
+
userFriendlyType: 'component',
|
|
55
|
+
userFriendlyTypePlural: 'components',
|
|
56
|
+
};
|
|
57
|
+
const SubComponentFields = {
|
|
58
|
+
isToplevel: false,
|
|
59
|
+
userFriendlyType: 'feature',
|
|
60
|
+
userFriendlyTypePlural: 'features',
|
|
61
|
+
};
|
|
42
62
|
exports.Components = {
|
|
43
63
|
[exports.AppKey]: {
|
|
44
64
|
dir: exports.AppKey,
|
|
45
|
-
|
|
65
|
+
userFriendlyName: 'App',
|
|
66
|
+
...TopLevelComponentFields,
|
|
46
67
|
},
|
|
47
68
|
[exports.ThemeKey]: {
|
|
48
69
|
dir: exports.ThemeKey,
|
|
49
|
-
|
|
70
|
+
userFriendlyName: 'Theme',
|
|
71
|
+
...TopLevelComponentFields,
|
|
50
72
|
},
|
|
51
73
|
[exports.CallingKey]: {
|
|
52
74
|
dir: exports.CallingKey,
|
|
53
75
|
parentComponent: exports.AppKey,
|
|
76
|
+
userFriendlyName: 'Calling',
|
|
77
|
+
...SubComponentFields,
|
|
54
78
|
},
|
|
55
79
|
[exports.CardsKey]: {
|
|
56
80
|
dir: 'cards',
|
|
57
81
|
parentComponent: exports.AppKey,
|
|
82
|
+
userFriendlyName: 'Card',
|
|
83
|
+
...SubComponentFields,
|
|
58
84
|
},
|
|
59
85
|
[exports.FunctionsKey]: {
|
|
60
86
|
dir: 'functions',
|
|
61
87
|
parentComponent: exports.AppKey,
|
|
88
|
+
userFriendlyName: 'Function',
|
|
89
|
+
...SubComponentFields,
|
|
62
90
|
},
|
|
63
91
|
[exports.MarketingEventsKey]: {
|
|
64
92
|
dir: 'marketing-events',
|
|
65
93
|
parentComponent: exports.AppKey,
|
|
94
|
+
userFriendlyName: 'Marketing Event',
|
|
95
|
+
...SubComponentFields,
|
|
66
96
|
},
|
|
67
97
|
MediaBridgeKey: {
|
|
68
98
|
dir: exports.MediaBridgeKey,
|
|
69
99
|
parentComponent: exports.AppKey,
|
|
100
|
+
userFriendlyName: 'Media Bridge',
|
|
101
|
+
...SubComponentFields,
|
|
70
102
|
},
|
|
71
103
|
[exports.TimelineEventsKey]: {
|
|
72
104
|
dir: 'timeline-events',
|
|
73
105
|
parentComponent: exports.AppKey,
|
|
106
|
+
userFriendlyName: 'Timeline Event',
|
|
107
|
+
...SubComponentFields,
|
|
74
108
|
},
|
|
75
109
|
[exports.VideoConferencingKey]: {
|
|
76
110
|
dir: exports.VideoConferencingKey,
|
|
77
111
|
parentComponent: exports.AppKey,
|
|
112
|
+
userFriendlyName: 'Video Conferencing',
|
|
113
|
+
...SubComponentFields,
|
|
78
114
|
},
|
|
79
115
|
[exports.WebhooksKey]: {
|
|
80
116
|
dir: exports.WebhooksKey,
|
|
81
117
|
parentComponent: exports.AppKey,
|
|
118
|
+
userFriendlyName: 'Webhooks',
|
|
119
|
+
...SubComponentFields,
|
|
82
120
|
},
|
|
83
121
|
[exports.WorkflowActionsKey]: {
|
|
84
122
|
dir: 'workflow-actions',
|
|
85
123
|
parentComponent: exports.AppKey,
|
|
124
|
+
userFriendlyName: 'Workflow Action',
|
|
125
|
+
...SubComponentFields,
|
|
86
126
|
},
|
|
87
127
|
};
|
|
88
|
-
|
|
89
|
-
|
|
128
|
+
const internalAppKey = 'APPLICATION';
|
|
129
|
+
exports.userFacingToInternalType = {
|
|
130
|
+
[exports.AppKey]: internalAppKey,
|
|
90
131
|
};
|
|
132
|
+
exports.internalTypeToUserFacing = Object.fromEntries(Object.entries(exports.userFacingToInternalType).map(([key, value]) => [value, key]));
|
|
91
133
|
exports.metafileExtension = '-hsmeta.json';
|
|
92
134
|
function getSubComponentDirsForParentType(parentComponent) {
|
|
93
135
|
return Object.values(exports.Components).reduce((acc, item) => {
|
package/src/lib/errors.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { CompiledError, Transformation } from './types';
|
|
1
|
+
import { CompiledError, Transformation, TranslationContext } from './types';
|
|
2
|
+
import { ErrorObject } from 'ajv-draft-04';
|
|
3
|
+
export declare function isTranslationError(error: unknown): error is TranslationError;
|
|
4
|
+
export declare function compileError(validatedTransformation: Transformation): CompiledError;
|
|
2
5
|
export declare class TranslationError extends Error {
|
|
3
6
|
private errors;
|
|
4
|
-
|
|
7
|
+
private translationContext;
|
|
8
|
+
constructor(message: string, transformations: Transformation[], errors: (ErrorObject[] | null | undefined)[], translationContext: TranslationContext);
|
|
5
9
|
toString(): string;
|
|
6
10
|
}
|
|
7
|
-
export declare function isTranslationError(error: unknown): error is TranslationError;
|
|
8
|
-
export declare function compileError(validatedTransformation: Transformation): CompiledError;
|
package/src/lib/errors.js
CHANGED
|
@@ -1,28 +1,152 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.TranslationError = void 0;
|
|
4
7
|
exports.isTranslationError = isTranslationError;
|
|
5
8
|
exports.compileError = compileError;
|
|
9
|
+
const copy_1 = require("../lang/copy");
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
function isTranslationError(error) {
|
|
12
|
+
return error instanceof TranslationError;
|
|
13
|
+
}
|
|
14
|
+
function compileError(validatedTransformation) {
|
|
15
|
+
const { fileParseResult } = validatedTransformation;
|
|
16
|
+
const { errors } = fileParseResult;
|
|
17
|
+
return {
|
|
18
|
+
errors,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
6
21
|
class TranslationError extends Error {
|
|
7
|
-
errors;
|
|
8
|
-
|
|
22
|
+
errors = [];
|
|
23
|
+
translationContext;
|
|
24
|
+
constructor(message, transformations, errors, translationContext) {
|
|
9
25
|
super(message);
|
|
10
26
|
this.name = 'TranslationError';
|
|
11
|
-
this.
|
|
27
|
+
this.translationContext = translationContext;
|
|
28
|
+
this.errors = transformations.map((transformation, index) => compileTranslationErrors(transformation, errors[index]));
|
|
12
29
|
}
|
|
30
|
+
// Returns a formatted string for all the errors in all the files
|
|
13
31
|
toString() {
|
|
14
|
-
|
|
32
|
+
const listOfErrors = this.errors.map(({ file, errors }) => {
|
|
33
|
+
if (errors.length === 0) {
|
|
34
|
+
return '';
|
|
35
|
+
}
|
|
36
|
+
return copy_1.errorMessages.validation.errorWithFileHeader(path_1.default.join(this.translationContext.projectSourceDir, file), errors);
|
|
37
|
+
});
|
|
38
|
+
return `${this.message}: ${listOfErrors}`;
|
|
15
39
|
}
|
|
16
40
|
}
|
|
17
41
|
exports.TranslationError = TranslationError;
|
|
18
|
-
function
|
|
19
|
-
|
|
42
|
+
function generateDotNotationPath(error) {
|
|
43
|
+
const { instancePath } = error;
|
|
44
|
+
const errorPath = instancePath
|
|
45
|
+
.replace(/\/nodes\//, '')
|
|
46
|
+
.split('/')
|
|
47
|
+
.filter(subPath => subPath !== '')
|
|
48
|
+
.join('.');
|
|
49
|
+
if (errorPath === '') {
|
|
50
|
+
// This is a top level error
|
|
51
|
+
return `config`;
|
|
52
|
+
}
|
|
53
|
+
return `config.${errorPath}`;
|
|
20
54
|
}
|
|
21
|
-
function
|
|
22
|
-
const
|
|
23
|
-
const { errors, file
|
|
55
|
+
function compileTranslationErrors(transformation, schemaErrors) {
|
|
56
|
+
const hasOneOfErrors = schemaErrors?.some(error => isOneOfError(error));
|
|
57
|
+
const { errors: existingErrors, file } = transformation.fileParseResult;
|
|
58
|
+
const errors = [...existingErrors];
|
|
59
|
+
// If there is a one of error, we need to preprocess the errors to group them by instancePath and keyword
|
|
60
|
+
// This allows us to group data from the errors that correspond to the same field
|
|
61
|
+
if (hasOneOfErrors) {
|
|
62
|
+
errors.push(...preprocessSpecialErrors(schemaErrors));
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
schemaErrors?.forEach(error => {
|
|
66
|
+
errors.push(generateErrorMessage(error));
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
return { file, errors };
|
|
70
|
+
}
|
|
71
|
+
function preprocessSpecialErrors(schemaErrors) {
|
|
72
|
+
const errors = [];
|
|
73
|
+
const preprocessedErrors = {};
|
|
74
|
+
schemaErrors?.forEach(error => {
|
|
75
|
+
// Filter out the oneOf errors, they are a secondary error caused by other errors
|
|
76
|
+
// and don't add any additional actionable context to the user
|
|
77
|
+
if (isOneOfError(error)) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const errorKey = `${error.instancePath}::${error.keyword}`;
|
|
81
|
+
if (preprocessedErrors[errorKey]) {
|
|
82
|
+
preprocessedErrors[errorKey].push(error);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
preprocessedErrors[errorKey] = [error];
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
Object.values(preprocessedErrors)?.forEach(value => {
|
|
89
|
+
const newValue = value.reduce((cur, next) => {
|
|
90
|
+
if (isEnumError(cur)) {
|
|
91
|
+
return mergeEnumErrors(cur, next);
|
|
92
|
+
}
|
|
93
|
+
return cur;
|
|
94
|
+
});
|
|
95
|
+
errors.push(generateErrorMessage(newValue));
|
|
96
|
+
});
|
|
97
|
+
return errors;
|
|
98
|
+
}
|
|
99
|
+
function generateErrorMessage(error) {
|
|
100
|
+
const errorPath = generateDotNotationPath(error);
|
|
101
|
+
const errorMessage = copy_1.errorMessages.validation.errorWithField(errorPath, error.message);
|
|
102
|
+
const params = Object.entries(error.params);
|
|
103
|
+
if (params.length === 0) {
|
|
104
|
+
return errorMessage;
|
|
105
|
+
}
|
|
106
|
+
const additionalContext = params
|
|
107
|
+
.filter(([_, value]) => value)
|
|
108
|
+
.map(([key, value]) => `${key}: ${Array.isArray(value) ? value.join(', ') : value}`);
|
|
109
|
+
if (isRequiredError(error)) {
|
|
110
|
+
return copy_1.errorMessages.validation.missingRequiredField(`${errorPath}.${error.params.missingProperty}`);
|
|
111
|
+
}
|
|
112
|
+
else if (isTypeError(error)) {
|
|
113
|
+
return copy_1.errorMessages.validation.errorWithField(generateDotNotationPath(error), error.message);
|
|
114
|
+
}
|
|
115
|
+
// Default case, it is not an error we know how to deal with
|
|
116
|
+
return `${errorMessage} ${additionalContext.length > 0
|
|
117
|
+
? `\n\t\t- ${additionalContext.join('\n\t\t- ')}`
|
|
118
|
+
: ''}`;
|
|
119
|
+
}
|
|
120
|
+
function mergeEnumErrors(cur, next) {
|
|
24
121
|
return {
|
|
25
|
-
|
|
26
|
-
|
|
122
|
+
...cur,
|
|
123
|
+
// Overwrite the schema path so we know it's been modified
|
|
124
|
+
schemaPath: 'CUSTOM',
|
|
125
|
+
params: {
|
|
126
|
+
...cur.params,
|
|
127
|
+
// Merge the allowed values from the enum errors
|
|
128
|
+
allowedValues: [
|
|
129
|
+
...cur.params.allowedValues,
|
|
130
|
+
...next.params.allowedValues,
|
|
131
|
+
],
|
|
132
|
+
},
|
|
27
133
|
};
|
|
28
134
|
}
|
|
135
|
+
const JSON_SCHEMA_VALIDATION_KEYWORDS = {
|
|
136
|
+
required: 'required',
|
|
137
|
+
type: 'type',
|
|
138
|
+
oneOf: 'oneOf',
|
|
139
|
+
enum: 'enum',
|
|
140
|
+
};
|
|
141
|
+
function isOneOfError(error) {
|
|
142
|
+
return error.keyword === JSON_SCHEMA_VALIDATION_KEYWORDS.oneOf;
|
|
143
|
+
}
|
|
144
|
+
function isRequiredError(error) {
|
|
145
|
+
return error.keyword === JSON_SCHEMA_VALIDATION_KEYWORDS.required;
|
|
146
|
+
}
|
|
147
|
+
function isEnumError(error) {
|
|
148
|
+
return error.keyword === JSON_SCHEMA_VALIDATION_KEYWORDS.enum;
|
|
149
|
+
}
|
|
150
|
+
function isTypeError(error) {
|
|
151
|
+
return error.keyword === JSON_SCHEMA_VALIDATION_KEYWORDS.type;
|
|
152
|
+
}
|
package/src/lib/files.js
CHANGED
|
@@ -8,6 +8,8 @@ const fs_1 = require("@hubspot/local-dev-lib/fs");
|
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
9
|
const constants_1 = require("./constants");
|
|
10
10
|
const fs_2 = __importDefault(require("fs"));
|
|
11
|
+
const copy_1 = require("../lang/copy");
|
|
12
|
+
const logger_1 = require("@hubspot/local-dev-lib/logger");
|
|
11
13
|
async function loadHsMetaFiles(translationContext) {
|
|
12
14
|
const metaFiles = await locateHsMetaFiles(translationContext);
|
|
13
15
|
return parseHsMetaFiles(metaFiles, translationContext);
|
|
@@ -28,7 +30,7 @@ async function locateHsMetaFiles(translationContext) {
|
|
|
28
30
|
metaFiles.push({ file, parentDirectory });
|
|
29
31
|
}
|
|
30
32
|
else {
|
|
31
|
-
|
|
33
|
+
logger_1.logger.warn(copy_1.logMessages.files.skippingPath(pathRelativeToProjectSrcDir));
|
|
32
34
|
}
|
|
33
35
|
return metaFiles;
|
|
34
36
|
}, []);
|
|
@@ -69,7 +71,7 @@ function parseFile(fileLoadResult) {
|
|
|
69
71
|
parsedFileContents = JSON.parse(fileLoadResult.content);
|
|
70
72
|
}
|
|
71
73
|
catch (_e) {
|
|
72
|
-
fileLoadResult.errors?.push(
|
|
74
|
+
fileLoadResult.errors?.push(copy_1.errorMessages.validation.invalidJson);
|
|
73
75
|
}
|
|
74
76
|
return { ...fileLoadResult, content: parsedFileContents };
|
|
75
77
|
}
|
package/src/lib/schemas.d.ts
CHANGED
package/src/lib/schemas.js
CHANGED
|
@@ -3,12 +3,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.getIntermediateRepresentationSchema = getIntermediateRepresentationSchema;
|
|
4
4
|
const http_1 = require("@hubspot/local-dev-lib/http");
|
|
5
5
|
const index_1 = require("@hubspot/local-dev-lib/errors/index");
|
|
6
|
+
const copy_1 = require("../lang/copy");
|
|
6
7
|
async function getIntermediateRepresentationSchema(translationContext) {
|
|
7
8
|
try {
|
|
8
|
-
const { accountId } = translationContext;
|
|
9
|
+
const { accountId, platformVersion } = translationContext;
|
|
9
10
|
const { data } = await http_1.http.get(accountId, {
|
|
10
|
-
|
|
11
|
-
url: `project-components-external/project-schemas/v3?platformVersion=testing`,
|
|
11
|
+
url: `project-components-external/project-schemas/v3/${platformVersion}`,
|
|
12
12
|
});
|
|
13
13
|
return data;
|
|
14
14
|
}
|
|
@@ -16,6 +16,6 @@ async function getIntermediateRepresentationSchema(translationContext) {
|
|
|
16
16
|
if ((0, index_1.isHubSpotHttpError)(e)) {
|
|
17
17
|
throw e;
|
|
18
18
|
}
|
|
19
|
-
throw new Error(
|
|
19
|
+
throw new Error(copy_1.errorMessages.api.failedToFetchSchemas, { cause: e });
|
|
20
20
|
}
|
|
21
21
|
}
|
package/src/lib/transform.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
import { FileParseResult, IntermediateRepresentation, Transformation } from './types';
|
|
2
|
+
export declare function mapToInternalType(type: string): string;
|
|
3
|
+
export declare function mapToUserFacingType(type: string): string;
|
|
2
4
|
export declare function transform(fileParseResults: FileParseResult[]): Transformation[];
|
|
3
5
|
export declare function getIntermediateRepresentation(transformations: Transformation[]): IntermediateRepresentation;
|
package/src/lib/transform.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.mapToInternalType = mapToInternalType;
|
|
4
|
+
exports.mapToUserFacingType = mapToUserFacingType;
|
|
3
5
|
exports.transform = transform;
|
|
4
6
|
exports.getIntermediateRepresentation = getIntermediateRepresentation;
|
|
5
7
|
const constants_1 = require("./constants");
|
|
8
|
+
const copy_1 = require("../lang/copy");
|
|
6
9
|
function calculateComponentDeps(fileValidationResult, parentComponents) {
|
|
7
10
|
return {
|
|
8
11
|
...fileValidationResult.content?.dependencies,
|
|
@@ -15,7 +18,10 @@ function calculateComponentDeps(fileValidationResult, parentComponents) {
|
|
|
15
18
|
};
|
|
16
19
|
}
|
|
17
20
|
function mapToInternalType(type) {
|
|
18
|
-
return (constants_1.
|
|
21
|
+
return (constants_1.userFacingToInternalType[type] || type || '').toUpperCase();
|
|
22
|
+
}
|
|
23
|
+
function mapToUserFacingType(type) {
|
|
24
|
+
return (constants_1.internalTypeToUserFacing[type] || type || '').toLowerCase();
|
|
19
25
|
}
|
|
20
26
|
function transform(fileParseResults) {
|
|
21
27
|
const parentTypes = Object.keys(constants_1.ProjectStructure);
|
|
@@ -38,6 +44,7 @@ function transform(fileParseResults) {
|
|
|
38
44
|
config,
|
|
39
45
|
componentType: mapToInternalType(type),
|
|
40
46
|
componentDeps: calculateComponentDeps(currentFile, parentComponents),
|
|
47
|
+
metaFilePath: currentFile.file,
|
|
41
48
|
files: {},
|
|
42
49
|
},
|
|
43
50
|
fileParseResult: currentFile,
|
|
@@ -51,7 +58,7 @@ function getIntermediateRepresentation(transformations) {
|
|
|
51
58
|
const duplicates = transformations
|
|
52
59
|
.filter(t => t.intermediateRepresentation?.uid === uid)
|
|
53
60
|
.map(t => t.fileParseResult.file);
|
|
54
|
-
throw new Error(
|
|
61
|
+
throw new Error(copy_1.errorMessages.project.duplicateUid(uid, duplicates));
|
|
55
62
|
}
|
|
56
63
|
return {
|
|
57
64
|
...acc,
|
package/src/lib/types.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ErrorObject } from 'ajv-draft-04';
|
|
1
2
|
export interface Components {
|
|
2
3
|
uid: string;
|
|
3
4
|
type: string;
|
|
@@ -18,6 +19,7 @@ export interface FileParseResult extends FileActionResult {
|
|
|
18
19
|
export interface IntermediateRepresentationNode {
|
|
19
20
|
componentType: string;
|
|
20
21
|
componentDeps: Record<string, string>;
|
|
22
|
+
metaFilePath: string;
|
|
21
23
|
uid: string;
|
|
22
24
|
config: unknown;
|
|
23
25
|
files: unknown;
|
|
@@ -27,12 +29,23 @@ export interface IntermediateRepresentation {
|
|
|
27
29
|
[key: string]: IntermediateRepresentationNode;
|
|
28
30
|
};
|
|
29
31
|
}
|
|
32
|
+
export interface IntermediateRepresentationNodeLocalDev extends IntermediateRepresentationNode {
|
|
33
|
+
localDev: {
|
|
34
|
+
componentRoot: string;
|
|
35
|
+
componentConfigPath: string;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export interface IntermediateRepresentationLocalDev {
|
|
39
|
+
intermediateNodesIndexedByUid: {
|
|
40
|
+
[key: string]: IntermediateRepresentationNodeLocalDev;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
30
43
|
export type Transformation = {
|
|
31
44
|
intermediateRepresentation?: IntermediateRepresentationNode | null;
|
|
32
45
|
fileParseResult: FileParseResult;
|
|
46
|
+
schemaValidationErrors?: ErrorObject[] | null;
|
|
33
47
|
};
|
|
34
48
|
export type CompiledError = {
|
|
35
|
-
message: string;
|
|
36
49
|
errors: string[];
|
|
37
50
|
};
|
|
38
51
|
export interface TranslationContext {
|
|
@@ -40,3 +53,6 @@ export interface TranslationContext {
|
|
|
40
53
|
platformVersion: string;
|
|
41
54
|
accountId: number;
|
|
42
55
|
}
|
|
56
|
+
export interface TranslationOptions {
|
|
57
|
+
skipValidation?: boolean;
|
|
58
|
+
}
|
package/src/lib/validation.d.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { CompiledError, IntermediateRepresentation, Transformation, TranslationContext } from './types';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
errors: CompiledError[];
|
|
5
|
-
} | {
|
|
2
|
+
import { ValidateFunction } from 'ajv-draft-04';
|
|
3
|
+
export type ValidResult = {
|
|
6
4
|
valid: true;
|
|
7
5
|
errors?: null;
|
|
6
|
+
schemaValidationErrors?: null;
|
|
8
7
|
};
|
|
9
|
-
export
|
|
8
|
+
export type ValidationResults = {
|
|
9
|
+
valid: false;
|
|
10
|
+
errors?: CompiledError;
|
|
11
|
+
schemaValidationErrors?: ValidateFunction['errors'];
|
|
12
|
+
} | ValidResult;
|
|
13
|
+
export declare function validateIntermediateRepresentation(intermediateRepresentation: IntermediateRepresentation, transformation: Transformation[], translationContext: TranslationContext): Promise<ValidResult>;
|
package/src/lib/validation.js
CHANGED
|
@@ -7,87 +7,85 @@ exports.validateIntermediateRepresentation = validateIntermediateRepresentation;
|
|
|
7
7
|
const schemas_1 = require("./schemas");
|
|
8
8
|
const ajv_draft_04_1 = __importDefault(require("ajv-draft-04"));
|
|
9
9
|
const errors_1 = require("./errors");
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
const copy_1 = require("../lang/copy");
|
|
11
|
+
const constants_1 = require("./constants");
|
|
12
|
+
const path_1 = __importDefault(require("path"));
|
|
13
|
+
const transform_1 = require("./transform");
|
|
14
|
+
function validateIntermediateRepresentationNode(schema, transformation, irNode, index, translationContext) {
|
|
15
|
+
if (transformation[index].fileParseResult.errors.length > 0) {
|
|
16
|
+
return {
|
|
17
|
+
valid: false,
|
|
18
|
+
errors: (0, errors_1.compileError)(transformation[index]),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
const ajv = new ajv_draft_04_1.default({ allErrors: true });
|
|
22
|
+
let shouldSkipValidation = false;
|
|
23
|
+
if (!irNode.uid) {
|
|
24
|
+
transformation[index].fileParseResult.errors.push(copy_1.errorMessages.validation.missingUid);
|
|
25
|
+
}
|
|
26
|
+
if (!irNode.config) {
|
|
27
|
+
transformation[index].fileParseResult.errors.push(copy_1.errorMessages.validation.missingConfig);
|
|
28
|
+
// If there is no config block, there is nothing to validation
|
|
29
|
+
shouldSkipValidation = true;
|
|
30
|
+
}
|
|
31
|
+
if (!schema[irNode.componentType]) {
|
|
32
|
+
if (!irNode.componentType) {
|
|
33
|
+
transformation[index].fileParseResult.errors.push(copy_1.errorMessages.validation.missingType);
|
|
16
34
|
}
|
|
17
|
-
|
|
18
|
-
transformation[index].fileParseResult.errors.push(
|
|
19
|
-
return {
|
|
20
|
-
valid: false,
|
|
21
|
-
errors: (0, errors_1.compileError)(transformation[index]),
|
|
22
|
-
};
|
|
35
|
+
else {
|
|
36
|
+
transformation[index].fileParseResult.errors.push(copy_1.errorMessages.validation.unsupportedType(irNode.componentType));
|
|
23
37
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
38
|
+
// If there is no schema for the component type, there is no way to validate
|
|
39
|
+
shouldSkipValidation = true;
|
|
40
|
+
}
|
|
41
|
+
const userFacingType = (0, transform_1.mapToUserFacingType)(irNode.componentType);
|
|
42
|
+
const component = constants_1.Components[userFacingType];
|
|
43
|
+
if (userFacingType && component) {
|
|
44
|
+
const expectedParentDir = component.parentComponent
|
|
45
|
+
? constants_1.Components[component.parentComponent].dir
|
|
46
|
+
: '';
|
|
47
|
+
const expectedLocation = path_1.default.join(expectedParentDir, component.dir);
|
|
48
|
+
const actualLocation = path_1.default.dirname(transformation[index].fileParseResult.file);
|
|
49
|
+
if (expectedLocation !== actualLocation) {
|
|
50
|
+
transformation[index].fileParseResult.errors.push(copy_1.errorMessages.validation.wrongDirectoryForComponent(actualLocation, userFacingType, component, path_1.default.join(translationContext.projectSourceDir, expectedLocation)));
|
|
36
51
|
}
|
|
37
|
-
|
|
52
|
+
}
|
|
53
|
+
if (shouldSkipValidation) {
|
|
38
54
|
return {
|
|
39
55
|
valid: false,
|
|
40
|
-
errors: (0, errors_1.compileError)(
|
|
56
|
+
errors: (0, errors_1.compileError)(transformation[index]),
|
|
41
57
|
};
|
|
42
|
-
}
|
|
43
|
-
const
|
|
58
|
+
}
|
|
59
|
+
const validate = ajv.compile(schema[irNode.componentType]);
|
|
60
|
+
const valid = validate(irNode.config);
|
|
44
61
|
if (valid) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
62
|
+
const { errors } = transformation[index].fileParseResult;
|
|
63
|
+
// Even through it passed the schema validation, it may have had other errors along the way
|
|
64
|
+
return errors.length === 0
|
|
65
|
+
? {
|
|
66
|
+
valid: true,
|
|
67
|
+
}
|
|
68
|
+
: {
|
|
69
|
+
valid: false,
|
|
70
|
+
errors: (0, errors_1.compileError)(transformation[index]),
|
|
71
|
+
};
|
|
48
72
|
}
|
|
49
|
-
const errors = results
|
|
50
|
-
.filter(item => item.errors !== undefined)
|
|
51
|
-
.map(result => result.errors);
|
|
52
73
|
return {
|
|
53
|
-
valid,
|
|
54
|
-
errors,
|
|
74
|
+
valid: false,
|
|
75
|
+
schemaValidationErrors: validate.errors,
|
|
55
76
|
};
|
|
56
77
|
}
|
|
57
|
-
function
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
errors.forEach(error => {
|
|
62
|
-
const { intermediateRepresentation, fileParseResult } = transformation;
|
|
63
|
-
fileParseResult.errors.push(generateErrorMessage(transformInstancePath(error.instancePath), error));
|
|
64
|
-
transformation = { intermediateRepresentation, fileParseResult };
|
|
78
|
+
async function validateIntermediateRepresentation(intermediateRepresentation, transformation, translationContext) {
|
|
79
|
+
const schema = await (0, schemas_1.getIntermediateRepresentationSchema)(translationContext);
|
|
80
|
+
const results = Object.values(intermediateRepresentation.intermediateNodesIndexedByUid).map((irNode, index) => {
|
|
81
|
+
return validateIntermediateRepresentationNode(schema, transformation, irNode, index, translationContext);
|
|
65
82
|
});
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
const missingRequiredFieldRegex = /must have required property '(.+)'/;
|
|
72
|
-
// TODO: This needs cleanup and better error messaging
|
|
73
|
-
function generateErrorMessage(path, error) {
|
|
74
|
-
let errorMessage = '';
|
|
75
|
-
const dotNotationPath = path.length === 0 || path[0] === '' ? 'Root Level' : path.join('.');
|
|
76
|
-
errorMessage = `Error with ${dotNotationPath}: ${error.message}`;
|
|
77
|
-
const params = Object.entries(error.params);
|
|
78
|
-
if (params.length > 0) {
|
|
79
|
-
const additionalContext = params
|
|
80
|
-
.filter(([_, value]) => value)
|
|
81
|
-
.map(([key, value]) => `${key}: ${value}`);
|
|
82
|
-
if (error.message &&
|
|
83
|
-
missingRequiredFieldRegex.test(error.message) &&
|
|
84
|
-
error.params) {
|
|
85
|
-
// TODO: This error message doesn't work well for nested objects, it makes it seem like the field is missing from the root object
|
|
86
|
-
return `Missing required field: '${error.params.missingProperty}'`;
|
|
87
|
-
}
|
|
88
|
-
errorMessage += `${additionalContext.length > 0
|
|
89
|
-
? `\n\t\t- ${additionalContext.join('\n\t\t- ')}`
|
|
90
|
-
: ''}`;
|
|
83
|
+
const valid = results.every(result => result.valid);
|
|
84
|
+
if (valid) {
|
|
85
|
+
return {
|
|
86
|
+
valid,
|
|
87
|
+
};
|
|
91
88
|
}
|
|
92
|
-
|
|
89
|
+
const schemaValidationErrors = results.map(result => result.schemaValidationErrors);
|
|
90
|
+
throw new errors_1.TranslationError(copy_1.errorMessages.project.failedToTranslateProject, transformation, schemaValidationErrors, translationContext);
|
|
93
91
|
}
|