byte-codec 0.0.0 → 1.0.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.
@@ -0,0 +1,254 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ workflow_dispatch:
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ commitlint:
14
+ name: Commitlint
15
+ runs-on: ubuntu-latest
16
+ timeout-minutes: 10
17
+ steps:
18
+ - uses: actions/checkout@v7
19
+ with:
20
+ fetch-depth: 0
21
+ - uses: wagoid/commitlint-github-action@v6
22
+
23
+ lint:
24
+ name: Lint
25
+ runs-on: ubuntu-latest
26
+ timeout-minutes: 10
27
+ steps:
28
+ - uses: actions/checkout@v7
29
+ - uses: pnpm/action-setup@v6
30
+ - uses: actions/setup-node@v7
31
+ with:
32
+ node-version: '22'
33
+ cache: pnpm
34
+ - run: pnpm install --frozen-lockfile
35
+ - run: pnpm lint
36
+
37
+ typecheck:
38
+ name: Typecheck
39
+ runs-on: ubuntu-latest
40
+ timeout-minutes: 10
41
+ steps:
42
+ - uses: actions/checkout@v7
43
+ - uses: pnpm/action-setup@v6
44
+ - uses: actions/setup-node@v7
45
+ with:
46
+ node-version: '22'
47
+ cache: pnpm
48
+ - run: pnpm install --frozen-lockfile
49
+ - run: pnpm typecheck
50
+
51
+ test:
52
+ name: Test
53
+ runs-on: ubuntu-latest
54
+ timeout-minutes: 10
55
+ permissions:
56
+ contents: read
57
+ code-quality: write # to upload the cobertura coverage report below
58
+ steps:
59
+ - uses: actions/checkout@v7
60
+ - uses: pnpm/action-setup@v6
61
+ - uses: actions/setup-node@v7
62
+ with:
63
+ node-version: '22'
64
+ cache: pnpm
65
+ - run: pnpm install --frozen-lockfile
66
+ - run: pnpm test
67
+
68
+ test-smoke:
69
+ name: Smoke test
70
+ runs-on: ubuntu-latest
71
+ timeout-minutes: 10
72
+ steps:
73
+ - uses: actions/checkout@v7
74
+ - uses: pnpm/action-setup@v6
75
+ - uses: actions/setup-node@v7
76
+ with:
77
+ node-version: '22'
78
+ cache: pnpm
79
+ - run: pnpm install --frozen-lockfile
80
+ - run: pnpm build && node -e "require('./dist/index.cjs')" && node --input-type=module -e "import('./dist/index.js').then(m => console.log(Object.keys(m).length + ' exports'))"
81
+
82
+ release:
83
+ name: Release
84
+ needs: [commitlint, lint, typecheck, test, test-smoke]
85
+ if: github.ref == 'refs/heads/main' && github.event_name == 'push'
86
+ runs-on: ubuntu-latest
87
+ timeout-minutes: 15
88
+ permissions:
89
+ contents: write # to push the release commit/tag and create the GitHub Release
90
+ issues: write # to comment on released issues
91
+ pull-requests: write # to comment on released pull requests
92
+ id-token: write # OIDC identity for npm trusted publishing (no NPM_TOKEN)
93
+ outputs:
94
+ published: ${{ steps.before.outputs.version != steps.after.outputs.version }}
95
+ version: ${{ steps.after.outputs.version }}
96
+ steps:
97
+ - uses: actions/checkout@v7
98
+ with:
99
+ # semantic-release analyses the full commit history since the last release.
100
+ fetch-depth: 0
101
+ - uses: pnpm/action-setup@v6
102
+ - uses: actions/setup-node@v7
103
+ with:
104
+ node-version: '22'
105
+ cache: pnpm
106
+ # registry-url is deliberately absent. Setting it makes setup-node write an .npmrc containing an _authToken line, and that line wins over the OIDC token exchange -- so the setting that looks like it configures the registry is exactly the one that would stop trusted publishing working.
107
+ - run: pnpm install --frozen-lockfile
108
+ - name: Read pre-release version
109
+ id: before
110
+ run: echo "version=$(node -p "require('./package.json').version")" >> "$GITHUB_OUTPUT"
111
+ - name: Upgrade npm for OIDC trusted publishing (needs npm CLI >=11.5.1)
112
+ run: npm install -g npm@latest
113
+ - name: Release
114
+ # HUSKY=0 so the commit-msg hook never fires against the automated release commit.
115
+ run: HUSKY=0 pnpm exec semantic-release
116
+ env:
117
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
118
+ # Blanked, not omitted -- an inherited NPM_TOKEN/NODE_AUTH_TOKEN from a workflow-level env block, reusable workflow, or composite action would otherwise be used in preference to the OIDC exchange.
119
+ NPM_TOKEN: ''
120
+ NODE_AUTH_TOKEN: ''
121
+ - name: Read post-release version
122
+ id: after
123
+ run: echo "version=$(node -p "require('./package.json').version")" >> "$GITHUB_OUTPUT"
124
+
125
+ notify-downstream:
126
+ name: Notify downstream repositories
127
+ needs: release
128
+ if: needs.release.outputs.published == 'true'
129
+ runs-on: ubuntu-latest
130
+ timeout-minutes: 5
131
+ permissions:
132
+ contents: read
133
+ steps:
134
+ - name: Generate a token for cross-repo dispatch
135
+ id: app-token
136
+ uses: actions/create-github-app-token@v2
137
+ with:
138
+ app-id: '4473709'
139
+ private-key: ${{ secrets.AUTOMERGE_APP_PRIVATE_KEY }}
140
+ repositories: |
141
+ documents.js
142
+ - name: Dispatch sibling-released event to downstream repos
143
+ env:
144
+ GH_TOKEN: ${{ steps.app-token.outputs.token }}
145
+ run: |
146
+ version="${{ needs.release.outputs.version }}"
147
+ for repo in documents.js; do
148
+ gh api "repos/ExaDev/$repo/dispatches" \
149
+ -f event_type=sibling-released \
150
+ -F "client_payload[package]=byte-codec" \
151
+ -F "client_payload[version]=$version"
152
+ done
153
+
154
+ publish-github-packages:
155
+ name: Publish alias to GitHub Packages
156
+ needs: release
157
+ if: needs.release.outputs.published == 'true'
158
+ runs-on: ubuntu-latest
159
+ timeout-minutes: 10
160
+ permissions:
161
+ contents: read
162
+ packages: write
163
+ steps:
164
+ - uses: actions/checkout@v7
165
+ with:
166
+ ref: main # the release commit semantic-release just pushed
167
+ - uses: pnpm/action-setup@v6
168
+ - uses: actions/setup-node@v7
169
+ with:
170
+ node-version: '22'
171
+ cache: pnpm
172
+ registry-url: 'https://npm.pkg.github.com'
173
+ # Explicit, not inferred: setup-node's scope default only matches the scoped package.json name if package.json is already scoped by the time this step runs, and the rewrite below happens after it.
174
+ scope: '@exadev'
175
+ # GitHub Packages has no OIDC trusted-publishing exchange, so this job (unlike release above) authenticates with a token -- GITHUB_TOKEN is sufficient and needs no separate secret.
176
+ - run: pnpm install --frozen-lockfile
177
+ - run: pnpm build
178
+ - name: Rewrite package name and registry for the GitHub Packages scope
179
+ # GitHub Packages requires the npm package name to be scoped to the repo owner. Rewriting the fields rather than keeping a second package.json means this alias cannot drift away from the real package's metadata. publishConfig.registry has to be overridden too: it takes precedence over the .npmrc registry-url set by setup-node above, so without this the publish silently targets registry.npmjs.org instead -- confirmed failure mode, not a hypothetical (404 on the npmjs.org registry, since GITHUB_TOKEN isn't a credential it recognises).
180
+ run: |
181
+ npm pkg set name="@exadev/byte-codec"
182
+ npm pkg set publishConfig.registry="https://npm.pkg.github.com"
183
+ - run: pnpm publish --access public --no-git-checks
184
+ env:
185
+ NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
186
+
187
+ publish-aliases:
188
+ name: Publish aliases (${{ matrix.name }})
189
+ needs: release
190
+ if: needs.release.outputs.published == 'true'
191
+ runs-on: ubuntu-latest
192
+ timeout-minutes: 10
193
+ permissions:
194
+ contents: read
195
+ id-token: write # OIDC identity for npm trusted publishing -- every leg in this job targets npmjs.org. The GitHub Packages alias is a separate job above specifically so it never holds this permission: pnpm attempts an OIDC exchange whenever id-token: write is available regardless of target registry, and GitHub Packages has no such exchange to attempt -- confirmed the hard way in a sibling repo when this leg used to share that job and failed with "401 Unauthorized - authentication token not provided" even with GITHUB_TOKEN correctly set, because the failed OIDC attempt never fell through to it.
196
+ strategy:
197
+ fail-fast: false
198
+ matrix:
199
+ include:
200
+ - { name: document-bytes, registry: 'https://registry.npmjs.org' }
201
+ steps:
202
+ - uses: actions/checkout@v7
203
+ with:
204
+ ref: main # the release commit semantic-release just pushed
205
+ - uses: pnpm/action-setup@v6
206
+ - uses: actions/setup-node@v7
207
+ with:
208
+ node-version: '22'
209
+ cache: pnpm
210
+ # registry-url is deliberately absent. Setting it makes setup-node write an .npmrc containing an _authToken line, and that line wins over the OIDC exchange -- so the setting that looks like it configures the registry is exactly the one that would break trusted publishing.
211
+ - run: pnpm install --frozen-lockfile
212
+ - run: pnpm build
213
+ - name: Rewrite package name for this alias
214
+ # Rewriting the checked-out package.json rather than keeping a package.json per alias means no alias can drift away from the real package's metadata -- each matrix leg gets its own fresh runner and checkout, so there is no cross-contamination between legs.
215
+ run: npm pkg set name="${{ matrix.name }}"
216
+ - run: pnpm publish --access public --no-git-checks
217
+ env:
218
+ # Blanked, not omitted -- an inherited NODE_AUTH_TOKEN would otherwise be used in preference to the OIDC exchange, matching the release job's own convention above.
219
+ NODE_AUTH_TOKEN: ''
220
+
221
+ attest:
222
+ name: Attest SBOM and build provenance
223
+ needs: release
224
+ if: needs.release.outputs.published == 'true'
225
+ runs-on: ubuntu-latest
226
+ timeout-minutes: 10
227
+ permissions:
228
+ contents: read
229
+ id-token: write
230
+ attestations: write
231
+ steps:
232
+ - uses: actions/checkout@v7
233
+ with:
234
+ ref: main # the release commit semantic-release just pushed
235
+ - uses: pnpm/action-setup@v6
236
+ - uses: actions/setup-node@v7
237
+ with:
238
+ node-version: '22'
239
+ cache: pnpm
240
+ - run: pnpm install --frozen-lockfile
241
+ - run: pnpm build
242
+ # Pack into a directory of its own, separate from dist/ (tsdown's raw build output). The attestation subject has to be the artefact that actually ships -- attesting dist/ itself would mix in files that never leave the repo, producing digests that match nothing a consumer can download.
243
+ - run: pnpm pack --pack-destination release-artifact
244
+ - run: pnpm sbom --sbom-format spdx --prod > release-artifact/sbom.spdx.json
245
+ # attest-sbom/attest-build-provenance are deprecated wrappers around actions/attest; called as two steps (sbom-path present vs. absent) rather than one, since actions/attest's docs don't clearly state whether passing both together attests SBOM and provenance in a single call or SBOM only -- two steps is the unambiguous equivalent of what this replaced.
246
+ - name: Attest SBOM
247
+ uses: actions/attest@v4
248
+ with:
249
+ subject-path: release-artifact/*.tgz
250
+ sbom-path: release-artifact/sbom.spdx.json
251
+ - name: Attest build provenance
252
+ uses: actions/attest@v4
253
+ with:
254
+ subject-path: release-artifact/*.tgz
@@ -0,0 +1,64 @@
1
+ name: Dependabot auto-merge
2
+
3
+ # Two auto-merge policies in one workflow. Sibling ExaDev packages (document-schema.js) merge unconditionally once green -- no cooldown, matching this family's existing convention. Every other (third-party) dependency merges only for a minor or patch bump; a major bump still opens a PR via Dependabot but is left for manual review. Third-party updates additionally carry dependabot.yml's own 7-day cooldown before Dependabot proposes them at all. Auto-merge is enforced by main's own CI actually passing on the PR's head commit -- this workflow only requests the merge, GitHub itself withholds it until checks conclude. The merge itself authenticates via a GitHub App installation token rather than GITHUB_TOKEN, so the resulting push to main triggers downstream workflow runs (ci.yml, release) instead of being silently skipped by GitHub's anti-recursion protection.
4
+
5
+ on:
6
+ # pull_request_target, not pull_request -- a workflow run triggered by dependabot[bot]'s own pull_request
7
+ # event gets NO secret access at all (a hard GitHub Actions security restriction against a compromised
8
+ # dependency update exfiltrating secrets), so secrets.AUTOMERGE_APP_PRIVATE_KEY would resolve empty
9
+ # regardless of how correctly the secret itself is configured. pull_request_target runs using the base
10
+ # branch's own workflow file and normal secret access instead. Safe here specifically because this job
11
+ # never checks out or executes the PR's own code -- it only calls the GitHub API (fetch-metadata, gh pr
12
+ # merge), which is exactly the case pull_request_target's own security guidance calls out as safe.
13
+ pull_request_target:
14
+
15
+ permissions:
16
+ contents: write
17
+ pull-requests: write
18
+
19
+ jobs:
20
+ auto-merge:
21
+ if: github.actor == 'dependabot[bot]'
22
+ runs-on: ubuntu-latest
23
+ timeout-minutes: 10
24
+ steps:
25
+ - name: Fetch Dependabot metadata
26
+ id: metadata
27
+ uses: dependabot/fetch-metadata@v2
28
+ with:
29
+ github-token: "${{ secrets.GITHUB_TOKEN }}"
30
+ - name: Decide whether to auto-merge
31
+ id: decide
32
+ env:
33
+ DEPENDENCY_NAMES: ${{ steps.metadata.outputs.dependency-names }}
34
+ UPDATE_TYPE: ${{ steps.metadata.outputs.update-type }}
35
+ run: |
36
+ siblings="document-schema.js"
37
+ is_sibling=false
38
+ for name in $(echo "$DEPENDENCY_NAMES" | tr ',' ' '); do
39
+ name=$(echo "$name" | xargs)
40
+ for sib in $siblings; do
41
+ if [ "$name" = "$sib" ]; then
42
+ is_sibling=true
43
+ fi
44
+ done
45
+ done
46
+ if [ "$is_sibling" = "true" ]; then
47
+ echo "merge=true" >> "$GITHUB_OUTPUT"
48
+ elif [ "$UPDATE_TYPE" = "version-update:semver-minor" ] || [ "$UPDATE_TYPE" = "version-update:semver-patch" ]; then
49
+ echo "merge=true" >> "$GITHUB_OUTPUT"
50
+ else
51
+ echo "merge=false" >> "$GITHUB_OUTPUT"
52
+ fi
53
+ - name: Generate a token for the merge
54
+ id: app-token
55
+ uses: actions/create-github-app-token@v2
56
+ with:
57
+ app-id: "4473709"
58
+ private-key: ${{ secrets.AUTOMERGE_APP_PRIVATE_KEY }}
59
+ - name: Enable auto-merge
60
+ if: steps.decide.outputs.merge == 'true'
61
+ run: gh pr merge --auto --rebase "$PR_URL"
62
+ env:
63
+ PR_URL: ${{ github.event.pull_request.html_url }}
64
+ GH_TOKEN: ${{ steps.app-token.outputs.token }}
@@ -0,0 +1,174 @@
1
+ name: Sibling dependency instant update
2
+
3
+ # Triggered the moment a sibling ExaDev package this repo depends on publishes a new version -- the publishing repo own ci.yml sends this repository_dispatch event right after semantic-release completes, instead of waiting for Dependabot own daily scheduled scan to notice. Bumps the named dependency, opens a PR, and requests auto-merge. The branch push and the PR-open call both authenticate via a freshly-minted GitHub App installation token, not the default GITHUB_TOKEN -- GITHUB_TOKEN-authenticated actions (a push OR opening a PR) do not cascade into further workflow runs, so a GITHUB_TOKEN-created PR would never trigger this repo own ci.yml pull_request checks at all, leaving nothing for auto-merge to wait on and letting GitHub treat the PR as already mergeable the instant it opens. Even with the App token making that cascade happen for real, there is a second, narrower race: GitHub can take a few seconds to actually register ci.yml own check-runs against the new PR, and calling gh pr merge --auto before any check has registered gets rejected with "Pull request is in clean status" -- confirmed happening in production even after the App-token fix landed. The wait step below polls for ci.yml own Test check-run to actually appear before requesting auto-merge, closing that race. This workflow never pushes directly to main -- the PR still only merges once this repo own CI genuinely passes on it. Note: every repo in this family also enforces a minimumReleaseAge pnpm supply-chain gate (see pnpm-workspace.yaml); pnpm add itself can refuse to install a version published within that window, in which case this workflow run fails cleanly and the bump is left for Dependabot own next daily scan to pick up once the window has passed -- a neutral fallback, not a regression, since that scan would have handled it anyway.
4
+
5
+ on:
6
+ repository_dispatch:
7
+ types: [sibling-released]
8
+ push:
9
+ branches: [main]
10
+ workflow_dispatch: {}
11
+
12
+ permissions:
13
+ contents: write
14
+ pull-requests: write
15
+
16
+ jobs:
17
+ bump-and-open-pr:
18
+ if: github.event_name == 'repository_dispatch'
19
+ runs-on: ubuntu-latest
20
+ timeout-minutes: 10
21
+ steps:
22
+ - uses: actions/checkout@v7
23
+ - uses: pnpm/action-setup@v6
24
+ - uses: actions/setup-node@v7
25
+ with:
26
+ node-version: '22'
27
+ cache: pnpm
28
+ - name: Bump the released dependency
29
+ env:
30
+ PACKAGE: ${{ github.event.client_payload.package }}
31
+ VERSION: ${{ github.event.client_payload.version }}
32
+ run: pnpm add "$PACKAGE@$VERSION"
33
+ - name: Check whether anything actually changed
34
+ id: diff
35
+ run: |
36
+ if git diff --quiet -- package.json pnpm-lock.yaml; then
37
+ echo "changed=false" >> "$GITHUB_OUTPUT"
38
+ else
39
+ echo "changed=true" >> "$GITHUB_OUTPUT"
40
+ fi
41
+ - name: Generate a token for the branch push and PR
42
+ if: steps.diff.outputs.changed == 'true'
43
+ id: app-token
44
+ uses: actions/create-github-app-token@v2
45
+ with:
46
+ app-id: '4473709'
47
+ private-key: ${{ secrets.AUTOMERGE_APP_PRIVATE_KEY }}
48
+ - name: Commit and open a PR
49
+ if: steps.diff.outputs.changed == 'true'
50
+ id: pr
51
+ env:
52
+ GH_TOKEN: ${{ steps.app-token.outputs.token }}
53
+ PACKAGE: ${{ github.event.client_payload.package }}
54
+ VERSION: ${{ github.event.client_payload.version }}
55
+ run: |
56
+ git config user.name "github-actions[bot]"
57
+ git config user.email "github-actions[bot]@users.noreply.github.com"
58
+ branch="sibling-update/${PACKAGE}-${VERSION}"
59
+ git checkout -b "$branch"
60
+ git add package.json pnpm-lock.yaml
61
+ body=$(printf 'Triggered instantly via repository_dispatch when %s published its own release,\nrather than waiting for the next scheduled Dependabot scan.' "$PACKAGE")
62
+ git commit -m "build(deps): bump ${PACKAGE} to ${VERSION}" -m "$body"
63
+ git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git"
64
+ git push origin "$branch"
65
+ pr_url=$(gh pr create --title "build(deps): bump ${PACKAGE} to ${VERSION}" --body "Automatic dependency bump triggered instantly by the ${PACKAGE} release." --base main --head "$branch")
66
+ echo "pr_url=$pr_url" >> "$GITHUB_OUTPUT"
67
+ echo "head_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
68
+ - name: Wait for this repo own real CI to register on the PR
69
+ if: steps.diff.outputs.changed == 'true'
70
+ env:
71
+ GH_TOKEN: ${{ steps.app-token.outputs.token }}
72
+ run: |
73
+ for i in $(seq 1 30); do
74
+ found=$(gh api "repos/${{ github.repository }}/commits/${{ steps.pr.outputs.head_sha }}/check-runs" --jq '[.check_runs[] | select(.name == "Test")] | length')
75
+ if [ "$found" -gt 0 ]; then
76
+ echo "ci.yml own Test check has registered after $((i * 3))s"
77
+ exit 0
78
+ fi
79
+ sleep 3
80
+ done
81
+ echo "::warning::ci.yml own Test check never registered after 90s -- proceeding anyway, but auto-merge may reject the PR as already clean or merge it immediately" >&2
82
+ - name: Enable auto-merge
83
+ if: steps.diff.outputs.changed == 'true'
84
+ env:
85
+ GH_TOKEN: ${{ steps.app-token.outputs.token }}
86
+ run: gh pr merge --auto --rebase "${{ steps.pr.outputs.pr_url }}"
87
+
88
+ heal-stranded-prs:
89
+ # gh pr merge --auto above only ever fires once, right after a PR opens. If enough other commits land on main before this repo's own CI finishes on that PR, the PR's branch falls behind and GitHub marks it CONFLICTING -- and a conflicting PR can never auto-merge, no matter how green its own CI is, with nothing to ever retry it. This job runs on every push to main (the only time a previously-clean PR can newly become conflicting) and regenerates any bump PR this same workflow opened that is now stuck: closes the stale PR/branch and re-runs the identical bump against the new main, rather than attempting a real git rebase -- a plain `pnpm add <package>@<version>` is fully idempotent and deterministic, so there is no conflict to resolve, only a fresh diff to produce.
90
+ if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
91
+ runs-on: ubuntu-latest
92
+ timeout-minutes: 15
93
+ steps:
94
+ - uses: actions/checkout@v7
95
+ - uses: pnpm/action-setup@v6
96
+ - uses: actions/setup-node@v7
97
+ with:
98
+ node-version: '22'
99
+ cache: pnpm
100
+ - name: Generate a token
101
+ id: app-token
102
+ uses: actions/create-github-app-token@v2
103
+ with:
104
+ app-id: '4473709'
105
+ private-key: ${{ secrets.AUTOMERGE_APP_PRIVATE_KEY }}
106
+ - name: Regenerate every conflicting sibling-update PR
107
+ env:
108
+ GH_TOKEN: ${{ steps.app-token.outputs.token }}
109
+ run: |
110
+ git config user.name "github-actions[bot]"
111
+ git config user.email "github-actions[bot]@users.noreply.github.com"
112
+ git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git"
113
+
114
+ # gh pr list's own `mergeable` field never computes a real value (always reports UNKNOWN) -- only a per-PR `gh pr view` triggers GitHub's own mergeable-status computation, and even that is asynchronous. So list candidates in bulk (reliable), then poll each one individually until its real status resolves.
115
+ candidates=$(gh pr list --state open --json number,headRefName \
116
+ --jq '[.[] | select(.headRefName | startswith("sibling-update/"))] | .[]')
117
+ if [ -z "$candidates" ]; then
118
+ echo "No open sibling-update PRs found."
119
+ exit 0
120
+ fi
121
+
122
+ echo "$candidates" | jq -c '.' | while read -r entry; do
123
+ number=$(echo "$entry" | jq -r '.number')
124
+ branch=$(echo "$entry" | jq -r '.headRefName')
125
+
126
+ mergeable="UNKNOWN"
127
+ for _attempt in $(seq 1 10); do
128
+ mergeable=$(gh pr view "$number" --json mergeable --jq '.mergeable')
129
+ if [ "$mergeable" != "UNKNOWN" ]; then
130
+ break
131
+ fi
132
+ sleep 3
133
+ done
134
+
135
+ if [ "$mergeable" != "CONFLICTING" ]; then
136
+ echo "PR #${number} (${branch}) is ${mergeable}, not stranded -- skipping."
137
+ continue
138
+ fi
139
+
140
+ package_version="${branch#sibling-update/}"
141
+ package="${package_version%-*}"
142
+ version="${package_version##*-}"
143
+ echo "Regenerating stranded PR #${number} (${branch}) for ${package}@${version}"
144
+
145
+ gh pr close "$number" --comment "Regenerating -- this branch fell behind main and could no longer auto-merge."
146
+
147
+ git fetch origin main
148
+ git checkout -B "$branch" origin/main
149
+ pnpm add "${package}@${version}"
150
+
151
+ if git diff --quiet -- package.json pnpm-lock.yaml; then
152
+ echo "main already has ${package}@${version} or newer -- nothing left to bump."
153
+ continue
154
+ fi
155
+
156
+ git add package.json pnpm-lock.yaml
157
+ git commit -m "build(deps): bump ${package} to ${version}"
158
+ git push --force origin "$branch"
159
+ pr_url=$(gh pr create --title "build(deps): bump ${package} to ${version}" \
160
+ --body "Regenerated automatically after the original PR fell behind main." \
161
+ --base main --head "$branch")
162
+ head_sha=$(git rev-parse HEAD)
163
+
164
+ for _attempt in $(seq 1 30); do
165
+ found=$(gh api "repos/${{ github.repository }}/commits/${head_sha}/check-runs" \
166
+ --jq '[.check_runs[] | select(.name == "Test")] | length')
167
+ if [ "$found" -gt 0 ]; then
168
+ break
169
+ fi
170
+ sleep 3
171
+ done
172
+
173
+ gh pr merge --auto --rebase "$pr_url"
174
+ done
@@ -0,0 +1 @@
1
+ pnpm exec commitlint --edit "$1"
@@ -0,0 +1,2 @@
1
+ # Lint and auto-fix staged TypeScript files.
2
+ pnpm exec lint-staged
@@ -0,0 +1,2 @@
1
+ # Run the full test suite before push to catch breakage early.
2
+ pnpm test
package/CHANGELOG.md ADDED
@@ -0,0 +1,12 @@
1
+ # 1.0.0 (2026-08-05)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * ci test and smoke steps for byte-codec's script set ([a64531d](https://github.com/ExaDev/byte-codec/commit/a64531df7cec8969ffe2f99cb4b100020b549ad4))
7
+ * ci yaml — remove stray key, fix job name, correct alias ([c1c6291](https://github.com/ExaDev/byte-codec/commit/c1c629166831899be2b07f761fafdd130babbe78))
8
+
9
+
10
+ ### Features
11
+
12
+ * initial byte-codec package — generic byte/image utilities ([698c6ab](https://github.com/ExaDev/byte-codec/commit/698c6abd755e72e40759569a3ffa6b0f2f5d9f21))
package/README.md ADDED
@@ -0,0 +1,43 @@
1
+ # byte-codec
2
+
3
+ [![GitHub](https://img.shields.io/badge/GitHub-181717?logo=github&logoColor=white)](https://github.com/ExaDev/byte-codec) [![npm](https://img.shields.io/badge/npm-CB3837?logo=npm&logoColor=white)](https://www.npmjs.com/package/byte-codec) [![Release](https://img.shields.io/github/v/release/ExaDev/byte-codec)](https://github.com/ExaDev/byte-codec/releases/latest)
4
+
5
+ > Generic byte-level primitives (ByteWriter, ByteReader, CRC-32, deflate/inflate) and PNG/JPEG image encoding/decoding with zero PDF knowledge — the shared utility package for the documents.js family.
6
+
7
+ Extracted from [pdf-codec](https://github.com/ExaDev/pdf-codec), where these utilities lived as a directory-isolated subgraph under `src/bytes/` + `src/image/` with no PDF imports. Both pdf-codec and documents.js consume them from this neutral home rather than one fetching byte utilities from a backend.
8
+
9
+ ## Getting started
10
+
11
+ Requires Node.js `>=20` and pnpm `11.6.0`.
12
+
13
+ ```sh
14
+ pnpm install
15
+ pnpm build # tsdown -> dist/ (ESM + CJS + .d.ts)
16
+ pnpm test # vitest
17
+ pnpm typecheck # tsc --noEmit
18
+ ```
19
+
20
+ ## What it provides
21
+
22
+ | Module | Exports |
23
+ |---|---|
24
+ | `bytes/writer` | `ByteWriter` (chunked growable byte-output builder), `concatBytes` |
25
+ | `bytes/reader` | `ByteReader` (sequential big/little-endian byte reader), `isAsciiWhitespace` |
26
+ | `bytes/crc32` | `crc32` (IEEE 802.3 / ZIP / PNG polynomial table-driven CRC-32) |
27
+ | `bytes/flate` | `deflate`, `inflate`, `inflateTolerant` (fflate-backed DEFLATE compression/decompression with a safety cap) |
28
+ | `image/png-encode` | `encodePng` (raw RGB/RGBA pixels → PNG bytes) |
29
+ | `image/png-decode` | `decodePng` (PNG bytes → raw pixels), `RawImage` |
30
+ | `image/png-filter` | `filterScanlines`, `unfilterScanlines` (the five PNG scanline filters) |
31
+ | `image/jpeg-info` | `readJpegInfo` (JPEG header reader: dimensions, components, progressive flag — no sample decoding) |
32
+
33
+ ## Install
34
+
35
+ ```sh
36
+ pnpm add byte-codec
37
+ # or
38
+ npm install byte-codec
39
+ ```
40
+
41
+ ## License
42
+
43
+ MIT
@@ -0,0 +1,8 @@
1
+ import { commitTypes } from './release.config';
2
+
3
+ export default {
4
+ extends: ['@commitlint/config-conventional'],
5
+ rules: {
6
+ 'type-enum': [2, 'always', commitTypes.map((t) => t.type)],
7
+ },
8
+ };
@@ -0,0 +1,19 @@
1
+ import js from '@eslint/js';
2
+ import tseslint from 'typescript-eslint';
3
+ import globals from 'globals';
4
+
5
+ export default tseslint.config(
6
+ js.configs.recommended,
7
+ ...tseslint.configs.recommended,
8
+ {
9
+ languageOptions: {
10
+ globals: { ...globals.node },
11
+ },
12
+ rules: {
13
+ '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
14
+ },
15
+ },
16
+ {
17
+ ignores: ['dist/**', 'node_modules/**'],
18
+ },
19
+ );
package/package.json CHANGED
@@ -1,5 +1,66 @@
1
1
  {
2
2
  "name": "byte-codec",
3
- "version": "0.0.0",
4
- "private": false
3
+ "version": "1.0.0",
4
+ "description": "Generic byte-level primitives (ByteWriter, ByteReader, CRC-32, deflate/inflate) and PNG/JPEG image encoding/decoding with zero PDF knowledge — the shared utility package for the documents.js family.",
5
+ "type": "module",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/ExaDev/byte-codec.git"
9
+ },
10
+ "homepage": "https://github.com/ExaDev/byte-codec#readme",
11
+ "bugs": {
12
+ "url": "https://github.com/ExaDev/byte-codec/issues"
13
+ },
14
+ "license": "MIT",
15
+ "exports": {
16
+ ".": {
17
+ "types": {
18
+ "import": "./dist/index.d.ts",
19
+ "require": "./dist/index.d.cts"
20
+ },
21
+ "import": "./dist/index.js",
22
+ "require": "./dist/index.cjs"
23
+ },
24
+ "./*": {
25
+ "types": {
26
+ "import": "./dist/*.d.ts",
27
+ "require": "./dist/*.d.cts"
28
+ },
29
+ "import": "./dist/*.js",
30
+ "require": "./dist/*.cjs"
31
+ }
32
+ },
33
+ "scripts": {
34
+ "build": "tsdown",
35
+ "typecheck": "tsc --noEmit",
36
+ "lint": "eslint . --fix --cache --max-warnings 0",
37
+ "test": "vitest run",
38
+ "test:watch": "vitest",
39
+ "prepare": "husky",
40
+ "lint:check": "eslint . --max-warnings 0"
41
+ },
42
+ "packageManager": "pnpm@11.6.0",
43
+ "dependencies": {
44
+ "fflate": "^0.8.2"
45
+ },
46
+ "devDependencies": {
47
+ "@commitlint/cli": "^21.2.1",
48
+ "@commitlint/config-conventional": "^21.2.0",
49
+ "@eslint/js": "^10.0.1",
50
+ "@semantic-release/changelog": "^7.0.0",
51
+ "@semantic-release/git": "^11.0.1",
52
+ "@types/node": "^26.1.2",
53
+ "eslint": "^10.8.0",
54
+ "globals": "^17.8.0",
55
+ "husky": "^9.1.7",
56
+ "lint-staged": "^17.2.0",
57
+ "semantic-release": "^25.0.8",
58
+ "tsdown": "^0.12.5",
59
+ "typescript": "^5.9.3",
60
+ "typescript-eslint": "^8.65.0",
61
+ "vitest": "^4.1.10"
62
+ },
63
+ "lint-staged": {
64
+ "*.ts": "eslint --fix"
65
+ }
5
66
  }