@hyperfrontend/versioning 0.2.0 → 0.3.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 (70) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +8 -6
  3. package/changelog/index.cjs.js +15 -4
  4. package/changelog/index.cjs.js.map +1 -1
  5. package/changelog/index.esm.js +15 -4
  6. package/changelog/index.esm.js.map +1 -1
  7. package/changelog/parse/index.cjs.js +62 -4
  8. package/changelog/parse/index.cjs.js.map +1 -1
  9. package/changelog/parse/index.esm.js +62 -4
  10. package/changelog/parse/index.esm.js.map +1 -1
  11. package/changelog/parse/parser.d.ts +0 -6
  12. package/changelog/parse/parser.d.ts.map +1 -1
  13. package/commits/classify/index.cjs.js +8 -6
  14. package/commits/classify/index.cjs.js.map +1 -1
  15. package/commits/classify/index.d.ts +1 -1
  16. package/commits/classify/index.d.ts.map +1 -1
  17. package/commits/classify/index.esm.js +8 -7
  18. package/commits/classify/index.esm.js.map +1 -1
  19. package/commits/classify/project-scopes.d.ts +10 -0
  20. package/commits/classify/project-scopes.d.ts.map +1 -1
  21. package/commits/index.cjs.js +8 -6
  22. package/commits/index.cjs.js.map +1 -1
  23. package/commits/index.esm.js +8 -7
  24. package/commits/index.esm.js.map +1 -1
  25. package/flow/executor/index.cjs.js +12 -0
  26. package/flow/executor/index.cjs.js.map +1 -1
  27. package/flow/executor/index.esm.js +12 -0
  28. package/flow/executor/index.esm.js.map +1 -1
  29. package/flow/index.cjs.js +89 -36
  30. package/flow/index.cjs.js.map +1 -1
  31. package/flow/index.esm.js +88 -37
  32. package/flow/index.esm.js.map +1 -1
  33. package/flow/models/index.cjs.js +13 -0
  34. package/flow/models/index.cjs.js.map +1 -1
  35. package/flow/models/index.d.ts +1 -1
  36. package/flow/models/index.d.ts.map +1 -1
  37. package/flow/models/index.esm.js +13 -1
  38. package/flow/models/index.esm.js.map +1 -1
  39. package/flow/models/types.d.ts +33 -1
  40. package/flow/models/types.d.ts.map +1 -1
  41. package/flow/presets/index.cjs.js +84 -36
  42. package/flow/presets/index.cjs.js.map +1 -1
  43. package/flow/presets/index.esm.js +84 -36
  44. package/flow/presets/index.esm.js.map +1 -1
  45. package/flow/steps/analyze-commits.d.ts.map +1 -1
  46. package/flow/steps/generate-changelog.d.ts +5 -0
  47. package/flow/steps/generate-changelog.d.ts.map +1 -1
  48. package/flow/steps/index.cjs.js +85 -36
  49. package/flow/steps/index.cjs.js.map +1 -1
  50. package/flow/steps/index.d.ts +1 -1
  51. package/flow/steps/index.d.ts.map +1 -1
  52. package/flow/steps/index.esm.js +85 -37
  53. package/flow/steps/index.esm.js.map +1 -1
  54. package/index.cjs.js +9223 -9172
  55. package/index.cjs.js.map +1 -1
  56. package/index.d.ts +3 -1
  57. package/index.d.ts.map +1 -1
  58. package/index.esm.js +9220 -9173
  59. package/index.esm.js.map +1 -1
  60. package/package.json +14 -1
  61. package/workspace/discovery/changelog-path.d.ts +3 -7
  62. package/workspace/discovery/changelog-path.d.ts.map +1 -1
  63. package/workspace/discovery/index.cjs.js +84 -5
  64. package/workspace/discovery/index.cjs.js.map +1 -1
  65. package/workspace/discovery/index.esm.js +84 -5
  66. package/workspace/discovery/index.esm.js.map +1 -1
  67. package/workspace/index.cjs.js +84 -5
  68. package/workspace/index.cjs.js.map +1 -1
  69. package/workspace/index.esm.js +84 -5
  70. package/workspace/index.esm.js.map +1 -1
@@ -2339,12 +2339,12 @@ function shouldPreserveScope(source) {
2339
2339
  * // Returns: ['app-demo', 'demo']
2340
2340
  */
2341
2341
  function deriveProjectScopes(options) {
2342
- const { projectName, packageName, additionalScopes = [] } = options;
2342
+ const { projectName, packageName, additionalScopes = [], prefixes = DEFAULT_PROJECT_PREFIXES } = options;
2343
2343
  const scopes = createSet();
2344
2344
  // Always include the full project name
2345
2345
  scopes.add(projectName);
2346
2346
  // Add variations based on common prefixes
2347
- const prefixVariations = extractPrefixVariations(projectName);
2347
+ const prefixVariations = extractPrefixVariations(projectName, prefixes);
2348
2348
  for (const variation of prefixVariations) {
2349
2349
  scopes.add(variation);
2350
2350
  }
@@ -2364,18 +2364,19 @@ function deriveProjectScopes(options) {
2364
2364
  return [...scopes];
2365
2365
  }
2366
2366
  /**
2367
- * Recognized project name prefixes that can be stripped for scope matching.
2367
+ * Default project name prefixes that can be stripped for scope matching.
2368
2368
  */
2369
- const PROJECT_PREFIXES = ['lib-', 'app-', 'e2e-', 'tool-', 'plugin-', 'feature-', 'package-'];
2369
+ const DEFAULT_PROJECT_PREFIXES = ['lib-', 'app-', 'e2e-', 'tool-', 'plugin-', 'feature-', 'package-'];
2370
2370
  /**
2371
2371
  * Generates scope variations by stripping recognized project prefixes.
2372
2372
  *
2373
2373
  * @param projectName - The project name to extract variations from
2374
+ * @param prefixes - Prefixes to check and strip
2374
2375
  * @returns Array of scope name variations
2375
2376
  */
2376
- function extractPrefixVariations(projectName) {
2377
+ function extractPrefixVariations(projectName, prefixes) {
2377
2378
  const variations = [];
2378
- for (const prefix of PROJECT_PREFIXES) {
2379
+ for (const prefix of prefixes) {
2379
2380
  if (projectName.startsWith(prefix)) {
2380
2381
  const withoutPrefix = projectName.slice(prefix.length);
2381
2382
  if (withoutPrefix) {
@@ -3219,6 +3220,10 @@ function splitLines(message) {
3219
3220
  return lines;
3220
3221
  }
3221
3222
 
3223
+ /**
3224
+ * Default changelog filename.
3225
+ */
3226
+ const DEFAULT_CHANGELOG_FILENAME = 'CHANGELOG.md';
3222
3227
  /**
3223
3228
  * Default scope filtering configuration.
3224
3229
  *
@@ -3230,6 +3235,7 @@ const DEFAULT_SCOPE_FILTERING_CONFIG = {
3230
3235
  includeScopes: [],
3231
3236
  excludeScopes: DEFAULT_EXCLUDE_SCOPES,
3232
3237
  trackDependencyChanges: false,
3238
+ projectPrefixes: DEFAULT_PROJECT_PREFIXES,
3233
3239
  infrastructure: undefined,
3234
3240
  infrastructureMatcher: undefined,
3235
3241
  };
@@ -3256,6 +3262,7 @@ const ANALYZE_COMMITS_STEP_ID = 'analyze-commits';
3256
3262
  function createAnalyzeCommitsStep() {
3257
3263
  return createStep(ANALYZE_COMMITS_STEP_ID, 'Analyze Commits', async (ctx) => {
3258
3264
  const { git, projectName, projectRoot, packageName, workspaceRoot, config, logger, state } = ctx;
3265
+ const maxFallback = config.maxCommitFallback ?? 500;
3259
3266
  // Use publishedCommit from registry (set by fetch-registry step)
3260
3267
  const { publishedCommit, isFirstRelease } = state;
3261
3268
  let rawCommits;
@@ -3272,13 +3279,13 @@ function createAnalyzeCommitsStep() {
3272
3279
  logger.warn(`Published commit ${publishedCommit.slice(0, 7)} not found in history. ` +
3273
3280
  `This may indicate a rebase or force push occurred after publishing v${state.publishedVersion}. ` +
3274
3281
  `Falling back to recent commit analysis.`);
3275
- rawCommits = git.getCommitLog({ maxCount: 100 });
3282
+ rawCommits = git.getCommitLog({ maxCount: maxFallback });
3276
3283
  // effectiveBaseCommit stays null - no compare URL will be generated
3277
3284
  }
3278
3285
  }
3279
3286
  else {
3280
3287
  // First release or no published version
3281
- rawCommits = git.getCommitLog({ maxCount: 100 });
3288
+ rawCommits = git.getCommitLog({ maxCount: maxFallback });
3282
3289
  logger.debug(`First release - analyzing up to ${rawCommits.length} commits`);
3283
3290
  }
3284
3291
  // Get scope filtering configuration
@@ -3320,7 +3327,7 @@ function createAnalyzeCommitsStep() {
3320
3327
  const relativePath = getRelativePath(workspaceRoot, projectRoot);
3321
3328
  const pathFilteredCommits = effectiveBaseCommit
3322
3329
  ? git.getCommitsSince(effectiveBaseCommit, { path: relativePath })
3323
- : git.getCommitLog({ maxCount: 100, path: relativePath });
3330
+ : git.getCommitLog({ maxCount: maxFallback, path: relativePath });
3324
3331
  fileCommitHashes = createSet(pathFilteredCommits.map((c) => c.hash));
3325
3332
  logger.debug(`Found ${fileCommitHashes.size} commits touching ${relativePath}`);
3326
3333
  }
@@ -3329,14 +3336,15 @@ function createAnalyzeCommitsStep() {
3329
3336
  projectName,
3330
3337
  packageName,
3331
3338
  additionalScopes: scopeFilteringConfig.includeScopes,
3339
+ prefixes: scopeFilteringConfig.projectPrefixes,
3332
3340
  });
3333
3341
  logger.debug(`Project scopes: ${projectScopes.join(', ')}`);
3334
3342
  // Build infrastructure commit hashes for file-based infrastructure detection
3335
- const infrastructureCommitHashes = buildInfrastructureCommitHashes(git, effectiveBaseCommit, rawCommits, parsedCommits, scopeFilteringConfig, logger);
3343
+ const infrastructureCommitHashes = buildInfrastructureCommitHashes(git, effectiveBaseCommit, rawCommits, parsedCommits, scopeFilteringConfig, logger, maxFallback);
3336
3344
  // Build dependency commit map if tracking is enabled (Phase 4)
3337
3345
  let dependencyCommitMap;
3338
3346
  if (scopeFilteringConfig.trackDependencyChanges) {
3339
- dependencyCommitMap = buildDependencyCommitMap(git, workspaceRoot, projectName, effectiveBaseCommit, logger);
3347
+ dependencyCommitMap = buildDependencyCommitMap(git, workspaceRoot, projectName, effectiveBaseCommit, logger, maxFallback);
3340
3348
  }
3341
3349
  // Create classification context
3342
3350
  const classificationContext = createClassificationContext(projectScopes, fileCommitHashes, {
@@ -3469,9 +3477,10 @@ function buildSummaryMessage(includedCount, totalCount, summary, strategy) {
3469
3477
  * @param config - Scope filtering configuration
3470
3478
  * @param logger - Logger with debug method for output
3471
3479
  * @param logger.debug - Debug logging function
3480
+ * @param maxFallback - Maximum commits to query when baseCommit is null
3472
3481
  * @returns Set of commit hashes classified as infrastructure
3473
3482
  */
3474
- function buildInfrastructureCommitHashes(git, baseCommit, rawCommits, parsedCommits, config, logger) {
3483
+ function buildInfrastructureCommitHashes(git, baseCommit, rawCommits, parsedCommits, config, logger, maxFallback) {
3475
3484
  // Collect all infrastructure commit hashes
3476
3485
  let infraHashes = createSet();
3477
3486
  // Method 1: Path-based detection (query git for commits touching infra paths)
@@ -3480,7 +3489,7 @@ function buildInfrastructureCommitHashes(git, baseCommit, rawCommits, parsedComm
3480
3489
  for (const infraPath of infraPaths) {
3481
3490
  const pathCommits = baseCommit
3482
3491
  ? git.getCommitsSince(baseCommit, { path: infraPath })
3483
- : git.getCommitLog({ maxCount: 100, path: infraPath });
3492
+ : git.getCommitLog({ maxCount: maxFallback, path: infraPath });
3484
3493
  for (const commit of pathCommits) {
3485
3494
  infraHashes = infraHashes.add(commit.hash);
3486
3495
  }
@@ -3548,9 +3557,10 @@ function combineMatcher(a, b) {
3548
3557
  * @param baseCommit - Base commit hash for commit range (null for first release/fallback)
3549
3558
  * @param logger - Logger with debug method for output
3550
3559
  * @param logger.debug - Debug logging function
3560
+ * @param maxFallback - Maximum commits to query when baseCommit is null
3551
3561
  * @returns Map of dependency names to commit hashes touching that dependency
3552
3562
  */
3553
- function buildDependencyCommitMap(git, workspaceRoot, projectName, baseCommit, logger) {
3563
+ function buildDependencyCommitMap(git, workspaceRoot, projectName, baseCommit, logger, maxFallback) {
3554
3564
  let dependencyMap = createMap();
3555
3565
  try {
3556
3566
  // Discover all projects in workspace using lib-project-scope
@@ -3575,7 +3585,7 @@ function buildDependencyCommitMap(git, workspaceRoot, projectName, baseCommit, l
3575
3585
  // Query git for commits touching this dependency's path
3576
3586
  const depCommits = baseCommit
3577
3587
  ? git.getCommitsSince(baseCommit, { path: depRoot })
3578
- : git.getCommitLog({ maxCount: 100, path: depRoot });
3588
+ : git.getCommitLog({ maxCount: maxFallback, path: depRoot });
3579
3589
  if (depCommits.length > 0) {
3580
3590
  const hashSet = createSet(depCommits.map((c) => c.hash));
3581
3591
  dependencyMap = dependencyMap.set(dep.target, hashSet);
@@ -5242,11 +5252,22 @@ function isWhitespace(char) {
5242
5252
  }
5243
5253
 
5244
5254
  /**
5245
- * Changelog Parser
5255
+ * Validates that a URL is actually a GitHub URL by parsing it properly.
5256
+ * This prevents SSRF attacks where 'github.com' could appear in path/query.
5246
5257
  *
5247
- * Parses a changelog markdown string into a structured Changelog object.
5248
- * Uses a state machine tokenizer for ReDoS-safe parsing.
5258
+ * @param url - The URL string to validate
5259
+ * @returns True if the URL host is github.com or a subdomain
5249
5260
  */
5261
+ function isGitHubUrl(url) {
5262
+ try {
5263
+ const parsed = createURL(url);
5264
+ // Check that the host is exactly github.com or ends with .github.com
5265
+ return parsed.host === 'github.com' || parsed.host.endsWith('.github.com');
5266
+ }
5267
+ catch {
5268
+ return false;
5269
+ }
5270
+ }
5250
5271
  /**
5251
5272
  * Parses a changelog markdown string into a Changelog object.
5252
5273
  *
@@ -5314,7 +5335,7 @@ function parseHeader(state) {
5314
5335
  description.push(`[${token.value}](${nextToken.value})`);
5315
5336
  links.push({ label: token.value, url: nextToken.value });
5316
5337
  // Try to detect repository URL
5317
- if (!state.repositoryUrl && nextToken.value.includes('github.com')) {
5338
+ if (!state.repositoryUrl && isGitHubUrl(nextToken.value)) {
5318
5339
  state.repositoryUrl = extractRepoUrl(nextToken.value);
5319
5340
  }
5320
5341
  advance(state); // skip link-text
@@ -6116,7 +6137,7 @@ const GENERATE_CHANGELOG_STEP_ID = 'generate-changelog';
6116
6137
  /**
6117
6138
  * Maps conventional commit types to changelog section types.
6118
6139
  */
6119
- const COMMIT_TYPE_TO_SECTION = {
6140
+ const DEFAULT_COMMIT_TYPE_TO_SECTION = {
6120
6141
  feat: 'features',
6121
6142
  fix: 'fixes',
6122
6143
  perf: 'performance',
@@ -6129,6 +6150,18 @@ const COMMIT_TYPE_TO_SECTION = {
6129
6150
  chore: 'chores',
6130
6151
  style: 'other',
6131
6152
  };
6153
+ /**
6154
+ * Resolves the commit type to section mapping by merging config with defaults.
6155
+ *
6156
+ * @param configMapping - User-provided partial mapping from FlowConfig
6157
+ * @returns Resolved mapping with user overrides applied
6158
+ */
6159
+ function resolveCommitTypeMapping(configMapping) {
6160
+ if (!configMapping) {
6161
+ return DEFAULT_COMMIT_TYPE_TO_SECTION;
6162
+ }
6163
+ return { ...DEFAULT_COMMIT_TYPE_TO_SECTION, ...configMapping };
6164
+ }
6132
6165
  /**
6133
6166
  * Checks if a commit source represents an indirect change.
6134
6167
  *
@@ -6142,16 +6175,22 @@ function isIndirectSource(source) {
6142
6175
  * Groups classified commits by their section type.
6143
6176
  *
6144
6177
  * @param commits - Array of classified commits
6178
+ * @param mapping - Commit type to section mapping
6145
6179
  * @returns Record of section type to classified commits
6146
6180
  */
6147
- function groupClassifiedCommitsBySection(commits) {
6181
+ function groupClassifiedCommitsBySection(commits, mapping) {
6148
6182
  const groups = {};
6149
6183
  for (const classified of commits) {
6150
- const sectionType = COMMIT_TYPE_TO_SECTION[classified.commit.type ?? 'chore'] ?? 'chores';
6151
- if (!groups[sectionType]) {
6152
- groups[sectionType] = [];
6184
+ const sectionType = mapping[classified.commit.type ?? 'chore'];
6185
+ // Skip if explicitly excluded (null)
6186
+ if (sectionType === null)
6187
+ continue;
6188
+ // Fallback to 'chores' for unmapped types
6189
+ const resolvedSection = sectionType ?? 'chores';
6190
+ if (!groups[resolvedSection]) {
6191
+ groups[resolvedSection] = [];
6153
6192
  }
6154
- groups[sectionType].push(classified);
6193
+ groups[resolvedSection].push(classified);
6155
6194
  }
6156
6195
  return groups;
6157
6196
  }
@@ -6159,16 +6198,22 @@ function groupClassifiedCommitsBySection(commits) {
6159
6198
  * Groups commits by their section type.
6160
6199
  *
6161
6200
  * @param commits - Array of conventional commits
6201
+ * @param mapping - Commit type to section mapping
6162
6202
  * @returns Record of section type to commits
6163
6203
  */
6164
- function groupCommitsBySection(commits) {
6204
+ function groupCommitsBySection(commits, mapping) {
6165
6205
  const groups = {};
6166
6206
  for (const commit of commits) {
6167
- const sectionType = COMMIT_TYPE_TO_SECTION[commit.type ?? 'chore'] ?? 'chores';
6168
- if (!groups[sectionType]) {
6169
- groups[sectionType] = [];
6207
+ const sectionType = mapping[commit.type ?? 'chore'];
6208
+ // Skip if explicitly excluded (null)
6209
+ if (sectionType === null)
6210
+ continue;
6211
+ // Fallback to 'chores' for unmapped types
6212
+ const resolvedSection = sectionType ?? 'chores';
6213
+ if (!groups[resolvedSection]) {
6214
+ groups[resolvedSection] = [];
6170
6215
  }
6171
- groups[sectionType].push(commit);
6216
+ groups[resolvedSection].push(commit);
6172
6217
  }
6173
6218
  return groups;
6174
6219
  }
@@ -6236,6 +6281,8 @@ function createGenerateChangelogStep() {
6236
6281
  return createStep(GENERATE_CHANGELOG_STEP_ID, 'Generate Changelog Entry', async (ctx) => {
6237
6282
  const { config, state } = ctx;
6238
6283
  const { commits, nextVersion, bumpType } = state;
6284
+ // Resolve commit type to section mapping
6285
+ const commitTypeMapping = resolveCommitTypeMapping(config.commitTypeToSection);
6239
6286
  // Skip if no bump needed
6240
6287
  if (!nextVersion || bumpType === 'none') {
6241
6288
  return createSkippedResult('No version bump, skipping changelog generation');
@@ -6297,7 +6344,7 @@ function createGenerateChangelogStep() {
6297
6344
  })));
6298
6345
  }
6299
6346
  // Group direct commits by section
6300
- const groupedDirect = groupClassifiedCommitsBySection(directCommits);
6347
+ const groupedDirect = groupClassifiedCommitsBySection(directCommits, commitTypeMapping);
6301
6348
  // Add other sections in conventional order (direct commits only)
6302
6349
  const sectionOrder = [
6303
6350
  { type: 'features', heading: 'Features' },
@@ -6325,7 +6372,7 @@ function createGenerateChangelogStep() {
6325
6372
  }
6326
6373
  else {
6327
6374
  // Fallback: use commits without classification (backward compatibility)
6328
- const grouped = groupCommitsBySection(commits);
6375
+ const grouped = groupCommitsBySection(commits, commitTypeMapping);
6329
6376
  // Add breaking changes section first if any
6330
6377
  const breakingCommits = commits.filter((c) => c.breaking);
6331
6378
  if (breakingCommits.length > 0) {
@@ -6401,14 +6448,15 @@ function createWriteChangelogStep() {
6401
6448
  if (!nextVersion || bumpType === 'none' || !changelogEntry || config.skipChangelog) {
6402
6449
  return createSkippedResult('No changelog to write');
6403
6450
  }
6404
- const changelogPath = `${projectRoot}/CHANGELOG.md`;
6451
+ const changelogFileName = config.changelogFileName ?? DEFAULT_CHANGELOG_FILENAME;
6452
+ const changelogPath = `${projectRoot}/${changelogFileName}`;
6405
6453
  let existingContent = '';
6406
6454
  // Read existing changelog
6407
6455
  try {
6408
6456
  existingContent = tree.read(changelogPath, 'utf-8') ?? '';
6409
6457
  }
6410
6458
  catch {
6411
- logger.debug('No existing CHANGELOG.md found');
6459
+ logger.debug(`No existing ${changelogFileName} found`);
6412
6460
  }
6413
6461
  // If no existing content, create new changelog
6414
6462
  if (!existingContent.trim()) {
@@ -6426,7 +6474,7 @@ function createWriteChangelogStep() {
6426
6474
  stateUpdates: {
6427
6475
  modifiedFiles: [...(state.modifiedFiles ?? []), changelogPath],
6428
6476
  },
6429
- message: `Created CHANGELOG.md with version ${nextVersion}`,
6477
+ message: `Created ${changelogFileName} with version ${nextVersion}`,
6430
6478
  };
6431
6479
  }
6432
6480
  // Parse existing and add entry
@@ -6460,7 +6508,7 @@ function createWriteChangelogStep() {
6460
6508
  stateUpdates: {
6461
6509
  modifiedFiles: [...(state.modifiedFiles ?? []), changelogPath],
6462
6510
  },
6463
- message: `Updated CHANGELOG.md with version ${nextVersion}`,
6511
+ message: `Updated ${changelogFileName} with version ${nextVersion}`,
6464
6512
  };
6465
6513
  }, {
6466
6514
  dependsOn: ['generate-changelog'],