@hominis/fireforge 0.49.2 → 0.51.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/CHANGELOG.md +87 -1
- package/README.md +1 -1
- package/dist/bin/fireforge.js +6 -0
- package/dist/build-info.json +4 -4
- package/dist/src/commands/build-prelock.d.ts +20 -0
- package/dist/src/commands/build-prelock.js +50 -0
- package/dist/src/commands/build.d.ts +3 -1
- package/dist/src/commands/build.js +13 -6
- package/dist/src/commands/check-report.d.ts +41 -0
- package/dist/src/commands/check-report.js +91 -0
- package/dist/src/commands/check-run.d.ts +33 -0
- package/dist/src/commands/check-run.js +112 -0
- package/dist/src/commands/check-steps.d.ts +75 -0
- package/dist/src/commands/check-steps.js +200 -0
- package/dist/src/commands/check.d.ts +32 -0
- package/dist/src/commands/check.js +160 -0
- package/dist/src/commands/config.js +5 -6
- package/dist/src/commands/declarations-report.d.ts +43 -0
- package/dist/src/commands/declarations-report.js +76 -0
- package/dist/src/commands/doctor-engine-eslint.js +6 -1
- package/dist/src/commands/doctor-source-pin.js +4 -1
- package/dist/src/commands/export-adopt-check.d.ts +38 -0
- package/dist/src/commands/export-adopt-check.js +98 -0
- package/dist/src/commands/export-flow.js +29 -14
- package/dist/src/commands/export-placement-gate.d.ts +6 -2
- package/dist/src/commands/export-placement-gate.js +8 -0
- package/dist/src/commands/export-shared.d.ts +2 -1
- package/dist/src/commands/export-staged-registration.d.ts +46 -0
- package/dist/src/commands/export-staged-registration.js +91 -0
- package/dist/src/commands/forward-import-remedy.d.ts +32 -0
- package/dist/src/commands/forward-import-remedy.js +63 -0
- package/dist/src/commands/furnace/apply.js +3 -3
- package/dist/src/commands/furnace/chrome-doc-remove.d.ts +2 -1
- package/dist/src/commands/furnace/chrome-doc-remove.js +2 -2
- package/dist/src/commands/furnace/chrome-doc.d.ts +2 -1
- package/dist/src/commands/furnace/chrome-doc.js +2 -2
- package/dist/src/commands/furnace/create.js +2 -2
- package/dist/src/commands/furnace/deploy-named.d.ts +48 -0
- package/dist/src/commands/furnace/deploy-named.js +107 -0
- package/dist/src/commands/furnace/deploy.d.ts +3 -2
- package/dist/src/commands/furnace/deploy.js +50 -86
- package/dist/src/commands/furnace/diff.js +26 -3
- package/dist/src/commands/furnace/index.js +66 -41
- package/dist/src/commands/furnace/init.js +13 -6
- package/dist/src/commands/furnace/override.js +4 -2
- package/dist/src/commands/furnace/preview.js +4 -3
- package/dist/src/commands/furnace/refresh.js +2 -2
- package/dist/src/commands/furnace/remove.js +4 -3
- package/dist/src/commands/furnace/rename-dry-run.d.ts +29 -0
- package/dist/src/commands/furnace/rename-dry-run.js +127 -0
- package/dist/src/commands/furnace/rename-helpers.d.ts +7 -0
- package/dist/src/commands/furnace/rename-helpers.js +15 -1
- package/dist/src/commands/furnace/rename-registrations.d.ts +41 -0
- package/dist/src/commands/furnace/rename-registrations.js +98 -0
- package/dist/src/commands/furnace/rename.d.ts +2 -1
- package/dist/src/commands/furnace/rename.js +28 -66
- package/dist/src/commands/furnace/scan.d.ts +9 -4
- package/dist/src/commands/furnace/scan.js +11 -9
- package/dist/src/commands/furnace/sync.js +3 -2
- package/dist/src/commands/furnace/validate-report.d.ts +45 -0
- package/dist/src/commands/furnace/validate-report.js +91 -0
- package/dist/src/commands/furnace/validate.d.ts +1 -1
- package/dist/src/commands/furnace/validate.js +151 -113
- package/dist/src/commands/furnace/validation-output.js +14 -3
- package/dist/src/commands/lint-per-patch.js +36 -61
- package/dist/src/commands/lint-per-run-checkjs.js +2 -2
- package/dist/src/commands/lint-report.d.ts +8 -4
- package/dist/src/commands/lint-report.js +20 -5
- package/dist/src/commands/lint.js +15 -34
- package/dist/src/commands/manifest.js +4 -0
- package/dist/src/commands/park.d.ts +22 -0
- package/dist/src/commands/park.js +232 -0
- package/dist/src/commands/patch/delete-gap-notice.js +3 -1
- package/dist/src/commands/patch/delete.d.ts +1 -1
- package/dist/src/commands/patch/delete.js +24 -104
- package/dist/src/commands/patch/index.d.ts +3 -3
- package/dist/src/commands/patch/index.js +10 -4
- package/dist/src/commands/patch/lint-ignore-from.d.ts +44 -0
- package/dist/src/commands/patch/lint-ignore-from.js +142 -0
- package/dist/src/commands/patch/lint-ignore.d.ts +5 -1
- package/dist/src/commands/patch/lint-ignore.js +78 -31
- package/dist/src/commands/patch/move-files-create.js +34 -18
- package/dist/src/commands/patch/move-files-into.js +15 -7
- package/dist/src/commands/patch/move-files.js +6 -2
- package/dist/src/commands/patch/removal-created-files.d.ts +48 -0
- package/dist/src/commands/patch/removal-created-files.js +83 -0
- package/dist/src/commands/patch/removal-dependents.d.ts +63 -0
- package/dist/src/commands/patch/removal-dependents.js +133 -0
- package/dist/src/commands/patch/rename.js +20 -1
- package/dist/src/commands/patch/reserve.d.ts +33 -0
- package/dist/src/commands/patch/reserve.js +152 -0
- package/dist/src/commands/patch/retire.d.ts +34 -0
- package/dist/src/commands/patch/retire.js +133 -0
- package/dist/src/commands/patch/split-plan.d.ts +17 -8
- package/dist/src/commands/patch/split-plan.js +60 -28
- package/dist/src/commands/patch/split-staged.d.ts +108 -0
- package/dist/src/commands/patch/split-staged.js +202 -0
- package/dist/src/commands/patch/split.d.ts +5 -0
- package/dist/src/commands/patch/split.js +62 -17
- package/dist/src/commands/patch/stage.d.ts +34 -0
- package/dist/src/commands/patch/stage.js +153 -0
- package/dist/src/commands/patch/staged-dependency-batch.d.ts +48 -0
- package/dist/src/commands/patch/staged-dependency-batch.js +176 -0
- package/dist/src/commands/patch/staged-dependency-entries.d.ts +36 -0
- package/dist/src/commands/patch/staged-dependency-entries.js +52 -0
- package/dist/src/commands/patch/staged-dependency-from.d.ts +14 -0
- package/dist/src/commands/patch/staged-dependency-from.js +108 -0
- package/dist/src/commands/patch/staged-dependency-remove.d.ts +31 -0
- package/dist/src/commands/patch/staged-dependency-remove.js +80 -0
- package/dist/src/commands/patch/staged-dependency.js +54 -139
- package/dist/src/commands/re-export-adjacent-registrations.d.ts +20 -0
- package/dist/src/commands/re-export-adjacent-registrations.js +75 -0
- package/dist/src/commands/re-export-adjacent.d.ts +50 -2
- package/dist/src/commands/re-export-adjacent.js +160 -40
- package/dist/src/commands/re-export-bulk-scan.d.ts +2 -1
- package/dist/src/commands/re-export-bulk-scan.js +22 -7
- package/dist/src/commands/re-export-drift.d.ts +36 -8
- package/dist/src/commands/re-export-drift.js +73 -49
- package/dist/src/commands/re-export-files.js +11 -8
- package/dist/src/commands/re-export-hunks.d.ts +35 -0
- package/dist/src/commands/re-export-hunks.js +220 -0
- package/dist/src/commands/re-export-lint.d.ts +2 -1
- package/dist/src/commands/re-export-lint.js +23 -10
- package/dist/src/commands/re-export-options.js +74 -6
- package/dist/src/commands/re-export-preflight.d.ts +23 -0
- package/dist/src/commands/re-export-preflight.js +160 -0
- package/dist/src/commands/re-export-purity-scope.d.ts +18 -0
- package/dist/src/commands/re-export-purity-scope.js +131 -0
- package/dist/src/commands/re-export-register.js +56 -17
- package/dist/src/commands/re-export-removed.d.ts +13 -2
- package/dist/src/commands/re-export-removed.js +57 -12
- package/dist/src/commands/re-export-scan-owner.d.ts +44 -0
- package/dist/src/commands/re-export-scan-owner.js +116 -0
- package/dist/src/commands/re-export-scan.d.ts +19 -9
- package/dist/src/commands/re-export-scan.js +60 -24
- package/dist/src/commands/re-export-single.js +122 -46
- package/dist/src/commands/re-export-stale-registrations.d.ts +18 -0
- package/dist/src/commands/re-export-stale-registrations.js +62 -0
- package/dist/src/commands/re-export-summary.d.ts +28 -0
- package/dist/src/commands/re-export-summary.js +36 -0
- package/dist/src/commands/re-export.js +71 -57
- package/dist/src/commands/rebase/index.js +17 -17
- package/dist/src/commands/run.js +16 -2
- package/dist/src/commands/status-check.d.ts +55 -16
- package/dist/src/commands/status-check.js +92 -37
- package/dist/src/commands/status-json.d.ts +22 -9
- package/dist/src/commands/status-json.js +47 -32
- package/dist/src/commands/status-output.js +3 -1
- package/dist/src/commands/status-scope.d.ts +37 -0
- package/dist/src/commands/status-scope.js +54 -0
- package/dist/src/commands/status.js +66 -23
- package/dist/src/commands/test-browser-preflight.js +19 -3
- package/dist/src/commands/test-build-furnace.d.ts +22 -0
- package/dist/src/commands/test-build-furnace.js +54 -0
- package/dist/src/commands/test-build-phase.js +64 -41
- package/dist/src/commands/test-checkjs.js +10 -2
- package/dist/src/commands/test-diagnose.js +7 -0
- package/dist/src/commands/test-dispatch.d.ts +39 -0
- package/dist/src/commands/test-dispatch.js +211 -0
- package/dist/src/commands/test-furnace-reach.d.ts +37 -0
- package/dist/src/commands/test-furnace-reach.js +132 -0
- package/dist/src/commands/test-modes.d.ts +16 -1
- package/dist/src/commands/test-modes.js +24 -1
- package/dist/src/commands/test-multi-verdict.d.ts +61 -0
- package/dist/src/commands/test-multi-verdict.js +111 -0
- package/dist/src/commands/test-orchestrate.d.ts +52 -0
- package/dist/src/commands/test-orchestrate.js +240 -0
- package/dist/src/commands/test-perf-scope.d.ts +50 -0
- package/dist/src/commands/test-perf-scope.js +138 -0
- package/dist/src/commands/test-prelock.d.ts +51 -0
- package/dist/src/commands/test-prelock.js +273 -0
- package/dist/src/commands/test-register.js +69 -10
- package/dist/src/commands/test-results.d.ts +62 -0
- package/dist/src/commands/test-results.js +62 -0
- package/dist/src/commands/test-run.d.ts +13 -55
- package/dist/src/commands/test-run.js +74 -99
- package/dist/src/commands/test-shards.d.ts +110 -0
- package/dist/src/commands/test-shards.js +255 -0
- package/dist/src/commands/test-stale-gate.d.ts +5 -1
- package/dist/src/commands/test-stale-gate.js +60 -28
- package/dist/src/commands/test-verdict.d.ts +11 -1
- package/dist/src/commands/test-verdict.js +28 -1
- package/dist/src/commands/test.d.ts +6 -1
- package/dist/src/commands/test.js +120 -124
- package/dist/src/commands/token-lint.d.ts +14 -0
- package/dist/src/commands/token-lint.js +151 -0
- package/dist/src/commands/token.js +10 -0
- package/dist/src/commands/verify-scope.d.ts +28 -0
- package/dist/src/commands/verify-scope.js +67 -0
- package/dist/src/commands/verify.d.ts +10 -2
- package/dist/src/commands/verify.js +67 -36
- package/dist/src/core/build-baseline-types.d.ts +46 -0
- package/dist/src/core/build-baseline-validate.js +19 -0
- package/dist/src/core/build-baseline.d.ts +5 -1
- package/dist/src/core/build-baseline.js +3 -1
- package/dist/src/core/build-overwrite-guard.d.ts +50 -0
- package/dist/src/core/build-overwrite-guard.js +84 -4
- package/dist/src/core/build-prepare.d.ts +13 -0
- package/dist/src/core/build-prepare.js +42 -10
- package/dist/src/core/build-run-record.d.ts +74 -0
- package/dist/src/core/build-run-record.js +136 -0
- package/dist/src/core/checkjs-support-heads.d.ts +41 -0
- package/dist/src/core/checkjs-support-heads.js +104 -0
- package/dist/src/core/config-paths.d.ts +2 -2
- package/dist/src/core/config-paths.js +10 -0
- package/dist/src/core/config-validate-check.d.ts +8 -0
- package/dist/src/core/config-validate-check.js +219 -0
- package/dist/src/core/config-validate-file-size.d.ts +9 -6
- package/dist/src/core/config-validate-file-size.js +91 -2
- package/dist/src/core/config-validate-history.d.ts +11 -0
- package/dist/src/core/config-validate-history.js +38 -0
- package/dist/src/core/config-validate-patch-policy.js +100 -0
- package/dist/src/core/config-validate-test-toolchains.js +21 -0
- package/dist/src/core/config-validate-token-scales.d.ts +9 -0
- package/dist/src/core/config-validate-token-scales.js +92 -0
- package/dist/src/core/config-validate.js +20 -5
- package/dist/src/core/config.d.ts +5 -0
- package/dist/src/core/config.js +16 -0
- package/dist/src/core/coverage-dispatch.d.ts +24 -0
- package/dist/src/core/coverage-dispatch.js +44 -0
- package/dist/src/core/coverage-extend.d.ts +70 -45
- package/dist/src/core/coverage-extend.js +199 -144
- package/dist/src/core/coverage-records.d.ts +108 -0
- package/dist/src/core/coverage-records.js +384 -0
- package/dist/src/core/destructive.d.ts +7 -4
- package/dist/src/core/destructive.js +10 -10
- package/dist/src/core/engine-session-lock.d.ts +16 -1
- package/dist/src/core/engine-session-lock.js +38 -10
- package/dist/src/core/export-moved-code-shape.d.ts +49 -11
- package/dist/src/core/export-moved-code-shape.js +135 -44
- package/dist/src/core/file-lock-queue.d.ts +67 -0
- package/dist/src/core/file-lock-queue.js +221 -0
- package/dist/src/core/file-lock-shared.d.ts +57 -0
- package/dist/src/core/file-lock-shared.js +117 -0
- package/dist/src/core/file-lock.d.ts +13 -25
- package/dist/src/core/file-lock.js +163 -140
- package/dist/src/core/furnace-apply-output.d.ts +10 -0
- package/dist/src/core/furnace-apply-output.js +31 -1
- package/dist/src/core/furnace-apply.js +22 -12
- package/dist/src/core/furnace-composed-source.d.ts +76 -0
- package/dist/src/core/furnace-composed-source.js +167 -0
- package/dist/src/core/furnace-config-validate-exceptions.d.ts +16 -0
- package/dist/src/core/furnace-config-validate-exceptions.js +102 -0
- package/dist/src/core/furnace-config-validate.js +14 -0
- package/dist/src/core/furnace-css-fragments.d.ts +10 -0
- package/dist/src/core/furnace-css-fragments.js +26 -0
- package/dist/src/core/furnace-operation.d.ts +3 -3
- package/dist/src/core/furnace-registration-jar-inplace.d.ts +12 -0
- package/dist/src/core/furnace-registration-jar-inplace.js +80 -0
- package/dist/src/core/furnace-registration-remove.d.ts +22 -1
- package/dist/src/core/furnace-registration-remove.js +53 -17
- package/dist/src/core/furnace-registration-rename.d.ts +26 -0
- package/dist/src/core/furnace-registration-rename.js +77 -0
- package/dist/src/core/furnace-registration.d.ts +14 -4
- package/dist/src/core/furnace-registration.js +44 -10
- package/dist/src/core/furnace-staleness.d.ts +7 -0
- package/dist/src/core/furnace-staleness.js +34 -24
- package/dist/src/core/furnace-validate-accessibility.d.ts +8 -4
- package/dist/src/core/furnace-validate-accessibility.js +71 -28
- package/dist/src/core/furnace-validate-exceptions.d.ts +48 -0
- package/dist/src/core/furnace-validate-exceptions.js +80 -0
- package/dist/src/core/furnace-validate-icon-button.d.ts +7 -0
- package/dist/src/core/furnace-validate-icon-button.js +78 -0
- package/dist/src/core/furnace-validate.js +1 -1
- package/dist/src/core/harness-port-fallback.d.ts +60 -0
- package/dist/src/core/harness-port-fallback.js +96 -0
- package/dist/src/core/history-log-file.d.ts +61 -0
- package/dist/src/core/history-log-file.js +164 -0
- package/dist/src/core/history-log.d.ts +2 -1
- package/dist/src/core/history-log.js +2 -1
- package/dist/src/core/json-preserving-edit.d.ts +22 -0
- package/dist/src/core/json-preserving-edit.js +199 -0
- package/dist/src/core/line-prefixer.d.ts +18 -0
- package/dist/src/core/line-prefixer.js +34 -0
- package/dist/src/core/mach.d.ts +7 -0
- package/dist/src/core/mach.js +32 -10
- package/dist/src/core/module-resolution-preflight.d.ts +19 -3
- package/dist/src/core/module-resolution-preflight.js +24 -6
- package/dist/src/core/negative-control-compiled.d.ts +27 -0
- package/dist/src/core/negative-control-compiled.js +73 -0
- package/dist/src/core/negative-control-diff.d.ts +51 -0
- package/dist/src/core/negative-control-diff.js +179 -0
- package/dist/src/core/negative-control.d.ts +150 -0
- package/dist/src/core/negative-control.js +399 -0
- package/dist/src/core/objdir-install-manifests.d.ts +73 -0
- package/dist/src/core/objdir-install-manifests.js +226 -0
- package/dist/src/core/park-store.d.ts +63 -0
- package/dist/src/core/park-store.js +216 -0
- package/dist/src/core/patch-export-update.d.ts +6 -0
- package/dist/src/core/patch-export-update.js +13 -3
- package/dist/src/core/patch-export.d.ts +3 -2
- package/dist/src/core/patch-export.js +22 -16
- package/dist/src/core/patch-files.js +26 -8
- package/dist/src/core/patch-lint-checkjs-any-import.d.ts +39 -0
- package/dist/src/core/patch-lint-checkjs-any-import.js +63 -0
- package/dist/src/core/patch-lint-checkjs-diagnostics.d.ts +17 -0
- package/dist/src/core/patch-lint-checkjs-diagnostics.js +32 -0
- package/dist/src/core/patch-lint-checkjs.d.ts +10 -1
- package/dist/src/core/patch-lint-checkjs.js +20 -13
- package/dist/src/core/patch-lint-code-lines.d.ts +59 -0
- package/dist/src/core/patch-lint-code-lines.js +105 -0
- package/dist/src/core/patch-lint-cross.d.ts +3 -1
- package/dist/src/core/patch-lint-cross.js +17 -4
- package/dist/src/core/patch-lint-css.d.ts +8 -2
- package/dist/src/core/patch-lint-css.js +24 -14
- package/dist/src/core/patch-lint-file-size.d.ts +12 -5
- package/dist/src/core/patch-lint-file-size.js +22 -11
- package/dist/src/core/patch-lint-forward-registration-extended.d.ts +23 -0
- package/dist/src/core/patch-lint-forward-registration-extended.js +36 -11
- package/dist/src/core/patch-lint-forward-registration.js +10 -0
- package/dist/src/core/patch-lint-ignore.d.ts +196 -0
- package/dist/src/core/patch-lint-ignore.js +429 -0
- package/dist/src/core/patch-lint-ownership.d.ts +10 -0
- package/dist/src/core/patch-lint-ownership.js +26 -0
- package/dist/src/core/patch-lint-size-thresholds.d.ts +52 -0
- package/dist/src/core/patch-lint-size-thresholds.js +110 -0
- package/dist/src/core/patch-lint-size.d.ts +117 -0
- package/dist/src/core/patch-lint-size.js +331 -0
- package/dist/src/core/patch-lint-staged-registration.d.ts +18 -1
- package/dist/src/core/patch-lint-staged-registration.js +20 -0
- package/dist/src/core/patch-lint-token-scale.d.ts +57 -0
- package/dist/src/core/patch-lint-token-scale.js +286 -0
- package/dist/src/core/patch-lint.d.ts +11 -83
- package/dist/src/core/patch-lint.js +66 -288
- package/dist/src/core/patch-lock.js +7 -6
- package/dist/src/core/patch-manifest-consistency.js +7 -6
- package/dist/src/core/patch-manifest-io.d.ts +6 -3
- package/dist/src/core/patch-manifest-io.js +27 -9
- package/dist/src/core/patch-manifest-key-order.d.ts +24 -0
- package/dist/src/core/patch-manifest-key-order.js +61 -0
- package/dist/src/core/patch-manifest-stage.d.ts +46 -0
- package/dist/src/core/patch-manifest-stage.js +96 -0
- package/dist/src/core/patch-manifest-validate.js +61 -3
- package/dist/src/core/patch-order-legal.d.ts +26 -0
- package/dist/src/core/patch-order-legal.js +88 -0
- package/dist/src/core/patch-policy-content.d.ts +40 -0
- package/dist/src/core/patch-policy-content.js +216 -0
- package/dist/src/core/patch-policy.d.ts +22 -5
- package/dist/src/core/patch-policy.js +26 -12
- package/dist/src/core/patch-retired.d.ts +57 -0
- package/dist/src/core/patch-retired.js +92 -0
- package/dist/src/core/patch-subset.d.ts +16 -0
- package/dist/src/core/patch-subset.js +66 -0
- package/dist/src/core/preflight-wait.d.ts +20 -0
- package/dist/src/core/preflight-wait.js +38 -0
- package/dist/src/core/rebase-from-version.d.ts +46 -0
- package/dist/src/core/rebase-from-version.js +71 -0
- package/dist/src/core/run-log.d.ts +26 -0
- package/dist/src/core/run-log.js +106 -4
- package/dist/src/core/run-profile.d.ts +7 -0
- package/dist/src/core/run-profile.js +7 -1
- package/dist/src/core/smoke-patterns.d.ts +5 -0
- package/dist/src/core/smoke-patterns.js +13 -0
- package/dist/src/core/test-harness-crash.d.ts +19 -1
- package/dist/src/core/test-harness-crash.js +8 -9
- package/dist/src/core/test-harness-signature.d.ts +33 -0
- package/dist/src/core/test-harness-signature.js +23 -1
- package/dist/src/core/test-raw-log.d.ts +66 -0
- package/dist/src/core/test-raw-log.js +122 -0
- package/dist/src/core/test-run-completeness.d.ts +90 -0
- package/dist/src/core/test-run-completeness.js +184 -0
- package/dist/src/core/test-stale-check.d.ts +27 -1
- package/dist/src/core/test-stale-check.js +39 -49
- package/dist/src/core/test-stale-exempt.d.ts +40 -0
- package/dist/src/core/test-stale-exempt.js +70 -0
- package/dist/src/core/test-xpcshell-retry.d.ts +2 -0
- package/dist/src/core/test-xpcshell-retry.js +2 -1
- package/dist/src/core/token-lint.d.ts +53 -0
- package/dist/src/core/token-lint.js +131 -0
- package/dist/src/core/token-sibling-sheets.d.ts +46 -0
- package/dist/src/core/token-sibling-sheets.js +119 -0
- package/dist/src/core/tree-guard.js +5 -0
- package/dist/src/core/typecheck-shim.d.ts +6 -2
- package/dist/src/core/typecheck-shim.js +205 -4
- package/dist/src/core/typecheck.js +3 -1
- package/dist/src/index.d.ts +14 -3
- package/dist/src/index.js +6 -1
- package/dist/src/types/commands/index.d.ts +2 -2
- package/dist/src/types/commands/options.d.ts +212 -24
- package/dist/src/types/commands/patches.d.ts +118 -1
- package/dist/src/types/config.d.ts +264 -0
- package/dist/src/types/config.js +9 -0
- package/dist/src/types/furnace.d.ts +63 -1
- package/dist/src/types/furnace.js +7 -0
- package/dist/src/types/index.d.ts +1 -1
- package/dist/src/utils/options.d.ts +36 -7
- package/dist/src/utils/options.js +94 -8
- package/dist/src/utils/path-glob.d.ts +15 -0
- package/dist/src/utils/path-glob.js +47 -0
- package/dist/src/utils/regex.d.ts +15 -3
- package/dist/src/utils/regex.js +37 -16
- package/docs/configuration.md +369 -11
- package/docs/exit-codes.md +18 -1
- package/docs/furnace.md +139 -3
- package/docs/lifecycle-invariants.md +65 -30
- package/docs/machine-output.md +120 -12
- package/docs/patch-workflow.md +458 -22
- package/docs/run-logs.md +20 -1
- package/docs/testing.md +346 -21
- package/package.json +3 -3
|
@@ -17,7 +17,8 @@ import { topologicalSortCustom } from './furnace-graph-utils.js';
|
|
|
17
17
|
import { resolveFurnaceMarkerComment } from './furnace-marker.js';
|
|
18
18
|
import { recordFurnaceRollbackFailure } from './furnace-operation.js';
|
|
19
19
|
import { assertFurnaceEngineDirReady } from './furnace-precondition.js';
|
|
20
|
-
import { addJarMnEntries, removeCustomElementRegistration
|
|
20
|
+
import { addJarMnEntries, removeCustomElementRegistration } from './furnace-registration.js';
|
|
21
|
+
import { removeJarMnFileEntries } from './furnace-registration-jar-inplace.js';
|
|
21
22
|
import { createRollbackJournal, restoreRollbackJournalOrThrow, snapshotFile, } from './furnace-rollback.js';
|
|
22
23
|
import { blockingStepErrors, hasBlockingStepErrors } from './furnace-step-errors.js';
|
|
23
24
|
import { runPostApplyConsistencyChecks } from './furnace-validate-registration.js';
|
|
@@ -39,6 +40,16 @@ function addMissingComponentError(result, name, directoryPath) {
|
|
|
39
40
|
error: `Component directory not found: ${directoryPath}`,
|
|
40
41
|
});
|
|
41
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* The skip both batch loops record when a component's source is unchanged
|
|
45
|
+
* since the last recorded apply and its engine copy still matches. `reason`
|
|
46
|
+
* keeps its long-standing text for API consumers; `kind` lets summaries
|
|
47
|
+
* name it.
|
|
48
|
+
*/
|
|
49
|
+
const ALREADY_CURRENT_SKIP = {
|
|
50
|
+
reason: 'No changes since last apply',
|
|
51
|
+
kind: 'already-current',
|
|
52
|
+
};
|
|
42
53
|
async function applyOverrideBatch(ctx) {
|
|
43
54
|
const { config, furnacePaths, state, engineDir, ftlDir, dryRun } = ctx;
|
|
44
55
|
const { result, allActions, newChecksums, rollbackJournal, componentName } = ctx;
|
|
@@ -66,7 +77,7 @@ async function applyOverrideBatch(ctx) {
|
|
|
66
77
|
const cachedEngine = extractComponentChecksums(state.engineChecksums, 'override', name);
|
|
67
78
|
const drifted = await hasOverrideEngineDrift({ engineDir, componentDir, ftlDir }, overrideConfig, cachedEngine);
|
|
68
79
|
if (!drifted) {
|
|
69
|
-
result.skipped.push({ name,
|
|
80
|
+
result.skipped.push({ name, ...ALREADY_CURRENT_SKIP });
|
|
70
81
|
recordComponentChecksums(newChecksums, previous, 'override', name);
|
|
71
82
|
continue;
|
|
72
83
|
}
|
|
@@ -122,8 +133,8 @@ async function applyOverrideBatch(ctx) {
|
|
|
122
133
|
/**
|
|
123
134
|
* After undeploying deleted files, the in-engine jar.mn and
|
|
124
135
|
* customElements.js still carry entries for the removed files. Re-sync them
|
|
125
|
-
* by removing
|
|
126
|
-
*
|
|
136
|
+
* by removing only the deleted files' jar.mn lines in place and adding any
|
|
137
|
+
* live file that has none. If the .mjs itself was deleted, also drop
|
|
127
138
|
* the customElements.js registration. Snapshots are taken under the same
|
|
128
139
|
* journal so the undo path is symmetric with apply.
|
|
129
140
|
*/
|
|
@@ -134,18 +145,17 @@ async function reconcileCustomRegistrationAfterUndeploy(args) {
|
|
|
134
145
|
const deletedRegistrationFiles = deletedFiles.filter((f) => f.endsWith('.mjs') || f.endsWith('.css'));
|
|
135
146
|
if (deletedRegistrationFiles.length === 0)
|
|
136
147
|
return;
|
|
137
|
-
// jar.mn re-sync
|
|
138
|
-
//
|
|
139
|
-
//
|
|
148
|
+
// jar.mn re-sync: drop only the deleted files' lines, so the rest of the
|
|
149
|
+
// widget's block keeps its order and the owning patch carries no reorder
|
|
150
|
+
// hunks. addJarMnEntries is idempotent, so the add pass inserts only a
|
|
151
|
+
// live file that has no line yet. The journal snapshot before the
|
|
152
|
+
// mutation gives us a clean rollback target.
|
|
140
153
|
if (rollbackJournal) {
|
|
141
154
|
await snapshotFile(rollbackJournal, join(engineDir, JAR_MN));
|
|
142
155
|
}
|
|
143
|
-
await
|
|
156
|
+
await removeJarMnFileEntries(engineDir, name, deletedRegistrationFiles);
|
|
144
157
|
const liveJarFiles = Object.keys(currentChecksums).filter((f) => f.endsWith('.mjs') || f.endsWith('.css'));
|
|
145
158
|
if (liveJarFiles.length > 0) {
|
|
146
|
-
// applyCustomComponent has already added live entries, and the remove
|
|
147
|
-
// above dropped them too, so re-add now to leave jar.mn in the correct
|
|
148
|
-
// state.
|
|
149
159
|
await addJarMnEntries(engineDir, name, liveJarFiles);
|
|
150
160
|
}
|
|
151
161
|
filesAffected.push(JAR_MN);
|
|
@@ -226,7 +236,7 @@ async function applyCustomBatch(ctx, root, markerComment) {
|
|
|
226
236
|
// re-apply even though the workspace is unchanged.
|
|
227
237
|
const drifted = await hasCustomEngineDrift({ root, name, componentDir, ftlDir }, customConfig);
|
|
228
238
|
if (!drifted) {
|
|
229
|
-
result.skipped.push({ name,
|
|
239
|
+
result.skipped.push({ name, ...ALREADY_CURRENT_SKIP });
|
|
230
240
|
recordComponentChecksums(newChecksums, previous, 'custom', name);
|
|
231
241
|
continue;
|
|
232
242
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Furnace-composed form of a component source, and the questions that
|
|
3
|
+
* need it.
|
|
4
|
+
*
|
|
5
|
+
* `furnace apply` does not copy every source verbatim: a stylesheet with
|
|
6
|
+
* `@fireforge-include` directives is deployed with each fragment body from
|
|
7
|
+
* `components/shared/` inlined between the directive and its
|
|
8
|
+
* `@fireforge-end-include` marker. Anything that compares a deployed engine
|
|
9
|
+
* file with its source has to compare against that composed form, or a
|
|
10
|
+
* freshly deployed widget reads as a large difference it is not:
|
|
11
|
+
*
|
|
12
|
+
* - the unexported-drift belt (`build`/`test --refuse-unexported-drift`),
|
|
13
|
+
* which must not refuse to overwrite a deployed file with the very bytes
|
|
14
|
+
* it already holds;
|
|
15
|
+
* - `furnace diff`, which must show what the next apply would change.
|
|
16
|
+
*/
|
|
17
|
+
import type { FurnaceConfig } from '../types/furnace.js';
|
|
18
|
+
/**
|
|
19
|
+
* Composes one component source into the bytes apply deploys: a stylesheet
|
|
20
|
+
* carrying include directives is fragment-expanded, anything else is
|
|
21
|
+
* returned as is.
|
|
22
|
+
*
|
|
23
|
+
* @param content - Workspace source content
|
|
24
|
+
* @param fileName - Source file name (only `.css` is ever expanded)
|
|
25
|
+
* @param sharedDir - Absolute path of `components/shared/`
|
|
26
|
+
* @returns The composed content
|
|
27
|
+
* @throws FurnaceError when an included fragment is missing or nested
|
|
28
|
+
*/
|
|
29
|
+
export declare function composeComponentSource(content: string, fileName: string, sharedDir: string): Promise<string>;
|
|
30
|
+
/** The workspace source apply deploys to one engine path. */
|
|
31
|
+
export interface FurnaceDeployedSource {
|
|
32
|
+
type: 'custom' | 'override';
|
|
33
|
+
component: string;
|
|
34
|
+
/** Source file name inside the component directory. */
|
|
35
|
+
fileName: string;
|
|
36
|
+
/** Absolute path of the workspace source. */
|
|
37
|
+
sourcePath: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Maps every engine-relative path `furnace apply` writes to the workspace
|
|
41
|
+
* source it writes there: custom `.mjs`/`.css` into `targetPath`, a custom
|
|
42
|
+
* component's own `.ftl` into the Fluent tree (never for `sharedFtl`), and
|
|
43
|
+
* each override copy candidate onto its engine target.
|
|
44
|
+
*
|
|
45
|
+
* @param root - Project root
|
|
46
|
+
* @param config - Loaded furnace.json
|
|
47
|
+
* @returns Engine-relative path (POSIX) → deployed source
|
|
48
|
+
*/
|
|
49
|
+
export declare function mapDeployedComponentSources(root: string, config: FurnaceConfig): Promise<Map<string, FurnaceDeployedSource>>;
|
|
50
|
+
/**
|
|
51
|
+
* Why a deployed engine file is safe to overwrite from its component
|
|
52
|
+
* source even though no patch body or baseline explains it:
|
|
53
|
+
*
|
|
54
|
+
* - `composed-source`: it already equals the Furnace-composed current
|
|
55
|
+
* source, so the overwrite writes the same bytes;
|
|
56
|
+
* - `fragment-refresh`: it differs from the composed source only inside
|
|
57
|
+
* Furnace-generated fragment expansions (a fragment changed since it was
|
|
58
|
+
* deployed), so the overwrite only refreshes generated text;
|
|
59
|
+
* - `previous-deploy`: it is exactly the content Furnace's last recorded
|
|
60
|
+
* apply deployed (the source has moved on since), so the overwrite loses
|
|
61
|
+
* nothing that was not already a component source.
|
|
62
|
+
*/
|
|
63
|
+
export type DeployedSourceMatch = 'composed-source' | 'fragment-refresh' | 'previous-deploy';
|
|
64
|
+
/**
|
|
65
|
+
* For each engine-relative path, decides whether its current engine content
|
|
66
|
+
* is a Furnace deployment of its component source (see
|
|
67
|
+
* {@link DeployedSourceMatch}). Paths with no component source, an
|
|
68
|
+
* unreadable engine copy, or content explained by none of the matches are
|
|
69
|
+
* absent from the result.
|
|
70
|
+
*
|
|
71
|
+
* @param root - Project root
|
|
72
|
+
* @param engineDir - Absolute engine directory
|
|
73
|
+
* @param files - Engine-relative paths to classify
|
|
74
|
+
* @returns Engine-relative path → match, for the matching paths only
|
|
75
|
+
*/
|
|
76
|
+
export declare function matchDeployedComponentFiles(root: string, engineDir: string, files: readonly string[]): Promise<Map<string, DeployedSourceMatch>>;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* The Furnace-composed form of a component source, and the questions that
|
|
4
|
+
* need it.
|
|
5
|
+
*
|
|
6
|
+
* `furnace apply` does not copy every source verbatim: a stylesheet with
|
|
7
|
+
* `@fireforge-include` directives is deployed with each fragment body from
|
|
8
|
+
* `components/shared/` inlined between the directive and its
|
|
9
|
+
* `@fireforge-end-include` marker. Anything that compares a deployed engine
|
|
10
|
+
* file with its source has to compare against that composed form, or a
|
|
11
|
+
* freshly deployed widget reads as a large difference it is not:
|
|
12
|
+
*
|
|
13
|
+
* - the unexported-drift belt (`build`/`test --refuse-unexported-drift`),
|
|
14
|
+
* which must not refuse to overwrite a deployed file with the very bytes
|
|
15
|
+
* it already holds;
|
|
16
|
+
* - `furnace diff`, which must show what the next apply would change.
|
|
17
|
+
*/
|
|
18
|
+
import { readdir } from 'node:fs/promises';
|
|
19
|
+
import { join } from 'node:path';
|
|
20
|
+
import { pathExists, readText } from '../utils/fs.js';
|
|
21
|
+
import { sha256Hex } from '../utils/hash.js';
|
|
22
|
+
import { normalizePathSlashes } from '../utils/paths.js';
|
|
23
|
+
import { isOverrideCopyCandidate } from './furnace-apply-helpers.js';
|
|
24
|
+
import { extractComponentChecksums, normalizeForChecksum } from './furnace-checksum-utils.js';
|
|
25
|
+
import { furnaceConfigExists, getFurnacePaths, loadFurnaceConfig, loadFurnaceState, } from './furnace-config.js';
|
|
26
|
+
import { resolveFtlDir } from './furnace-constants.js';
|
|
27
|
+
import { expandCssFragments, listFragmentIncludes, stripExpandedFragments, } from './furnace-css-fragments.js';
|
|
28
|
+
import { isRegularFile } from './furnace-dir-entry.js';
|
|
29
|
+
/**
|
|
30
|
+
* Composes one component source into the bytes apply deploys: a stylesheet
|
|
31
|
+
* carrying include directives is fragment-expanded, anything else is
|
|
32
|
+
* returned as is.
|
|
33
|
+
*
|
|
34
|
+
* @param content - Workspace source content
|
|
35
|
+
* @param fileName - Source file name (only `.css` is ever expanded)
|
|
36
|
+
* @param sharedDir - Absolute path of `components/shared/`
|
|
37
|
+
* @returns The composed content
|
|
38
|
+
* @throws FurnaceError when an included fragment is missing or nested
|
|
39
|
+
*/
|
|
40
|
+
export async function composeComponentSource(content, fileName, sharedDir) {
|
|
41
|
+
if (!fileName.endsWith('.css') || listFragmentIncludes(content).length === 0)
|
|
42
|
+
return content;
|
|
43
|
+
return (await expandCssFragments(content, sharedDir)).expanded;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Maps every engine-relative path `furnace apply` writes to the workspace
|
|
47
|
+
* source it writes there: custom `.mjs`/`.css` into `targetPath`, a custom
|
|
48
|
+
* component's own `.ftl` into the Fluent tree (never for `sharedFtl`), and
|
|
49
|
+
* each override copy candidate onto its engine target.
|
|
50
|
+
*
|
|
51
|
+
* @param root - Project root
|
|
52
|
+
* @param config - Loaded furnace.json
|
|
53
|
+
* @returns Engine-relative path (POSIX) → deployed source
|
|
54
|
+
*/
|
|
55
|
+
export async function mapDeployedComponentSources(root, config) {
|
|
56
|
+
const furnacePaths = getFurnacePaths(root);
|
|
57
|
+
const ftlDir = resolveFtlDir(config.ftlBasePath);
|
|
58
|
+
const sources = new Map();
|
|
59
|
+
for (const [component, custom] of Object.entries(config.custom)) {
|
|
60
|
+
const componentDir = join(furnacePaths.customDir, component);
|
|
61
|
+
for (const fileName of await listSourceFiles(componentDir)) {
|
|
62
|
+
let engineRel;
|
|
63
|
+
if (fileName.endsWith('.mjs') || fileName.endsWith('.css')) {
|
|
64
|
+
engineRel = normalizePathSlashes(join(custom.targetPath, fileName));
|
|
65
|
+
}
|
|
66
|
+
else if (fileName === `${component}.ftl` && custom.localized && !custom.sharedFtl) {
|
|
67
|
+
engineRel = normalizePathSlashes(join(ftlDir, fileName));
|
|
68
|
+
}
|
|
69
|
+
if (engineRel === undefined)
|
|
70
|
+
continue;
|
|
71
|
+
sources.set(engineRel, {
|
|
72
|
+
type: 'custom',
|
|
73
|
+
component,
|
|
74
|
+
fileName,
|
|
75
|
+
sourcePath: join(componentDir, fileName),
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
for (const [component, override] of Object.entries(config.overrides)) {
|
|
80
|
+
const componentDir = join(furnacePaths.overridesDir, component);
|
|
81
|
+
for (const fileName of await listSourceFiles(componentDir)) {
|
|
82
|
+
if (!isOverrideCopyCandidate(fileName, override.type))
|
|
83
|
+
continue;
|
|
84
|
+
// Same target rule as `getOverrideEngineTargetPath`, engine-relative.
|
|
85
|
+
const base = fileName.endsWith('.ftl') ? ftlDir : override.basePath;
|
|
86
|
+
sources.set(normalizePathSlashes(join(base, fileName)), {
|
|
87
|
+
type: 'override',
|
|
88
|
+
component,
|
|
89
|
+
fileName,
|
|
90
|
+
sourcePath: join(componentDir, fileName),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return sources;
|
|
95
|
+
}
|
|
96
|
+
async function listSourceFiles(componentDir) {
|
|
97
|
+
if (!(await pathExists(componentDir)))
|
|
98
|
+
return [];
|
|
99
|
+
const entries = await readdir(componentDir, { withFileTypes: true, encoding: 'utf8' });
|
|
100
|
+
return entries.filter((entry) => isRegularFile(entry)).map((entry) => entry.name);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Classifies one deployed engine file against its component source.
|
|
104
|
+
*
|
|
105
|
+
* @returns The match, or undefined when the engine copy is explained by
|
|
106
|
+
* none of them (a real, unrecorded engine edit)
|
|
107
|
+
*/
|
|
108
|
+
async function matchDeployedSource(engineContent, source, sharedDir, appliedChecksums) {
|
|
109
|
+
const sourceContent = await readText(source.sourcePath);
|
|
110
|
+
const engine = normalizeForChecksum(engineContent);
|
|
111
|
+
let composed;
|
|
112
|
+
try {
|
|
113
|
+
composed = await composeComponentSource(sourceContent, source.fileName, sharedDir);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// A missing fragment: no composed form exists. The other two
|
|
117
|
+
// comparisons below still apply.
|
|
118
|
+
}
|
|
119
|
+
if (composed !== undefined && normalizeForChecksum(composed) === engine) {
|
|
120
|
+
return 'composed-source';
|
|
121
|
+
}
|
|
122
|
+
const strippedEngine = stripExpandedFragments(engine);
|
|
123
|
+
if (listFragmentIncludes(sourceContent).length > 0 &&
|
|
124
|
+
strippedEngine === stripExpandedFragments(normalizeForChecksum(sourceContent))) {
|
|
125
|
+
return 'fragment-refresh';
|
|
126
|
+
}
|
|
127
|
+
const recorded = extractComponentChecksums(appliedChecksums, source.type, source.component)[source.fileName];
|
|
128
|
+
// Apply records the checksum of the (unexpanded) source it deployed, so
|
|
129
|
+
// an engine copy with its expansions collapsed hashes to that value
|
|
130
|
+
// exactly when it is that deployment.
|
|
131
|
+
if (recorded !== undefined && sha256Hex(strippedEngine) === recorded) {
|
|
132
|
+
return 'previous-deploy';
|
|
133
|
+
}
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* For each engine-relative path, decides whether its current engine content
|
|
138
|
+
* is a Furnace deployment of its component source (see
|
|
139
|
+
* {@link DeployedSourceMatch}). Paths with no component source, an
|
|
140
|
+
* unreadable engine copy, or content explained by none of the matches are
|
|
141
|
+
* absent from the result.
|
|
142
|
+
*
|
|
143
|
+
* @param root - Project root
|
|
144
|
+
* @param engineDir - Absolute engine directory
|
|
145
|
+
* @param files - Engine-relative paths to classify
|
|
146
|
+
* @returns Engine-relative path → match, for the matching paths only
|
|
147
|
+
*/
|
|
148
|
+
export async function matchDeployedComponentFiles(root, engineDir, files) {
|
|
149
|
+
const matches = new Map();
|
|
150
|
+
if (files.length === 0 || !(await furnaceConfigExists(root)))
|
|
151
|
+
return matches;
|
|
152
|
+
const sources = await mapDeployedComponentSources(root, await loadFurnaceConfig(root));
|
|
153
|
+
const { sharedDir } = getFurnacePaths(root);
|
|
154
|
+
const { appliedChecksums } = await loadFurnaceState(root);
|
|
155
|
+
for (const file of files) {
|
|
156
|
+
const source = sources.get(file);
|
|
157
|
+
if (source === undefined)
|
|
158
|
+
continue;
|
|
159
|
+
const enginePath = join(engineDir, file);
|
|
160
|
+
if (!(await pathExists(enginePath)))
|
|
161
|
+
continue;
|
|
162
|
+
const match = await matchDeployedSource(await readText(enginePath), source, sharedDir, appliedChecksums);
|
|
163
|
+
if (match !== undefined)
|
|
164
|
+
matches.set(file, match);
|
|
165
|
+
}
|
|
166
|
+
return matches;
|
|
167
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type FurnaceOptInCheck, type FurnaceValidationException } from '../types/furnace.js';
|
|
2
|
+
/**
|
|
3
|
+
* Parses `validationExceptions[]`: `{ component, check, tag?, reason }`
|
|
4
|
+
* objects, no other keys, every field a non-empty string, and no two
|
|
5
|
+
* entries for the same component, check and tag.
|
|
6
|
+
*/
|
|
7
|
+
export declare function parseValidationExceptions(raw: unknown): FurnaceValidationException[];
|
|
8
|
+
/**
|
|
9
|
+
* Parses `tokenSiblingSheets`: an object from a sheet glob to a non-empty
|
|
10
|
+
* list of partner paths or globs.
|
|
11
|
+
*/
|
|
12
|
+
export declare function parseTokenSiblingSheets(raw: unknown): Record<string, string[]>;
|
|
13
|
+
/** Parses `minComponents`: a positive integer. */
|
|
14
|
+
export declare function parseMinComponents(raw: unknown): number;
|
|
15
|
+
/** Parses `optInChecks[]`: names from {@link FURNACE_OPT_IN_CHECKS}. */
|
|
16
|
+
export declare function parseOptInChecks(raw: unknown): FurnaceOptInCheck[];
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Validators for the `furnace validate` and `token lint` policy fields of
|
|
4
|
+
* furnace.json: `validationExceptions[]`, `optInChecks[]`, `minComponents`
|
|
5
|
+
* and `tokenSiblingSheets`.
|
|
6
|
+
*
|
|
7
|
+
* Strict on purpose, like `buildAudit.unpackaged`: every exception is a
|
|
8
|
+
* standing suppression of a finding, so a malformed one must fail at load
|
|
9
|
+
* rather than quietly suppress nothing (or everything).
|
|
10
|
+
*/
|
|
11
|
+
import { FurnaceError } from '../errors/furnace.js';
|
|
12
|
+
import { FURNACE_OPT_IN_CHECKS, } from '../types/furnace.js';
|
|
13
|
+
import { isArray, isObject, isString } from '../utils/validation.js';
|
|
14
|
+
const EXCEPTION_KEYS = new Set(['component', 'check', 'tag', 'reason']);
|
|
15
|
+
function requireNonEmptyString(entry, key, field) {
|
|
16
|
+
const value = entry[key];
|
|
17
|
+
if (!isString(value) || value.trim().length === 0) {
|
|
18
|
+
throw new FurnaceError(`Furnace config: "${field}.${key}" must be a non-empty string`);
|
|
19
|
+
}
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Parses `validationExceptions[]`: `{ component, check, tag?, reason }`
|
|
24
|
+
* objects, no other keys, every field a non-empty string, and no two
|
|
25
|
+
* entries for the same component, check and tag.
|
|
26
|
+
*/
|
|
27
|
+
export function parseValidationExceptions(raw) {
|
|
28
|
+
if (!isArray(raw)) {
|
|
29
|
+
throw new FurnaceError('Furnace config: "validationExceptions" must be an array');
|
|
30
|
+
}
|
|
31
|
+
const seen = new Set();
|
|
32
|
+
return raw.map((entry, index) => {
|
|
33
|
+
const field = `validationExceptions[${String(index)}]`;
|
|
34
|
+
if (!isObject(entry)) {
|
|
35
|
+
throw new FurnaceError(`Furnace config: "${field}" must be an object with "component", "check" and "reason"`);
|
|
36
|
+
}
|
|
37
|
+
for (const key of Object.keys(entry)) {
|
|
38
|
+
if (!EXCEPTION_KEYS.has(key)) {
|
|
39
|
+
throw new FurnaceError(`Furnace config: "${field}" has unknown key "${key}"`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const exception = {
|
|
43
|
+
component: requireNonEmptyString(entry, 'component', field),
|
|
44
|
+
check: requireNonEmptyString(entry, 'check', field),
|
|
45
|
+
reason: requireNonEmptyString(entry, 'reason', field),
|
|
46
|
+
};
|
|
47
|
+
if (entry['tag'] !== undefined)
|
|
48
|
+
exception.tag = requireNonEmptyString(entry, 'tag', field);
|
|
49
|
+
const identity = `${exception.component}\u0000${exception.check}\u0000${exception.tag ?? ''}`;
|
|
50
|
+
if (seen.has(identity)) {
|
|
51
|
+
const on = exception.tag !== undefined ? ` on <${exception.tag}>` : '';
|
|
52
|
+
throw new FurnaceError(`Furnace config: "${field}" repeats the exception for ${exception.component} [${exception.check}]${on}`);
|
|
53
|
+
}
|
|
54
|
+
seen.add(identity);
|
|
55
|
+
return exception;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
function isOptInCheck(value) {
|
|
59
|
+
return FURNACE_OPT_IN_CHECKS.includes(value);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Parses `tokenSiblingSheets`: an object from a sheet glob to a non-empty
|
|
63
|
+
* list of partner paths or globs.
|
|
64
|
+
*/
|
|
65
|
+
export function parseTokenSiblingSheets(raw) {
|
|
66
|
+
if (!isObject(raw)) {
|
|
67
|
+
throw new FurnaceError('Furnace config: "tokenSiblingSheets" must be an object');
|
|
68
|
+
}
|
|
69
|
+
const out = {};
|
|
70
|
+
for (const [pattern, partners] of Object.entries(raw)) {
|
|
71
|
+
const valid = isArray(partners) &&
|
|
72
|
+
partners.length > 0 &&
|
|
73
|
+
partners.every((partner) => isString(partner) && partner.trim() !== '');
|
|
74
|
+
if (pattern.includes('..') || !valid || partners.some((p) => String(p).includes('..'))) {
|
|
75
|
+
throw new FurnaceError(`Furnace config: "tokenSiblingSheets.${pattern}" must be a non-empty list of engine-relative paths or globs (no "..")`);
|
|
76
|
+
}
|
|
77
|
+
out[pattern] = partners;
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
/** Parses `minComponents`: a positive integer. */
|
|
82
|
+
export function parseMinComponents(raw) {
|
|
83
|
+
if (typeof raw !== 'number' || !Number.isInteger(raw) || raw < 1) {
|
|
84
|
+
throw new FurnaceError('Furnace config: "minComponents" must be a positive integer');
|
|
85
|
+
}
|
|
86
|
+
return raw;
|
|
87
|
+
}
|
|
88
|
+
/** Parses `optInChecks[]`: names from {@link FURNACE_OPT_IN_CHECKS}. */
|
|
89
|
+
export function parseOptInChecks(raw) {
|
|
90
|
+
if (!isArray(raw)) {
|
|
91
|
+
throw new FurnaceError('Furnace config: "optInChecks" must be an array');
|
|
92
|
+
}
|
|
93
|
+
const checks = [];
|
|
94
|
+
for (const value of raw) {
|
|
95
|
+
if (!isString(value) || !isOptInCheck(value)) {
|
|
96
|
+
throw new FurnaceError(`Furnace config: "optInChecks" entry ${JSON.stringify(value)} is not an opt-in check. Valid: ${FURNACE_OPT_IN_CHECKS.join(', ')}`);
|
|
97
|
+
}
|
|
98
|
+
if (!checks.includes(value))
|
|
99
|
+
checks.push(value);
|
|
100
|
+
}
|
|
101
|
+
return checks;
|
|
102
|
+
}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { FurnaceError } from '../errors/furnace.js';
|
|
9
9
|
import { isJsonObject, isString } from '../utils/validation.js';
|
|
10
10
|
import { parseStringArray } from './furnace-config-array-utils.js';
|
|
11
|
+
import { parseMinComponents, parseOptInChecks, parseTokenSiblingSheets, parseValidationExceptions, } from './furnace-config-validate-exceptions.js';
|
|
11
12
|
/**
|
|
12
13
|
* Validates an override component config object.
|
|
13
14
|
* @param data - Raw data to validate
|
|
@@ -93,6 +94,9 @@ export function applyOptionalFurnaceFields(migrated, config) {
|
|
|
93
94
|
if (migrated['platformPrefixes'] !== undefined) {
|
|
94
95
|
config.platformPrefixes = parseStringArray(migrated['platformPrefixes'], 'platformPrefixes');
|
|
95
96
|
}
|
|
97
|
+
if (migrated['tokenSiblingSheets'] !== undefined) {
|
|
98
|
+
config.tokenSiblingSheets = parseTokenSiblingSheets(migrated['tokenSiblingSheets']);
|
|
99
|
+
}
|
|
96
100
|
if (migrated['runtimeVariables'] !== undefined) {
|
|
97
101
|
config.runtimeVariables = parseStringArray(migrated['runtimeVariables'], 'runtimeVariables');
|
|
98
102
|
}
|
|
@@ -129,4 +133,14 @@ export function applyOptionalFurnaceFields(migrated, config) {
|
|
|
129
133
|
}
|
|
130
134
|
config.scanPaths = paths;
|
|
131
135
|
}
|
|
136
|
+
// `furnace validate` policy: reviewed exceptions and opt-in checks.
|
|
137
|
+
if (migrated['validationExceptions'] !== undefined) {
|
|
138
|
+
config.validationExceptions = parseValidationExceptions(migrated['validationExceptions']);
|
|
139
|
+
}
|
|
140
|
+
if (migrated['optInChecks'] !== undefined) {
|
|
141
|
+
config.optInChecks = parseOptInChecks(migrated['optInChecks']);
|
|
142
|
+
}
|
|
143
|
+
if (migrated['minComponents'] !== undefined) {
|
|
144
|
+
config.minComponents = parseMinComponents(migrated['minComponents']);
|
|
145
|
+
}
|
|
132
146
|
}
|
|
@@ -35,6 +35,16 @@ export declare function listFragmentIncludes(css: string): string[];
|
|
|
35
35
|
* against its workspace source and to re-expand idempotently.
|
|
36
36
|
*/
|
|
37
37
|
export declare function stripExpandedFragments(css: string): string;
|
|
38
|
+
/**
|
|
39
|
+
* Marks the lines of a stylesheet that belong to a fenced fragment
|
|
40
|
+
* expansion: the directive, the inlined body and the end marker. Index `i`
|
|
41
|
+
* is line `i + 1` of `css`. Fences are found exactly as
|
|
42
|
+
* {@link stripExpandedFragments} finds them, so a bare directive or an
|
|
43
|
+
* unterminated fence marks nothing. The patch-size metric reads this
|
|
44
|
+
* off a file's post-image, because a hunk can start inside a span and
|
|
45
|
+
* carry no directive of its own.
|
|
46
|
+
*/
|
|
47
|
+
export declare function expandedFragmentLineMask(css: string): boolean[];
|
|
38
48
|
/**
|
|
39
49
|
* Expands every `@fireforge-include` directive in `css` with the current
|
|
40
50
|
* content of its fragment file from `sharedDir`, fencing each expansion
|
|
@@ -78,6 +78,32 @@ export function stripExpandedFragments(css) {
|
|
|
78
78
|
}
|
|
79
79
|
return out.join('\n');
|
|
80
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* Marks the lines of a stylesheet that belong to a fenced fragment
|
|
83
|
+
* expansion: the directive, the inlined body and the end marker. Index `i`
|
|
84
|
+
* is line `i + 1` of `css`. Fences are found exactly as
|
|
85
|
+
* {@link stripExpandedFragments} finds them, so a bare directive or an
|
|
86
|
+
* unterminated fence marks nothing. The patch-size metric reads this
|
|
87
|
+
* off a file's post-image, because a hunk can start inside a span and
|
|
88
|
+
* carry no directive of its own.
|
|
89
|
+
*/
|
|
90
|
+
export function expandedFragmentLineMask(css) {
|
|
91
|
+
const lines = css.split('\n');
|
|
92
|
+
const mask = lines.map(() => false);
|
|
93
|
+
for (let i = 0; i < lines.length; i++) {
|
|
94
|
+
const inc = INCLUDE_PATTERN.exec(lines[i] ?? '');
|
|
95
|
+
if (!inc?.[1])
|
|
96
|
+
continue;
|
|
97
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
98
|
+
if (END_INCLUDE_PATTERN.exec(lines[j] ?? '')?.[1] !== inc[1])
|
|
99
|
+
continue;
|
|
100
|
+
mask.fill(true, i, j + 1);
|
|
101
|
+
i = j;
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return mask;
|
|
106
|
+
}
|
|
81
107
|
/**
|
|
82
108
|
* Expands every `@fireforge-include` directive in `css` with the current
|
|
83
109
|
* content of its fragment file from `sharedDir`, fencing each expansion
|
|
@@ -63,9 +63,9 @@ export interface RunFurnaceMutationOptions {
|
|
|
63
63
|
* never passed: `furnace deploy --wait-lock 1800` waited 1800 s for the
|
|
64
64
|
* engine session lock and then met `.fireforge/furnace.lock` with a fixed
|
|
65
65
|
* 30 s, which is the worst shape a budget can have: it pays the whole
|
|
66
|
-
* wait and still refuses.
|
|
67
|
-
*
|
|
68
|
-
*
|
|
66
|
+
* wait and still refuses. Every furnace leaf that takes this lock threads
|
|
67
|
+
* its `--wait-lock` here. When absent, {@link sessionWaitLockMs} supplies
|
|
68
|
+
* the session-wide budget (`FIREFORGE_WAIT_LOCK`).
|
|
69
69
|
*/
|
|
70
70
|
lockTimeoutMs?: number;
|
|
71
71
|
/**
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Removes the lines registering `files` for `tagName`, leaving every other
|
|
3
|
+
* line of the block where it is. Returns how many lines were removed.
|
|
4
|
+
*/
|
|
5
|
+
export declare function removeJarMnFileEntries(engineDir: string, tagName: string, files: readonly string[]): Promise<number>;
|
|
6
|
+
/**
|
|
7
|
+
* Rewrites `oldTag`'s lines in place as `newTag`'s. A line whose renamed
|
|
8
|
+
* file is in `liveFiles` keeps its position and layout; one whose file is
|
|
9
|
+
* gone is dropped. Returns the renamed files that already had a line; the
|
|
10
|
+
* caller's `addJarMnEntries` pass then inserts only the rest.
|
|
11
|
+
*/
|
|
12
|
+
export declare function renameJarMnEntriesInPlace(engineDir: string, oldTag: string, newTag: string, liveFiles: readonly string[], renameFile: (file: string) => string): Promise<string[]>;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* In-place edits to a widget's `toolkit/content/jar.mn` block.
|
|
4
|
+
*
|
|
5
|
+
* Undeploying one file and renaming a component used to drop every line of
|
|
6
|
+
* the widget and re-add the live ones, which re-sorted the whole block and
|
|
7
|
+
* left the owning patch carrying reorder hunks. These helpers touch only
|
|
8
|
+
* the lines that change: a deleted file's line goes, a renamed component's
|
|
9
|
+
* lines are rewritten where they stand, and anything still missing is left
|
|
10
|
+
* for `addJarMnEntries` to insert.
|
|
11
|
+
*/
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { pathExists, readText, writeText } from '../utils/fs.js';
|
|
14
|
+
import { escapeRegex } from '../utils/regex.js';
|
|
15
|
+
import { JAR_MN } from './furnace-constants.js';
|
|
16
|
+
/**
|
|
17
|
+
* The file a widget jar.mn line registers: the `(widgets/<tag>/<file>)`
|
|
18
|
+
* source mapping, or the legacy `content/global/elements/<tag>.<ext>`
|
|
19
|
+
* target written before the mapping existed. Undefined when the line is
|
|
20
|
+
* not one of this tag's.
|
|
21
|
+
*/
|
|
22
|
+
function registeredFile(line, tagName) {
|
|
23
|
+
const tag = escapeRegex(tagName);
|
|
24
|
+
const source = new RegExp(`\\(widgets/${tag}/([^)]+)\\)`).exec(line);
|
|
25
|
+
if (source)
|
|
26
|
+
return source[1];
|
|
27
|
+
const legacy = new RegExp(`content/global/elements/(${tag}\\.[^\\s]+)`).exec(line);
|
|
28
|
+
return legacy?.[1];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Removes the lines registering `files` for `tagName`, leaving every other
|
|
32
|
+
* line of the block where it is. Returns how many lines were removed.
|
|
33
|
+
*/
|
|
34
|
+
export async function removeJarMnFileEntries(engineDir, tagName, files) {
|
|
35
|
+
const filePath = join(engineDir, JAR_MN);
|
|
36
|
+
if (files.length === 0 || !(await pathExists(filePath)))
|
|
37
|
+
return 0;
|
|
38
|
+
const doomed = new Set(files);
|
|
39
|
+
const lines = (await readText(filePath)).split('\n');
|
|
40
|
+
const kept = lines.filter((line) => {
|
|
41
|
+
const file = registeredFile(line, tagName);
|
|
42
|
+
return file === undefined || !doomed.has(file);
|
|
43
|
+
});
|
|
44
|
+
const removed = lines.length - kept.length;
|
|
45
|
+
if (removed > 0)
|
|
46
|
+
await writeText(filePath, kept.join('\n'));
|
|
47
|
+
return removed;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Rewrites `oldTag`'s lines in place as `newTag`'s. A line whose renamed
|
|
51
|
+
* file is in `liveFiles` keeps its position and layout; one whose file is
|
|
52
|
+
* gone is dropped. Returns the renamed files that already had a line; the
|
|
53
|
+
* caller's `addJarMnEntries` pass then inserts only the rest.
|
|
54
|
+
*/
|
|
55
|
+
export async function renameJarMnEntriesInPlace(engineDir, oldTag, newTag, liveFiles, renameFile) {
|
|
56
|
+
const filePath = join(engineDir, JAR_MN);
|
|
57
|
+
if (!(await pathExists(filePath)))
|
|
58
|
+
return [];
|
|
59
|
+
const live = new Set(liveFiles);
|
|
60
|
+
const covered = [];
|
|
61
|
+
const out = [];
|
|
62
|
+
for (const line of (await readText(filePath)).split('\n')) {
|
|
63
|
+
const file = registeredFile(line, oldTag);
|
|
64
|
+
if (file === undefined) {
|
|
65
|
+
out.push(line);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const renamed = renameFile(file);
|
|
69
|
+
if (!live.has(renamed) || covered.includes(renamed))
|
|
70
|
+
continue;
|
|
71
|
+
// Textual substitution keeps the indent, column spacing and any
|
|
72
|
+
// trailing comment the line carried.
|
|
73
|
+
out.push(line
|
|
74
|
+
.replace(`content/global/elements/${file}`, `content/global/elements/${renamed}`)
|
|
75
|
+
.replace(`(widgets/${oldTag}/${file})`, `(widgets/${newTag}/${renamed})`));
|
|
76
|
+
covered.push(renamed);
|
|
77
|
+
}
|
|
78
|
+
await writeText(filePath, out.join('\n'));
|
|
79
|
+
return covered;
|
|
80
|
+
}
|