@hubspot/project-parsing-lib 0.2.0 → 0.2.1-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.
Files changed (66) hide show
  1. package/README.md +4 -35
  2. package/package.json +124 -22
  3. package/src/exports/constants.d.ts +1 -0
  4. package/src/exports/constants.js +1 -0
  5. package/src/exports/lang.d.ts +1 -0
  6. package/src/exports/lang.js +1 -0
  7. package/src/exports/migrate.d.ts +1 -0
  8. package/src/exports/migrate.js +1 -0
  9. package/src/exports/profiles.d.ts +3 -0
  10. package/src/exports/profiles.js +2 -0
  11. package/src/exports/projects.d.ts +4 -0
  12. package/src/exports/projects.js +3 -0
  13. package/src/exports/schema.d.ts +2 -0
  14. package/src/exports/schema.js +1 -0
  15. package/src/exports/themes.d.ts +2 -0
  16. package/src/exports/themes.js +1 -0
  17. package/src/exports/transform.d.ts +2 -0
  18. package/src/exports/transform.js +1 -0
  19. package/src/exports/translate.d.ts +3 -0
  20. package/src/exports/translate.js +2 -0
  21. package/src/exports/uid.d.ts +1 -0
  22. package/src/exports/uid.js +1 -0
  23. package/src/exports/validation.d.ts +3 -0
  24. package/src/exports/validation.js +2 -0
  25. package/src/exports/workspaces.d.ts +2 -0
  26. package/src/exports/workspaces.js +1 -0
  27. package/src/lang/copy.d.ts +8 -1
  28. package/src/lang/copy.js +30 -33
  29. package/src/lib/constants.d.ts +55 -28
  30. package/src/lib/constants.js +172 -121
  31. package/src/lib/errors.d.ts +4 -3
  32. package/src/lib/errors.js +62 -38
  33. package/src/lib/files.d.ts +10 -1
  34. package/src/lib/files.js +51 -40
  35. package/src/lib/localDev.d.ts +4 -0
  36. package/src/lib/localDev.js +72 -0
  37. package/src/lib/migrate.d.ts +1 -0
  38. package/src/lib/migrate.js +43 -45
  39. package/src/lib/migrateThemes.d.ts +25 -0
  40. package/src/lib/migrateThemes.js +120 -0
  41. package/src/lib/minimalArboristTree.d.ts +118 -0
  42. package/src/lib/minimalArboristTree.js +32 -0
  43. package/src/lib/platformVersion.d.ts +3 -0
  44. package/src/lib/platformVersion.js +16 -0
  45. package/src/lib/profiles.d.ts +6 -1
  46. package/src/lib/profiles.js +95 -40
  47. package/src/lib/project.d.ts +13 -0
  48. package/src/lib/project.js +36 -0
  49. package/src/lib/schemas.d.ts +2 -2
  50. package/src/lib/schemas.js +11 -11
  51. package/src/lib/transform.d.ts +4 -2
  52. package/src/lib/transform.js +100 -53
  53. package/src/lib/translate.d.ts +3 -0
  54. package/src/lib/translate.js +62 -0
  55. package/src/lib/types.d.ts +30 -6
  56. package/src/lib/types.js +1 -2
  57. package/src/lib/uid.d.ts +2 -0
  58. package/src/lib/uid.js +14 -9
  59. package/src/lib/utils.d.ts +3 -0
  60. package/src/lib/utils.js +16 -0
  61. package/src/lib/validation.d.ts +4 -4
  62. package/src/lib/validation.js +61 -53
  63. package/src/lib/workspaces.d.ts +68 -0
  64. package/src/lib/workspaces.js +290 -0
  65. package/src/index.d.ts +0 -18
  66. package/src/index.js +0 -86
@@ -0,0 +1,290 @@
1
+ import fs from 'fs';
2
+ import fsPromises from 'fs/promises';
3
+ import path from 'path';
4
+ import os from 'os';
5
+ import { glob } from 'glob';
6
+ import packlist from 'npm-packlist';
7
+ import { logger } from '@hubspot/local-dev-lib/logger';
8
+ import { walk } from '@hubspot/local-dev-lib/fs';
9
+ import { createMinimalTree } from './minimalArboristTree.js';
10
+ /**
11
+ * Error thrown when a workspace directory cannot be resolved
12
+ */
13
+ export class WorkspaceResolutionError extends Error {
14
+ workspaceDir;
15
+ sourcePackageJson;
16
+ constructor(workspaceDir, sourcePackageJson, cause) {
17
+ super(`Failed to resolve workspace directory "${workspaceDir}" declared in ${sourcePackageJson}${cause ? `: ${cause.message}` : ''}`);
18
+ this.workspaceDir = workspaceDir;
19
+ this.sourcePackageJson = sourcePackageJson;
20
+ this.name = 'WorkspaceResolutionError';
21
+ if (cause) {
22
+ this.cause = cause;
23
+ }
24
+ }
25
+ }
26
+ /**
27
+ * Error thrown when a file: dependency cannot be resolved
28
+ */
29
+ export class FileDependencyResolutionError extends Error {
30
+ packageName;
31
+ dependencyPath;
32
+ sourcePackageJson;
33
+ constructor(packageName, dependencyPath, sourcePackageJson, cause) {
34
+ super(`Failed to resolve file: dependency "${packageName}" at path "${dependencyPath}" declared in ${sourcePackageJson}${cause ? `: ${cause.message}` : ''}`);
35
+ this.packageName = packageName;
36
+ this.dependencyPath = dependencyPath;
37
+ this.sourcePackageJson = sourcePackageJson;
38
+ this.name = 'FileDependencyResolutionError';
39
+ if (cause) {
40
+ this.cause = cause;
41
+ }
42
+ }
43
+ }
44
+ /**
45
+ * Expands tilde (~) in paths to the user's home directory
46
+ */
47
+ function expandTildePath(filePath) {
48
+ return filePath.startsWith('~')
49
+ ? path.join(os.homedir(), filePath.slice(1))
50
+ : filePath;
51
+ }
52
+ /**
53
+ * Finds and parses all package.json files in a directory.
54
+ * This is the single entry point for discovering package.json files and parsing their contents.
55
+ */
56
+ export async function findAndParsePackageJsonFiles(srcDir) {
57
+ const allFiles = await walk(srcDir, ['node_modules']);
58
+ const packageJsonPaths = allFiles.filter(file => path.basename(file) === 'package.json');
59
+ return packageJsonPaths.map(filePath => {
60
+ try {
61
+ return {
62
+ path: filePath,
63
+ dir: path.dirname(filePath),
64
+ content: JSON.parse(fs.readFileSync(filePath, 'utf8')),
65
+ };
66
+ }
67
+ catch (e) {
68
+ logger.debug(`Failed to parse package.json at ${filePath}: ${e}`);
69
+ return {
70
+ path: filePath,
71
+ dir: path.dirname(filePath),
72
+ content: null,
73
+ };
74
+ }
75
+ });
76
+ }
77
+ /**
78
+ * Extracts workspace patterns from parsed package.json content
79
+ */
80
+ function extractWorkspaceGlobs(packageJson) {
81
+ const workspaces = packageJson.workspaces;
82
+ if (!workspaces) {
83
+ return [];
84
+ }
85
+ // Handle array format: "workspaces": ["packages/*"]
86
+ if (Array.isArray(workspaces)) {
87
+ return workspaces;
88
+ }
89
+ // Handle object format: "workspaces": {"packages": ["packages/*"]}
90
+ if (workspaces.packages && Array.isArray(workspaces.packages)) {
91
+ return workspaces.packages;
92
+ }
93
+ return [];
94
+ }
95
+ /**
96
+ * Resolves workspace glob patterns to actual directories
97
+ */
98
+ export async function resolveWorkspaceDirectories(baseDir, workspaceGlobs) {
99
+ const resolvedDirs = new Set();
100
+ for (const pattern of workspaceGlobs) {
101
+ const expandedPattern = expandTildePath(pattern);
102
+ const absolutePattern = path.resolve(baseDir, expandedPattern);
103
+ try {
104
+ // Use glob to find matching directories
105
+ const matches = await glob(absolutePattern, {
106
+ absolute: true,
107
+ withFileTypes: false,
108
+ });
109
+ // Filter to directories that contain package.json
110
+ for (const match of matches) {
111
+ try {
112
+ const stats = await fsPromises.stat(match);
113
+ if (stats.isDirectory()) {
114
+ const packageJsonPath = path.join(match, 'package.json');
115
+ try {
116
+ await fsPromises.access(packageJsonPath, fs.constants.F_OK);
117
+ resolvedDirs.add(match);
118
+ }
119
+ catch {
120
+ // Directory exists but doesn't contain package.json - skip silently
121
+ }
122
+ }
123
+ }
124
+ catch (e) {
125
+ // Inaccessible directories may indicate permission issues or broken symlinks
126
+ logger.warn(`Cannot access directory ${match}: ${e}`);
127
+ }
128
+ }
129
+ }
130
+ catch (e) {
131
+ logger.debug(`Failed to resolve workspace pattern "${pattern}": ${e}`);
132
+ }
133
+ }
134
+ return Array.from(resolvedDirs);
135
+ }
136
+ /**
137
+ * Collects all workspace directories that need to be uploaded.
138
+ * Handles edge cases like circular references, symlinks, and overlapping paths.
139
+ * Returns mappings that track which package.json defined each workspace.
140
+ */
141
+ export async function collectWorkspaceDirectories(parsedPackageJsons) {
142
+ const workspaceMappings = [];
143
+ // Track (sourcePackageJsonPath, workspaceDir) pairs to avoid duplicates within
144
+ // the same source, while still allowing the same workspace dir to appear in
145
+ // mappings from multiple different package.json files. This ensures every
146
+ // package.json that declares a workspace gets its workspaces field rewritten,
147
+ // regardless of whether another package.json already references the same dir.
148
+ const visited = new Set();
149
+ for (const parsed of parsedPackageJsons) {
150
+ if (!parsed.content) {
151
+ continue;
152
+ }
153
+ const workspaceGlobs = extractWorkspaceGlobs(parsed.content);
154
+ if (workspaceGlobs.length === 0) {
155
+ continue;
156
+ }
157
+ logger.debug(`Found workspaces in ${parsed.path}: ${workspaceGlobs.join(', ')}`);
158
+ const resolved = await resolveWorkspaceDirectories(parsed.dir, workspaceGlobs);
159
+ for (const dir of resolved) {
160
+ // Resolve symlinks to real path
161
+ let realDir;
162
+ try {
163
+ realDir = await fsPromises.realpath(dir);
164
+ }
165
+ catch (e) {
166
+ throw new WorkspaceResolutionError(dir, parsed.path, e instanceof Error ? e : undefined);
167
+ }
168
+ // Skip duplicate (source, workspace) pairs only — the same workspace dir
169
+ // may legitimately be referenced by multiple package.json files and each
170
+ // needs its own mapping so its workspaces field is rewritten correctly.
171
+ const pairKey = `${parsed.path}::${realDir}`;
172
+ if (visited.has(pairKey)) {
173
+ logger.debug(`Skipping duplicate workspace mapping: ${realDir} from ${parsed.path}`);
174
+ continue;
175
+ }
176
+ visited.add(pairKey);
177
+ // Include all workspaces - both internal (inside srcDir) and external.
178
+ // The CLI determines the archive path:
179
+ // - Internal: preserved as-is (already included via srcDir walk)
180
+ // - External: _workspaces/<name>-<hash>
181
+ workspaceMappings.push({
182
+ workspaceDir: realDir,
183
+ sourcePackageJsonPath: parsed.path,
184
+ });
185
+ logger.debug(`Resolved workspace: ${realDir}`);
186
+ }
187
+ }
188
+ return workspaceMappings;
189
+ }
190
+ /**
191
+ * Extracts file: dependencies from parsed package.json content.
192
+ * Only scans production dependencies since devDependencies and optionalDependencies
193
+ * are not needed for the build pipeline.
194
+ */
195
+ function extractFileDependencies(packageJson) {
196
+ const fileDeps = [];
197
+ const deps = packageJson.dependencies;
198
+ if (!deps) {
199
+ return fileDeps;
200
+ }
201
+ for (const [packageName, version] of Object.entries(deps)) {
202
+ // Only handle file: dependencies. workspace: dependencies reference packages
203
+ // already collected via the workspaces field, so they don't need separate handling.
204
+ if (typeof version === 'string' && version.startsWith('file:')) {
205
+ const filePath = version.slice(5); // Remove 'file:' prefix
206
+ fileDeps.push({ packageName, filePath });
207
+ }
208
+ }
209
+ return fileDeps;
210
+ }
211
+ /**
212
+ * Collects all file: dependencies that need to be uploaded.
213
+ * Returns mappings that track the package name, resolved path, and source package.json.
214
+ */
215
+ export async function collectFileDependencies(parsedPackageJsons) {
216
+ const fileDependencyMappings = [];
217
+ const visited = new Set();
218
+ for (const parsed of parsedPackageJsons) {
219
+ if (!parsed.content) {
220
+ continue;
221
+ }
222
+ const fileDeps = extractFileDependencies(parsed.content);
223
+ if (fileDeps.length === 0) {
224
+ continue;
225
+ }
226
+ logger.debug(`Found file: dependencies in ${parsed.path}: ${fileDeps.map(d => d.packageName).join(', ')}`);
227
+ for (const { packageName, filePath } of fileDeps) {
228
+ const expandedPath = expandTildePath(filePath);
229
+ const absolutePath = path.resolve(parsed.dir, expandedPath);
230
+ // Resolve symlinks to real path
231
+ let realPath;
232
+ try {
233
+ realPath = await fsPromises.realpath(absolutePath);
234
+ }
235
+ catch (e) {
236
+ throw new FileDependencyResolutionError(packageName, absolutePath, parsed.path, e instanceof Error ? e : undefined);
237
+ }
238
+ // Verify it's a directory with a package.json
239
+ const stats = await fsPromises.stat(realPath);
240
+ if (!stats.isDirectory()) {
241
+ throw new FileDependencyResolutionError(packageName, realPath, parsed.path, new Error('Path is not a directory'));
242
+ }
243
+ const depPackageJsonPath = path.join(realPath, 'package.json');
244
+ try {
245
+ await fsPromises.access(depPackageJsonPath, fs.constants.F_OK);
246
+ }
247
+ catch {
248
+ throw new FileDependencyResolutionError(packageName, realPath, parsed.path, new Error('Directory does not contain package.json'));
249
+ }
250
+ // Skip if already visited (same path referenced multiple times)
251
+ if (visited.has(realPath)) {
252
+ logger.debug(`Skipping already visited file: dependency: ${realPath}`);
253
+ continue;
254
+ }
255
+ visited.add(realPath);
256
+ // Include all file: dependencies - both internal (inside srcDir) and external
257
+ // The CLI determines the archive path:
258
+ // - Internal: _workspaces/<relative-path>
259
+ // - External: _workspaces/external/<name>-<hash>
260
+ fileDependencyMappings.push({
261
+ packageName,
262
+ localPath: realPath,
263
+ sourcePackageJsonPath: parsed.path,
264
+ });
265
+ logger.debug(`Resolved file: dependency ${packageName}: ${realPath}`);
266
+ }
267
+ }
268
+ return fileDependencyMappings;
269
+ }
270
+ /**
271
+ * Returns the set of files that npm would include when publishing a package.
272
+ * Uses npm-packlist which respects the "files" field in package.json,
273
+ * .npmignore, and .gitignore rules.
274
+ *
275
+ * @throws Error if packlist fails to generate the file list
276
+ */
277
+ export async function getPackableFiles(dir) {
278
+ try {
279
+ const tree = createMinimalTree(dir);
280
+ // Cast to Parameters<typeof packlist>[0] since npm-packlist only uses
281
+ // a subset of the Arborist Node properties at runtime
282
+ const files = await packlist(tree);
283
+ return new Set(files);
284
+ }
285
+ catch (e) {
286
+ const errorMessage = `Failed to get packlist for ${dir}: ${e instanceof Error ? e.message : String(e)}`;
287
+ logger.error(errorMessage);
288
+ throw new Error(errorMessage, { cause: e });
289
+ }
290
+ }
package/src/index.d.ts DELETED
@@ -1,18 +0,0 @@
1
- import { IntermediateRepresentation, IntermediateRepresentationNode, IntermediateRepresentationLocalDev, TranslationContext, TranslationOptions, IntermediateRepresentationNodeLocalDev } from './lib/types';
2
- import { ValidateFunction, AnySchema, ErrorObject } from 'ajv/dist/2020';
3
- export declare function translate(translationContext: TranslationContext, translationOptions?: TranslationOptions): Promise<IntermediateRepresentation>;
4
- export declare function translateForLocalDev(translationContext: TranslationContext, translationOptions?: Pick<TranslationOptions, 'profile'>): Promise<IntermediateRepresentationLocalDev>;
5
- export { isTranslationError } from './lib/errors';
6
- export { IntermediateRepresentation, IntermediateRepresentationNode, IntermediateRepresentationLocalDev, IntermediateRepresentationNodeLocalDev, TranslationContext, };
7
- export { getHsProfileFilename } from './lib/profiles';
8
- export { loadHsProfileFile, getAllHsProfiles } from './lib/files';
9
- export { migrate } from './lib/migrate';
10
- export { validateUid } from './lib/uid';
11
- export { mapToUserFriendlyName, mapToInternalType } from './lib/transform';
12
- export { getIntermediateRepresentationSchema } from './lib/schemas';
13
- export { createAjvInstance } from './lib/validation';
14
- export { ValidateFunction, AnySchema, ErrorObject };
15
- export { metafileExtension, hsProjectJsonFilename } from './lib/constants';
16
- export { Components } from './lib/types';
17
- export { AjvErrorKeyword } from './lib/errors';
18
- export { getInvalidJsonError, getMissingTypeError, getMissingConfigError, getMissingAccountIdError, getMissingRequiredFieldError, getFailedToFetchSchemasError, getUnsupportedTypeError, } from './lang/copy';
package/src/index.js DELETED
@@ -1,86 +0,0 @@
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.getUnsupportedTypeError = exports.getFailedToFetchSchemasError = exports.getMissingRequiredFieldError = exports.getMissingAccountIdError = exports.getMissingConfigError = exports.getMissingTypeError = exports.getInvalidJsonError = exports.AjvErrorKeyword = exports.hsProjectJsonFilename = exports.metafileExtension = exports.createAjvInstance = exports.getIntermediateRepresentationSchema = exports.mapToInternalType = exports.mapToUserFriendlyName = exports.validateUid = exports.migrate = exports.getAllHsProfiles = exports.loadHsProfileFile = exports.getHsProfileFilename = exports.isTranslationError = void 0;
7
- exports.translate = translate;
8
- exports.translateForLocalDev = translateForLocalDev;
9
- const files_1 = require("./lib/files");
10
- const validation_1 = require("./lib/validation");
11
- const transform_1 = require("./lib/transform");
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;
19
- const metafileContents = await (0, files_1.loadHsMetaFiles)(translationContext);
20
- if (metafileContents.length === 0) {
21
- throw new Error(copy_1.errorMessages.project.noHsMetaFiles);
22
- }
23
- let hsProfileContents;
24
- if (translationOptions.profile) {
25
- hsProfileContents = (0, files_1.loadHsProfileFile)(translationContext.projectSourceDir, translationOptions.profile);
26
- }
27
- const transformation = (0, transform_1.transform)(metafileContents, translationContext, hsProfileContents);
28
- const intermediateRepresentation = (0, transform_1.getIntermediateRepresentation)(transformation, skipValidation);
29
- // Remove once extensions and serverless functions are supported
30
- if (!skipValidation) {
31
- await (0, validation_1.validateIntermediateRepresentation)(intermediateRepresentation, transformation, translationContext);
32
- }
33
- return intermediateRepresentation;
34
- }
35
- async function translateForLocalDev(translationContext, translationOptions) {
36
- const IR = await translate(translationContext, {
37
- skipValidation: true,
38
- profile: translationOptions?.profile,
39
- });
40
- const localDevIr = {
41
- intermediateNodesIndexedByUid: {},
42
- };
43
- Object.entries(IR.intermediateNodesIndexedByUid).forEach(([uid, node]) => {
44
- const component = IR.intermediateNodesIndexedByUid[uid];
45
- const componentConfigPath = path_1.default.join(translationContext.projectSourceDir, component.metaFilePath);
46
- localDevIr.intermediateNodesIndexedByUid[uid] = {
47
- ...node,
48
- localDev: {
49
- componentRoot: path_1.default.dirname(componentConfigPath),
50
- componentConfigPath,
51
- },
52
- };
53
- });
54
- return localDevIr;
55
- }
56
- var errors_1 = require("./lib/errors");
57
- Object.defineProperty(exports, "isTranslationError", { enumerable: true, get: function () { return errors_1.isTranslationError; } });
58
- var profiles_1 = require("./lib/profiles");
59
- Object.defineProperty(exports, "getHsProfileFilename", { enumerable: true, get: function () { return profiles_1.getHsProfileFilename; } });
60
- var files_2 = require("./lib/files");
61
- Object.defineProperty(exports, "loadHsProfileFile", { enumerable: true, get: function () { return files_2.loadHsProfileFile; } });
62
- Object.defineProperty(exports, "getAllHsProfiles", { enumerable: true, get: function () { return files_2.getAllHsProfiles; } });
63
- var migrate_1 = require("./lib/migrate");
64
- Object.defineProperty(exports, "migrate", { enumerable: true, get: function () { return migrate_1.migrate; } });
65
- var uid_1 = require("./lib/uid");
66
- Object.defineProperty(exports, "validateUid", { enumerable: true, get: function () { return uid_1.validateUid; } });
67
- var transform_2 = require("./lib/transform");
68
- Object.defineProperty(exports, "mapToUserFriendlyName", { enumerable: true, get: function () { return transform_2.mapToUserFriendlyName; } });
69
- Object.defineProperty(exports, "mapToInternalType", { enumerable: true, get: function () { return transform_2.mapToInternalType; } });
70
- var schemas_1 = require("./lib/schemas");
71
- Object.defineProperty(exports, "getIntermediateRepresentationSchema", { enumerable: true, get: function () { return schemas_1.getIntermediateRepresentationSchema; } });
72
- var validation_2 = require("./lib/validation");
73
- Object.defineProperty(exports, "createAjvInstance", { enumerable: true, get: function () { return validation_2.createAjvInstance; } });
74
- var constants_1 = require("./lib/constants");
75
- Object.defineProperty(exports, "metafileExtension", { enumerable: true, get: function () { return constants_1.metafileExtension; } });
76
- Object.defineProperty(exports, "hsProjectJsonFilename", { enumerable: true, get: function () { return constants_1.hsProjectJsonFilename; } });
77
- var errors_2 = require("./lib/errors");
78
- Object.defineProperty(exports, "AjvErrorKeyword", { enumerable: true, get: function () { return errors_2.AjvErrorKeyword; } });
79
- var copy_2 = require("./lang/copy");
80
- Object.defineProperty(exports, "getInvalidJsonError", { enumerable: true, get: function () { return copy_2.getInvalidJsonError; } });
81
- Object.defineProperty(exports, "getMissingTypeError", { enumerable: true, get: function () { return copy_2.getMissingTypeError; } });
82
- Object.defineProperty(exports, "getMissingConfigError", { enumerable: true, get: function () { return copy_2.getMissingConfigError; } });
83
- Object.defineProperty(exports, "getMissingAccountIdError", { enumerable: true, get: function () { return copy_2.getMissingAccountIdError; } });
84
- Object.defineProperty(exports, "getMissingRequiredFieldError", { enumerable: true, get: function () { return copy_2.getMissingRequiredFieldError; } });
85
- Object.defineProperty(exports, "getFailedToFetchSchemasError", { enumerable: true, get: function () { return copy_2.getFailedToFetchSchemasError; } });
86
- Object.defineProperty(exports, "getUnsupportedTypeError", { enumerable: true, get: function () { return copy_2.getUnsupportedTypeError; } });