@hubspot/project-parsing-lib 0.2.2 → 0.3.0-experimental.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -35
- package/package.json +135 -22
- package/src/exports/constants.d.ts +1 -0
- package/src/exports/constants.js +1 -0
- package/src/exports/lang.d.ts +1 -0
- package/src/exports/lang.js +1 -0
- package/src/exports/migrate.d.ts +1 -0
- package/src/exports/migrate.js +1 -0
- package/src/exports/profiles.d.ts +3 -0
- package/src/exports/profiles.js +2 -0
- package/src/exports/projects.d.ts +5 -0
- package/src/exports/projects.js +3 -0
- package/src/exports/schema.d.ts +2 -0
- package/src/exports/schema.js +1 -0
- package/src/exports/themes.d.ts +2 -0
- package/src/exports/themes.js +1 -0
- package/src/exports/transform.d.ts +2 -0
- package/src/exports/transform.js +1 -0
- package/src/exports/translate.d.ts +4 -0
- package/src/exports/translate.js +2 -0
- package/src/exports/uid.d.ts +1 -0
- package/src/exports/uid.js +1 -0
- package/src/exports/validation.d.ts +3 -0
- package/src/exports/validation.js +2 -0
- package/src/exports/workspaces.d.ts +2 -0
- package/src/exports/workspaces.js +1 -0
- package/src/lang/copy.d.ts +19 -3
- package/src/lang/copy.js +40 -34
- package/src/lib/constants.d.ts +57 -29
- package/src/lib/constants.js +173 -126
- package/src/lib/errors.d.ts +3 -3
- package/src/lib/errors.js +25 -33
- package/src/lib/files.d.ts +13 -7
- package/src/lib/files.js +56 -49
- package/src/lib/localDev.d.ts +4 -0
- package/src/lib/localDev.js +72 -0
- package/src/lib/migrate.d.ts +1 -0
- package/src/lib/migrate.js +43 -45
- package/src/lib/migrateThemes.d.ts +25 -0
- package/src/lib/migrateThemes.js +120 -0
- package/src/lib/minimalArboristTree.d.ts +118 -0
- package/src/lib/minimalArboristTree.js +32 -0
- package/src/lib/platformVersion.d.ts +8 -0
- package/src/lib/platformVersion.js +74 -0
- package/src/lib/profiles.d.ts +6 -1
- package/src/lib/profiles.js +98 -40
- package/src/lib/project.d.ts +6 -0
- package/src/lib/project.js +73 -17
- package/src/lib/schemas.d.ts +2 -2
- package/src/lib/schemas.js +11 -11
- package/src/lib/transform.d.ts +4 -2
- package/src/lib/transform.js +109 -59
- package/src/lib/translate.d.ts +11 -0
- package/src/lib/translate.js +65 -0
- package/src/lib/types.d.ts +43 -6
- package/src/lib/types.js +1 -2
- package/src/lib/uid.d.ts +2 -0
- package/src/lib/uid.js +14 -9
- package/src/lib/utils.d.ts +3 -0
- package/src/lib/utils.js +16 -0
- package/src/lib/validation.d.ts +4 -4
- package/src/lib/validation.js +66 -53
- package/src/lib/workspaces.d.ts +113 -0
- package/src/lib/workspaces.js +403 -0
- package/src/index.d.ts +0 -18
- package/src/index.js +0 -87
package/src/lib/validation.js
CHANGED
|
@@ -1,85 +1,89 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
};
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
(
|
|
1
|
+
import { getIntermediateRepresentationSchema } from './schemas.js';
|
|
2
|
+
import { Ajv2020, } from 'ajv/dist/2020.js';
|
|
3
|
+
import { compileError, TranslationError } from './errors.js';
|
|
4
|
+
import { errorMessages } from '../lang/copy.js';
|
|
5
|
+
import { AUTO_GENERATED_COMPONENT_TYPES, Components, PLATFORM_VERSIONS, } from './constants.js';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { findTransformationByUid, mapToUserFacingType } from './transform.js';
|
|
8
|
+
import { meetsMinimumPlatformVersion } from './platformVersion.js';
|
|
9
|
+
import { validateUid } from './uid.js';
|
|
10
|
+
// ajv-formats does not play nicely with ESM and TS
|
|
11
|
+
import addFormatsModule from 'ajv-formats';
|
|
12
|
+
const addFormats = addFormatsModule;
|
|
13
|
+
export function createAjvInstance() {
|
|
14
|
+
const ajv = new Ajv2020({
|
|
15
|
+
allErrors: true,
|
|
16
|
+
code: { esm: true },
|
|
17
|
+
strict: false, // Allow Draft 2020-12 features
|
|
18
|
+
});
|
|
19
|
+
addFormats(ajv);
|
|
20
20
|
return ajv;
|
|
21
21
|
}
|
|
22
|
-
function validateIntermediateRepresentationNode(schema, transformation, irNode,
|
|
23
|
-
if (
|
|
22
|
+
function validateIntermediateRepresentationNode(schema, transformation, irNode, translationContext) {
|
|
23
|
+
if (AUTO_GENERATED_COMPONENT_TYPES.includes(mapToUserFacingType(irNode.componentType))) {
|
|
24
24
|
// Skip validation for auto-generated components
|
|
25
25
|
return {
|
|
26
26
|
valid: true,
|
|
27
27
|
};
|
|
28
28
|
}
|
|
29
|
-
if (transformation
|
|
29
|
+
if (transformation.fileParseResult.errors.length > 0) {
|
|
30
30
|
return {
|
|
31
31
|
valid: false,
|
|
32
|
-
errors:
|
|
32
|
+
errors: compileError(transformation),
|
|
33
33
|
};
|
|
34
34
|
}
|
|
35
35
|
const ajv = createAjvInstance();
|
|
36
36
|
let shouldSkipValidation = false;
|
|
37
37
|
if (!irNode.uid) {
|
|
38
|
-
transformation
|
|
38
|
+
transformation.fileParseResult.errors.push(errorMessages.validation.missingUid);
|
|
39
39
|
}
|
|
40
40
|
else {
|
|
41
|
-
const uidValidationResult =
|
|
41
|
+
const uidValidationResult = validateUid(irNode.uid);
|
|
42
42
|
if (uidValidationResult) {
|
|
43
|
-
transformation
|
|
43
|
+
transformation.fileParseResult.errors.push(uidValidationResult);
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
if (!irNode.config) {
|
|
47
|
-
transformation
|
|
47
|
+
transformation.fileParseResult.errors.push(errorMessages.validation.missingConfig);
|
|
48
48
|
// If there is no config block, there is nothing to validation
|
|
49
49
|
shouldSkipValidation = true;
|
|
50
50
|
}
|
|
51
|
+
if (irNode.permissions &&
|
|
52
|
+
!meetsMinimumPlatformVersion(translationContext.platformVersion, PLATFORM_VERSIONS.v2026_09_BETA)) {
|
|
53
|
+
transformation.fileParseResult.errors.push(errorMessages.validation.permissionsNotSupported(translationContext.platformVersion));
|
|
54
|
+
}
|
|
51
55
|
if (!schema[irNode.componentType]) {
|
|
52
56
|
if (!irNode.componentType) {
|
|
53
|
-
transformation
|
|
57
|
+
transformation.fileParseResult.errors.push(errorMessages.validation.missingType);
|
|
54
58
|
}
|
|
55
59
|
else {
|
|
56
|
-
transformation
|
|
60
|
+
transformation.fileParseResult.errors.push(errorMessages.validation.unsupportedType(irNode.componentType));
|
|
57
61
|
}
|
|
58
62
|
// If there is no schema for the component type, there is no way to validate
|
|
59
63
|
shouldSkipValidation = true;
|
|
60
64
|
}
|
|
61
|
-
const userFacingType =
|
|
62
|
-
const component =
|
|
65
|
+
const userFacingType = mapToUserFacingType(irNode.componentType);
|
|
66
|
+
const component = Components[userFacingType];
|
|
63
67
|
if (userFacingType && component) {
|
|
64
68
|
const expectedParentDir = component.parentComponent
|
|
65
|
-
?
|
|
69
|
+
? Components[component.parentComponent].dir
|
|
66
70
|
: '';
|
|
67
|
-
const expectedLocation =
|
|
68
|
-
const actualLocation =
|
|
71
|
+
const expectedLocation = path.join(expectedParentDir, component.dir);
|
|
72
|
+
const actualLocation = path.dirname(transformation.fileParseResult.file);
|
|
69
73
|
if (expectedLocation !== actualLocation) {
|
|
70
|
-
transformation
|
|
74
|
+
transformation.fileParseResult.errors.push(errorMessages.validation.wrongDirectoryForComponent(actualLocation, userFacingType, component, path.join(translationContext.projectSourceDir, expectedLocation)));
|
|
71
75
|
}
|
|
72
76
|
}
|
|
73
77
|
if (shouldSkipValidation) {
|
|
74
78
|
return {
|
|
75
79
|
valid: false,
|
|
76
|
-
errors:
|
|
80
|
+
errors: compileError(transformation),
|
|
77
81
|
};
|
|
78
82
|
}
|
|
79
83
|
const validate = ajv.compile(schema[irNode.componentType]);
|
|
80
84
|
const valid = validate(irNode.config);
|
|
81
85
|
if (valid) {
|
|
82
|
-
const { errors } = transformation
|
|
86
|
+
const { errors } = transformation.fileParseResult;
|
|
83
87
|
// Even through it passed the schema validation, it may have had other errors along the way
|
|
84
88
|
return errors.length === 0
|
|
85
89
|
? {
|
|
@@ -87,7 +91,7 @@ function validateIntermediateRepresentationNode(schema, transformation, irNode,
|
|
|
87
91
|
}
|
|
88
92
|
: {
|
|
89
93
|
valid: false,
|
|
90
|
-
errors:
|
|
94
|
+
errors: compileError(transformation),
|
|
91
95
|
};
|
|
92
96
|
}
|
|
93
97
|
return {
|
|
@@ -95,18 +99,24 @@ function validateIntermediateRepresentationNode(schema, transformation, irNode,
|
|
|
95
99
|
schemaValidationErrors: validate.errors,
|
|
96
100
|
};
|
|
97
101
|
}
|
|
98
|
-
async function validateIntermediateRepresentation(intermediateRepresentation,
|
|
99
|
-
const
|
|
102
|
+
export async function validateIntermediateRepresentation(intermediateRepresentation, transformations, translationContext) {
|
|
103
|
+
const hasAnyFileParseErrors = transformations.some(t => t.fileParseResult.errors.length > 0);
|
|
104
|
+
const schema = await getIntermediateRepresentationSchema(translationContext);
|
|
100
105
|
const potentialDuplicatesByComponent = {};
|
|
101
|
-
const results = Object.values(intermediateRepresentation.intermediateNodesIndexedByUid).map(
|
|
102
|
-
const userFacingType =
|
|
103
|
-
const component =
|
|
106
|
+
const results = Object.values(intermediateRepresentation.intermediateNodesIndexedByUid).map(irNode => {
|
|
107
|
+
const userFacingType = mapToUserFacingType(irNode.componentType);
|
|
108
|
+
const component = Components[userFacingType];
|
|
104
109
|
if (component && component.singularComponent) {
|
|
105
|
-
potentialDuplicatesByComponent[userFacingType] =
|
|
106
|
-
|
|
107
|
-
|
|
110
|
+
potentialDuplicatesByComponent[userFacingType] =
|
|
111
|
+
potentialDuplicatesByComponent[userFacingType]
|
|
112
|
+
? [...potentialDuplicatesByComponent[userFacingType], irNode]
|
|
113
|
+
: [irNode];
|
|
108
114
|
}
|
|
109
|
-
|
|
115
|
+
const transformation = findTransformationByUid(transformations, irNode.uid);
|
|
116
|
+
if (!transformation) {
|
|
117
|
+
return { valid: false, errors: [errorMessages.validation.missingUid] };
|
|
118
|
+
}
|
|
119
|
+
return validateIntermediateRepresentationNode(schema, transformation, irNode, translationContext);
|
|
110
120
|
});
|
|
111
121
|
let hasDuplicates = false;
|
|
112
122
|
Object.entries(potentialDuplicatesByComponent).forEach(([componentType, potentialDuplicates]) => {
|
|
@@ -115,17 +125,20 @@ async function validateIntermediateRepresentation(intermediateRepresentation, tr
|
|
|
115
125
|
}
|
|
116
126
|
hasDuplicates = true;
|
|
117
127
|
potentialDuplicates.forEach(duplicate => {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
128
|
+
const potentialDuplicateTransformation = findTransformationByUid(transformations, duplicate.uid);
|
|
129
|
+
if (potentialDuplicateTransformation) {
|
|
130
|
+
potentialDuplicateTransformation.fileParseResult.errors.push(errorMessages.project.duplicateComponent(componentType));
|
|
131
|
+
}
|
|
121
132
|
});
|
|
122
133
|
});
|
|
123
|
-
const valid = !
|
|
134
|
+
const valid = !hasAnyFileParseErrors &&
|
|
135
|
+
!hasDuplicates &&
|
|
136
|
+
results.every(result => result.valid);
|
|
124
137
|
if (valid) {
|
|
125
138
|
return {
|
|
126
139
|
valid,
|
|
127
140
|
};
|
|
128
141
|
}
|
|
129
|
-
const schemaValidationErrors = results.map(result => result.schemaValidationErrors);
|
|
130
|
-
throw new
|
|
142
|
+
const schemaValidationErrors = results.map(result => 'schemaValidationErrors' in result ? result.schemaValidationErrors : null);
|
|
143
|
+
throw new TranslationError(errorMessages.project.failedToTranslateProject, transformations, schemaValidationErrors, translationContext);
|
|
131
144
|
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error thrown when a workspace directory cannot be resolved
|
|
3
|
+
*/
|
|
4
|
+
export declare class WorkspaceResolutionError extends Error {
|
|
5
|
+
workspaceDir: string;
|
|
6
|
+
sourcePackageJson: string;
|
|
7
|
+
constructor(workspaceDir: string, sourcePackageJson: string, cause?: Error);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Error thrown when a file: dependency cannot be resolved
|
|
11
|
+
*/
|
|
12
|
+
export declare class FileDependencyResolutionError extends Error {
|
|
13
|
+
packageName: string;
|
|
14
|
+
dependencyPath: string;
|
|
15
|
+
sourcePackageJson: string;
|
|
16
|
+
constructor(packageName: string, dependencyPath: string, sourcePackageJson: string, cause?: Error);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Error thrown when a `workspace:` protocol dependency does not match any known
|
|
20
|
+
* workspace member. npm itself does not support the `workspace:` protocol —
|
|
21
|
+
* only pnpm and yarn berry do — so the install fails with a confusing message
|
|
22
|
+
* server-side. Surface a clearer error up front.
|
|
23
|
+
*/
|
|
24
|
+
export declare class OrphanWorkspaceProtocolError extends Error {
|
|
25
|
+
packageName: string;
|
|
26
|
+
sourcePackageJson: string;
|
|
27
|
+
constructor(packageName: string, sourcePackageJson: string);
|
|
28
|
+
}
|
|
29
|
+
interface PackageJson {
|
|
30
|
+
name?: string;
|
|
31
|
+
workspaces?: string[] | {
|
|
32
|
+
packages?: string[];
|
|
33
|
+
nohoist?: string[];
|
|
34
|
+
};
|
|
35
|
+
dependencies?: Record<string, string>;
|
|
36
|
+
}
|
|
37
|
+
export interface ParsedPackageJson {
|
|
38
|
+
path: string;
|
|
39
|
+
dir: string;
|
|
40
|
+
content: PackageJson | null;
|
|
41
|
+
}
|
|
42
|
+
export interface WorkspaceMapping {
|
|
43
|
+
workspaceDir: string;
|
|
44
|
+
sourcePackageJsonPath: string;
|
|
45
|
+
}
|
|
46
|
+
export type FileDependencyKind = 'directory' | 'tarball';
|
|
47
|
+
export type LocalDependencyProtocol = 'file' | 'link';
|
|
48
|
+
export interface FileDependencyMapping {
|
|
49
|
+
packageName: string;
|
|
50
|
+
localPath: string;
|
|
51
|
+
sourcePackageJsonPath: string;
|
|
52
|
+
kind: FileDependencyKind;
|
|
53
|
+
protocol: LocalDependencyProtocol;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Parses a `file:` or `link:` dependency spec into its protocol and raw path.
|
|
57
|
+
* Handles all npm-supported shapes:
|
|
58
|
+
* - file:./relative/path
|
|
59
|
+
* - file:relative/path (no leading slash/dot)
|
|
60
|
+
* - file:/absolute/path
|
|
61
|
+
* - file://./relative — npm-style file URL with relative path
|
|
62
|
+
* - file:///absolute — file URL with absolute path
|
|
63
|
+
* - link:./relative/path (symlink semantics for directories only)
|
|
64
|
+
*/
|
|
65
|
+
export declare function parseLocalSpec(spec: string): {
|
|
66
|
+
protocol: LocalDependencyProtocol;
|
|
67
|
+
rawPath: string;
|
|
68
|
+
} | null;
|
|
69
|
+
/**
|
|
70
|
+
* Finds and parses all package.json files in a directory.
|
|
71
|
+
* This is the single entry point for discovering package.json files and parsing their contents.
|
|
72
|
+
*/
|
|
73
|
+
export declare function findAndParsePackageJsonFiles(srcDir: string): Promise<ParsedPackageJson[]>;
|
|
74
|
+
/**
|
|
75
|
+
* Resolves workspace glob patterns to actual directories.
|
|
76
|
+
*
|
|
77
|
+
* Uses @npmcli/map-workspaces (npm's reference impl) so that negation patterns
|
|
78
|
+
* like `["packages/*", "!packages/excluded"]` are honored — a hand-rolled
|
|
79
|
+
* per-pattern glob() silently accepted negations without actually excluding.
|
|
80
|
+
*/
|
|
81
|
+
export declare function resolveWorkspaceDirectories(baseDir: string, workspaceGlobs: string[]): Promise<string[]>;
|
|
82
|
+
/**
|
|
83
|
+
* Collects all workspace directories that need to be uploaded.
|
|
84
|
+
* Handles edge cases like circular references, symlinks, and overlapping paths.
|
|
85
|
+
* Returns mappings that track which package.json defined each workspace.
|
|
86
|
+
*/
|
|
87
|
+
export declare function collectWorkspaceDirectories(parsedPackageJsons: ParsedPackageJson[]): Promise<WorkspaceMapping[]>;
|
|
88
|
+
/**
|
|
89
|
+
* Collects all `file:` and `link:` dependencies that need to be uploaded.
|
|
90
|
+
* Returns mappings that track the package name, resolved path, source
|
|
91
|
+
* package.json, kind (directory vs tarball) and protocol (file vs link).
|
|
92
|
+
*/
|
|
93
|
+
export declare function collectFileDependencies(parsedPackageJsons: ParsedPackageJson[]): Promise<FileDependencyMapping[]>;
|
|
94
|
+
/**
|
|
95
|
+
* Validates that every `workspace:` protocol dependency in the parsed package
|
|
96
|
+
* trees matches a known workspace member. Throws OrphanWorkspaceProtocolError
|
|
97
|
+
* for the first orphan found.
|
|
98
|
+
*
|
|
99
|
+
* npm itself does not implement the `workspace:` protocol; only pnpm and yarn
|
|
100
|
+
* berry do. If a project uses `workspace:*` without a matching member declared
|
|
101
|
+
* under `workspaces`, the install fails server-side with a confusing message —
|
|
102
|
+
* fail fast with a clearer one.
|
|
103
|
+
*/
|
|
104
|
+
export declare function validateWorkspaceProtocolDependencies(parsedPackageJsons: ParsedPackageJson[], workspaceMappings: WorkspaceMapping[]): void;
|
|
105
|
+
/**
|
|
106
|
+
* Returns the set of files that npm would include when publishing a package.
|
|
107
|
+
* Uses npm-packlist which respects the "files" field in package.json,
|
|
108
|
+
* .npmignore, and .gitignore rules.
|
|
109
|
+
*
|
|
110
|
+
* @throws Error if packlist fails to generate the file list
|
|
111
|
+
*/
|
|
112
|
+
export declare function getPackableFiles(dir: string): Promise<Set<string>>;
|
|
113
|
+
export {};
|