Skip to content

refactor to get rid of legacy libraries and legacy js patterns#50

Merged
ndu2 merged 23 commits into
mainfrom
dropXlsOutput
Jul 4, 2026
Merged

refactor to get rid of legacy libraries and legacy js patterns#50
ndu2 merged 23 commits into
mainfrom
dropXlsOutput

Conversation

@ndu2

@ndu2 ndu2 commented Jul 4, 2026

Copy link
Copy Markdown
Owner

No description provided.

ndu and others added 23 commits July 4, 2026 12:07
post/get/graphQL called reject() on non-ok or non-JSON responses but
didn't return, so the chain kept running and tried to parse the body
anyway, potentially resolving after already rejecting.
Replace manual new Promise(...) wrapping and .then/.catch chains with
async/await now that the underlying calls (fetch, throttle, resource
methods) are already promise-based. Behavior is unchanged: rejections
become thrown errors, still caught identically by callers' .catch().
Replace all _.each/_.map/_.filter/_.uniq/_.difference/_.intersection/
_.reduce/_.mapObject/_.object/_.extend/_.isObject/_.isArray/_.max/
_.sortBy calls with native Array/Object equivalents, and remove three
dead `import _ from 'underscore'` statements that had no usages.
shelljs was pulled in for a single call, shell.mkdir('-p', dir), used
three times to create the global/cache/frame directories. Node's
fs.mkdirSync(dir, { recursive: true }) does the same without a shell
subprocess or the extra dependency weight.
The only remaining prompt usage was a single yes/no confirmation
(overwrite existing report file). @inquirer/confirm matches the
@inquirer/core generation already used by checkbox/select, so no
duplicate @inquirer/core install, and drops prompt plus its 14
transitive dependencies.
Replaces the promise-chain waterfall of new-Promise wrappers with a flat
async action. Fixes error-handling defects in the old chain:

- .catch(error => done(error)).then(() => done()) called the parallel
  worker callback twice on failure (merge requests, timelogs)
- report.getIssues() had no rejection handler, hanging the run on error
- .catch(e => reject(e)).then(...) resolved after rejecting and reset
  the project config even on failure

Behavior is unchanged: same stage order, same messages, Cli.x/Cli.error
still terminate the process on fatal errors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The private token was appended to every GET query string and injected
into every POST body in addition to the PRIVATE-TOKEN header. Tokens in
URLs leak into proxy/server logs and shell history; the header alone is
sufficient for both REST endpoints (GraphQL already uses a Bearer
header).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds the first tests for the report aggregation (per-user/per-project
totals, estimate/spent sums, free and half-price label accounting,
sorting, per-day consolidation, column preparation) and for Frame
(new-resource detection, start/stop file round-trip, duration, date
validation). These protect the areas the ongoing refactoring touches
most.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
String throws lose stack traces and break instanceof checks. GitlabClient
errors now include the HTTP method, path, status and (truncated) response
body, so failed syncs report the actual GitLab error instead of a bare
'response not OK'. Cli.error accepts Error instances since commands pass
thrown values straight through. Also guards against responses without a
content-type header.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
frame.write() used unlinkSync + appendFileSync; a crash between the two
lost the frame, and a crash mid-append left a truncated JSON file. Write
to <file>.tmp and rename over the target instead — rename is atomic on
the same filesystem, and FrameCollection only picks up *.json, so a
leftover .tmp can never be parsed as a frame. Also removes the unused
assertFile() helper.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Moves calculate() into src/reporting/stats.js as a standalone
calculateStats(config, report) function; the Output constructor assigns
its result, so subclasses keep reading this.times/this.stats/etc.
unchanged. Billing logic (free/half-price labels, now via
Array.some/includes instead of bitwise |= loops) no longer lives in the
presentation layer, and the specs test the function directly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Report.getProject() and Owner.getGroup() now take the project/group path
as a parameter instead of reading it from config, so the report command
no longer rewrites config 'project' on every loop iteration — that was
only safe because the loop ran with a single runner. Also drops the
unused EventEmitter inheritance from the config class.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
parallel() now takes an async (task) => void worker (plain sync
functions also work); FrameCollection/ReportCollection.forEach pass the
same contract through. Removes the try/done boilerplate in every caller
and fixes the remaining double-callback in Report.process(), where
getStats() failures invoked done twice via .catch(done).then(done).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Class declarations now follow the PascalCase convention already used by
GitlabClient/Output/Timekeeper (default exports, so no import changes;
output classes are named TableOutput/CsvOutput/etc. to avoid clashing
with their Table/Csv imports). ReportCollection's module-level projlist
becomes an instance property, so the de-duplication no longer leaks
across collection instances.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
All date handling goes through src/core/dayjs.js, which configures the
utc, timezone, relativeTime (fromNow) and advancedFormat ('Do' token)
plugins once. moment is in maintenance mode and was the heaviest
dependency in the pkg bundle; dayjs is API-compatible for everything we
use (format/startOf/endOf/subtract/add/diff/isValid/isSame/isBefore/
tz/clone/fromNow, ISO JSON serialization, Sunday week start). The
GraphQL date format 'Y-M-D' becomes 'YYYY-M-D' since single 'Y' is not
a dayjs token; moment-only suppressDeprecationWarnings knobs are gone.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Only eachLimit was used; parallel() now runs N runner loops pulling
from a shared queue, stops starting new tasks after the first failure
and rejects with that error — same contract as before, one dependency
less. Covered by a new spec (all tasks processed, concurrency bound,
fail-fast, sync workers, empty input).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
colors is unmaintained (and had the 2022 sabotage release); it also
worked by patching String.prototype, which let several files use .red/
.magenta without importing anything. picocolors is explicit function
calls (pc.red(x)), so every consumer now imports it directly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
read-yaml was a decade-old wrapper; FileConfig now uses js-yaml's load
with fs.readFileSync directly (same semantics, including undefined for
empty files). node-spinner 0.0.4 is replaced by a four-frame local
spinner in cli.js — the only thing it was used for.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ndu2 ndu2 merged commit b1666f3 into main Jul 4, 2026
1 check passed
@ndu2 ndu2 deleted the dropXlsOutput branch July 4, 2026 10:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant