@hubspot/project-parsing-lib 0.0.5-experimental.0 → 0.0.6-beta.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hubspot/project-parsing-lib",
3
- "version": "0.0.5-experimental.0",
3
+ "version": "0.0.6-beta.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,5 +1,3 @@
1
- import { IntermediateRepresentation, IntermediateRepresentationNode, IntermediateRepresentationLocalDev, TranslationContext, TranslationOptions, IntermediateRepresentationNodeLocalDev } from './lib/types';
1
+ import { IntermediateRepresentation, TranslationContext, TranslationOptions } from './lib/types';
2
2
  export declare function translate(translationContext: TranslationContext, translationOptions?: TranslationOptions): Promise<IntermediateRepresentation>;
3
- export declare function translateForLocalDev(translationContext: TranslationContext): Promise<IntermediateRepresentationLocalDev>;
4
3
  export { isTranslationError } from './lib/errors';
5
- export { IntermediateRepresentation, IntermediateRepresentationNode, IntermediateRepresentationLocalDev, IntermediateRepresentationNodeLocalDev, TranslationContext, };
package/src/index.js CHANGED
@@ -1,16 +1,12 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
3
  exports.isTranslationError = void 0;
7
4
  exports.translate = translate;
8
- exports.translateForLocalDev = translateForLocalDev;
9
5
  const files_1 = require("./lib/files");
10
6
  const validation_1 = require("./lib/validation");
11
7
  const transform_1 = require("./lib/transform");
8
+ const errors_1 = require("./lib/errors");
12
9
  const copy_1 = require("./lang/copy");
13
- const path_1 = __importDefault(require("path"));
14
10
  const defaultOptions = {
15
11
  skipValidation: false,
16
12
  };
@@ -22,29 +18,13 @@ async function translate(translationContext, translationOptions = defaultOptions
22
18
  }
23
19
  const transformation = (0, transform_1.transform)(metafileContents);
24
20
  const intermediateRepresentation = (0, transform_1.getIntermediateRepresentation)(transformation);
25
- // Remove once extensions and serverless functions are supported
26
21
  if (!skipValidation) {
27
- await (0, validation_1.validateIntermediateRepresentation)(intermediateRepresentation, transformation, translationContext);
22
+ const { valid, errors } = await (0, validation_1.validateIntermediateRepresentation)(intermediateRepresentation, transformation, translationContext);
23
+ if (!valid) {
24
+ throw new errors_1.TranslationError(copy_1.errorMessages.project.failedToTranslateProject, errors);
25
+ }
28
26
  }
29
27
  return intermediateRepresentation;
30
28
  }
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; } });
29
+ var errors_2 = require("./lib/errors");
30
+ Object.defineProperty(exports, "isTranslationError", { enumerable: true, get: function () { return errors_2.isTranslationError; } });
@@ -1,4 +1,3 @@
1
- import { ComponentMetadata } from '../lib/constants';
2
1
  export declare const errorMessages: {
3
2
  api: {
4
3
  failedToFetchSchemas: string;
@@ -8,16 +7,14 @@ export declare const errorMessages: {
8
7
  failedToTranslateProject: string;
9
8
  duplicateUid: (uid: string, files: string[]) => string;
10
9
  };
11
- validation: {
12
- errorWithFileHeader: (file: string, errors: string[]) => string;
10
+ file: {
11
+ errorWithFileHeader: (file: string) => string;
13
12
  missingRequiredField: (field: string) => string;
14
13
  missingUid: string;
15
14
  missingType: string;
16
- missingConfig: string;
17
15
  unsupportedType: (type: string) => string;
18
16
  errorWithField: (field: string, error: string | undefined) => string;
19
17
  invalidJson: string;
20
- wrongDirectoryForComponent: (directory: string, componentType: string, componentMetadata: ComponentMetadata, correctDir: string) => string;
21
18
  };
22
19
  };
23
20
  export declare const logMessages: {
package/src/lang/copy.js CHANGED
@@ -6,22 +6,18 @@ exports.errorMessages = {
6
6
  failedToFetchSchemas: 'Failed to fetch schemas',
7
7
  },
8
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- ')}`,
9
+ noHsMetaFiles: 'No *-hsmeta.json files found, make sure you are inside a project',
10
+ failedToTranslateProject: 'Failed to translate project',
11
+ duplicateUid: (uid, files) => `Duplicate uid '${uid}' found in ${files.join(', ')}`,
12
12
  },
13
- validation: {
14
- errorWithFileHeader: (file, errors) => `\n\nEncountered the following errors for ${file}:\n\t- ${errors.join('\n\t- ')}`,
13
+ file: {
14
+ errorWithFileHeader: (file) => `Encountered the following errors for ${file}:`,
15
15
  missingRequiredField: (field) => `Missing required field: '${field}'`,
16
16
  missingUid: `Missing required field: 'uid'`,
17
17
  missingType: `Missing required field: 'type'`,
18
- missingConfig: `Missing required field: 'config'`,
19
18
  unsupportedType: (type) => `Unsupported type: ${type.toLowerCase()}`,
20
19
  errorWithField: (field, error) => `Error with ${field}: ${error || 'Unknown error'}`,
21
20
  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
21
  },
26
22
  };
27
23
  exports.logMessages = {
@@ -1,5 +1,7 @@
1
1
  export declare const AppKey = "app";
2
2
  export declare const ThemeKey = "theme";
3
+ export declare const AppObjectKey = "app-object";
4
+ export declare const AppObjectAssociationKey = "app-object-association";
3
5
  export declare const CallingKey = "calling";
4
6
  export declare const CardsKey = "card";
5
7
  export declare const FunctionsKey = "function";
@@ -9,17 +11,13 @@ export declare const TimelineEventsKey = "timeline-event";
9
11
  export declare const VideoConferencingKey = "video-conferencing";
10
12
  export declare const WebhooksKey = "webhooks";
11
13
  export declare const WorkflowActionsKey = "workflow-action";
12
- export interface ComponentMetadata {
14
+ interface ComponentMetadata {
13
15
  dir: string;
14
- isToplevel: boolean;
16
+ isToplevel?: boolean;
15
17
  parentComponent?: string;
16
- userFriendlyName: string;
17
- userFriendlyType: string;
18
- userFriendlyTypePlural: string;
19
18
  }
20
19
  export declare const Components: Record<string, ComponentMetadata>;
21
- export declare const userFacingToInternalType: Record<string, string>;
22
- export declare const internalTypeToUserFacing: Record<string, string>;
20
+ export declare const externalTypeToInternalType: Record<string, string>;
23
21
  export declare const metafileExtension = "-hsmeta.json";
24
22
  export declare const allowedAppSubComponentsDirs: string[];
25
23
  export declare const allowedThemeSubComponentsDirs: string[];
@@ -29,3 +27,4 @@ export declare const ProjectStructure: {
29
27
  };
30
28
  export declare const allowedComponentDirectories: string[];
31
29
  export declare const allowedSubComponentDirectories: string[];
30
+ export {};
@@ -33,13 +33,15 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
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;
36
+ exports.allowedSubComponentDirectories = exports.allowedComponentDirectories = exports.ProjectStructure = exports.allowedThemeSubComponentsDirs = exports.allowedAppSubComponentsDirs = exports.metafileExtension = exports.externalTypeToInternalType = exports.Components = exports.WorkflowActionsKey = exports.WebhooksKey = exports.VideoConferencingKey = exports.TimelineEventsKey = exports.MediaBridgeKey = exports.MarketingEventsKey = exports.FunctionsKey = exports.CardsKey = exports.CallingKey = exports.AppObjectAssociationKey = exports.AppObjectKey = exports.ThemeKey = exports.AppKey = void 0;
37
37
  // Top Level Component types
38
38
  const path = __importStar(require("path"));
39
39
  // Component types
40
40
  exports.AppKey = 'app';
41
41
  exports.ThemeKey = 'theme';
42
42
  // Sub-Component types
43
+ exports.AppObjectKey = 'app-object';
44
+ exports.AppObjectAssociationKey = 'app-object-association';
43
45
  exports.CallingKey = 'calling';
44
46
  exports.CardsKey = 'card';
45
47
  exports.FunctionsKey = 'function';
@@ -49,87 +51,63 @@ exports.TimelineEventsKey = 'timeline-event';
49
51
  exports.VideoConferencingKey = 'video-conferencing';
50
52
  exports.WebhooksKey = 'webhooks';
51
53
  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
- };
62
54
  exports.Components = {
63
55
  [exports.AppKey]: {
64
56
  dir: exports.AppKey,
65
- userFriendlyName: 'App',
66
- ...TopLevelComponentFields,
57
+ isToplevel: true,
58
+ },
59
+ [exports.AppObjectKey]: {
60
+ dir: 'app-objects',
61
+ parentComponent: exports.AppKey,
62
+ },
63
+ [exports.AppObjectAssociationKey]: {
64
+ dir: 'app-object-associations',
65
+ parentComponent: exports.AppKey,
67
66
  },
68
67
  [exports.ThemeKey]: {
69
68
  dir: exports.ThemeKey,
70
- userFriendlyName: 'Theme',
71
- ...TopLevelComponentFields,
69
+ isToplevel: true,
72
70
  },
73
71
  [exports.CallingKey]: {
74
72
  dir: exports.CallingKey,
75
73
  parentComponent: exports.AppKey,
76
- userFriendlyName: 'Calling',
77
- ...SubComponentFields,
78
74
  },
79
75
  [exports.CardsKey]: {
80
76
  dir: 'cards',
81
77
  parentComponent: exports.AppKey,
82
- userFriendlyName: 'Card',
83
- ...SubComponentFields,
84
78
  },
85
79
  [exports.FunctionsKey]: {
86
80
  dir: 'functions',
87
81
  parentComponent: exports.AppKey,
88
- userFriendlyName: 'Function',
89
- ...SubComponentFields,
90
82
  },
91
83
  [exports.MarketingEventsKey]: {
92
84
  dir: 'marketing-events',
93
85
  parentComponent: exports.AppKey,
94
- userFriendlyName: 'Marketing Event',
95
- ...SubComponentFields,
96
86
  },
97
87
  MediaBridgeKey: {
98
88
  dir: exports.MediaBridgeKey,
99
89
  parentComponent: exports.AppKey,
100
- userFriendlyName: 'Media Bridge',
101
- ...SubComponentFields,
102
90
  },
103
91
  [exports.TimelineEventsKey]: {
104
92
  dir: 'timeline-events',
105
93
  parentComponent: exports.AppKey,
106
- userFriendlyName: 'Timeline Event',
107
- ...SubComponentFields,
108
94
  },
109
95
  [exports.VideoConferencingKey]: {
110
96
  dir: exports.VideoConferencingKey,
111
97
  parentComponent: exports.AppKey,
112
- userFriendlyName: 'Video Conferencing',
113
- ...SubComponentFields,
114
98
  },
115
99
  [exports.WebhooksKey]: {
116
100
  dir: exports.WebhooksKey,
117
101
  parentComponent: exports.AppKey,
118
- userFriendlyName: 'Webhooks',
119
- ...SubComponentFields,
120
102
  },
121
103
  [exports.WorkflowActionsKey]: {
122
104
  dir: 'workflow-actions',
123
105
  parentComponent: exports.AppKey,
124
- userFriendlyName: 'Workflow Action',
125
- ...SubComponentFields,
126
106
  },
127
107
  };
128
- const internalAppKey = 'APPLICATION';
129
- exports.userFacingToInternalType = {
130
- [exports.AppKey]: internalAppKey,
108
+ exports.externalTypeToInternalType = {
109
+ [exports.AppKey]: 'application',
131
110
  };
132
- exports.internalTypeToUserFacing = Object.fromEntries(Object.entries(exports.userFacingToInternalType).map(([key, value]) => [value, key]));
133
111
  exports.metafileExtension = '-hsmeta.json';
134
112
  function getSubComponentDirsForParentType(parentComponent) {
135
113
  return Object.values(exports.Components).reduce((acc, item) => {
@@ -1,10 +1,8 @@
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;
1
+ import { CompiledError, Transformation } from './types';
5
2
  export declare class TranslationError extends Error {
6
3
  private errors;
7
- private translationContext;
8
- constructor(message: string, transformations: Transformation[], errors: (ErrorObject[] | null | undefined)[], translationContext: TranslationContext);
4
+ constructor(message: string, errors: CompiledError[]);
9
5
  toString(): string;
10
6
  }
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,152 +1,29 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
3
  exports.TranslationError = void 0;
7
4
  exports.isTranslationError = isTranslationError;
8
5
  exports.compileError = compileError;
9
6
  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
- }
21
7
  class TranslationError extends Error {
22
- errors = [];
23
- translationContext;
24
- constructor(message, transformations, errors, translationContext) {
8
+ errors;
9
+ constructor(message, errors) {
25
10
  super(message);
26
11
  this.name = 'TranslationError';
27
- this.translationContext = translationContext;
28
- this.errors = transformations.map((transformation, index) => compileTranslationErrors(transformation, errors[index]));
12
+ this.errors = errors;
29
13
  }
30
- // Returns a formatted string for all the errors in all the files
31
14
  toString() {
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
+ return `${this.message}: ${this.errors.map(({ message, errors }) => `\n\n${message} \n\t- ${errors.join('\n\t- ')}`)}`;
39
16
  }
40
17
  }
41
18
  exports.TranslationError = TranslationError;
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}`;
54
- }
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
- : ''}`;
19
+ function isTranslationError(error) {
20
+ return error instanceof TranslationError;
119
21
  }
120
- function mergeEnumErrors(cur, next) {
22
+ function compileError(validatedTransformation) {
23
+ const { fileParseResult } = validatedTransformation;
24
+ const { errors, file: filePath } = fileParseResult;
121
25
  return {
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
- },
26
+ message: copy_1.errorMessages.file.errorWithFileHeader(filePath),
27
+ errors,
133
28
  };
134
29
  }
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
@@ -71,7 +71,7 @@ function parseFile(fileLoadResult) {
71
71
  parsedFileContents = JSON.parse(fileLoadResult.content);
72
72
  }
73
73
  catch (_e) {
74
- fileLoadResult.errors?.push(copy_1.errorMessages.validation.invalidJson);
74
+ fileLoadResult.errors?.push(copy_1.errorMessages.file.invalidJson);
75
75
  }
76
76
  return { ...fileLoadResult, content: parsedFileContents };
77
77
  }
@@ -1,3 +1,3 @@
1
- import { AnySchema } from 'ajv-draft-04';
1
+ import { AnySchema } from 'ajv';
2
2
  import { TranslationContext } from './types';
3
3
  export declare function getIntermediateRepresentationSchema(translationContext: TranslationContext): Promise<Record<string, AnySchema>>;
@@ -1,5 +1,3 @@
1
1
  import { FileParseResult, IntermediateRepresentation, Transformation } from './types';
2
- export declare function mapToInternalType(type: string): string;
3
- export declare function mapToUserFacingType(type: string): string;
4
2
  export declare function transform(fileParseResults: FileParseResult[]): Transformation[];
5
3
  export declare function getIntermediateRepresentation(transformations: Transformation[]): IntermediateRepresentation;
@@ -1,37 +1,44 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.mapToInternalType = mapToInternalType;
4
- exports.mapToUserFacingType = mapToUserFacingType;
5
3
  exports.transform = transform;
6
4
  exports.getIntermediateRepresentation = getIntermediateRepresentation;
7
5
  const constants_1 = require("./constants");
8
6
  const copy_1 = require("../lang/copy");
9
- function calculateComponentDeps(fileValidationResult, parentComponents) {
10
- return {
11
- ...fileValidationResult.content?.dependencies,
12
- // If the file has a parent, add it as a dependency
13
- ...(fileValidationResult.parentDirectory
14
- ? {
15
- app: parentComponents[fileValidationResult.parentDirectory],
16
- }
17
- : {}),
18
- };
7
+ function calculateComponentDeps(fileValidationResult, parentComponents, appObjects) {
8
+ let dependencies = {};
9
+ // If there are dependencies in the config file, pass them through
10
+ if (!fileValidationResult.content?.dependencies) {
11
+ dependencies = { ...fileValidationResult.content?.dependencies };
12
+ }
13
+ const { type } = fileValidationResult.content;
14
+ // If the component is a child of the App component, add the app and appObjects as dependencies
15
+ if (constants_1.Components[type]?.parentComponent === constants_1.AppKey) {
16
+ const parentUid = parentComponents[fileValidationResult.parentDirectory];
17
+ if (parentUid) {
18
+ dependencies.app = parentUid;
19
+ }
20
+ if (type !== constants_1.AppObjectKey) {
21
+ dependencies.allAppObjects = appObjects;
22
+ }
23
+ }
24
+ return dependencies;
19
25
  }
20
26
  function mapToInternalType(type) {
21
- return (constants_1.userFacingToInternalType[type] || type || '').toUpperCase();
22
- }
23
- function mapToUserFacingType(type) {
24
- return (constants_1.internalTypeToUserFacing[type] || type || '').toLowerCase();
27
+ return (constants_1.externalTypeToInternalType[type] || type || '').toUpperCase();
25
28
  }
26
29
  function transform(fileParseResults) {
27
30
  const parentTypes = Object.keys(constants_1.ProjectStructure);
31
+ const parentComponents = {};
32
+ const appObjects = [];
28
33
  // Compute the parent components, so we can add them as dependencies to the child components
29
- const parentComponents = fileParseResults.reduce((acc, file) => {
34
+ fileParseResults.forEach(file => {
30
35
  if (file.content?.type && parentTypes.includes(file.content.type)) {
31
- acc[file.content.type] = file.content.uid;
36
+ parentComponents[file.content.type] = file.content.uid;
32
37
  }
33
- return acc;
34
- }, {});
38
+ if (file.content?.type === constants_1.AppObjectKey) {
39
+ appObjects.push(file.content.uid);
40
+ }
41
+ });
35
42
  return fileParseResults.map((currentFile) => {
36
43
  if (!currentFile.content) {
37
44
  currentFile.errors?.push(`File content is missing for ${currentFile.file}`);
@@ -43,7 +50,7 @@ function transform(fileParseResults) {
43
50
  uid,
44
51
  config,
45
52
  componentType: mapToInternalType(type),
46
- componentDeps: calculateComponentDeps(currentFile, parentComponents),
53
+ componentDeps: calculateComponentDeps(currentFile, parentComponents, appObjects),
47
54
  metaFilePath: currentFile.file,
48
55
  files: {},
49
56
  },
@@ -1,9 +1,9 @@
1
- import { ErrorObject } from 'ajv-draft-04';
1
+ export type Dependencies = Record<string, string | string[]>;
2
2
  export interface Components {
3
3
  uid: string;
4
4
  type: string;
5
5
  config: unknown;
6
- dependencies?: Record<string, unknown>;
6
+ dependencies?: Dependencies;
7
7
  }
8
8
  export interface FileActionResult {
9
9
  file: string;
@@ -18,7 +18,7 @@ export interface FileParseResult extends FileActionResult {
18
18
  }
19
19
  export interface IntermediateRepresentationNode {
20
20
  componentType: string;
21
- componentDeps: Record<string, string>;
21
+ componentDeps: Dependencies;
22
22
  metaFilePath: string;
23
23
  uid: string;
24
24
  config: unknown;
@@ -29,23 +29,12 @@ export interface IntermediateRepresentation {
29
29
  [key: string]: IntermediateRepresentationNode;
30
30
  };
31
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
- }
43
32
  export type Transformation = {
44
33
  intermediateRepresentation?: IntermediateRepresentationNode | null;
45
34
  fileParseResult: FileParseResult;
46
- schemaValidationErrors?: ErrorObject[] | null;
47
35
  };
48
36
  export type CompiledError = {
37
+ message: string;
49
38
  errors: string[];
50
39
  };
51
40
  export interface TranslationContext {
@@ -1,13 +1,9 @@
1
1
  import { CompiledError, IntermediateRepresentation, Transformation, TranslationContext } from './types';
2
- import { ValidateFunction } from 'ajv-draft-04';
3
- export type ValidResult = {
2
+ export type ValidationResults = {
3
+ valid: false;
4
+ errors: CompiledError[];
5
+ } | {
4
6
  valid: true;
5
7
  errors?: null;
6
- schemaValidationErrors?: null;
7
8
  };
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>;
9
+ export declare function validateIntermediateRepresentation(intermediateRepresentation: IntermediateRepresentation, transformation: Transformation[], translationContext: TranslationContext): Promise<ValidationResults>;
@@ -8,77 +8,41 @@ 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
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);
11
+ async function validateIntermediateRepresentation(intermediateRepresentation, transformation, translationContext) {
12
+ const schema = await (0, schemas_1.getIntermediateRepresentationSchema)(translationContext);
13
+ const results = Object.values(intermediateRepresentation.intermediateNodesIndexedByUid).map((irNode, index) => {
14
+ const ajv = new ajv_draft_04_1.default({ allErrors: true });
15
+ if (!irNode.uid) {
16
+ transformation[index].fileParseResult.errors.push(copy_1.errorMessages.file.missingUid);
34
17
  }
35
- else {
36
- transformation[index].fileParseResult.errors.push(copy_1.errorMessages.validation.unsupportedType(irNode.componentType));
18
+ if (!schema[irNode.componentType]) {
19
+ if (!irNode.componentType) {
20
+ transformation[index].fileParseResult.errors.push(copy_1.errorMessages.file.missingType);
21
+ }
22
+ transformation[index].fileParseResult.errors.push(copy_1.errorMessages.file.unsupportedType(irNode.componentType));
23
+ return {
24
+ valid: false,
25
+ errors: (0, errors_1.compileError)(transformation[index]),
26
+ };
37
27
  }
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)));
28
+ const validate = ajv.compile(schema[irNode.componentType]);
29
+ const valid = validate(irNode.config);
30
+ if (valid) {
31
+ const { errors } = transformation[index].fileParseResult;
32
+ return errors.length === 0
33
+ ? {
34
+ valid: true,
35
+ }
36
+ : {
37
+ valid: false,
38
+ errors: (0, errors_1.compileError)(transformation[index]),
39
+ };
51
40
  }
52
- }
53
- if (shouldSkipValidation) {
41
+ const transformationWithUpdatedErrors = hydrateValidationErrorsIntoTransformation(transformation[index], validate.errors);
54
42
  return {
55
43
  valid: false,
56
- errors: (0, errors_1.compileError)(transformation[index]),
44
+ errors: (0, errors_1.compileError)(transformationWithUpdatedErrors),
57
45
  };
58
- }
59
- const validate = ajv.compile(schema[irNode.componentType]);
60
- const valid = validate(irNode.config);
61
- if (valid) {
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
- };
72
- }
73
- return {
74
- valid: false,
75
- schemaValidationErrors: validate.errors,
76
- };
77
- }
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);
82
46
  });
83
47
  const valid = results.every(result => result.valid);
84
48
  if (valid) {
@@ -86,6 +50,48 @@ async function validateIntermediateRepresentation(intermediateRepresentation, tr
86
50
  valid,
87
51
  };
88
52
  }
89
- const schemaValidationErrors = results.map(result => result.schemaValidationErrors);
90
- throw new errors_1.TranslationError(copy_1.errorMessages.project.failedToTranslateProject, transformation, schemaValidationErrors, translationContext);
53
+ const errors = results
54
+ .filter(item => item.errors !== undefined)
55
+ .map(result => result.errors);
56
+ return {
57
+ valid,
58
+ errors,
59
+ };
60
+ }
61
+ function hydrateValidationErrorsIntoTransformation(transformation, errors) {
62
+ if (!errors) {
63
+ return transformation;
64
+ }
65
+ errors.forEach(error => {
66
+ const { intermediateRepresentation, fileParseResult } = transformation;
67
+ fileParseResult.errors.push(generateErrorMessage(transformInstancePath(error.instancePath), error));
68
+ transformation = { intermediateRepresentation, fileParseResult };
69
+ });
70
+ return transformation;
71
+ }
72
+ function transformInstancePath(instancePath) {
73
+ return instancePath.replace(/\/nodes\//, '').split('/');
74
+ }
75
+ const missingRequiredFieldRegex = /must have required property '(.+)'/;
76
+ // TODO: This needs cleanup and better error messaging
77
+ function generateErrorMessage(path, error) {
78
+ let errorMessage = '';
79
+ const dotNotationPath = path.length === 0 || path[0] === '' ? 'Root Level' : path.join('.');
80
+ errorMessage = copy_1.errorMessages.file.errorWithField(dotNotationPath, error.message);
81
+ const params = Object.entries(error.params);
82
+ if (params.length > 0) {
83
+ const additionalContext = params
84
+ .filter(([_, value]) => value)
85
+ .map(([key, value]) => `${key}: ${value}`);
86
+ if (error.message &&
87
+ missingRequiredFieldRegex.test(error.message) &&
88
+ error.params) {
89
+ // TODO: This error message doesn't work well for nested objects, it makes it seem like the field is missing from the root object
90
+ return copy_1.errorMessages.file.missingRequiredField(error.params.missingProperty);
91
+ }
92
+ errorMessage += `${additionalContext.length > 0
93
+ ? `\n\t\t- ${additionalContext.join('\n\t\t- ')}`
94
+ : ''}`;
95
+ }
96
+ return errorMessage;
91
97
  }