@johnhenry/letterpress 0.2.1 → 0.2.2

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.
@@ -12,3 +12,6 @@ jobs:
12
12
  node-versions: '["26"]'
13
13
  os: '["ubuntu-latest"]'
14
14
  gate-commands: npm test
15
+
16
+ workflow-lint:
17
+ uses: johnhenry/workflows/.github/workflows/workflow-lint.yml@v1
@@ -1,25 +1,22 @@
1
1
  name: Publish to npm
2
2
 
3
+ # Publish model: main is the release branch. A push to main publishes only if
4
+ # package.json's version isn't on npm yet (otherwise a clean no-op), then tags
5
+ # v<version> and creates a GitHub Release. See johnhenry/workflows' README.
3
6
  on:
4
- release:
5
- types: [published]
6
7
  push:
7
- # Redundant with the release trigger above -- see fileable's
8
- # publish.yml / johnhenry/workflows' README for the full incident
9
- # writeup: creating a release very soon after a push/merge to the
10
- # same commit can silently drop the release event for a job whose
11
- # body is uses: <external-repo>/...@v1. This gives a second,
12
- # idempotency-safe chance to fire.
13
- tags:
14
- - "v*"
8
+ branches: [main]
15
9
  workflow_dispatch: {}
16
10
 
11
+ concurrency:
12
+ group: publish-${{ github.ref }}
13
+ cancel-in-progress: false
14
+
17
15
  jobs:
18
16
  publish:
19
17
  permissions:
20
- contents: read
21
- id-token: write # required by the reusable workflow's provenance step --
22
- # see johnhenry/workflows' README for why this must be declared here too.
18
+ contents: write # tag + GitHub Release by-product of npm-publish.yml
19
+ id-token: write # npm provenance
23
20
  uses: johnhenry/workflows/.github/workflows/npm-publish.yml@v1
24
21
  with:
25
22
  node-version: "26"
package/CHANGELOG.md CHANGED
@@ -6,6 +6,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
6
6
 
7
7
  This project will adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) once it reaches 1.0.0.
8
8
 
9
+ ## [0.2.2] - 2026-10-14
10
+
11
+ ### Fixed
12
+
13
+ - 🐛 **Multi-line bodies duplicated earlier lines** (#20). In
14
+ `utility/parse-http-text.mjs`, the line buffer was not cleared after each
15
+ committed body line, so a body of `a\nb\nc` came out as `a\nab\nabc`.
16
+ This affected `createResponse`/`createRoute`/`tagRequest`/`tagResponse`
17
+ (a regression since `0.2.0`; `0.0.3` was correct).
18
+
9
19
  ## [0.2.1] - 2026-10-01
10
20
 
11
21
  ### Fixed
@@ -117,7 +127,7 @@ already behaved) is the one `create-route.mjs` was always meant to have.
117
127
  carried over during the leroute -> `@johnhenry/letterpress` rename --
118
128
  it simply wasn't in the initial `@johnhenry/letterpress` source, a gap
119
129
  in that migration rather than an intentional removal. It was discovered
120
- while porting a downstream consumer, `prism` (a live HTTP request
130
+ while porting a private downstream consumer (a live HTTP request
121
131
  inspector/proxy), whose proxy mode depends on it for rewriting proxied
122
132
  requests/responses. Ported unchanged in behavior as `rewrite.mjs`
123
133
  (`createRewriter`, plus `rewriteRequest`/`rewriteResponse`/`addRule`/
@@ -133,7 +143,7 @@ already behaved) is the one `create-route.mjs` was always meant to have.
133
143
 
134
144
  - ✨ **`router.mount(prefix, subHandler)`**: another `leroute` capability
135
145
  (`create-lerouter.mjs`'s `router.mount`) dropped during the same rename,
136
- found via the same `prism` port -- prism's own top-level router uses it
146
+ found via the same port -- that app's own top-level router uses it
137
147
  to mount its dashboard sub-router under `/inspect`. Delegates every
138
148
  request under `prefix` to `subHandler` with the prefix stripped from the
139
149
  forwarded request's path; `subHandler` may be a plain `(Request) =>
@@ -155,9 +165,9 @@ already behaved) is the one `create-route.mjs` was always meant to have.
155
165
  and forwarded to `defaultHandler` too. This was also present in `leroute`
156
166
  (`router(request, ctx = {})`) and dropped during the rename; brought back
157
167
  specifically so `mount()` can hand a mounted sub-router its `mountPrefix`
158
- (used by `prism` to reconstruct the full request path for display in its
168
+ (used by that app to reconstruct the full request path for display in its
159
169
  captured-request feed, purely cosmetic but a real fidelity gap otherwise)
160
- -- caught by testing `mount()` end-to-end against `prism` itself, not by
170
+ -- caught by testing `mount()` end-to-end against that app itself, not by
161
171
  a direct comparison against the old source this time. Existing single-arg
162
172
  `router(request)` callers are unaffected (`ctx` defaults to `{}`).
163
173
 
package/README.md CHANGED
@@ -265,7 +265,7 @@ rewriter.addRule({ match: { path: "/teapot" }, action: { setStatus: 418 } });
265
265
  > This existed in `leroute` (the package letterpress was renamed from --
266
266
  > see the provenance note near the top of this README) but was dropped
267
267
  > during that rename. It's ported here, discovered missing while porting a
268
- > downstream consumer (`prism`, a request inspector/proxy) that relies on
268
+ > downstream consumer (a private request inspector/proxy) that relies on
269
269
  > it for proxy-mode rewriting -- see CHANGELOG.md.
270
270
 
271
271
  ### `deconstruct`, `cook`
@@ -318,13 +318,6 @@ is designed to pair with a sibling package that does.
318
318
  `Route`-shaped function works too; `leserve` is the tested, documented
319
319
  pairing, not a hard dependency (it's a `devDependency` here, used only in
320
320
  the demo scripts).
321
- - **[`@johnhenry/prism`](https://github.com/johnhenry/prism)** -- a live
322
- HTTP request inspector/proxy that uses `createRouter()` as its top-level
323
- router. Porting it surfaced two real gaps left over from letterpress's
324
- own rename from `leroute`: `router.mount()` (with the `ctx`-forwarding
325
- its `mountPrefix` depends on) and `createRewriter`
326
- (`@johnhenry/letterpress/rewrite`) -- both closed as part of that port,
327
- see CHANGELOG.md.
328
321
 
329
322
  ## 🤝 Contributing
330
323
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@johnhenry/letterpress",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "A tagged-template-string HTTP routing library for defining and matching request handlers with plain JavaScript syntax.",
5
5
  "main": "index.mjs",
6
6
  "types": "types.d.ts",
package/rewrite.mjs CHANGED
@@ -8,7 +8,7 @@
8
8
  * see the README's provenance note) as `createRewriter`, but was dropped
9
9
  * during the leroute -> @johnhenry/letterpress rename. It's ported here
10
10
  * unchanged in behavior, discovered missing while porting a downstream
11
- * consumer (`prism`, a request inspector/proxy) that relies on it for
11
+ * consumer (a private request inspector/proxy) that relies on it for
12
12
  * proxy-mode rewriting.
13
13
  *
14
14
  * Rules format:
@@ -1,6 +1,6 @@
1
1
  import { test } from "node:test";
2
2
  import { strict as assert } from "node:assert";
3
- import { tagRequest, tagResponse } from "../index.mjs";
3
+ import { tagRequest, tagResponse, createResponse } from "../index.mjs";
4
4
 
5
5
  const createReadableStream = (data) =>
6
6
  new ReadableStream({
@@ -251,3 +251,34 @@ hello`;
251
251
  assert.equal(auto.headers.get("content-length"), "5");
252
252
  });
253
253
  });
254
+
255
+ test("multi-line bodies are not duplicated (issue #20)", async (t) => {
256
+ await t.test("tagResponse", async () => {
257
+ const response = await tagResponse`HTTP/1.1 200 OK
258
+
259
+ a
260
+ b
261
+ c`;
262
+ assert.equal(await response.text(), "a\nb\nc");
263
+ });
264
+
265
+ await t.test("tagRequest", async () => {
266
+ const request = await tagRequest()`POST /x HTTP/1.1
267
+
268
+ a
269
+ b
270
+ c`;
271
+ assert.equal(await request.text(), "a\nb\nc");
272
+ });
273
+
274
+ await t.test("createResponse with substitution in a later line", async () => {
275
+ const name = "x";
276
+ const response = await createResponse`HTTP/1.1 200 OK
277
+ Content-Type: text/plain
278
+
279
+ line1
280
+ line2 ${name}
281
+ line3`;
282
+ assert.equal(await response.text(), "line1\nline2 x\nline3");
283
+ });
284
+ });
@@ -142,6 +142,7 @@ export const parseHttpText = async (strings, substitutions, options = {}) => {
142
142
  if (p > 0) {
143
143
  if (zone === "body") {
144
144
  commitBodyLine(lineBuffer, false);
145
+ lineBuffer = "";
145
146
  } else {
146
147
  flushLine();
147
148
  }