@hominis/fireforge 0.35.0 → 0.37.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 +42 -0
- package/README.md +34 -9
- package/dist/bin/fireforge.js +21 -6
- package/dist/src/cli.js +22 -5
- package/dist/src/commands/bootstrap-checks.js +0 -1
- package/dist/src/commands/bootstrap.d.ts +2 -1
- package/dist/src/commands/bootstrap.js +16 -7
- package/dist/src/commands/build.js +5 -4
- package/dist/src/commands/config.js +30 -10
- package/dist/src/commands/discard.js +18 -3
- package/dist/src/commands/doctor/post-rebase-audit.js +0 -1
- package/dist/src/commands/doctor-check-core.js +0 -1
- package/dist/src/commands/doctor-external-toolchains.d.ts +3 -0
- package/dist/src/commands/doctor-external-toolchains.js +54 -0
- package/dist/src/commands/doctor-furnace-jar.js +0 -1
- package/dist/src/commands/doctor-furnace-manifest-sync.js +0 -1
- package/dist/src/commands/doctor-furnace.js +2 -2
- package/dist/src/commands/doctor-orphaned-harness.d.ts +52 -0
- package/dist/src/commands/doctor-orphaned-harness.js +132 -0
- package/dist/src/commands/doctor-working-tree.js +0 -1
- package/dist/src/commands/doctor.js +6 -1
- package/dist/src/commands/download.js +28 -13
- package/dist/src/commands/export-all.js +5 -3
- package/dist/src/commands/export-flow.js +12 -7
- package/dist/src/commands/export-placement-gate.js +0 -1
- package/dist/src/commands/export-placement-policy.js +0 -1
- package/dist/src/commands/export-shared.d.ts +5 -1
- package/dist/src/commands/export-shared.js +18 -6
- package/dist/src/commands/export.js +22 -10
- package/dist/src/commands/furnace/apply.js +22 -6
- package/dist/src/commands/furnace/chrome-doc-remove.js +0 -1
- package/dist/src/commands/furnace/chrome-doc-templates.js +0 -1
- package/dist/src/commands/furnace/chrome-doc-tests.js +0 -1
- package/dist/src/commands/furnace/chrome-doc.js +0 -1
- package/dist/src/commands/furnace/create-browser-test.js +0 -1
- package/dist/src/commands/furnace/create-dry-run.js +0 -1
- package/dist/src/commands/furnace/create-features.js +0 -1
- package/dist/src/commands/furnace/create-mochikit.js +0 -1
- package/dist/src/commands/furnace/create-readback.js +0 -1
- package/dist/src/commands/furnace/create-templates.js +0 -1
- package/dist/src/commands/furnace/create-validation.js +0 -1
- package/dist/src/commands/furnace/create-xpcshell.js +0 -1
- package/dist/src/commands/furnace/create.js +0 -1
- package/dist/src/commands/furnace/deploy.js +7 -83
- package/dist/src/commands/furnace/diff.js +0 -1
- package/dist/src/commands/furnace/index.js +59 -25
- package/dist/src/commands/furnace/init.js +0 -1
- package/dist/src/commands/furnace/list.js +0 -1
- package/dist/src/commands/furnace/override.js +0 -1
- package/dist/src/commands/furnace/preview.js +3 -3
- package/dist/src/commands/furnace/refresh.js +0 -1
- package/dist/src/commands/furnace/remove-state.d.ts +15 -0
- package/dist/src/commands/furnace/remove-state.js +47 -1
- package/dist/src/commands/furnace/remove.js +3 -9
- package/dist/src/commands/furnace/rename-browser-test.js +1 -5
- package/dist/src/commands/furnace/rename-helpers.d.ts +6 -0
- package/dist/src/commands/furnace/rename-helpers.js +30 -1
- package/dist/src/commands/furnace/rename-xpcshell.js +7 -6
- package/dist/src/commands/furnace/rename.js +15 -36
- package/dist/src/commands/furnace/scan.js +0 -1
- package/dist/src/commands/furnace/status.js +0 -1
- package/dist/src/commands/furnace/sync.js +76 -4
- package/dist/src/commands/furnace/validate.js +0 -1
- package/dist/src/commands/furnace/validation-output.js +0 -1
- package/dist/src/commands/import.js +35 -9
- package/dist/src/commands/lint-per-patch.js +19 -7
- package/dist/src/commands/lint.js +13 -3
- package/dist/src/commands/manifest.js +1 -2
- package/dist/src/commands/package.js +2 -1
- package/dist/src/commands/patch/compact.js +1 -1
- package/dist/src/commands/patch/delete.js +1 -1
- package/dist/src/commands/patch/index.js +1 -1
- package/dist/src/commands/patch/lint-ignore.js +21 -6
- package/dist/src/commands/patch/move-files.js +1 -1
- package/dist/src/commands/patch/patch-context.js +0 -1
- package/dist/src/commands/patch/rename.js +4 -4
- package/dist/src/commands/patch/reorder.js +3 -4
- package/dist/src/commands/patch/split-plan.js +2 -1
- package/dist/src/commands/patch/split.js +4 -4
- package/dist/src/commands/patch/staged-dependency.d.ts +1 -1
- package/dist/src/commands/patch/staged-dependency.js +114 -52
- package/dist/src/commands/patch/tier.js +18 -5
- package/dist/src/commands/re-export-bulk-scan.js +0 -1
- package/dist/src/commands/re-export-files.js +5 -1
- package/dist/src/commands/re-export-options.js +0 -1
- package/dist/src/commands/re-export-scan.js +0 -1
- package/dist/src/commands/re-export.js +14 -4
- package/dist/src/commands/rebase/abort.js +2 -9
- package/dist/src/commands/rebase/confirm.js +0 -1
- package/dist/src/commands/rebase/conflict-summary.js +0 -1
- package/dist/src/commands/rebase/continue.js +0 -1
- package/dist/src/commands/rebase/index.d.ts +1 -1
- package/dist/src/commands/rebase/index.js +10 -15
- package/dist/src/commands/rebase/patch-loop.js +38 -7
- package/dist/src/commands/rebase/summary.js +2 -3
- package/dist/src/commands/rebase.js +0 -1
- package/dist/src/commands/register.js +1 -1
- package/dist/src/commands/reset.js +16 -9
- package/dist/src/commands/resolve.js +2 -2
- package/dist/src/commands/run.js +4 -4
- package/dist/src/commands/setup-support.js +0 -1
- package/dist/src/commands/setup.js +1 -2
- package/dist/src/commands/source.js +1 -2
- package/dist/src/commands/status-output.js +1 -12
- package/dist/src/commands/status.js +147 -133
- package/dist/src/commands/test-appdir.js +0 -1
- package/dist/src/commands/test-diagnose.js +38 -3
- package/dist/src/commands/test-modes.d.ts +15 -0
- package/dist/src/commands/test-modes.js +61 -0
- package/dist/src/commands/test-register.d.ts +4 -0
- package/dist/src/commands/test-register.js +64 -0
- package/dist/src/commands/test-run.d.ts +23 -7
- package/dist/src/commands/test-run.js +15 -14
- package/dist/src/commands/test.d.ts +0 -4
- package/dist/src/commands/test.js +147 -96
- package/dist/src/commands/token-coverage.js +0 -1
- package/dist/src/commands/token.js +1 -2
- package/dist/src/commands/typecheck.js +1 -1
- package/dist/src/commands/verify.js +3 -9
- package/dist/src/commands/watch.js +5 -1
- package/dist/src/commands/wire.js +1 -1
- package/dist/src/core/ast-utils.js +0 -1
- package/dist/src/core/brand-validation.js +0 -1
- package/dist/src/core/branding.js +0 -1
- package/dist/src/core/browser-wire.js +0 -1
- package/dist/src/core/build-audit-platform.js +0 -1
- package/dist/src/core/build-audit-registration.js +0 -1
- package/dist/src/core/build-audit-resolve.js +0 -1
- package/dist/src/core/build-audit-transforms.js +0 -1
- package/dist/src/core/build-audit.js +0 -1
- package/dist/src/core/build-baseline-types.d.ts +24 -0
- package/dist/src/core/build-baseline-types.js +0 -1
- package/dist/src/core/build-baseline.d.ts +5 -2
- package/dist/src/core/build-baseline.js +5 -2
- package/dist/src/core/build-prepare.js +2 -2
- package/dist/src/core/config-mutate.js +0 -1
- package/dist/src/core/config-paths.d.ts +2 -2
- package/dist/src/core/config-paths.js +6 -1
- package/dist/src/core/config-state.js +0 -1
- package/dist/src/core/config-validate-patch-policy.js +0 -1
- package/dist/src/core/config-validate-test-toolchains.d.ts +8 -0
- package/dist/src/core/config-validate-test-toolchains.js +105 -0
- package/dist/src/core/config-validate.js +3 -1
- package/dist/src/core/config.js +0 -1
- package/dist/src/core/destructive.js +0 -1
- package/dist/src/core/diff-hunks.js +13 -7
- package/dist/src/core/engine-changes.js +0 -1
- package/dist/src/core/engine-session-lock.d.ts +12 -0
- package/dist/src/core/engine-session-lock.js +45 -0
- package/dist/src/core/file-lock.d.ts +8 -0
- package/dist/src/core/file-lock.js +157 -48
- package/dist/src/core/firefox-archive.d.ts +4 -0
- package/dist/src/core/firefox-archive.js +6 -1
- package/dist/src/core/firefox-cache.js +60 -4
- package/dist/src/core/firefox-download.js +0 -1
- package/dist/src/core/firefox-extract.js +75 -16
- package/dist/src/core/firefox-ignorefile.js +0 -1
- package/dist/src/core/firefox.d.ts +12 -0
- package/dist/src/core/firefox.js +64 -2
- package/dist/src/core/furnace-apply-dry-run.js +0 -1
- package/dist/src/core/furnace-apply-ftl.d.ts +7 -3
- package/dist/src/core/furnace-apply-ftl.js +10 -5
- package/dist/src/core/furnace-apply-helpers.js +0 -1
- package/dist/src/core/furnace-apply-output.js +2 -2
- package/dist/src/core/furnace-apply.d.ts +7 -1
- package/dist/src/core/furnace-apply.js +47 -13
- package/dist/src/core/furnace-checksum-utils.js +0 -1
- package/dist/src/core/furnace-config-array-utils.js +0 -1
- package/dist/src/core/furnace-config-custom.js +10 -1
- package/dist/src/core/furnace-config-order.js +0 -1
- package/dist/src/core/furnace-config-tokens.js +0 -1
- package/dist/src/core/furnace-config-validate.js +0 -1
- package/dist/src/core/furnace-config.d.ts +11 -0
- package/dist/src/core/furnace-config.js +37 -1
- package/dist/src/core/furnace-constants.js +0 -1
- package/dist/src/core/furnace-css-fragments.js +0 -1
- package/dist/src/core/furnace-graph-utils.js +0 -1
- package/dist/src/core/furnace-jsconfig.d.ts +2 -3
- package/dist/src/core/furnace-jsconfig.js +25 -18
- package/dist/src/core/furnace-marker.js +0 -1
- package/dist/src/core/furnace-operation.js +0 -1
- package/dist/src/core/furnace-refresh.js +0 -1
- package/dist/src/core/furnace-registration-ast.d.ts +14 -0
- package/dist/src/core/furnace-registration-ast.js +1 -2
- package/dist/src/core/furnace-registration-remove.js +0 -1
- package/dist/src/core/furnace-registration-validate.js +1 -5
- package/dist/src/core/furnace-registration.js +0 -1
- package/dist/src/core/furnace-rollback.js +0 -1
- package/dist/src/core/furnace-scanner.js +0 -1
- package/dist/src/core/furnace-stale-export.d.ts +59 -0
- package/dist/src/core/furnace-stale-export.js +123 -0
- package/dist/src/core/furnace-staleness.js +0 -1
- package/dist/src/core/furnace-state-persist.d.ts +72 -0
- package/dist/src/core/furnace-state-persist.js +118 -0
- package/dist/src/core/furnace-step-errors.d.ts +23 -0
- package/dist/src/core/furnace-step-errors.js +25 -0
- package/dist/src/core/furnace-stories.js +0 -1
- package/dist/src/core/furnace-validate-accessibility.js +0 -1
- package/dist/src/core/furnace-validate-checks.js +0 -1
- package/dist/src/core/furnace-validate-compatibility.js +9 -3
- package/dist/src/core/furnace-validate-helpers.js +0 -1
- package/dist/src/core/furnace-validate-registration.js +7 -7
- package/dist/src/core/furnace-validate-structure.d.ts +8 -0
- package/dist/src/core/furnace-validate-structure.js +14 -4
- package/dist/src/core/furnace-validate.js +0 -1
- package/dist/src/core/furnace-version-drift.d.ts +6 -3
- package/dist/src/core/furnace-version-drift.js +6 -4
- package/dist/src/core/git-base.js +0 -1
- package/dist/src/core/git-diff.js +0 -1
- package/dist/src/core/git-file-ops.js +0 -1
- package/dist/src/core/git-status.d.ts +13 -0
- package/dist/src/core/git-status.js +32 -2
- package/dist/src/core/git.d.ts +10 -1
- package/dist/src/core/git.js +63 -6
- package/dist/src/core/license-headers.js +0 -1
- package/dist/src/core/lint-cache.js +0 -1
- package/dist/src/core/lint-projection.js +0 -1
- package/dist/src/core/mach-build-artifacts.js +0 -1
- package/dist/src/core/mach-error-hints.js +0 -1
- package/dist/src/core/mach-known-noise-filter.d.ts +60 -0
- package/dist/src/core/mach-known-noise-filter.js +193 -0
- package/dist/src/core/mach-mozconfig.js +0 -1
- package/dist/src/core/mach-python.js +0 -1
- package/dist/src/core/mach-resource-shim.js +0 -1
- package/dist/src/core/mach.d.ts +8 -0
- package/dist/src/core/mach.js +36 -4
- package/dist/src/core/manifest-helpers.js +0 -1
- package/dist/src/core/manifest-register.js +0 -1
- package/dist/src/core/manifest-rules.js +0 -1
- package/dist/src/core/manifest-tokenizers.js +0 -1
- package/dist/src/core/marionette-port.d.ts +8 -0
- package/dist/src/core/marionette-port.js +34 -3
- package/dist/src/core/marionette-preflight.js +0 -1
- package/dist/src/core/markdown-table.js +0 -1
- package/dist/src/core/ownership-table.js +2 -9
- package/dist/src/core/parser-fallback.js +0 -1
- package/dist/src/core/patch-apply-fuzz.d.ts +27 -9
- package/dist/src/core/patch-apply-fuzz.js +71 -34
- package/dist/src/core/patch-apply.d.ts +30 -0
- package/dist/src/core/patch-apply.js +101 -18
- package/dist/src/core/patch-artifact-normalize.d.ts +18 -5
- package/dist/src/core/patch-artifact-normalize.js +19 -15
- package/dist/src/core/patch-export-coverage.js +0 -1
- package/dist/src/core/patch-export-metadata.js +0 -1
- package/dist/src/core/patch-export-update.js +2 -3
- package/dist/src/core/patch-export.d.ts +2 -6
- package/dist/src/core/patch-export.js +24 -48
- package/dist/src/core/patch-files.js +0 -1
- package/dist/src/core/patch-identifier-suggest.js +0 -1
- package/dist/src/core/patch-lint-checkjs.js +0 -1
- package/dist/src/core/patch-lint-chrome-jsdoc.js +0 -1
- package/dist/src/core/patch-lint-cross.js +80 -3
- package/dist/src/core/patch-lint-css.js +0 -1
- package/dist/src/core/patch-lint-diff-tag.js +0 -1
- package/dist/src/core/patch-lint-diff.d.ts +2 -1
- package/dist/src/core/patch-lint-diff.js +17 -32
- package/dist/src/core/patch-lint-imports.js +0 -1
- package/dist/src/core/patch-lint-jsdoc.js +0 -1
- package/dist/src/core/patch-lint-observer.js +0 -1
- package/dist/src/core/patch-lint-ownership.js +0 -1
- package/dist/src/core/patch-lint-reexports.js +0 -1
- package/dist/src/core/patch-lint.d.ts +37 -2
- package/dist/src/core/patch-lint.js +47 -34
- package/dist/src/core/patch-lock.js +0 -1
- package/dist/src/core/patch-manifest-consistency.js +9 -1
- package/dist/src/core/patch-manifest-io.d.ts +19 -2
- package/dist/src/core/patch-manifest-io.js +57 -21
- package/dist/src/core/patch-manifest-query.js +18 -10
- package/dist/src/core/patch-manifest-resolve.js +0 -1
- package/dist/src/core/patch-manifest-validate.js +24 -1
- package/dist/src/core/patch-manifest.d.ts +1 -1
- package/dist/src/core/patch-manifest.js +1 -2
- package/dist/src/core/patch-parse.d.ts +59 -0
- package/dist/src/core/patch-parse.js +209 -92
- package/dist/src/core/patch-policy.js +0 -1
- package/dist/src/core/patch-registration-refs.js +16 -26
- package/dist/src/core/patch-source-metadata.js +0 -1
- package/dist/src/core/patch-transform.d.ts +4 -0
- package/dist/src/core/patch-transform.js +30 -38
- package/dist/src/core/rebase-session.js +0 -1
- package/dist/src/core/register-browser-content.js +0 -1
- package/dist/src/core/register-module.js +0 -1
- package/dist/src/core/register-result.js +0 -1
- package/dist/src/core/register-scaffold.js +0 -1
- package/dist/src/core/register-shared-css.js +0 -1
- package/dist/src/core/register-test-manifest.js +0 -1
- package/dist/src/core/register-xpcshell-test.js +0 -1
- package/dist/src/core/shared-ftl.js +0 -1
- package/dist/src/core/signal-critical.js +0 -1
- package/dist/src/core/smoke-patterns.js +0 -1
- package/dist/src/core/state-file.js +0 -1
- package/dist/src/core/status-classify.d.ts +16 -0
- package/dist/src/core/status-classify.js +24 -16
- package/dist/src/core/test-harness-crash.d.ts +80 -7
- package/dist/src/core/test-harness-crash.js +251 -15
- package/dist/src/core/test-harness-output.js +0 -1
- package/dist/src/core/test-path-scope.d.ts +21 -13
- package/dist/src/core/test-path-scope.js +43 -28
- package/dist/src/core/test-stale-check.d.ts +29 -1
- package/dist/src/core/test-stale-check.js +59 -1
- package/dist/src/core/test-stale-symlink.js +0 -1
- package/dist/src/core/test-xpcshell-retry.js +0 -1
- package/dist/src/core/token-coverage.js +0 -1
- package/dist/src/core/token-dark-mode.js +0 -1
- package/dist/src/core/token-docs.js +0 -1
- package/dist/src/core/token-manager.js +0 -1
- package/dist/src/core/token-scaffold.js +0 -1
- package/dist/src/core/token-variant.js +0 -1
- package/dist/src/core/toolchain-preflight.d.ts +31 -10
- package/dist/src/core/toolchain-preflight.js +97 -36
- package/dist/src/core/typecheck-shim.js +13 -1
- package/dist/src/core/typecheck.js +0 -1
- package/dist/src/core/wire-destroy.js +0 -1
- package/dist/src/core/wire-dom-fragment.js +0 -1
- package/dist/src/core/wire-init.js +0 -1
- package/dist/src/core/wire-subscript.js +0 -1
- package/dist/src/core/wire-targets.js +0 -1
- package/dist/src/core/wire-utils.js +0 -1
- package/dist/src/core/xpcshell-appdir.js +1 -4
- package/dist/src/errors/base.d.ts +29 -0
- package/dist/src/errors/base.js +35 -1
- package/dist/src/errors/build.js +0 -1
- package/dist/src/errors/codes.d.ts +6 -0
- package/dist/src/errors/codes.js +6 -1
- package/dist/src/errors/config.js +0 -1
- package/dist/src/errors/download.js +0 -1
- package/dist/src/errors/furnace.d.ts +11 -1
- package/dist/src/errors/furnace.js +6 -5
- package/dist/src/errors/git.js +0 -1
- package/dist/src/errors/patch.d.ts +17 -0
- package/dist/src/errors/patch.js +29 -1
- package/dist/src/errors/rebase.js +0 -1
- package/dist/src/errors/run.js +1 -1
- package/dist/src/index.js +0 -1
- package/dist/src/types/cli.js +0 -1
- package/dist/src/types/commands/index.d.ts +1 -1
- package/dist/src/types/commands/index.js +0 -1
- package/dist/src/types/commands/options.d.ts +40 -9
- package/dist/src/types/commands/options.js +0 -1
- package/dist/src/types/commands/patches.d.ts +37 -0
- package/dist/src/types/commands/patches.js +0 -1
- package/dist/src/types/commands/project.d.ts +11 -2
- package/dist/src/types/commands/project.js +0 -1
- package/dist/src/types/config.d.ts +32 -0
- package/dist/src/types/config.js +0 -1
- package/dist/src/types/furnace.d.ts +23 -0
- package/dist/src/types/furnace.js +0 -1
- package/dist/src/types/index.js +0 -1
- package/dist/src/types/typecheck.js +0 -1
- package/dist/src/utils/elapsed.js +0 -1
- package/dist/src/utils/errors.js +0 -1
- package/dist/src/utils/fs.js +37 -5
- package/dist/src/utils/logger.d.ts +7 -0
- package/dist/src/utils/logger.js +85 -3
- package/dist/src/utils/options.d.ts +15 -0
- package/dist/src/utils/options.js +25 -1
- package/dist/src/utils/package-root.js +11 -4
- package/dist/src/utils/parse.js +0 -1
- package/dist/src/utils/paths.js +0 -1
- package/dist/src/utils/platform.js +0 -1
- package/dist/src/utils/process-group.d.ts +33 -0
- package/dist/src/utils/process-group.js +161 -0
- package/dist/src/utils/process.d.ts +48 -4
- package/dist/src/utils/process.js +258 -59
- package/dist/src/utils/regex.d.ts +13 -3
- package/dist/src/utils/regex.js +53 -4
- package/dist/src/utils/validation.d.ts +0 -12
- package/dist/src/utils/validation.js +13 -26
- package/package.json +9 -8
|
@@ -1,16 +1,30 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
3
|
import { constants as osConstants } from 'node:os';
|
|
4
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
5
|
+
import { ExecTimeoutError } from '../errors/base.js';
|
|
6
|
+
import { killProcessTree, sweepProcessGroup } from './process-group.js';
|
|
4
7
|
// 50 MB cap per stream to prevent OOM — large toolchain builds (e.g. Firefox, Chromium)
|
|
5
8
|
// can easily blow past this, so we truncate rather than let the buffer grow unbounded.
|
|
9
|
+
// Callers for whom truncation is unacceptable (e.g. the archive-safety preflight
|
|
10
|
+
// scanning a full `tar -tvf` listing) must check `wasTruncated` and fail hard.
|
|
6
11
|
const MAX_OUTPUT_SIZE = 50 * 1024 * 1024;
|
|
7
12
|
function createStreamCollector(mirror) {
|
|
8
13
|
const chunks = [];
|
|
9
14
|
let totalLength = 0;
|
|
10
15
|
let truncated = false;
|
|
16
|
+
// A per-stream StringDecoder keeps multibyte UTF-8 characters split across
|
|
17
|
+
// pipe chunk boundaries intact. Per-chunk `Buffer.toString()` turned a
|
|
18
|
+
// straddling character into two U+FFFD replacement chars, which silently
|
|
19
|
+
// broke regex matching over captured output (smoke-run allowlists, mach
|
|
20
|
+
// error hints) whenever a non-ASCII path or localized message landed on a
|
|
21
|
+
// 64 KB chunk boundary.
|
|
22
|
+
const decoder = new StringDecoder('utf8');
|
|
11
23
|
return {
|
|
12
24
|
onData: (data) => {
|
|
13
|
-
const chunk =
|
|
25
|
+
const chunk = decoder.write(data);
|
|
26
|
+
if (chunk.length === 0)
|
|
27
|
+
return;
|
|
14
28
|
mirror?.write(chunk);
|
|
15
29
|
if (truncated)
|
|
16
30
|
return;
|
|
@@ -27,6 +41,7 @@ function createStreamCollector(mirror) {
|
|
|
27
41
|
}
|
|
28
42
|
},
|
|
29
43
|
getText: () => chunks.join(''),
|
|
44
|
+
wasTruncated: () => truncated,
|
|
30
45
|
};
|
|
31
46
|
}
|
|
32
47
|
function buildSignalFromTimeout(timeout) {
|
|
@@ -34,6 +49,19 @@ function buildSignalFromTimeout(timeout) {
|
|
|
34
49
|
return undefined;
|
|
35
50
|
return AbortSignal.timeout(timeout);
|
|
36
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Maps a child-process `error` event to the rejection the caller sees.
|
|
54
|
+
* When the caller set a `timeout` and the error is the AbortError produced
|
|
55
|
+
* by `AbortSignal.timeout` firing, rejects with a typed
|
|
56
|
+
* {@link ExecTimeoutError} naming the command and budget instead of Node's
|
|
57
|
+
* opaque `AbortError: The operation was aborted`.
|
|
58
|
+
*/
|
|
59
|
+
function toExecRejection(error, command, args, timeout) {
|
|
60
|
+
if (timeout !== undefined && error.name === 'AbortError') {
|
|
61
|
+
return new ExecTimeoutError(command, args, timeout, error);
|
|
62
|
+
}
|
|
63
|
+
return error;
|
|
64
|
+
}
|
|
37
65
|
function exitCodeFromClose(code, signal) {
|
|
38
66
|
if (code !== null) {
|
|
39
67
|
return code;
|
|
@@ -66,13 +94,15 @@ export async function exec(command, args, options = {}) {
|
|
|
66
94
|
child.stdout.on('data', out.onData);
|
|
67
95
|
child.stderr.on('data', err.onData);
|
|
68
96
|
child.on('error', (error) => {
|
|
69
|
-
reject(error);
|
|
97
|
+
reject(toExecRejection(error, command, args, options.timeout));
|
|
70
98
|
});
|
|
71
99
|
child.on('close', (code, signal) => {
|
|
72
100
|
resolve({
|
|
73
101
|
stdout: out.getText(),
|
|
74
102
|
stderr: err.getText(),
|
|
75
103
|
exitCode: exitCodeFromClose(code, signal),
|
|
104
|
+
stdoutTruncated: out.wasTruncated(),
|
|
105
|
+
stderrTruncated: err.wasTruncated(),
|
|
76
106
|
});
|
|
77
107
|
});
|
|
78
108
|
});
|
|
@@ -86,26 +116,99 @@ export async function exec(command, args, options = {}) {
|
|
|
86
116
|
*/
|
|
87
117
|
export async function execStream(command, args, options = {}) {
|
|
88
118
|
return new Promise((resolve, reject) => {
|
|
119
|
+
const usesProcessGroup = options.processGroup === true && process.platform !== 'win32';
|
|
89
120
|
const child = spawn(command, args, {
|
|
90
121
|
cwd: options.cwd,
|
|
91
122
|
env: { ...process.env, ...options.env },
|
|
92
123
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
93
124
|
signal: buildSignalFromTimeout(options.timeout),
|
|
125
|
+
detached: usesProcessGroup,
|
|
94
126
|
});
|
|
127
|
+
const groupPid = usesProcessGroup ? child.pid : undefined;
|
|
128
|
+
const stdoutDecoder = new StringDecoder('utf8');
|
|
129
|
+
const stderrDecoder = new StringDecoder('utf8');
|
|
95
130
|
child.stdout.on('data', (data) => {
|
|
96
|
-
|
|
131
|
+
const chunk = stdoutDecoder.write(data);
|
|
132
|
+
if (chunk.length > 0)
|
|
133
|
+
options.onStdout?.(chunk);
|
|
97
134
|
});
|
|
98
135
|
child.stderr.on('data', (data) => {
|
|
99
|
-
|
|
136
|
+
const chunk = stderrDecoder.write(data);
|
|
137
|
+
if (chunk.length > 0)
|
|
138
|
+
options.onStderr?.(chunk);
|
|
100
139
|
});
|
|
140
|
+
// A detached group leader no longer receives the terminal's Ctrl+C, so
|
|
141
|
+
// the forwarder is mandatory (not just graceful-UX) when processGroup
|
|
142
|
+
// is set. Group-aware: kills route to the whole tree.
|
|
143
|
+
const forwarder = options.processGroup === true
|
|
144
|
+
? installGracefulShutdownForwarder(child, 1500, (signal) => {
|
|
145
|
+
killProcessTree(child, signal, usesProcessGroup);
|
|
146
|
+
})
|
|
147
|
+
: undefined;
|
|
101
148
|
child.on('error', (error) => {
|
|
102
|
-
|
|
149
|
+
// Abort/startup failure: make sure a partially-started tree does not
|
|
150
|
+
// outlive the dispatch (a mach dying at startup used to strand
|
|
151
|
+
// multiprocessing workers).
|
|
152
|
+
if (options.processGroup === true) {
|
|
153
|
+
killProcessTree(child, 'SIGKILL', usesProcessGroup);
|
|
154
|
+
}
|
|
155
|
+
forwarder?.dispose();
|
|
156
|
+
reject(toExecRejection(error, command, args, options.timeout));
|
|
103
157
|
});
|
|
104
158
|
child.on('close', (code, signal) => {
|
|
105
|
-
|
|
159
|
+
forwarder?.dispose();
|
|
160
|
+
const finish = () => {
|
|
161
|
+
resolve(exitCodeFromClose(code, signal));
|
|
162
|
+
};
|
|
163
|
+
if (groupPid !== undefined) {
|
|
164
|
+
sweepProcessGroup(groupPid).then(finish, finish);
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
finish();
|
|
168
|
+
}
|
|
106
169
|
});
|
|
107
170
|
});
|
|
108
171
|
}
|
|
172
|
+
/**
|
|
173
|
+
* Close-promises of children whose shutdown the CLI must wait for when a
|
|
174
|
+
* termination signal arrives. The bin signal handler used to `process.exit`
|
|
175
|
+
* within a microtask of the first Ctrl+C, which made `execInherit`'s
|
|
176
|
+
* documented SIGTERM → grace → SIGKILL escalation unreachable: the parent
|
|
177
|
+
* was gone before the 1500 ms grace timer could fire, so a hung Firefox
|
|
178
|
+
* was orphaned forever instead of being SIGKILLed and a healthy one lost
|
|
179
|
+
* its AsyncShutdown flush window.
|
|
180
|
+
*/
|
|
181
|
+
const activeChildClosures = new Set();
|
|
182
|
+
function trackChildClosure() {
|
|
183
|
+
let resolveClosed;
|
|
184
|
+
const closed = new Promise((resolve) => {
|
|
185
|
+
resolveClosed = resolve;
|
|
186
|
+
});
|
|
187
|
+
activeChildClosures.add(closed);
|
|
188
|
+
return {
|
|
189
|
+
settle: () => {
|
|
190
|
+
resolveClosed?.();
|
|
191
|
+
activeChildClosures.delete(closed);
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Waits (bounded) for every tracked child process to close. Called by the
|
|
197
|
+
* bin signal handler after forwarding SIGINT/SIGTERM, so the parent stays
|
|
198
|
+
* alive long enough for the grace-then-SIGKILL escalation to actually run.
|
|
199
|
+
* Resolves immediately when no children are active.
|
|
200
|
+
*/
|
|
201
|
+
export async function waitForActiveChildShutdown(timeoutMs) {
|
|
202
|
+
if (activeChildClosures.size === 0)
|
|
203
|
+
return;
|
|
204
|
+
await Promise.race([
|
|
205
|
+
Promise.allSettled([...activeChildClosures]),
|
|
206
|
+
new Promise((resolve) => {
|
|
207
|
+
const timer = setTimeout(resolve, timeoutMs);
|
|
208
|
+
timer.unref();
|
|
209
|
+
}),
|
|
210
|
+
]);
|
|
211
|
+
}
|
|
109
212
|
/**
|
|
110
213
|
* Executes a command and inherits stdio (shows output directly).
|
|
111
214
|
*
|
|
@@ -124,21 +227,42 @@ export async function execStream(command, args, options = {}) {
|
|
|
124
227
|
*/
|
|
125
228
|
export async function execInherit(command, args, options = {}) {
|
|
126
229
|
return new Promise((resolve, reject) => {
|
|
230
|
+
const usesProcessGroup = options.processGroup === true && process.platform !== 'win32';
|
|
127
231
|
const child = spawn(command, args, {
|
|
128
232
|
cwd: options.cwd,
|
|
129
233
|
env: { ...process.env, ...options.env },
|
|
130
234
|
stdio: 'inherit',
|
|
131
235
|
signal: buildSignalFromTimeout(options.timeout),
|
|
236
|
+
detached: usesProcessGroup,
|
|
132
237
|
});
|
|
238
|
+
const groupPid = usesProcessGroup ? child.pid : undefined;
|
|
133
239
|
const graceMs = options.shutdownGraceMs ?? 1500;
|
|
134
|
-
const { dispose } = installGracefulShutdownForwarder(child, graceMs
|
|
240
|
+
const { dispose } = installGracefulShutdownForwarder(child, graceMs, options.processGroup === true
|
|
241
|
+
? (signal) => {
|
|
242
|
+
killProcessTree(child, signal, usesProcessGroup);
|
|
243
|
+
}
|
|
244
|
+
: undefined);
|
|
245
|
+
const closure = trackChildClosure();
|
|
135
246
|
child.on('error', (error) => {
|
|
247
|
+
if (options.processGroup === true) {
|
|
248
|
+
killProcessTree(child, 'SIGKILL', usesProcessGroup);
|
|
249
|
+
}
|
|
136
250
|
dispose();
|
|
137
|
-
|
|
251
|
+
closure.settle();
|
|
252
|
+
reject(toExecRejection(error, command, args, options.timeout));
|
|
138
253
|
});
|
|
139
254
|
child.on('close', (code, signal) => {
|
|
140
255
|
dispose();
|
|
141
|
-
|
|
256
|
+
const finish = () => {
|
|
257
|
+
closure.settle();
|
|
258
|
+
resolve(exitCodeFromClose(code, signal));
|
|
259
|
+
};
|
|
260
|
+
if (groupPid !== undefined) {
|
|
261
|
+
sweepProcessGroup(groupPid).then(finish, finish);
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
finish();
|
|
265
|
+
}
|
|
142
266
|
});
|
|
143
267
|
});
|
|
144
268
|
}
|
|
@@ -150,14 +274,21 @@ export async function execInherit(command, args, options = {}) {
|
|
|
150
274
|
* `close` and `error` handlers so the process does not accumulate signal
|
|
151
275
|
* listeners across repeated spawns.
|
|
152
276
|
*/
|
|
153
|
-
function installGracefulShutdownForwarder(child, graceMs) {
|
|
277
|
+
function installGracefulShutdownForwarder(child, graceMs, killTarget) {
|
|
154
278
|
let graceTimer;
|
|
155
279
|
const forwarded = new Set();
|
|
280
|
+
const sendKill = (signal) => {
|
|
281
|
+
if (killTarget) {
|
|
282
|
+
killTarget(signal);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
child.kill(signal);
|
|
286
|
+
};
|
|
156
287
|
const escalate = () => {
|
|
157
288
|
if (child.exitCode !== null || child.signalCode !== null)
|
|
158
289
|
return;
|
|
159
290
|
try {
|
|
160
|
-
|
|
291
|
+
sendKill('SIGKILL');
|
|
161
292
|
}
|
|
162
293
|
catch {
|
|
163
294
|
// Child is already gone — nothing to do.
|
|
@@ -171,7 +302,7 @@ function installGracefulShutdownForwarder(child, graceMs) {
|
|
|
171
302
|
}
|
|
172
303
|
forwarded.add(signal);
|
|
173
304
|
try {
|
|
174
|
-
|
|
305
|
+
sendKill('SIGTERM');
|
|
175
306
|
}
|
|
176
307
|
catch {
|
|
177
308
|
// If the child can't accept SIGTERM (already dead), nothing to do.
|
|
@@ -208,29 +339,52 @@ function installGracefulShutdownForwarder(child, graceMs) {
|
|
|
208
339
|
*/
|
|
209
340
|
export async function execInheritCapture(command, args, options = {}) {
|
|
210
341
|
return new Promise((resolve, reject) => {
|
|
342
|
+
const usesProcessGroup = options.processGroup === true && process.platform !== 'win32';
|
|
211
343
|
const child = spawn(command, args, {
|
|
212
344
|
cwd: options.cwd,
|
|
213
345
|
env: { ...process.env, ...options.env },
|
|
214
346
|
stdio: ['inherit', 'pipe', 'pipe'],
|
|
215
347
|
signal: buildSignalFromTimeout(options.timeout),
|
|
348
|
+
detached: usesProcessGroup,
|
|
216
349
|
});
|
|
350
|
+
const groupPid = usesProcessGroup ? child.pid : undefined;
|
|
217
351
|
const out = createStreamCollector(process.stdout);
|
|
218
352
|
const err = createStreamCollector(process.stderr);
|
|
219
353
|
child.stdout.on('data', out.onData);
|
|
220
354
|
child.stderr.on('data', err.onData);
|
|
221
355
|
const graceMs = options.shutdownGraceMs ?? 1500;
|
|
222
|
-
const { dispose } = installGracefulShutdownForwarder(child, graceMs
|
|
356
|
+
const { dispose } = installGracefulShutdownForwarder(child, graceMs, options.processGroup === true
|
|
357
|
+
? (signal) => {
|
|
358
|
+
killProcessTree(child, signal, usesProcessGroup);
|
|
359
|
+
}
|
|
360
|
+
: undefined);
|
|
361
|
+
const closure = trackChildClosure();
|
|
223
362
|
child.on('error', (error) => {
|
|
363
|
+
if (options.processGroup === true) {
|
|
364
|
+
killProcessTree(child, 'SIGKILL', usesProcessGroup);
|
|
365
|
+
}
|
|
224
366
|
dispose();
|
|
225
|
-
|
|
367
|
+
closure.settle();
|
|
368
|
+
reject(toExecRejection(error, command, args, options.timeout));
|
|
226
369
|
});
|
|
227
370
|
child.on('close', (code, signal) => {
|
|
228
371
|
dispose();
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
372
|
+
const finish = () => {
|
|
373
|
+
closure.settle();
|
|
374
|
+
resolve({
|
|
375
|
+
stdout: out.getText(),
|
|
376
|
+
stderr: err.getText(),
|
|
377
|
+
exitCode: exitCodeFromClose(code, signal),
|
|
378
|
+
stdoutTruncated: out.wasTruncated(),
|
|
379
|
+
stderrTruncated: err.wasTruncated(),
|
|
380
|
+
});
|
|
381
|
+
};
|
|
382
|
+
if (groupPid !== undefined) {
|
|
383
|
+
sweepProcessGroup(groupPid).then(finish, finish);
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
finish();
|
|
387
|
+
}
|
|
234
388
|
});
|
|
235
389
|
});
|
|
236
390
|
}
|
|
@@ -248,8 +402,12 @@ export async function execInheritCapture(command, args, options = {}) {
|
|
|
248
402
|
* that inherited the group.
|
|
249
403
|
*
|
|
250
404
|
* Windows fallback: `detached: true` does not create an equivalent group
|
|
251
|
-
* there, so we
|
|
252
|
-
*
|
|
405
|
+
* there, so we kill the descendant tree with `taskkill /pid <pid> /T /F`
|
|
406
|
+
* instead. taskkill has no graceful-signal equivalent — both the deadline
|
|
407
|
+
* and the grace re-invocation are forced kills, so Windows children get
|
|
408
|
+
* no shutdown window (best-effort; Windows is untested, see README).
|
|
409
|
+
* `child.kill()` is still sent as a direct-child fallback in case
|
|
410
|
+
* taskkill itself is unavailable.
|
|
253
411
|
*/
|
|
254
412
|
export async function execSmokeRun(command, args, options) {
|
|
255
413
|
return new Promise((resolve, reject) => {
|
|
@@ -261,45 +419,30 @@ export async function execSmokeRun(command, args, options) {
|
|
|
261
419
|
// A new process group on POSIX lets us signal the whole descendant
|
|
262
420
|
// tree at once, which is essential for mach → python → firefox →
|
|
263
421
|
// content-process chains. On Windows spawn ignores this for our
|
|
264
|
-
// purposes and we fall back to
|
|
422
|
+
// purposes and we fall back to a taskkill /T tree kill below.
|
|
265
423
|
detached: usesProcessGroup,
|
|
266
424
|
});
|
|
267
425
|
const out = createStreamCollector(options.mirror?.stdout);
|
|
268
426
|
const err = createStreamCollector(options.mirror?.stderr);
|
|
427
|
+
const stdoutLineDecoder = new StringDecoder('utf8');
|
|
428
|
+
const stderrLineDecoder = new StringDecoder('utf8');
|
|
269
429
|
let stdoutBuffer = '';
|
|
270
430
|
let stderrBuffer = '';
|
|
271
431
|
child.stdout.on('data', (data) => {
|
|
272
432
|
out.onData(data);
|
|
273
|
-
stdoutBuffer +=
|
|
433
|
+
stdoutBuffer += stdoutLineDecoder.write(data);
|
|
274
434
|
stdoutBuffer = dispatchCompleteLines(stdoutBuffer, options.onStdoutLine);
|
|
275
435
|
});
|
|
276
436
|
child.stderr.on('data', (data) => {
|
|
277
437
|
err.onData(data);
|
|
278
|
-
stderrBuffer +=
|
|
438
|
+
stderrBuffer += stderrLineDecoder.write(data);
|
|
279
439
|
stderrBuffer = dispatchCompleteLines(stderrBuffer, options.onStderrLine);
|
|
280
440
|
});
|
|
281
441
|
let timedOut = false;
|
|
282
442
|
let graceTimer;
|
|
443
|
+
let signalGraceTimer;
|
|
283
444
|
const signalChildGroup = (signal) => {
|
|
284
|
-
|
|
285
|
-
return;
|
|
286
|
-
const targetPid = child.pid;
|
|
287
|
-
if (targetPid === undefined)
|
|
288
|
-
return;
|
|
289
|
-
try {
|
|
290
|
-
if (usesProcessGroup) {
|
|
291
|
-
// Negative PID routes to the process group, killing the Python
|
|
292
|
-
// wrapper, the firefox it forked, and every content process
|
|
293
|
-
// that inherited the group.
|
|
294
|
-
process.kill(-targetPid, signal);
|
|
295
|
-
}
|
|
296
|
-
else {
|
|
297
|
-
child.kill(signal);
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
catch {
|
|
301
|
-
// Already gone. Nothing to do.
|
|
302
|
-
}
|
|
445
|
+
killProcessTree(child, signal, usesProcessGroup);
|
|
303
446
|
};
|
|
304
447
|
const deadlineTimer = setTimeout(() => {
|
|
305
448
|
timedOut = true;
|
|
@@ -310,16 +453,52 @@ export async function execSmokeRun(command, args, options) {
|
|
|
310
453
|
graceTimer.unref();
|
|
311
454
|
}, options.smokeTimeoutMs);
|
|
312
455
|
deadlineTimer.unref();
|
|
456
|
+
// Forward parent SIGINT/SIGTERM to the whole child process group. The
|
|
457
|
+
// smoke child is a group leader (detached), so it does NOT receive the
|
|
458
|
+
// terminal's Ctrl+C SIGINT — without this forwarder, the parent exits
|
|
459
|
+
// and the entire mach → firefox tree is orphaned. Second signal (or
|
|
460
|
+
// grace expiry) escalates to a group SIGKILL.
|
|
461
|
+
const forwardedSignals = new Set();
|
|
462
|
+
const onParentSignal = (signal) => {
|
|
463
|
+
if (forwardedSignals.has(signal)) {
|
|
464
|
+
signalChildGroup('SIGKILL');
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
forwardedSignals.add(signal);
|
|
468
|
+
signalChildGroup('SIGTERM');
|
|
469
|
+
signalGraceTimer = setTimeout(() => {
|
|
470
|
+
signalChildGroup('SIGKILL');
|
|
471
|
+
}, options.killGraceMs ?? 10000);
|
|
472
|
+
signalGraceTimer.unref();
|
|
473
|
+
};
|
|
474
|
+
const onSigint = () => {
|
|
475
|
+
onParentSignal('SIGINT');
|
|
476
|
+
};
|
|
477
|
+
const onSigterm = () => {
|
|
478
|
+
onParentSignal('SIGTERM');
|
|
479
|
+
};
|
|
480
|
+
process.on('SIGINT', onSigint);
|
|
481
|
+
process.on('SIGTERM', onSigterm);
|
|
482
|
+
const closure = trackChildClosure();
|
|
483
|
+
const cleanupSignalForwarding = () => {
|
|
484
|
+
process.off('SIGINT', onSigint);
|
|
485
|
+
process.off('SIGTERM', onSigterm);
|
|
486
|
+
if (signalGraceTimer)
|
|
487
|
+
clearTimeout(signalGraceTimer);
|
|
488
|
+
closure.settle();
|
|
489
|
+
};
|
|
313
490
|
child.on('error', (error) => {
|
|
314
491
|
clearTimeout(deadlineTimer);
|
|
315
492
|
if (graceTimer)
|
|
316
493
|
clearTimeout(graceTimer);
|
|
494
|
+
cleanupSignalForwarding();
|
|
317
495
|
reject(error);
|
|
318
496
|
});
|
|
319
497
|
child.on('close', (code, signal) => {
|
|
320
498
|
clearTimeout(deadlineTimer);
|
|
321
499
|
if (graceTimer)
|
|
322
500
|
clearTimeout(graceTimer);
|
|
501
|
+
cleanupSignalForwarding();
|
|
323
502
|
// Flush any remaining partial line (child ended without a trailing newline).
|
|
324
503
|
if (stdoutBuffer.length > 0)
|
|
325
504
|
options.onStdoutLine?.(stdoutBuffer);
|
|
@@ -335,24 +514,45 @@ export async function execSmokeRun(command, args, options) {
|
|
|
335
514
|
});
|
|
336
515
|
}
|
|
337
516
|
/**
|
|
338
|
-
*
|
|
339
|
-
*
|
|
340
|
-
*
|
|
517
|
+
* Cap on the partial-line tail kept between chunks. A child emitting one
|
|
518
|
+
* enormous line with no terminator (minified JS dumped to stderr, a raw
|
|
519
|
+
* binary blob) previously grew the buffer without bound — the 50 MB cap
|
|
520
|
+
* only guarded the collector, not these line buffers. When the tail
|
|
521
|
+
* exceeds the cap it is dispatched as a synthetic (oversized) line so the
|
|
522
|
+
* matchers still get to see its content, then the buffer resets.
|
|
523
|
+
*/
|
|
524
|
+
const MAX_PARTIAL_LINE_SIZE = 1024 * 1024;
|
|
525
|
+
/**
|
|
526
|
+
* Drains complete lines from `buffer`, dispatching each to `cb`. Treats
|
|
527
|
+
* `\n`, `\r\n`, and lone `\r` as line terminators — the lone-`\r` case
|
|
528
|
+
* matters for progress-bar style output (mach, cargo) that repaints a line
|
|
529
|
+
* with carriage returns and never sends a newline; before `\r` counted as a
|
|
530
|
+
* terminator those updates accumulated indefinitely. A single trailing `\r`
|
|
531
|
+
* is held back since it may be the first half of a `\r\n` pair split across
|
|
532
|
+
* chunks. Returns the remaining partial line — callers keep accumulating
|
|
533
|
+
* into it.
|
|
341
534
|
*/
|
|
342
535
|
function dispatchCompleteLines(buffer, cb) {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
536
|
+
let searchFrom = 0;
|
|
537
|
+
for (;;) {
|
|
538
|
+
const nl = buffer.indexOf('\n', searchFrom);
|
|
539
|
+
const cr = buffer.indexOf('\r', searchFrom);
|
|
540
|
+
const idx = nl === -1 ? cr : cr === -1 ? nl : Math.min(nl, cr);
|
|
541
|
+
if (idx === -1)
|
|
542
|
+
break;
|
|
543
|
+
if (buffer[idx] === '\r' && idx === buffer.length - 1) {
|
|
544
|
+
// Possible first half of a chunk-split \r\n — wait for the next chunk.
|
|
545
|
+
break;
|
|
546
|
+
}
|
|
547
|
+
const line = buffer.slice(0, idx);
|
|
548
|
+
const terminatorLength = buffer[idx] === '\r' && buffer[idx + 1] === '\n' ? 2 : 1;
|
|
549
|
+
buffer = buffer.slice(idx + terminatorLength);
|
|
550
|
+
searchFrom = 0;
|
|
551
|
+
cb?.(line);
|
|
350
552
|
}
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
buffer = buffer.slice(idx + 1);
|
|
355
|
-
cb(line);
|
|
553
|
+
if (buffer.length > MAX_PARTIAL_LINE_SIZE) {
|
|
554
|
+
cb?.(buffer);
|
|
555
|
+
return '';
|
|
356
556
|
}
|
|
357
557
|
return buffer;
|
|
358
558
|
}
|
|
@@ -388,4 +588,3 @@ export async function executableExists(name) {
|
|
|
388
588
|
const path = await findExecutable(name);
|
|
389
589
|
return path !== undefined;
|
|
390
590
|
}
|
|
391
|
-
//# sourceMappingURL=process.js.map
|
|
@@ -7,9 +7,19 @@ export declare function escapeRegex(str: string): string;
|
|
|
7
7
|
*/
|
|
8
8
|
export declare function hasRawCssColors(content: string): boolean;
|
|
9
9
|
/**
|
|
10
|
-
* Strips JS single-line and multi-line comments from source code, replacing
|
|
11
|
-
* with spaces
|
|
12
|
-
*
|
|
10
|
+
* Strips JS single-line and multi-line comments from source code, replacing
|
|
11
|
+
* comment bytes with spaces (newlines inside block comments are kept) so
|
|
12
|
+
* both character offsets AND line numbers are preserved. String literals
|
|
13
|
+
* are preserved intact.
|
|
14
|
+
*
|
|
15
|
+
* Uses acorn's tokenizer, which understands regex literals — the previous
|
|
16
|
+
* pure-regex implementation did not model them, so two adjacent slashes
|
|
17
|
+
* inside a regex (e.g. the tail of `/https?:\/\//`) were treated as a `//`
|
|
18
|
+
* line comment and the rest of the line was blanked, corrupting whatever
|
|
19
|
+
* lint pass consumed the stripped text. Firefox chrome sources contain
|
|
20
|
+
* preprocessor directives (`#ifdef`) and other constructs acorn cannot
|
|
21
|
+
* tokenize; those fall back to the legacy regex strip, which handles
|
|
22
|
+
* strings but NOT regex literals (documented limitation of the fallback).
|
|
13
23
|
*/
|
|
14
24
|
export declare function stripJsComments(source: string): string;
|
|
15
25
|
/**
|
package/dist/src/utils/regex.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
import { tokenizer as acornTokenizer, tokTypes as acornTokTypes } from 'acorn';
|
|
2
3
|
/**
|
|
3
4
|
* Escapes special regex characters in a string.
|
|
4
5
|
*/
|
|
@@ -22,11 +23,60 @@ export function hasRawCssColors(content) {
|
|
|
22
23
|
return CSS_HEX_COLOR.test(content) || CSS_RGB_COLOR.test(content) || CSS_HSL_COLOR.test(content);
|
|
23
24
|
}
|
|
24
25
|
/**
|
|
25
|
-
* Strips JS single-line and multi-line comments from source code, replacing
|
|
26
|
-
* with spaces
|
|
27
|
-
*
|
|
26
|
+
* Strips JS single-line and multi-line comments from source code, replacing
|
|
27
|
+
* comment bytes with spaces (newlines inside block comments are kept) so
|
|
28
|
+
* both character offsets AND line numbers are preserved. String literals
|
|
29
|
+
* are preserved intact.
|
|
30
|
+
*
|
|
31
|
+
* Uses acorn's tokenizer, which understands regex literals — the previous
|
|
32
|
+
* pure-regex implementation did not model them, so two adjacent slashes
|
|
33
|
+
* inside a regex (e.g. the tail of `/https?:\/\//`) were treated as a `//`
|
|
34
|
+
* line comment and the rest of the line was blanked, corrupting whatever
|
|
35
|
+
* lint pass consumed the stripped text. Firefox chrome sources contain
|
|
36
|
+
* preprocessor directives (`#ifdef`) and other constructs acorn cannot
|
|
37
|
+
* tokenize; those fall back to the legacy regex strip, which handles
|
|
38
|
+
* strings but NOT regex literals (documented limitation of the fallback).
|
|
28
39
|
*/
|
|
29
40
|
export function stripJsComments(source) {
|
|
41
|
+
const comments = [];
|
|
42
|
+
try {
|
|
43
|
+
const t = acornTokenizer(source, {
|
|
44
|
+
ecmaVersion: 'latest',
|
|
45
|
+
sourceType: 'module',
|
|
46
|
+
allowHashBang: true,
|
|
47
|
+
onComment: (_isBlock, _text, start, end) => {
|
|
48
|
+
comments.push({ start, end });
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
// Drive the tokenizer to EOF; we only care about onComment callbacks.
|
|
52
|
+
for (;;) {
|
|
53
|
+
const token = t.getToken();
|
|
54
|
+
if (token.type === acornTokTypes.eof)
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return stripJsCommentsLegacy(source);
|
|
60
|
+
}
|
|
61
|
+
if (comments.length === 0)
|
|
62
|
+
return source;
|
|
63
|
+
const chars = source.split('');
|
|
64
|
+
for (const { start, end } of comments) {
|
|
65
|
+
for (let i = start; i < end; i++) {
|
|
66
|
+
if (chars[i] !== '\n' && chars[i] !== '\r') {
|
|
67
|
+
chars[i] = ' ';
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return chars.join('');
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Legacy pure-regex comment strip, used only when acorn cannot tokenize
|
|
75
|
+
* the source (preprocessor directives, syntax errors). Strings survive;
|
|
76
|
+
* regex literals are NOT modeled, so `//` inside a regex blanks the rest
|
|
77
|
+
* of the line.
|
|
78
|
+
*/
|
|
79
|
+
function stripJsCommentsLegacy(source) {
|
|
30
80
|
return source.replace(/\/\/.*$|\/\*[\s\S]*?\*\/|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`/gm, (match) => (match.startsWith('/') ? ' '.repeat(match.length) : match));
|
|
31
81
|
}
|
|
32
82
|
/**
|
|
@@ -37,4 +87,3 @@ export function countRawCssColors(content) {
|
|
|
37
87
|
(content.match(CSS_RGB_COLOR_G)?.length ?? 0) +
|
|
38
88
|
(content.match(CSS_HSL_COLOR_G)?.length ?? 0));
|
|
39
89
|
}
|
|
40
|
-
//# sourceMappingURL=regex.js.map
|
|
@@ -52,18 +52,6 @@ export declare function isObject(value: unknown): value is Record<string, unknow
|
|
|
52
52
|
* @returns True if value is an array
|
|
53
53
|
*/
|
|
54
54
|
export declare function isArray(value: unknown): value is unknown[];
|
|
55
|
-
/**
|
|
56
|
-
* Asserts that a value is a string or throws.
|
|
57
|
-
* @param value - Value to check
|
|
58
|
-
* @param name - Name of the field for error messages
|
|
59
|
-
*/
|
|
60
|
-
export declare function assertString(value: unknown, name: string): asserts value is string;
|
|
61
|
-
/**
|
|
62
|
-
* Asserts that a value is a non-null object or throws.
|
|
63
|
-
* @param value - Value to check
|
|
64
|
-
* @param name - Name of the field for error messages
|
|
65
|
-
*/
|
|
66
|
-
export declare function assertObject(value: unknown, name: string): asserts value is Record<string, unknown>;
|
|
67
55
|
/**
|
|
68
56
|
* Validates a Firefox version string.
|
|
69
57
|
* Accepts formats like "140.9.0", "140.9.1", "140.9.0esr", "147.0b1"
|