@hubspot/project-parsing-lib 0.0.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/LICENSE +12 -0
- package/README.md +38 -0
- package/package.json +40 -0
- package/src/index.d.ts +3 -0
- package/src/index.js +23 -0
- package/src/lib/constants.d.ts +27 -0
- package/src/lib/constants.js +106 -0
- package/src/lib/errors.d.ts +8 -0
- package/src/lib/errors.js +28 -0
- package/src/lib/files.d.ts +2 -0
- package/src/lib/files.js +79 -0
- package/src/lib/schemas.d.ts +3 -0
- package/src/lib/schemas.js +19 -0
- package/src/lib/transform.d.ts +3 -0
- package/src/lib/transform.js +53 -0
- package/src/lib/types.d.ts +42 -0
- package/src/lib/types.js +2 -0
- package/src/lib/validation.d.ts +9 -0
- package/src/lib/validation.js +93 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Copyright 2019 HubSpot, Inc.
|
|
2
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
3
|
+
you may not use this file except in compliance with the License.
|
|
4
|
+
You may obtain a copy of the License at
|
|
5
|
+
|
|
6
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
7
|
+
|
|
8
|
+
Unless required by applicable law or agreed to in writing, software
|
|
9
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
10
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
11
|
+
See the License for the specific language governing permissions and
|
|
12
|
+
limitations under the License.
|
package/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# project-translation-layer
|
|
2
|
+
|
|
3
|
+
## Description
|
|
4
|
+
This project is a translation layer that can be used to translate a project to its immediate representation prior to being uploaded
|
|
5
|
+
to the project v2 service.
|
|
6
|
+
|
|
7
|
+
## Flow Chart
|
|
8
|
+
|
|
9
|
+
```mermaid
|
|
10
|
+
sequenceDiagram
|
|
11
|
+
autonumber
|
|
12
|
+
|
|
13
|
+
actor dev as External Developer
|
|
14
|
+
participant cli as HS CLI
|
|
15
|
+
participant ppl as Project Parsing Library
|
|
16
|
+
participant pv3 as Projects v3
|
|
17
|
+
|
|
18
|
+
dev ->> cli: Developer runs `hs project upload`
|
|
19
|
+
cli ->> cli: Loads project config
|
|
20
|
+
cli ->> ppl: CLI calls `translate` function with the values required from the project config
|
|
21
|
+
ppl ->> ppl: Walks the project directory looking for the hsmeta files
|
|
22
|
+
loop For each hsmeta file
|
|
23
|
+
ppl ->> ppl: Checks if file is valid JSON
|
|
24
|
+
ppl ->> ppl: Checks if the file is in a valid location
|
|
25
|
+
ppl ->> ppl: Generates the IR
|
|
26
|
+
end
|
|
27
|
+
ppl ->> pv3: Fetch the schemas used to validate the generated IR
|
|
28
|
+
loop For each hsmeta file
|
|
29
|
+
ppl ->> ppl: Validate the IR config block against the schema
|
|
30
|
+
end
|
|
31
|
+
alt Validation successful for all schemas
|
|
32
|
+
ppl ->> cli: Pass the IR back to the CLI
|
|
33
|
+
cli ->> cli: Zip the project contents
|
|
34
|
+
cli ->> pv3: Upload the project and the IR
|
|
35
|
+
else Validation failed for 1+ schema(s)
|
|
36
|
+
ppl ->> cli: Log the error and exit the upload
|
|
37
|
+
end
|
|
38
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hubspot/project-parsing-lib",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Parsing library for converting projects directory structures to their intermediate representation",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"devDependencies": {
|
|
8
|
+
"@inquirer/prompts": "^7.1.0",
|
|
9
|
+
"@types/jest": "^29.5.14",
|
|
10
|
+
"@types/semver": "^7.5.8",
|
|
11
|
+
"@typescript-eslint/eslint-plugin": "^8.11.0",
|
|
12
|
+
"@typescript-eslint/parser": "^8.11.0",
|
|
13
|
+
"eslint": "^8.56.0",
|
|
14
|
+
"eslint-plugin-import": "^2.31.0",
|
|
15
|
+
"husky": "^9.1.7",
|
|
16
|
+
"jest": "^29.5.0",
|
|
17
|
+
"lint-staged": "^10.5.4",
|
|
18
|
+
"prettier": "^1.19.1",
|
|
19
|
+
"semver": "^7.6.3",
|
|
20
|
+
"ts-jest": "^29.2.5",
|
|
21
|
+
"ts-node": "^10.9.2",
|
|
22
|
+
"typescript": "^5.6.2"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@hubspot/local-dev-lib": "^3.1.0",
|
|
26
|
+
"ajv": "^8.17.1",
|
|
27
|
+
"ajv-draft-04": "^1.0.0"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "ts-node ./scripts/build.ts",
|
|
31
|
+
"lint": "eslint --max-warnings=0 . && prettier ./src/** --check",
|
|
32
|
+
"local-dev": "yarn build && yarn link --cwd=./dist && tsc --watch --rootDir . --outdir dist",
|
|
33
|
+
"prettier:write": "prettier ./src/** --write",
|
|
34
|
+
"test": "NODE_NO_WARNINGS=1 node --experimental-vm-modules ./node_modules/.bin/jest",
|
|
35
|
+
"release": "yarn ts-node ./scripts/release.ts release"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18"
|
|
39
|
+
}
|
|
40
|
+
}
|
package/src/index.d.ts
ADDED
package/src/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isTranslationError = void 0;
|
|
4
|
+
exports.translate = translate;
|
|
5
|
+
const files_1 = require("./lib/files");
|
|
6
|
+
const validation_1 = require("./lib/validation");
|
|
7
|
+
const transform_1 = require("./lib/transform");
|
|
8
|
+
const errors_1 = require("./lib/errors");
|
|
9
|
+
async function translate(translationContext) {
|
|
10
|
+
const metafileContents = await (0, files_1.loadHsMetaFiles)(translationContext);
|
|
11
|
+
if (metafileContents.length === 0) {
|
|
12
|
+
throw new Error('No *-hsmeta.json files found, make sure you are inside a project');
|
|
13
|
+
}
|
|
14
|
+
const transformation = (0, transform_1.transform)(metafileContents);
|
|
15
|
+
const intermediateRepresentation = (0, transform_1.getIntermediateRepresentation)(transformation);
|
|
16
|
+
const { valid, errors } = await (0, validation_1.validateIntermediateRepresentation)(intermediateRepresentation, transformation, translationContext);
|
|
17
|
+
if (!valid) {
|
|
18
|
+
throw new errors_1.TranslationError('Failed to translate project', errors);
|
|
19
|
+
}
|
|
20
|
+
return intermediateRepresentation;
|
|
21
|
+
}
|
|
22
|
+
var errors_2 = require("./lib/errors");
|
|
23
|
+
Object.defineProperty(exports, "isTranslationError", { enumerable: true, get: function () { return errors_2.isTranslationError; } });
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export declare const AppKey = "app";
|
|
2
|
+
export declare const ThemeKey = "theme";
|
|
3
|
+
export declare const CallingKey = "calling";
|
|
4
|
+
export declare const CardsKey = "card";
|
|
5
|
+
export declare const FunctionsKey = "function";
|
|
6
|
+
export declare const MarketingEventsKey = "marketing-event";
|
|
7
|
+
export declare const MediaBridgeKey = "media-bridge";
|
|
8
|
+
export declare const TimelineEventsKey = "timeline-event";
|
|
9
|
+
export declare const VideoConferencingKey = "video-conferencing";
|
|
10
|
+
export declare const WebhooksKey = "webhooks";
|
|
11
|
+
export declare const WorkflowActionsKey = "workflow-action";
|
|
12
|
+
interface ComponentMetadata {
|
|
13
|
+
dir: string;
|
|
14
|
+
isToplevel?: boolean;
|
|
15
|
+
parentComponent?: string;
|
|
16
|
+
}
|
|
17
|
+
export declare const Components: Record<string, ComponentMetadata>;
|
|
18
|
+
export declare const metafileExtension = "-hsmeta.json";
|
|
19
|
+
export declare const allowedAppSubComponentsDirs: string[];
|
|
20
|
+
export declare const allowedThemeSubComponentsDirs: string[];
|
|
21
|
+
export declare const ProjectStructure: {
|
|
22
|
+
readonly app: string[];
|
|
23
|
+
readonly theme: string[];
|
|
24
|
+
};
|
|
25
|
+
export declare const allowedComponentDirectories: string[];
|
|
26
|
+
export declare const allowedSubComponentDirectories: string[];
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.allowedSubComponentDirectories = exports.allowedComponentDirectories = exports.ProjectStructure = exports.allowedThemeSubComponentsDirs = exports.allowedAppSubComponentsDirs = exports.metafileExtension = 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
|
+
// Top Level Component types
|
|
28
|
+
const path = __importStar(require("path"));
|
|
29
|
+
// Component types
|
|
30
|
+
exports.AppKey = 'app';
|
|
31
|
+
exports.ThemeKey = 'theme';
|
|
32
|
+
// Sub-Component types
|
|
33
|
+
exports.CallingKey = 'calling';
|
|
34
|
+
exports.CardsKey = 'card';
|
|
35
|
+
exports.FunctionsKey = 'function';
|
|
36
|
+
exports.MarketingEventsKey = 'marketing-event';
|
|
37
|
+
exports.MediaBridgeKey = 'media-bridge';
|
|
38
|
+
exports.TimelineEventsKey = 'timeline-event';
|
|
39
|
+
exports.VideoConferencingKey = 'video-conferencing';
|
|
40
|
+
exports.WebhooksKey = 'webhooks';
|
|
41
|
+
exports.WorkflowActionsKey = 'workflow-action';
|
|
42
|
+
exports.Components = {
|
|
43
|
+
[exports.AppKey]: {
|
|
44
|
+
dir: exports.AppKey,
|
|
45
|
+
isToplevel: true,
|
|
46
|
+
},
|
|
47
|
+
[exports.ThemeKey]: {
|
|
48
|
+
dir: exports.ThemeKey,
|
|
49
|
+
isToplevel: true,
|
|
50
|
+
},
|
|
51
|
+
[exports.CallingKey]: {
|
|
52
|
+
dir: exports.CallingKey,
|
|
53
|
+
parentComponent: exports.AppKey,
|
|
54
|
+
},
|
|
55
|
+
[exports.CardsKey]: {
|
|
56
|
+
dir: 'cards',
|
|
57
|
+
parentComponent: exports.AppKey,
|
|
58
|
+
},
|
|
59
|
+
[exports.FunctionsKey]: {
|
|
60
|
+
dir: 'functions',
|
|
61
|
+
parentComponent: exports.AppKey,
|
|
62
|
+
},
|
|
63
|
+
[exports.MarketingEventsKey]: {
|
|
64
|
+
dir: 'marketing-events',
|
|
65
|
+
parentComponent: exports.AppKey,
|
|
66
|
+
},
|
|
67
|
+
MediaBridgeKey: {
|
|
68
|
+
dir: exports.MediaBridgeKey,
|
|
69
|
+
parentComponent: exports.AppKey,
|
|
70
|
+
},
|
|
71
|
+
[exports.TimelineEventsKey]: {
|
|
72
|
+
dir: 'timeline-events',
|
|
73
|
+
parentComponent: exports.AppKey,
|
|
74
|
+
},
|
|
75
|
+
[exports.VideoConferencingKey]: {
|
|
76
|
+
dir: exports.VideoConferencingKey,
|
|
77
|
+
parentComponent: exports.AppKey,
|
|
78
|
+
},
|
|
79
|
+
[exports.WebhooksKey]: {
|
|
80
|
+
dir: exports.WebhooksKey,
|
|
81
|
+
parentComponent: exports.AppKey,
|
|
82
|
+
},
|
|
83
|
+
[exports.WorkflowActionsKey]: {
|
|
84
|
+
dir: 'workflow-actions',
|
|
85
|
+
parentComponent: exports.AppKey,
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
exports.metafileExtension = '-hsmeta.json';
|
|
89
|
+
function getSubComponentDirsForParentType(parentComponent) {
|
|
90
|
+
return Object.values(exports.Components).reduce((acc, item) => {
|
|
91
|
+
if (item.parentComponent === parentComponent) {
|
|
92
|
+
acc.push(item.dir);
|
|
93
|
+
}
|
|
94
|
+
return acc;
|
|
95
|
+
}, []);
|
|
96
|
+
}
|
|
97
|
+
exports.allowedAppSubComponentsDirs = getSubComponentDirsForParentType(exports.AppKey);
|
|
98
|
+
exports.allowedThemeSubComponentsDirs = getSubComponentDirsForParentType(exports.ThemeKey);
|
|
99
|
+
exports.ProjectStructure = {
|
|
100
|
+
[exports.AppKey]: exports.allowedAppSubComponentsDirs,
|
|
101
|
+
[exports.ThemeKey]: exports.allowedThemeSubComponentsDirs,
|
|
102
|
+
};
|
|
103
|
+
exports.allowedComponentDirectories = Object.keys(exports.ProjectStructure);
|
|
104
|
+
exports.allowedSubComponentDirectories = Object.entries(exports.ProjectStructure)
|
|
105
|
+
.map(([key, value]) => value.map((value) => path.join(key, value)))
|
|
106
|
+
.flat();
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { CompiledError, Transformation } from './types';
|
|
2
|
+
export declare class TranslationError extends Error {
|
|
3
|
+
private errors;
|
|
4
|
+
constructor(message: string, errors: CompiledError[]);
|
|
5
|
+
toString(): string;
|
|
6
|
+
}
|
|
7
|
+
export declare function isTranslationError(error: unknown): error is TranslationError;
|
|
8
|
+
export declare function compileError(validatedTransformation: Transformation): CompiledError;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TranslationError = void 0;
|
|
4
|
+
exports.isTranslationError = isTranslationError;
|
|
5
|
+
exports.compileError = compileError;
|
|
6
|
+
class TranslationError extends Error {
|
|
7
|
+
errors;
|
|
8
|
+
constructor(message, errors) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = 'TranslationError';
|
|
11
|
+
this.errors = errors;
|
|
12
|
+
}
|
|
13
|
+
toString() {
|
|
14
|
+
return `${this.message}: ${this.errors.map(({ message, errors }) => `\n\n${message} \n\t- ${errors.join('\n\t- ')}`)}`;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
exports.TranslationError = TranslationError;
|
|
18
|
+
function isTranslationError(error) {
|
|
19
|
+
return error instanceof TranslationError;
|
|
20
|
+
}
|
|
21
|
+
function compileError(validatedTransformation) {
|
|
22
|
+
const { fileParseResult } = validatedTransformation;
|
|
23
|
+
const { errors, file: filePath } = fileParseResult;
|
|
24
|
+
return {
|
|
25
|
+
message: `Encountered the following errors for ${filePath}:`,
|
|
26
|
+
errors,
|
|
27
|
+
};
|
|
28
|
+
}
|
package/src/lib/files.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.loadHsMetaFiles = loadHsMetaFiles;
|
|
7
|
+
const fs_1 = require("@hubspot/local-dev-lib/fs");
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const constants_1 = require("./constants");
|
|
10
|
+
const fs_2 = __importDefault(require("fs"));
|
|
11
|
+
async function loadHsMetaFiles(translationContext) {
|
|
12
|
+
const metaFiles = await locateHsMetaFiles(translationContext);
|
|
13
|
+
return parseHsMetaFiles(metaFiles, translationContext);
|
|
14
|
+
}
|
|
15
|
+
async function locateHsMetaFiles(translationContext) {
|
|
16
|
+
const { projectSourceDir } = translationContext;
|
|
17
|
+
return (await (0, fs_1.walk)(projectSourceDir, ['node_modules'])).reduce((metaFiles, file) => {
|
|
18
|
+
// Ignore the node_modules directory
|
|
19
|
+
if (file.includes('node_modules')) {
|
|
20
|
+
return metaFiles;
|
|
21
|
+
}
|
|
22
|
+
const pathRelativeToProjectSrcDir = path_1.default.relative(projectSourceDir, file);
|
|
23
|
+
if (!pathRelativeToProjectSrcDir.endsWith(constants_1.metafileExtension)) {
|
|
24
|
+
return metaFiles;
|
|
25
|
+
}
|
|
26
|
+
const { dir: metaFileDir } = path_1.default.parse(pathRelativeToProjectSrcDir);
|
|
27
|
+
const parent = metaFileDir.split(path_1.default.sep)[0];
|
|
28
|
+
if (constants_1.allowedComponentDirectories.includes(metaFileDir)) {
|
|
29
|
+
metaFiles.push({ file });
|
|
30
|
+
}
|
|
31
|
+
else if (constants_1.allowedSubComponentDirectories.includes(metaFileDir)) {
|
|
32
|
+
metaFiles.push({ file, parent });
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
console.warn(`Skipping ${pathRelativeToProjectSrcDir} as it is not in a valid directory`);
|
|
36
|
+
}
|
|
37
|
+
return metaFiles;
|
|
38
|
+
}, []);
|
|
39
|
+
}
|
|
40
|
+
async function parseHsMetaFiles(metaFileLocations, translationContext) {
|
|
41
|
+
const fileLoadResults = await Promise.all(metaFileLocations.map(fileLocation => {
|
|
42
|
+
return loadFile(fileLocation, translationContext);
|
|
43
|
+
}));
|
|
44
|
+
return fileLoadResults.map(result => parseFile(result));
|
|
45
|
+
}
|
|
46
|
+
function loadFile(metaFileLocation, translationContext) {
|
|
47
|
+
const { projectSourceDir } = translationContext;
|
|
48
|
+
return new Promise(async (resolve) => {
|
|
49
|
+
const { file, parent } = metaFileLocation;
|
|
50
|
+
fs_2.default.readFile(file, 'utf-8', (err, content) => {
|
|
51
|
+
if (err) {
|
|
52
|
+
return resolve({
|
|
53
|
+
file,
|
|
54
|
+
errors: [err.message],
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
resolve({
|
|
58
|
+
file: path_1.default.relative(projectSourceDir, file),
|
|
59
|
+
content,
|
|
60
|
+
parent,
|
|
61
|
+
errors: [],
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
function parseFile(fileLoadResult) {
|
|
67
|
+
if (!fileLoadResult.content) {
|
|
68
|
+
// This is just to please TS, revisit this hack
|
|
69
|
+
return { ...fileLoadResult, content: undefined };
|
|
70
|
+
}
|
|
71
|
+
let parsedFileContents;
|
|
72
|
+
try {
|
|
73
|
+
parsedFileContents = JSON.parse(fileLoadResult.content);
|
|
74
|
+
}
|
|
75
|
+
catch (_e) {
|
|
76
|
+
fileLoadResult.errors?.push('Invalid JSON');
|
|
77
|
+
}
|
|
78
|
+
return { ...fileLoadResult, content: parsedFileContents };
|
|
79
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getIntermediateRepresentationSchema = getIntermediateRepresentationSchema;
|
|
4
|
+
const http_1 = require("@hubspot/local-dev-lib/http");
|
|
5
|
+
async function getIntermediateRepresentationSchema(translationContext) {
|
|
6
|
+
// This is just a placeholder that resolves the hardcoded object until we
|
|
7
|
+
// implement the actual logic to load the schemas from the backend
|
|
8
|
+
try {
|
|
9
|
+
const { accountId } = translationContext;
|
|
10
|
+
const { data } = await http_1.http.get(accountId, {
|
|
11
|
+
// TODO: update this to use the real platform version
|
|
12
|
+
url: `project-components-external/project-schemas/v3?platformVersion=testing`,
|
|
13
|
+
});
|
|
14
|
+
return data;
|
|
15
|
+
}
|
|
16
|
+
catch (e) {
|
|
17
|
+
throw new Error('Failed to fetch schemas', { cause: e });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { FileParseResult, IntermediateRepresentation, Transformation } from './types';
|
|
2
|
+
export declare function transform(fileParseResults: FileParseResult[]): Transformation[];
|
|
3
|
+
export declare function getIntermediateRepresentation(transformations: Transformation[]): IntermediateRepresentation;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.transform = transform;
|
|
4
|
+
exports.getIntermediateRepresentation = getIntermediateRepresentation;
|
|
5
|
+
const constants_1 = require("./constants");
|
|
6
|
+
function calculateComponentDeps(fileValidationResult, parentComponents) {
|
|
7
|
+
return {
|
|
8
|
+
...fileValidationResult.content?.dependencies,
|
|
9
|
+
// If the file has a parent, add it as a dependency
|
|
10
|
+
...(fileValidationResult.parent
|
|
11
|
+
? { upstreamBuildArtifact: parentComponents[fileValidationResult.parent] }
|
|
12
|
+
: {}),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function transform(fileParseResults) {
|
|
16
|
+
const parentTypes = Object.keys(constants_1.ProjectStructure);
|
|
17
|
+
// Compute the parent components, so we can add them as dependencies to the child components
|
|
18
|
+
const parentComponents = fileParseResults.reduce((acc, file) => {
|
|
19
|
+
if (file.content?.type && parentTypes.includes(file.content.type)) {
|
|
20
|
+
acc[file.content.type] = file.content.uid;
|
|
21
|
+
}
|
|
22
|
+
return acc;
|
|
23
|
+
}, {});
|
|
24
|
+
return fileParseResults.map((currentFile) => {
|
|
25
|
+
if (!currentFile.content) {
|
|
26
|
+
currentFile.errors?.push(`File content is missing for ${currentFile.file}`);
|
|
27
|
+
return { intermediateRepresentation: null, fileParseResult: currentFile };
|
|
28
|
+
}
|
|
29
|
+
const { config, uid } = currentFile.content;
|
|
30
|
+
return {
|
|
31
|
+
intermediateRepresentation: {
|
|
32
|
+
uid,
|
|
33
|
+
config,
|
|
34
|
+
componentType: currentFile.content?.type.toUpperCase() || '',
|
|
35
|
+
componentDeps: calculateComponentDeps(currentFile, parentComponents),
|
|
36
|
+
files: {},
|
|
37
|
+
},
|
|
38
|
+
fileParseResult: currentFile,
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
function getIntermediateRepresentation(transformations) {
|
|
43
|
+
const nodes = transformations.reduce((acc, current) => {
|
|
44
|
+
return {
|
|
45
|
+
...acc,
|
|
46
|
+
[current.intermediateRepresentation
|
|
47
|
+
.uid]: current.intermediateRepresentation,
|
|
48
|
+
};
|
|
49
|
+
}, {});
|
|
50
|
+
return {
|
|
51
|
+
intermediateNodesIndexedByUid: nodes,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export interface Components {
|
|
2
|
+
uid: string;
|
|
3
|
+
type: string;
|
|
4
|
+
config: unknown;
|
|
5
|
+
dependencies?: Record<string, unknown>;
|
|
6
|
+
}
|
|
7
|
+
export interface FileActionResult {
|
|
8
|
+
file: string;
|
|
9
|
+
parent?: string;
|
|
10
|
+
errors: string[];
|
|
11
|
+
}
|
|
12
|
+
export interface FileLoadResult extends FileActionResult {
|
|
13
|
+
content?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface FileParseResult extends FileActionResult {
|
|
16
|
+
content?: Components;
|
|
17
|
+
}
|
|
18
|
+
export interface IntermediateRepresentationNode {
|
|
19
|
+
componentType: string;
|
|
20
|
+
componentDeps: Record<string, string>;
|
|
21
|
+
uid: string;
|
|
22
|
+
config: unknown;
|
|
23
|
+
files: unknown;
|
|
24
|
+
}
|
|
25
|
+
export interface IntermediateRepresentation {
|
|
26
|
+
intermediateNodesIndexedByUid: {
|
|
27
|
+
[key: string]: IntermediateRepresentationNode;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export type Transformation = {
|
|
31
|
+
intermediateRepresentation?: IntermediateRepresentationNode | null;
|
|
32
|
+
fileParseResult: FileParseResult;
|
|
33
|
+
};
|
|
34
|
+
export type CompiledError = {
|
|
35
|
+
message: string;
|
|
36
|
+
errors: string[];
|
|
37
|
+
};
|
|
38
|
+
export interface TranslationContext {
|
|
39
|
+
projectSourceDir: string;
|
|
40
|
+
platformVersion: string;
|
|
41
|
+
accountId: number;
|
|
42
|
+
}
|
package/src/lib/types.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { CompiledError, IntermediateRepresentation, Transformation, TranslationContext } from './types';
|
|
2
|
+
export type ValidationResults = {
|
|
3
|
+
valid: false;
|
|
4
|
+
errors: CompiledError[];
|
|
5
|
+
} | {
|
|
6
|
+
valid: true;
|
|
7
|
+
errors?: null;
|
|
8
|
+
};
|
|
9
|
+
export declare function validateIntermediateRepresentation(intermediateRepresentation: IntermediateRepresentation, transformation: Transformation[], translationContext: TranslationContext): Promise<ValidationResults>;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.validateIntermediateRepresentation = validateIntermediateRepresentation;
|
|
7
|
+
const schemas_1 = require("./schemas");
|
|
8
|
+
const ajv_draft_04_1 = __importDefault(require("ajv-draft-04"));
|
|
9
|
+
const errors_1 = require("./errors");
|
|
10
|
+
async function validateIntermediateRepresentation(intermediateRepresentation, transformation, translationContext) {
|
|
11
|
+
const schema = await (0, schemas_1.getIntermediateRepresentationSchema)(translationContext);
|
|
12
|
+
const results = Object.values(intermediateRepresentation.intermediateNodesIndexedByUid).map((irNode, index) => {
|
|
13
|
+
const ajv = new ajv_draft_04_1.default({ allErrors: true });
|
|
14
|
+
if (!irNode.uid) {
|
|
15
|
+
transformation[index].fileParseResult.errors.push(`Missing required field: 'uid'`);
|
|
16
|
+
}
|
|
17
|
+
if (!schema[irNode.componentType]) {
|
|
18
|
+
transformation[index].fileParseResult.errors.push(`Unsupported 'type': '${irNode.componentType.toLowerCase()}'`);
|
|
19
|
+
return {
|
|
20
|
+
valid: false,
|
|
21
|
+
errors: (0, errors_1.compileError)(transformation[index]),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
const validate = ajv.compile(schema[irNode.componentType]);
|
|
25
|
+
const valid = validate(irNode.config);
|
|
26
|
+
if (valid) {
|
|
27
|
+
const { errors } = transformation[index].fileParseResult;
|
|
28
|
+
return errors.length === 0
|
|
29
|
+
? {
|
|
30
|
+
valid: true,
|
|
31
|
+
}
|
|
32
|
+
: {
|
|
33
|
+
valid: false,
|
|
34
|
+
errors: (0, errors_1.compileError)(transformation[index]),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const transformationWithUpdatedErrors = hydrateValidationErrorsIntoTransformation(transformation[index], validate.errors);
|
|
38
|
+
return {
|
|
39
|
+
valid: false,
|
|
40
|
+
errors: (0, errors_1.compileError)(transformationWithUpdatedErrors),
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
const valid = results.every(result => result.valid);
|
|
44
|
+
if (valid) {
|
|
45
|
+
return {
|
|
46
|
+
valid,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const errors = results
|
|
50
|
+
.filter(item => item.errors !== undefined)
|
|
51
|
+
.map(result => result.errors);
|
|
52
|
+
return {
|
|
53
|
+
valid,
|
|
54
|
+
errors,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function hydrateValidationErrorsIntoTransformation(transformation, errors) {
|
|
58
|
+
if (!errors) {
|
|
59
|
+
return transformation;
|
|
60
|
+
}
|
|
61
|
+
errors.forEach(error => {
|
|
62
|
+
const { intermediateRepresentation, fileParseResult } = transformation;
|
|
63
|
+
fileParseResult.errors.push(generateErrorMessage(transformInstancePath(error.instancePath), error));
|
|
64
|
+
transformation = { intermediateRepresentation, fileParseResult };
|
|
65
|
+
});
|
|
66
|
+
return transformation;
|
|
67
|
+
}
|
|
68
|
+
function transformInstancePath(instancePath) {
|
|
69
|
+
return instancePath.replace(/\/nodes\//, '').split('/');
|
|
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
|
+
: ''}`;
|
|
91
|
+
}
|
|
92
|
+
return errorMessage;
|
|
93
|
+
}
|