Merge commit '36c66dd290d3ce6eb1ccd310d0c658d4a32bb8eb' as 'repos/effect'
This commit is contained in:
226
repos/effect/packages/tools/bundle/README.md
Normal file
226
repos/effect/packages/tools/bundle/README.md
Normal file
@@ -0,0 +1,226 @@
|
||||
# Bundle Size Development Workflow
|
||||
|
||||
This package contains the internal bundle-size tooling used to measure Effect
|
||||
entrypoints with Rollup, minification, and gzip.
|
||||
|
||||
## AI Agent Instructions
|
||||
|
||||
Use this workflow when a user wants to quickly understand how a local source
|
||||
change affects bundle size for one or more explicitly selected fixture files.
|
||||
|
||||
Do not scan `scratchpad/` or `packages/tools/bundle/fixtures/` for this workflow.
|
||||
Only measure files that the user explicitly names, or files you create
|
||||
specifically for the current investigation.
|
||||
|
||||
Before running a comparison, ask the user which mode they want:
|
||||
|
||||
- default cleanup mode for a single check or one-off investigation;
|
||||
- keep-base mode for many repeated measurements, followed by explicit cleanup at
|
||||
the end.
|
||||
|
||||
Use cleanup mode unless the user says they expect to run multiple trials.
|
||||
|
||||
If the user asks what a generated bundle is made of, use the bundle composition
|
||||
workflow instead of the size comparison workflow.
|
||||
|
||||
### Analyze Bundle Composition
|
||||
|
||||
Use this workflow when the user gives an explicit fixture and wants to
|
||||
understand which modules make up the produced bundle.
|
||||
|
||||
Agent procedure:
|
||||
|
||||
1. Confirm the exact fixture path or paths to analyze. Do not scan directories.
|
||||
2. Run `pnpm bundle-analyze` with only those explicit paths.
|
||||
3. Read the printed Markdown table and identify the generated `*.raw-data.json`
|
||||
path for each fixture.
|
||||
4. Analyze `*.raw-data.json` first. Use `*.treemap.html` only when the user wants
|
||||
a visual artifact to open.
|
||||
5. Report the largest modules, notable dependency groups, and any surprising
|
||||
inclusions. Mention the generated artifact paths in the answer.
|
||||
|
||||
Prefer the repository-level wrapper:
|
||||
|
||||
```sh
|
||||
pnpm bundle-analyze scratchpad/my-fixture.ts
|
||||
```
|
||||
|
||||
You can pass multiple explicit files:
|
||||
|
||||
```sh
|
||||
pnpm bundle-analyze \
|
||||
scratchpad/schema-codec.ts \
|
||||
scratchpad/schema-arbitrary.ts
|
||||
```
|
||||
|
||||
The wrapper builds the current checkout with `pnpm build` before generating
|
||||
analysis artifacts, so the bundle is not produced from stale `dist` files.
|
||||
|
||||
By default, artifacts are written to `tmp/bundle-analysis`. Use `--output-dir`
|
||||
to choose another directory:
|
||||
|
||||
```sh
|
||||
pnpm bundle-analyze --output-dir tmp/schema-analysis scratchpad/my-fixture.ts
|
||||
```
|
||||
|
||||
For each selected fixture, the tool writes:
|
||||
|
||||
- `<name>.min.js`: the generated Rollup output for inspection;
|
||||
- `<name>.treemap.html`: an interactive treemap for human inspection;
|
||||
- `<name>.raw-data.json`: raw visualizer data suitable for AI analysis.
|
||||
|
||||
The analysis build disables identifier mangling so module and export names stay
|
||||
readable in the visualizer output. Use `bundle-compare-selected` or `report`
|
||||
when exact size measurement is the goal.
|
||||
|
||||
After running the command, inspect the Markdown table it prints to find the exact
|
||||
paths. For an AI analysis, read the `*.raw-data.json` file first and summarize
|
||||
the largest modules and dependency groups. Use the `*.treemap.html` file when
|
||||
the user wants to inspect the bundle visually.
|
||||
|
||||
The raw data contains a tree plus module metadata. For a quick size-oriented
|
||||
summary, rank module parts by `renderedLength` or `gzipLength`, then map each
|
||||
part through `metaUid` to `nodeMetas[metaUid].id`.
|
||||
|
||||
Example inspection command:
|
||||
|
||||
```sh
|
||||
node -e 'const data = JSON.parse(require("node:fs").readFileSync(process.argv[1], "utf8")); console.log(Object.entries(data.nodeParts).map(([uid, part]) => ({ uid, id: data.nodeMetas[part.metaUid]?.id, rendered: part.renderedLength, gzip: part.gzipLength })).sort((a, b) => b.rendered - a.rendered).slice(0, 20))' tmp/bundle-analysis/my-fixture.raw-data.json
|
||||
```
|
||||
|
||||
Do not treat the generated `.min.js` size from this workflow as the exact bundle
|
||||
size comparison number. The analysis build keeps names readable for the
|
||||
visualizer. Use `bundle-compare-selected` or `report` for exact size reporting.
|
||||
|
||||
Do not use this workflow to compare against a base ref. Use
|
||||
`bundle-compare-selected` for size impact and `bundle-analyze` for current bundle
|
||||
composition.
|
||||
|
||||
### Compare Explicit Scratchpad Fixtures
|
||||
|
||||
Prefer the repository-level wrapper:
|
||||
|
||||
```sh
|
||||
pnpm bundle-compare-selected --base main scratchpad/my-fixture.ts
|
||||
```
|
||||
|
||||
You can pass multiple explicit files:
|
||||
|
||||
```sh
|
||||
pnpm bundle-compare-selected --base main \
|
||||
scratchpad/schema-codec.ts \
|
||||
scratchpad/schema-arbitrary.ts
|
||||
```
|
||||
|
||||
If `--base` is omitted, the script compares against `main`.
|
||||
|
||||
By default, the wrapper removes `tmp/bundle-base` before exiting, including on
|
||||
failure. This avoids leaving extra git worktree state after a one-off
|
||||
measurement.
|
||||
|
||||
For repeated measurements where the user explicitly wants a faster feedback loop,
|
||||
pass `--keep-base`:
|
||||
|
||||
```sh
|
||||
pnpm bundle-compare-selected --base main --keep-base scratchpad/my-fixture.ts
|
||||
```
|
||||
|
||||
When using `--keep-base`, clean up `tmp/bundle-base` after the last measurement.
|
||||
The explicit final cleanup command is:
|
||||
|
||||
```sh
|
||||
git worktree remove --force tmp/bundle-base
|
||||
```
|
||||
|
||||
The wrapper:
|
||||
|
||||
- builds the current checkout with `pnpm build`;
|
||||
- creates or reuses `tmp/bundle-base` at the requested base ref;
|
||||
- builds the base checkout when needed and writes
|
||||
`tmp/bundle-base/.bundle-build-stamp` only after a successful build;
|
||||
- reuses a kept base checkout only when its HEAD and `.bundle-build-stamp`
|
||||
both match the requested base ref;
|
||||
- copies only the selected files into the base checkout at the same relative
|
||||
paths;
|
||||
- invokes the internal bundle CLI to compare current vs base sizes.
|
||||
|
||||
The selected files are copied into the base checkout so the report isolates
|
||||
source changes instead of changes to the fixture text.
|
||||
|
||||
Expected output is a Markdown table:
|
||||
|
||||
```md
|
||||
| File Name | Current Size | Previous Size | Difference |
|
||||
| :------------------------- | :----------: | :-----------: | :---------------: |
|
||||
| `scratchpad/my-fixture.ts` | 42.10 KB | 40.80 KB | +1.30 KB (+3.19%) |
|
||||
```
|
||||
|
||||
### Internal CLI
|
||||
|
||||
Use the internal CLI directly only when the base checkout is already prepared
|
||||
and built:
|
||||
|
||||
```sh
|
||||
node packages/tools/bundle/src/bin.ts compare-selected \
|
||||
--base-dir tmp/bundle-base \
|
||||
scratchpad/my-fixture.ts
|
||||
```
|
||||
|
||||
The `--base-dir` value must be the root of the base checkout, not the base
|
||||
fixture directory.
|
||||
|
||||
### Current Size Only
|
||||
|
||||
If the user only asks for the current bundled size, use:
|
||||
|
||||
```sh
|
||||
pnpm --dir packages/tools/bundle report ../../../scratchpad/my-fixture.ts
|
||||
```
|
||||
|
||||
This does not compare against a base ref.
|
||||
|
||||
### Fixture Guidance
|
||||
|
||||
Keep temporary fixtures in `scratchpad/`.
|
||||
|
||||
Good temporary fixtures are small, focused entrypoints that import public
|
||||
package APIs:
|
||||
|
||||
```ts
|
||||
import * as Effect from "effect/Effect"
|
||||
import * as Schema from "effect/Schema"
|
||||
|
||||
const schema = Schema.Struct({
|
||||
name: Schema.String
|
||||
})
|
||||
|
||||
Schema.decodeUnknownEffect(schema)({ name: "effect" }).pipe(Effect.runFork)
|
||||
```
|
||||
|
||||
Prefer self-contained fixtures. The compare-selected workflow copies only the
|
||||
explicit entry files into the base checkout. If a fixture imports local relative
|
||||
helpers, either avoid that shape or make the entry fixture self-contained before
|
||||
measuring.
|
||||
|
||||
Do not add temporary fixtures to `packages/tools/bundle/fixtures/`. That
|
||||
directory is for stable comparison fixtures used by the regular bundle-size
|
||||
workflow.
|
||||
|
||||
### Cleanup
|
||||
|
||||
The wrapper cleans up `tmp/bundle-base` by default. If `--keep-base` was used for
|
||||
a multi-run investigation, remove the worktree when the user is done.
|
||||
|
||||
To remove it:
|
||||
|
||||
```sh
|
||||
git worktree remove --force tmp/bundle-base
|
||||
```
|
||||
|
||||
To verify that cleanup succeeded:
|
||||
|
||||
```sh
|
||||
git worktree list
|
||||
```
|
||||
|
||||
Only the main repository worktree should remain.
|
||||
4
repos/effect/packages/tools/bundle/docgen.json
Normal file
4
repos/effect/packages/tools/bundle/docgen.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"$schema": "../../../node_modules/@effect/docgen/schema.json",
|
||||
"exclude": ["src/bin.ts"]
|
||||
}
|
||||
3
repos/effect/packages/tools/bundle/fixtures/basic.ts
Normal file
3
repos/effect/packages/tools/bundle/fixtures/basic.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import * as Effect from "effect/Effect"
|
||||
|
||||
Effect.succeed(123).pipe(Effect.runFork)
|
||||
25
repos/effect/packages/tools/bundle/fixtures/batching.ts
Normal file
25
repos/effect/packages/tools/bundle/fixtures/batching.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import * as Array from "effect/Array"
|
||||
import * as Effect from "effect/Effect"
|
||||
import * as Exit from "effect/Exit"
|
||||
import * as Request from "effect/Request"
|
||||
import * as Resolver from "effect/RequestResolver"
|
||||
|
||||
class GetNameById extends Request.TaggedClass("GetNameById")<{
|
||||
readonly id: number
|
||||
}, string> {}
|
||||
|
||||
const UserResolver = Resolver.make<GetNameById>((entries) =>
|
||||
Effect.sync(() => {
|
||||
for (const entry of entries) {
|
||||
entry.completeUnsafe(Exit.succeed(`User ${entry.request.id}`))
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
const effect = Effect.forEach(
|
||||
Array.range(1, 100_000),
|
||||
(id) => Effect.request(new GetNameById({ id }), UserResolver),
|
||||
{ concurrency: "unbounded" }
|
||||
)
|
||||
|
||||
Effect.runFork(effect)
|
||||
5
repos/effect/packages/tools/bundle/fixtures/brand.ts
Normal file
5
repos/effect/packages/tools/bundle/fixtures/brand.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import * as Brand from "effect/Brand"
|
||||
import * as Schema from "effect/Schema"
|
||||
|
||||
type Positive = number & Brand.Brand<"Positive">
|
||||
const Positive = Brand.check<Positive>(Schema.isGreaterThan(0))
|
||||
10
repos/effect/packages/tools/bundle/fixtures/cache.ts
Normal file
10
repos/effect/packages/tools/bundle/fixtures/cache.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import * as Cache from "effect/Cache"
|
||||
import * as Effect from "effect/Effect"
|
||||
|
||||
Cache.make({
|
||||
capacity: 1024,
|
||||
lookup: (key: string) => Effect.succeed(key)
|
||||
}).pipe(
|
||||
Effect.flatMap(Cache.get("1")),
|
||||
Effect.runFork
|
||||
)
|
||||
13
repos/effect/packages/tools/bundle/fixtures/config.ts
Normal file
13
repos/effect/packages/tools/bundle/fixtures/config.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import * as Config from "effect/Config"
|
||||
import * as Effect from "effect/Effect"
|
||||
import * as Schema from "effect/Schema"
|
||||
|
||||
const schema = Schema.Struct({
|
||||
API_KEY: Schema.String,
|
||||
PORT: Schema.Int,
|
||||
LOCALHOST: Schema.URL
|
||||
})
|
||||
|
||||
const config = Config.schema(schema)
|
||||
|
||||
Effect.runFork(config)
|
||||
9
repos/effect/packages/tools/bundle/fixtures/differ.ts
Normal file
9
repos/effect/packages/tools/bundle/fixtures/differ.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import * as Schema from "effect/Schema"
|
||||
|
||||
const schema = Schema.Struct({
|
||||
id: Schema.Number,
|
||||
name: Schema.String,
|
||||
price: Schema.Number
|
||||
})
|
||||
|
||||
Schema.toDifferJsonPatch(schema)
|
||||
12
repos/effect/packages/tools/bundle/fixtures/http-client.ts
Normal file
12
repos/effect/packages/tools/bundle/fixtures/http-client.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import * as Effect from "effect/Effect"
|
||||
import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"
|
||||
import * as HttpClient from "effect/unstable/http/HttpClient"
|
||||
|
||||
Effect.gen(function*() {
|
||||
const client = yield* HttpClient.HttpClient
|
||||
const res = yield* client.get("https://jsonplaceholder.typicode.com/posts/1")
|
||||
yield* res.json
|
||||
}).pipe(
|
||||
Effect.provide(FetchHttpClient.layer),
|
||||
Effect.runPromise
|
||||
)
|
||||
5
repos/effect/packages/tools/bundle/fixtures/logger.ts
Normal file
5
repos/effect/packages/tools/bundle/fixtures/logger.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import * as Effect from "effect/Effect"
|
||||
|
||||
Effect.log("hello").pipe(
|
||||
Effect.runFork
|
||||
)
|
||||
13
repos/effect/packages/tools/bundle/fixtures/metric.ts
Normal file
13
repos/effect/packages/tools/bundle/fixtures/metric.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import * as Effect from "effect/Effect"
|
||||
import * as Metric from "effect/Metric"
|
||||
|
||||
const program = Effect.gen(function*() {
|
||||
yield* Effect.succeed(1).pipe(
|
||||
Effect.forkChild({ startImmediately: true })
|
||||
)
|
||||
})
|
||||
|
||||
program.pipe(
|
||||
Metric.enableRuntimeMetrics,
|
||||
Effect.runFork
|
||||
)
|
||||
6
repos/effect/packages/tools/bundle/fixtures/optic.ts
Normal file
6
repos/effect/packages/tools/bundle/fixtures/optic.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import * as Optic from "effect/Optic"
|
||||
|
||||
type S = { readonly a: number }
|
||||
const optic = Optic.id<S>().key("a")
|
||||
|
||||
optic.getResult({ a: 1 })
|
||||
22
repos/effect/packages/tools/bundle/fixtures/pubsub.ts
Normal file
22
repos/effect/packages/tools/bundle/fixtures/pubsub.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import * as Effect from "effect/Effect"
|
||||
import * as PubSub from "effect/PubSub"
|
||||
|
||||
const program = Effect.gen(function*() {
|
||||
const pubsub = yield* PubSub.unbounded<number>()
|
||||
|
||||
yield* Effect.gen(function*() {
|
||||
const subscription = yield* PubSub.subscribe(pubsub)
|
||||
while (true) {
|
||||
const element = yield* PubSub.take(subscription)
|
||||
console.log(element)
|
||||
}
|
||||
}).pipe(Effect.forkScoped({ startImmediately: true }))
|
||||
|
||||
yield* PubSub.publishAll(pubsub, [1, 2])
|
||||
yield* PubSub.publishAll(pubsub, [3, 4]).pipe(Effect.delay("100 millis"), Effect.forkScoped)
|
||||
yield* PubSub.publishAll(pubsub, [5, 6, 7, 8]).pipe(Effect.delay("200 millis"), Effect.forkScoped)
|
||||
|
||||
yield* Effect.sleep("500 millis")
|
||||
})
|
||||
|
||||
Effect.runFork(Effect.scoped(program))
|
||||
18
repos/effect/packages/tools/bundle/fixtures/queue.ts
Normal file
18
repos/effect/packages/tools/bundle/fixtures/queue.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import * as Effect from "effect/Effect"
|
||||
import * as Queue from "effect/Queue"
|
||||
|
||||
const program = Effect.gen(function*() {
|
||||
const queue = yield* Queue.make<number>()
|
||||
|
||||
yield* Effect.gen(function*() {
|
||||
yield* Queue.takeN(queue, 3)
|
||||
}).pipe(Effect.forever, Effect.forkScoped)
|
||||
|
||||
yield* Queue.offerAll(queue, [1, 2])
|
||||
yield* Queue.offerAll(queue, [3, 4]).pipe(Effect.delay("100 millis"), Effect.forkScoped)
|
||||
yield* Queue.offerAll(queue, [5, 6, 7, 8]).pipe(Effect.delay("200 millis"), Effect.forkScoped)
|
||||
|
||||
yield* Effect.sleep("500 millis")
|
||||
})
|
||||
|
||||
Effect.runFork(Effect.scoped(program))
|
||||
9
repos/effect/packages/tools/bundle/fixtures/schedule.ts
Normal file
9
repos/effect/packages/tools/bundle/fixtures/schedule.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import * as Effect from "effect/Effect"
|
||||
import * as Schedule from "effect/Schedule"
|
||||
|
||||
Effect.succeed(123).pipe(
|
||||
Effect.repeat({
|
||||
schedule: Schedule.spaced("100 millis")
|
||||
}),
|
||||
Effect.runFork
|
||||
)
|
||||
12
repos/effect/packages/tools/bundle/fixtures/schema-class.ts
Normal file
12
repos/effect/packages/tools/bundle/fixtures/schema-class.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import * as Effect from "effect/Effect"
|
||||
import * as Schema from "effect/Schema"
|
||||
|
||||
class A extends Schema.Class<A>("A")({
|
||||
a: Schema.String,
|
||||
b: Schema.optional(Schema.FiniteFromString),
|
||||
c: Schema.Array(Schema.String)
|
||||
}) {}
|
||||
|
||||
Schema.decodeUnknownEffect(A)({ a: "a", b: 1, c: ["c"] }).pipe(
|
||||
Effect.runFork
|
||||
)
|
||||
@@ -0,0 +1,16 @@
|
||||
import * as SchemaRepresentation from "effect/SchemaRepresentation"
|
||||
|
||||
const doc = SchemaRepresentation.fromJsonSchemaDocument({
|
||||
"dialect": "draft-2020-12",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"a": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {}
|
||||
})
|
||||
|
||||
console.dir(doc, { depth: null })
|
||||
@@ -0,0 +1,16 @@
|
||||
import * as Schema from "effect/Schema"
|
||||
import * as SchemaRepresentation from "effect/SchemaRepresentation"
|
||||
|
||||
const schema = Schema.toCodecJson(Schema.Struct({
|
||||
a: Schema.String,
|
||||
b: Schema.optional(Schema.FiniteFromString),
|
||||
c: Schema.Array(Schema.String)
|
||||
}))
|
||||
|
||||
const json = Schema.encodeSync(SchemaRepresentation.DocumentFromJson)(
|
||||
SchemaRepresentation.fromAST(schema.ast)
|
||||
)
|
||||
|
||||
SchemaRepresentation.toSchema(
|
||||
Schema.decodeSync(SchemaRepresentation.DocumentFromJson)(JSON.parse(JSON.stringify(json)))
|
||||
)
|
||||
@@ -0,0 +1,20 @@
|
||||
import * as Duration from "effect/Duration"
|
||||
import * as Effect from "effect/Effect"
|
||||
import * as Schema from "effect/Schema"
|
||||
import * as SchemaTransformation from "effect/SchemaTransformation"
|
||||
|
||||
const schema = Schema.String.pipe(Schema.decodeTo(
|
||||
Schema.String,
|
||||
SchemaTransformation.transformOrFail({
|
||||
decode: (s) =>
|
||||
Effect.gen(function*() {
|
||||
yield* Effect.clockWith((clock) => clock.sleep(Duration.millis(300)))
|
||||
return s
|
||||
}),
|
||||
encode: (_) => Effect.succeed(_)
|
||||
})
|
||||
))
|
||||
|
||||
Schema.decodeUnknownEffect(schema)({ a: "a", b: 1, c: ["c"] }).pipe(
|
||||
Effect.runFork
|
||||
)
|
||||
@@ -0,0 +1,8 @@
|
||||
import * as Effect from "effect/Effect"
|
||||
import * as Schema from "effect/Schema"
|
||||
|
||||
const schema = Schema.String
|
||||
|
||||
Schema.decodeUnknownEffect(schema)("a").pipe(
|
||||
Effect.runFork
|
||||
)
|
||||
@@ -0,0 +1,8 @@
|
||||
import * as Effect from "effect/Effect"
|
||||
import * as Schema from "effect/Schema"
|
||||
|
||||
const schema = Schema.TemplateLiteral(["a", Schema.String])
|
||||
|
||||
Schema.decodeUnknownEffect(schema)("abc").pipe(
|
||||
Effect.runFork
|
||||
)
|
||||
@@ -0,0 +1,9 @@
|
||||
import * as Schema from "effect/Schema"
|
||||
|
||||
const schema = Schema.Struct({
|
||||
a: Schema.String,
|
||||
b: Schema.optional(Schema.FiniteFromString),
|
||||
c: Schema.Array(Schema.String)
|
||||
})
|
||||
|
||||
Schema.toArbitraryLazy(schema)
|
||||
@@ -0,0 +1,12 @@
|
||||
import * as Schema from "effect/Schema"
|
||||
import * as SchemaRepresentation from "effect/SchemaRepresentation"
|
||||
|
||||
const schema = Schema.Struct({
|
||||
a: Schema.String,
|
||||
b: Schema.optional(Schema.FiniteFromString),
|
||||
c: Schema.Array(Schema.String)
|
||||
})
|
||||
|
||||
const representation = Schema.toRepresentation(schema)
|
||||
|
||||
SchemaRepresentation.toCodeDocument(SchemaRepresentation.toMultiDocument(representation))
|
||||
@@ -0,0 +1,9 @@
|
||||
import * as Schema from "effect/Schema"
|
||||
|
||||
const schema = Schema.Struct({
|
||||
a: Schema.String,
|
||||
b: Schema.optional(Schema.FiniteFromString),
|
||||
c: Schema.Array(Schema.String)
|
||||
})
|
||||
|
||||
Schema.toCodecJson(schema)
|
||||
@@ -0,0 +1,9 @@
|
||||
import * as Schema from "effect/Schema"
|
||||
|
||||
const schema = Schema.Struct({
|
||||
a: Schema.String,
|
||||
b: Schema.optional(Schema.FiniteFromString),
|
||||
c: Schema.Array(Schema.String)
|
||||
})
|
||||
|
||||
Schema.toEquivalence(schema)
|
||||
@@ -0,0 +1,9 @@
|
||||
import * as Schema from "effect/Schema"
|
||||
|
||||
const schema = Schema.Struct({
|
||||
a: Schema.String,
|
||||
b: Schema.optional(Schema.FiniteFromString),
|
||||
c: Schema.Array(Schema.String)
|
||||
})
|
||||
|
||||
Schema.toFormatter(schema)
|
||||
@@ -0,0 +1,9 @@
|
||||
import * as Schema from "effect/Schema"
|
||||
|
||||
const schema = Schema.Struct({
|
||||
a: Schema.String,
|
||||
b: Schema.optional(Schema.FiniteFromString),
|
||||
c: Schema.Array(Schema.String)
|
||||
})
|
||||
|
||||
Schema.toJsonSchemaDocument(schema)
|
||||
@@ -0,0 +1,9 @@
|
||||
import * as Schema from "effect/Schema"
|
||||
|
||||
const schema = Schema.Struct({
|
||||
a: Schema.String,
|
||||
b: Schema.optional(Schema.FiniteFromString),
|
||||
c: Schema.Array(Schema.String)
|
||||
})
|
||||
|
||||
Schema.toRepresentation(schema)
|
||||
12
repos/effect/packages/tools/bundle/fixtures/schema.ts
Normal file
12
repos/effect/packages/tools/bundle/fixtures/schema.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import * as Effect from "effect/Effect"
|
||||
import * as Schema from "effect/Schema"
|
||||
|
||||
const schema = Schema.Struct({
|
||||
a: Schema.String,
|
||||
b: Schema.optional(Schema.FiniteFromString),
|
||||
c: Schema.Array(Schema.String)
|
||||
})
|
||||
|
||||
Schema.decodeUnknownEffect(schema)({ a: "a", b: 1, c: ["c"] }).pipe(
|
||||
Effect.runFork
|
||||
)
|
||||
21
repos/effect/packages/tools/bundle/fixtures/stm.ts
Normal file
21
repos/effect/packages/tools/bundle/fixtures/stm.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import * as Effect from "effect/Effect"
|
||||
import * as TxRef from "effect/TxRef"
|
||||
|
||||
const program = Effect.gen(function*() {
|
||||
const ref = yield* TxRef.make(0)
|
||||
|
||||
yield* Effect.forkChild(Effect.forever(
|
||||
TxRef.update(ref, (n) => n + 1).pipe(Effect.delay("100 millis"))
|
||||
))
|
||||
|
||||
yield* Effect.tx(Effect.gen(function*() {
|
||||
const value = yield* TxRef.get(ref)
|
||||
if (value < 10) {
|
||||
yield* Effect.log(`retry due to value: ${value}`)
|
||||
return yield* Effect.txRetry
|
||||
}
|
||||
yield* Effect.log(`transaction done with value: ${value}`)
|
||||
}))
|
||||
})
|
||||
|
||||
Effect.runPromise(program).catch(console.error)
|
||||
7
repos/effect/packages/tools/bundle/fixtures/stream.ts
Normal file
7
repos/effect/packages/tools/bundle/fixtures/stream.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import * as Effect from "effect/Effect"
|
||||
import * as Stream from "effect/Stream"
|
||||
|
||||
Stream.range(1, 100_000).pipe(
|
||||
Stream.runDrain,
|
||||
Effect.runSync
|
||||
)
|
||||
51
repos/effect/packages/tools/bundle/package.json
Normal file
51
repos/effect/packages/tools/bundle/package.json
Normal file
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"name": "@effect/bundle",
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"description": "Bundle size testing infrastructure for Effect packages",
|
||||
"homepage": "https://effect.website",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Effect-TS/effect.git",
|
||||
"directory": "packages/tools/bundle"
|
||||
},
|
||||
"sideEffects": [],
|
||||
"bin": {
|
||||
"effect-bundle": "./src/bin.ts"
|
||||
},
|
||||
"exports": {
|
||||
"./package.json": "./package.json",
|
||||
"./*": "./src/*.ts"
|
||||
},
|
||||
"files": [
|
||||
"src/**/*.ts",
|
||||
"dist/**/*.js",
|
||||
"dist/**/*.js.map",
|
||||
"dist/**/*.d.ts",
|
||||
"dist/**/*.d.ts.map"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc -b tsconfig.src.json && pnpm babel",
|
||||
"babel": "babel dist --plugins annotate-pure-calls --out-dir dist --source-maps",
|
||||
"check": "tsc -b tsconfig.json",
|
||||
"compare": "node src/bin.ts compare",
|
||||
"report": "node src/bin.ts report",
|
||||
"visualize": "node src/bin.ts visualize"
|
||||
},
|
||||
"dependencies": {
|
||||
"@effect/platform-node": "workspace:^",
|
||||
"@rollup/plugin-node-resolve": "^16.0.3",
|
||||
"@rollup/plugin-replace": "^6.0.3",
|
||||
"@rollup/plugin-terser": "^1.0.0",
|
||||
"effect": "workspace:^",
|
||||
"glob": "^13.0.6",
|
||||
"rollup": "^4.62.2",
|
||||
"rollup-plugin-esbuild": "^6.2.1",
|
||||
"rollup-plugin-visualizer": "^7.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^26.1.1"
|
||||
}
|
||||
}
|
||||
123
repos/effect/packages/tools/bundle/src/Cli.ts
Normal file
123
repos/effect/packages/tools/bundle/src/Cli.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* Command definitions for the `effect-bundle` bundle-size CLI.
|
||||
*
|
||||
* This module wires the top-level `bundle` command to the reporting service and
|
||||
* exposes the workflows used when maintaining fixture bundle sizes. `compare`
|
||||
* builds the package's local fixtures and compares them with matching fixture
|
||||
* files from another checkout, `report` bundles an explicit list of entrypoints
|
||||
* and prints a Markdown table, `compare-selected` compares explicit entrypoints
|
||||
* against a base checkout, `visualize-selected` analyzes explicit entrypoints,
|
||||
* and `visualize` prompts for local fixtures before producing visualization
|
||||
* output for inspection.
|
||||
*
|
||||
* Command output is intentionally split by workflow. `compare` requires an
|
||||
* existing `--base-dir` (`-b`) and writes its Markdown report to `--output-path`
|
||||
* (`-o`), defaulting to `stats.txt` resolved from the current working directory.
|
||||
* `report` accepts one or more existing files and writes to stdout. `visualize`
|
||||
* uses `--output-dir` (`-o`) for generated bundle artifacts, so `-o` names a
|
||||
* file for `compare` but a directory for `visualize`.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*/
|
||||
import * as Console from "effect/Console"
|
||||
import * as Effect from "effect/Effect"
|
||||
import * as FileSystem from "effect/FileSystem"
|
||||
import * as Path from "effect/Path"
|
||||
import * as Argument from "effect/unstable/cli/Argument"
|
||||
import * as Command from "effect/unstable/cli/Command"
|
||||
import * as Flag from "effect/unstable/cli/Flag"
|
||||
import * as Prompt from "effect/unstable/cli/Prompt"
|
||||
import { Fixtures } from "./Fixtures.ts"
|
||||
import { Reporter } from "./Reporter.ts"
|
||||
|
||||
const baseDirectory = Flag.directory("base-dir", { mustExist: true }).pipe(
|
||||
Flag.withAlias("b"),
|
||||
Flag.withDescription("The base directory to use for bundle size comparisons")
|
||||
)
|
||||
|
||||
const outputPath = Flag.file("output-path").pipe(
|
||||
Flag.withAlias("o"),
|
||||
Flag.withDescription("The name of the file to write the bundle size report to"),
|
||||
Flag.withDefault("stats.txt"),
|
||||
Flag.mapEffect(Effect.fnUntraced(function*(outputPath) {
|
||||
const path = yield* Path.Path
|
||||
return path.resolve(outputPath)
|
||||
}))
|
||||
)
|
||||
|
||||
const compare = Command.make("compare", { baseDirectory, outputPath }).pipe(
|
||||
Command.withHandler(Effect.fnUntraced(function*({ baseDirectory, outputPath }) {
|
||||
const fs = yield* FileSystem.FileSystem
|
||||
const reporter = yield* Reporter
|
||||
const report = yield* reporter.report({ baseDirectory })
|
||||
yield* fs.writeFileString(outputPath, report)
|
||||
yield* Effect.log(`Bundle size report written to: '${outputPath}'`)
|
||||
}))
|
||||
)
|
||||
|
||||
const outputDirectory = Flag.directory("output-dir").pipe(
|
||||
Flag.withAlias("o"),
|
||||
Flag.withDescription("The name of the directory to write the bundle size visualizations to"),
|
||||
Flag.mapEffect(Effect.fnUntraced(function*(outputPath) {
|
||||
const path = yield* Path.Path
|
||||
return path.resolve(outputPath)
|
||||
}))
|
||||
)
|
||||
|
||||
const visualize = Command.make("visualize", { outputDirectory }).pipe(
|
||||
Command.withHandler(Effect.fnUntraced(function*({ outputDirectory }) {
|
||||
const path = yield* Path.Path
|
||||
const { fixtures, fixturesDir } = yield* Fixtures
|
||||
const reporter = yield* Reporter
|
||||
|
||||
const paths = yield* Prompt.multiSelect({
|
||||
message: "Select files whose bundle size you would like to visualize",
|
||||
choices: fixtures.map((fixture) => ({
|
||||
title: fixture,
|
||||
value: path.join(fixturesDir, fixture)
|
||||
}))
|
||||
})
|
||||
|
||||
const report = yield* reporter.visualize({ paths, outputDirectory })
|
||||
yield* Console.log(report)
|
||||
}))
|
||||
)
|
||||
|
||||
const reportPaths = Argument.file("paths", { mustExist: true }).pipe(
|
||||
Argument.withDescription("Fixture files to include in the report"),
|
||||
Argument.variadic({ min: 1 })
|
||||
)
|
||||
|
||||
const report = Command.make("report", { paths: reportPaths }).pipe(
|
||||
Command.withHandler(Effect.fnUntraced(function*({ paths }) {
|
||||
const reporter = yield* Reporter
|
||||
const report = yield* reporter.reportSelected({ paths })
|
||||
yield* Console.log(report)
|
||||
}))
|
||||
)
|
||||
|
||||
const compareSelected = Command.make("compare-selected", { baseDirectory, paths: reportPaths }).pipe(
|
||||
Command.withHandler(Effect.fnUntraced(function*({ baseDirectory, paths }) {
|
||||
const reporter = yield* Reporter
|
||||
const report = yield* reporter.reportSelectedComparison({ baseDirectory, paths })
|
||||
yield* Console.log(report)
|
||||
}))
|
||||
)
|
||||
|
||||
const visualizeSelected = Command.make("visualize-selected", { outputDirectory, paths: reportPaths }).pipe(
|
||||
Command.withHandler(Effect.fnUntraced(function*({ outputDirectory, paths }) {
|
||||
const reporter = yield* Reporter
|
||||
const report = yield* reporter.visualize({ outputDirectory, paths })
|
||||
yield* Console.log(report)
|
||||
}))
|
||||
)
|
||||
|
||||
/**
|
||||
* Bundle analysis CLI command with subcommands for comparing fixture bundle sizes, reporting selected fixtures, and generating visualizations.
|
||||
*
|
||||
* @category commands
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export const cli = Command.make("bundle").pipe(
|
||||
Command.withSubcommands([compare, compareSelected, report, visualize, visualizeSelected])
|
||||
)
|
||||
51
repos/effect/packages/tools/bundle/src/Fixtures.ts
Normal file
51
repos/effect/packages/tools/bundle/src/Fixtures.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Discovers the TypeScript entrypoint fixtures used by the bundle-size tools.
|
||||
*
|
||||
* The bundle CLI uses these fixture names to build current bundle reports,
|
||||
* compare them against a base directory, and populate the visualization
|
||||
* selector. Fixtures are intentionally discovered from the package's local
|
||||
* `fixtures` directory as top-level `.ts` files and sorted by name so reports
|
||||
* are deterministic.
|
||||
*
|
||||
* When adding or renaming fixtures, keep in mind that comparison reports match
|
||||
* files by basename between the current fixtures directory and the provided
|
||||
* base directory. New fixtures without a matching base file are reported as
|
||||
* unchanged. Each fixture is bundled as its own Rollup entrypoint, so it should
|
||||
* represent the import shape being measured and avoid depending on incidental
|
||||
* fixture discovery order.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*/
|
||||
import * as Array from "effect/Array"
|
||||
import * as Context from "effect/Context"
|
||||
import * as Effect from "effect/Effect"
|
||||
import * as Layer from "effect/Layer"
|
||||
import * as Order from "effect/Order"
|
||||
import * as Glob from "glob"
|
||||
|
||||
/**
|
||||
* Context service that discovers and sorts TypeScript fixture files used by the bundle size tooling.
|
||||
*
|
||||
* @category services
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export class Fixtures extends Context.Service<Fixtures>()(
|
||||
"@effect/bundle/Fixtures",
|
||||
{
|
||||
make: Effect.gen(function*() {
|
||||
const fixturesDir = new URL("../fixtures/", import.meta.url).pathname
|
||||
|
||||
const fixtures = yield* Effect.promise(() => Glob.glob("*.ts", { cwd: fixturesDir })).pipe(
|
||||
Effect.map(Array.sort(Order.String)),
|
||||
Effect.orDie
|
||||
)
|
||||
|
||||
return {
|
||||
fixtures,
|
||||
fixturesDir
|
||||
} as const
|
||||
})
|
||||
}
|
||||
) {
|
||||
static readonly layer = Layer.effect(this, this.make)
|
||||
}
|
||||
176
repos/effect/packages/tools/bundle/src/Plugins.ts
Normal file
176
repos/effect/packages/tools/bundle/src/Plugins.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
/**
|
||||
* Utilities for assembling the Rollup plugin pipeline used by the Effect
|
||||
* bundle-size tooling.
|
||||
*
|
||||
* This module is responsible for the bundler-specific work that turns local
|
||||
* fixture entrypoints into comparable ESM output: resolving Effect package
|
||||
* imports against each package's built `dist` files, replacing production
|
||||
* environment checks, lowering TypeScript with esbuild, minifying with terser,
|
||||
* and optionally adding a bundle visualizer. It is primarily used by the
|
||||
* Rollup service when measuring gzipped fixture sizes or opening a
|
||||
* visualization for bundle inspection.
|
||||
*
|
||||
* Keep plugin ordering intentional when changing this module. Local package
|
||||
* resolution must run before normal node resolution so workspace imports are
|
||||
* measured from built artifacts, esbuild must emit ESM for Rollup to continue
|
||||
* tree-shaking, and terser mangling is disabled while visualizing so reported
|
||||
* module names stay readable.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*/
|
||||
import { nodeResolve } from "@rollup/plugin-node-resolve"
|
||||
import replace from "@rollup/plugin-replace"
|
||||
import terser from "@rollup/plugin-terser"
|
||||
import type * as Path from "effect/Path"
|
||||
import * as Predicate from "effect/Predicate"
|
||||
import type { Plugin } from "rollup"
|
||||
import esbuild from "rollup-plugin-esbuild"
|
||||
import { type PluginVisualizerOptions, visualizer } from "rollup-plugin-visualizer"
|
||||
|
||||
const EFFECT_PACKAGE_REGEX = /^(@effect\/[\w-]+|effect)(\/.*)?$/
|
||||
const TYPE_SCRIPT_EXTENSIONS = new Set([".ts", ".tsx", ".mts", ".cts"])
|
||||
|
||||
const toLocalDistPath = (pathService: Path.Path, packageDir: string, resolvedId: string): string => {
|
||||
const srcDir = pathService.join(packageDir, "src")
|
||||
const relative = pathService.relative(srcDir, resolvedId)
|
||||
if (relative === "" || relative.startsWith("..") || pathService.isAbsolute(relative)) {
|
||||
return resolvedId
|
||||
}
|
||||
const extension = pathService.extname(relative)
|
||||
if (!TYPE_SCRIPT_EXTENSIONS.has(extension)) {
|
||||
return resolvedId
|
||||
}
|
||||
return pathService.join(packageDir, "dist", relative.slice(0, -extension.length) + ".js")
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for configuring Rollup plugins.
|
||||
*
|
||||
* @category options
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export interface PluginOptions {
|
||||
readonly nodeTarget?: string | undefined
|
||||
readonly minify?: boolean | undefined
|
||||
readonly mangle?: boolean | undefined
|
||||
readonly visualize?: boolean | undefined
|
||||
readonly visualizations?: ReadonlyArray<VisualizationOutput> | undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Output generated by the Rollup visualizer plugin.
|
||||
*
|
||||
* @category options
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export interface VisualizationOutput {
|
||||
readonly filename: string
|
||||
readonly template: NonNullable<PluginVisualizerOptions["template"]>
|
||||
readonly title?: string | undefined
|
||||
}
|
||||
|
||||
interface ResolvedPluginOptions {
|
||||
readonly nodeTarget: string
|
||||
readonly minify: boolean
|
||||
readonly mangle: boolean
|
||||
readonly visualize: boolean
|
||||
readonly visualizations: ReadonlyArray<VisualizationOutput>
|
||||
}
|
||||
|
||||
const defaultPluginOptions: ResolvedPluginOptions = {
|
||||
nodeTarget: "node20",
|
||||
minify: true,
|
||||
mangle: true,
|
||||
visualize: false,
|
||||
visualizations: []
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges provided options with defaults.
|
||||
*/
|
||||
const resolvePluginOptions = (options: PluginOptions): ResolvedPluginOptions => ({
|
||||
nodeTarget: options.nodeTarget ?? defaultPluginOptions.nodeTarget,
|
||||
minify: options.minify ?? defaultPluginOptions.minify,
|
||||
mangle: options.mangle ?? defaultPluginOptions.mangle,
|
||||
visualize: options.visualize ?? defaultPluginOptions.visualize,
|
||||
visualizations: options.visualizations ?? defaultPluginOptions.visualizations
|
||||
})
|
||||
|
||||
/**
|
||||
* Creates a custom Rollup plugin that resolves Effect package imports to their
|
||||
* local dist directories.
|
||||
*
|
||||
* @category constructors
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export const createResolveLocalPackageImports = (pathService: Path.Path): Plugin => ({
|
||||
name: "rollup-plugin-resolve-imports",
|
||||
async resolveId(source, importer) {
|
||||
const match = source.match(EFFECT_PACKAGE_REGEX)
|
||||
if (Predicate.isNotNull(match)) {
|
||||
const packageName = match[1]
|
||||
const packageJson = await this.resolve(`${packageName}/package.json`, importer, { skipSelf: true })
|
||||
if (packageJson === null) return null
|
||||
const resolved = await this.resolve(source, importer, { skipSelf: true })
|
||||
if (resolved === null) return null
|
||||
return {
|
||||
...resolved,
|
||||
id: toLocalDistPath(pathService, pathService.dirname(packageJson.id), resolved.id),
|
||||
external: false
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Creates the full Rollup plugin pipeline for bundling.
|
||||
*
|
||||
* @category constructors
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export const createPlugins = (pathService: Path.Path, options: PluginOptions = {}): Array<Plugin> => {
|
||||
const resolved = resolvePluginOptions(options)
|
||||
const plugins: Array<Plugin> = [
|
||||
createResolveLocalPackageImports(pathService),
|
||||
nodeResolve(),
|
||||
// @ts-expect-error see https://github.com/rollup/plugins/issues/1662
|
||||
replace({
|
||||
"process.env.NODE_ENV": JSON.stringify("production"),
|
||||
preventAssignment: true
|
||||
}),
|
||||
esbuild({
|
||||
target: resolved.nodeTarget,
|
||||
format: "esm",
|
||||
treeShaking: true
|
||||
}),
|
||||
// @ts-expect-error see https://github.com/rollup/plugins/issues/1662
|
||||
terser({
|
||||
format: { comments: false },
|
||||
compress: resolved.minify,
|
||||
mangle: resolved.mangle && !resolved.visualize
|
||||
})
|
||||
]
|
||||
|
||||
if (resolved.visualizations.length > 0) {
|
||||
for (const output of resolved.visualizations) {
|
||||
const visualizerOptions: PluginVisualizerOptions = {
|
||||
filename: output.filename,
|
||||
gzipSize: true,
|
||||
open: false,
|
||||
template: output.template
|
||||
}
|
||||
if (output.title !== undefined) {
|
||||
visualizerOptions.title = output.title
|
||||
}
|
||||
plugins.push(visualizer(visualizerOptions))
|
||||
}
|
||||
} else if (resolved.visualize) {
|
||||
plugins.push(visualizer({
|
||||
open: true,
|
||||
gzipSize: true
|
||||
}))
|
||||
}
|
||||
|
||||
return plugins
|
||||
}
|
||||
299
repos/effect/packages/tools/bundle/src/Reporter.ts
Normal file
299
repos/effect/packages/tools/bundle/src/Reporter.ts
Normal file
@@ -0,0 +1,299 @@
|
||||
/**
|
||||
* Bundle report generation for the Effect bundle-size tooling.
|
||||
*
|
||||
* The reporter coordinates fixture discovery with the Rollup service to turn
|
||||
* measured fixture bundles into Markdown tables or visualization output. It is
|
||||
* used by the bundle CLI to compare the current workspace against a checked-out
|
||||
* base directory, to print a one-off report for selected entry files, and to
|
||||
* generate visualizations when a size change needs inspection.
|
||||
*
|
||||
* Reports compare files by basename and display gzipped Rollup output sizes in
|
||||
* decimal kilobytes. Base fixtures are bundled only when the matching file
|
||||
* exists; if a current fixture has no matching basename in the base directory it
|
||||
* is reported as unchanged. Visualization artifacts are named from entry file
|
||||
* stems in the requested output directory, so duplicate names can make the
|
||||
* output misleading.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*/
|
||||
import * as Context from "effect/Context"
|
||||
import * as Data from "effect/Data"
|
||||
import * as Effect from "effect/Effect"
|
||||
import * as FileSystem from "effect/FileSystem"
|
||||
import { constFalse } from "effect/Function"
|
||||
import * as Layer from "effect/Layer"
|
||||
import * as Path from "effect/Path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
import { Fixtures } from "./Fixtures.ts"
|
||||
import type { BundleStats } from "./Rollup.ts"
|
||||
import { Rollup } from "./Rollup.ts"
|
||||
|
||||
/**
|
||||
* Error raised when generating a bundle size report or visualization fails.
|
||||
*
|
||||
* @category errors
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export class ReporterError extends Data.TaggedError("ReporterError")<{
|
||||
readonly cause: unknown
|
||||
}> {}
|
||||
|
||||
/**
|
||||
* Options for generating a bundle size comparison report against fixture files from a base directory.
|
||||
*
|
||||
* @category options
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export interface ReportOptions {
|
||||
readonly baseDirectory: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for generating bundle visualizations for selected entry files into an output directory.
|
||||
*
|
||||
* @category options
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export interface VisualizeOptions {
|
||||
readonly paths: ReadonlyArray<string>
|
||||
readonly outputDirectory: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for generating a bundle size report for an explicit list of entry files.
|
||||
*
|
||||
* @category options
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export interface ReportSelectedOptions {
|
||||
readonly paths: ReadonlyArray<string>
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for generating a bundle size comparison report for explicit entry files against a base checkout.
|
||||
*
|
||||
* @category options
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export interface ReportSelectedComparisonOptions {
|
||||
readonly baseDirectory: string
|
||||
readonly paths: ReadonlyArray<string>
|
||||
}
|
||||
|
||||
/**
|
||||
* Context service for producing bundle size reports and visualizations from Rollup-generated fixture stats.
|
||||
*
|
||||
* @category services
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export class Reporter extends Context.Service<Reporter>()(
|
||||
"@effect/bundle/Reporter",
|
||||
{
|
||||
make: Effect.gen(function*() {
|
||||
const fs = yield* FileSystem.FileSystem
|
||||
const path = yield* Path.Path
|
||||
const { fixtures, fixturesDir } = yield* Fixtures
|
||||
const rollup = yield* Rollup
|
||||
const currentDirectory = path.resolve(fileURLToPath(new URL("../../../../", import.meta.url)))
|
||||
|
||||
const calculateDifference = (current: BundleStats, previous: BundleStats) => {
|
||||
const currSize = current.sizeInBytes
|
||||
const prevSize = previous.sizeInBytes
|
||||
const diff = currSize - prevSize
|
||||
const diffPct = prevSize === 0 ? 0 : (Math.abs(diff) / prevSize) * 100
|
||||
const currKb = (currSize / 1000).toFixed(2)
|
||||
const prevKb = (prevSize / 1000).toFixed(2)
|
||||
const diffKb = (Math.abs(diff) / 1000).toFixed(2)
|
||||
const filename = path.basename(current.path)
|
||||
return {
|
||||
diff,
|
||||
diffPct,
|
||||
currKb,
|
||||
prevKb,
|
||||
diffKb,
|
||||
filename
|
||||
}
|
||||
}
|
||||
|
||||
const createComparisonReport = (
|
||||
entries: ReadonlyArray<{
|
||||
readonly current: BundleStats
|
||||
readonly previous: BundleStats
|
||||
readonly filename: string
|
||||
}>
|
||||
): string => {
|
||||
const lines: Array<string> = [
|
||||
"| File Name | Current Size | Previous Size | Difference |",
|
||||
"|:----------|:------------:|:-------------:|:----------:|"
|
||||
]
|
||||
for (const { current, previous, filename } of entries) {
|
||||
const comparison = calculateDifference(current, previous)
|
||||
const currKb = `${comparison.currKb} KB`
|
||||
const prevKb = `${comparison.prevKb} KB`
|
||||
const diffKb = `${comparison.diffKb} KB`
|
||||
const diffPct = `${comparison.diffPct.toFixed(2)}%`
|
||||
const sign = comparison.diff === 0 ? "" : comparison.diff > 0 ? "+" : "-"
|
||||
const line = `| \`${filename}\` | ${currKb} | ${prevKb} | ${sign}${diffKb} (${sign}${diffPct}) |`
|
||||
lines.push(line)
|
||||
}
|
||||
return lines.join("\n") + "\n"
|
||||
}
|
||||
|
||||
const createReport = (curr: ReadonlyArray<BundleStats>, prev: ReadonlyArray<BundleStats>): string => {
|
||||
const entries: Array<{
|
||||
readonly current: BundleStats
|
||||
readonly previous: BundleStats
|
||||
readonly filename: string
|
||||
}> = []
|
||||
for (const current of curr) {
|
||||
const previous = prev.find((previous) => {
|
||||
return path.basename(previous.path) === path.basename(current.path)
|
||||
}) ?? current
|
||||
entries.push({
|
||||
current,
|
||||
previous,
|
||||
filename: path.basename(current.path)
|
||||
})
|
||||
}
|
||||
return createComparisonReport(entries)
|
||||
}
|
||||
|
||||
const createSelectedReport = (stats: ReadonlyArray<BundleStats>): string => {
|
||||
const lines: Array<string> = [
|
||||
"| File Name | Current Size |",
|
||||
"|:----------|:------------:|"
|
||||
]
|
||||
|
||||
for (const current of stats) {
|
||||
const filename = `\`${path.basename(current.path)}\``
|
||||
const currKb = `${(current.sizeInBytes / 1000).toFixed(2)} KB`
|
||||
const line = `| ${filename} | ${currKb} |`
|
||||
lines.push(line)
|
||||
}
|
||||
|
||||
return lines.join("\n") + "\n"
|
||||
}
|
||||
|
||||
const createVisualizationReport = (paths: ReadonlyArray<string>, outputDirectory: string): string => {
|
||||
const lines: Array<string> = [
|
||||
"| File Name | Generated Bundle | Treemap | Raw Data |",
|
||||
"|:----------|:----------------|:--------|:---------|"
|
||||
]
|
||||
for (const entryPath of paths) {
|
||||
const name = path.parse(entryPath).name
|
||||
const filename = path.relative(currentDirectory, path.resolve(entryPath))
|
||||
const minified = path.join(outputDirectory, `${name}.min.js`)
|
||||
const treemap = path.join(outputDirectory, `${name}.treemap.html`)
|
||||
const rawData = path.join(outputDirectory, `${name}.raw-data.json`)
|
||||
lines.push(`| \`${filename}\` | \`${minified}\` | \`${treemap}\` | \`${rawData}\` |`)
|
||||
}
|
||||
return lines.join("\n") + "\n"
|
||||
}
|
||||
|
||||
const report = Effect.fn("Reporter.report")(
|
||||
function*(options: ReportOptions) {
|
||||
yield* Effect.logInfo(`Found ${fixtures.length} files to bundle`)
|
||||
|
||||
const currentPaths = fixtures.map((fixture) => path.join(fixturesDir, fixture))
|
||||
const previousPaths = yield* Effect.filter(
|
||||
fixtures.map((fixture) => path.join(options.baseDirectory, fixture)),
|
||||
(previousPath) => fs.exists(previousPath).pipe(Effect.orElseSucceed(constFalse)),
|
||||
{ concurrency: fixtures.length }
|
||||
)
|
||||
|
||||
const [currentStats, previousStats] = yield* Effect.all([
|
||||
rollup.bundleAll({
|
||||
paths: currentPaths
|
||||
}),
|
||||
rollup.bundleAll({
|
||||
paths: previousPaths
|
||||
})
|
||||
], { concurrency: 2 })
|
||||
|
||||
yield* Effect.logInfo("Bundling complete! Generating bundle size report...")
|
||||
|
||||
return createReport(currentStats, previousStats)
|
||||
}
|
||||
)
|
||||
|
||||
const visualize = Effect.fn("Reporter.visualize")(
|
||||
function*(options: VisualizeOptions) {
|
||||
yield* fs.makeDirectory(options.outputDirectory, { recursive: true })
|
||||
yield* rollup.bundleAll({
|
||||
paths: options.paths,
|
||||
outputDirectory: options.outputDirectory,
|
||||
visualize: true
|
||||
})
|
||||
return createVisualizationReport(options.paths, options.outputDirectory)
|
||||
}
|
||||
)
|
||||
|
||||
const reportSelected = Effect.fn("Reporter.reportSelected")(
|
||||
function*(options: ReportSelectedOptions) {
|
||||
yield* Effect.logInfo(`Found ${options.paths.length} files to bundle`)
|
||||
const stats = yield* rollup.bundleAll({ paths: options.paths })
|
||||
yield* Effect.logInfo("Bundling complete! Generating bundle size report...")
|
||||
return createSelectedReport(stats)
|
||||
}
|
||||
)
|
||||
|
||||
const reportSelectedComparison = Effect.fn("Reporter.reportSelectedComparison")(
|
||||
function*(options: ReportSelectedComparisonOptions) {
|
||||
yield* Effect.logInfo(`Found ${options.paths.length} files to compare`)
|
||||
const baseDirectory = path.resolve(options.baseDirectory)
|
||||
const currentPaths = options.paths.map((currentPath) => path.resolve(currentPath))
|
||||
const previousPaths = yield* Effect.forEach(
|
||||
currentPaths,
|
||||
Effect.fnUntraced(function*(currentPath) {
|
||||
const relativePath = path.relative(currentDirectory, currentPath)
|
||||
if (relativePath === "" || relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
|
||||
return yield* Effect.fail(
|
||||
new ReporterError({
|
||||
cause: `Selected bundle entry must be inside ${currentDirectory}: ${currentPath}`
|
||||
})
|
||||
)
|
||||
}
|
||||
const previousPath = path.join(baseDirectory, relativePath)
|
||||
yield* fs.makeDirectory(path.dirname(previousPath), { recursive: true })
|
||||
yield* fs.copy(currentPath, previousPath, { overwrite: true })
|
||||
return previousPath
|
||||
}),
|
||||
{ concurrency: currentPaths.length }
|
||||
)
|
||||
|
||||
const [currentStats, previousStats] = yield* Effect.all([
|
||||
rollup.bundleAll({ paths: currentPaths }),
|
||||
rollup.bundleAll({ paths: previousPaths })
|
||||
], { concurrency: 2 })
|
||||
|
||||
const entries: Array<{
|
||||
readonly current: BundleStats
|
||||
readonly previous: BundleStats
|
||||
readonly filename: string
|
||||
}> = []
|
||||
for (let i = 0; i < currentStats.length; i++) {
|
||||
entries.push({
|
||||
current: currentStats[i]!,
|
||||
previous: previousStats[i]!,
|
||||
filename: path.relative(currentDirectory, currentStats[i]!.path)
|
||||
})
|
||||
}
|
||||
yield* Effect.logInfo("Bundling complete! Generating bundle size report...")
|
||||
return createComparisonReport(entries)
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
report,
|
||||
reportSelectedComparison,
|
||||
reportSelected,
|
||||
visualize
|
||||
} as const
|
||||
})
|
||||
}
|
||||
) {
|
||||
static readonly layer = Layer.effect(this, this.make).pipe(
|
||||
Layer.provide(Fixtures.layer),
|
||||
Layer.provide(Rollup.layer)
|
||||
)
|
||||
}
|
||||
201
repos/effect/packages/tools/bundle/src/Rollup.ts
Normal file
201
repos/effect/packages/tools/bundle/src/Rollup.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
/**
|
||||
* Rollup-backed bundling and size measurement for the Effect bundle-size tools.
|
||||
*
|
||||
* This module provides the service used by the bundle CLI and reporter to turn
|
||||
* fixture or selected TypeScript entrypoints into ESM Rollup output, optionally
|
||||
* write a minified artifact, and return the gzipped byte count used in
|
||||
* bundle-size comparisons.
|
||||
*
|
||||
* Bundles are generated in memory so the emitted code can be streamed to both
|
||||
* gzip measurement and optional file output. Only Rollup `chunk` outputs are
|
||||
* included; assets are ignored, and when Rollup creates multiple chunks (for
|
||||
* example because of dynamic imports or shared chunks) their code is streamed
|
||||
* together for measurement. The optional output file is named from the
|
||||
* entrypoint stem, so it is best treated as an inspection artifact rather than
|
||||
* a complete Rollup output directory.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*/
|
||||
import * as NodeStream from "@effect/platform-node/NodeStream"
|
||||
import * as Context from "effect/Context"
|
||||
import * as Data from "effect/Data"
|
||||
import * as Effect from "effect/Effect"
|
||||
import * as FiberSet from "effect/FiberSet"
|
||||
import * as FileSystem from "effect/FileSystem"
|
||||
import * as Layer from "effect/Layer"
|
||||
import * as Path from "effect/Path"
|
||||
import * as Stream from "effect/Stream"
|
||||
import { createGzip } from "node:zlib"
|
||||
import type { RollupOptions } from "rollup"
|
||||
import { rollup } from "rollup"
|
||||
import { createPlugins, type VisualizationOutput } from "./Plugins.ts"
|
||||
|
||||
/**
|
||||
* Error raised when Rollup bundling, output generation, or bundle size measurement fails.
|
||||
*
|
||||
* @category errors
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export class RollupError extends Data.TaggedError("RollupError")<{
|
||||
readonly cause: unknown
|
||||
}> {}
|
||||
|
||||
/**
|
||||
* Bundle size statistics for an entry file, including its path and gzipped size in bytes.
|
||||
*
|
||||
* @category models
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export class BundleStats extends Data.TaggedClass("BundleStats")<{
|
||||
readonly path: string
|
||||
readonly sizeInBytes: number
|
||||
}> {}
|
||||
|
||||
/**
|
||||
* Options for bundling one entry file, optionally writing a minified output and generating a visualization.
|
||||
*
|
||||
* @category options
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export interface BundleOptions {
|
||||
readonly path: string
|
||||
readonly visualize?: boolean | undefined
|
||||
readonly outputDirectory?: string | undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for bundling multiple entry files with shared visualization and output-directory settings.
|
||||
*
|
||||
* @category options
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export interface BundleAllOptions {
|
||||
readonly paths: ReadonlyArray<string>
|
||||
readonly visualize?: boolean | undefined
|
||||
readonly outputDirectory?: string | undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Context service for bundling entry files with Rollup and measuring their gzipped output size.
|
||||
*
|
||||
* @category services
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export class Rollup extends Context.Service<Rollup>()(
|
||||
"@effect/bundle/Rollup",
|
||||
{
|
||||
make: Effect.gen(function*() {
|
||||
const pathService = yield* Path.Path
|
||||
const fs = yield* FileSystem.FileSystem
|
||||
|
||||
const createVisualizationOutputs = (options: BundleOptions): ReadonlyArray<VisualizationOutput> => {
|
||||
if (!options.visualize || !options.outputDirectory) {
|
||||
return []
|
||||
}
|
||||
const name = pathService.parse(options.path).name
|
||||
return [
|
||||
{
|
||||
filename: pathService.join(options.outputDirectory, `${name}.treemap.html`),
|
||||
template: "treemap",
|
||||
title: `${name} bundle treemap`
|
||||
},
|
||||
{
|
||||
filename: pathService.join(options.outputDirectory, `${name}.raw-data.json`),
|
||||
template: "raw-data",
|
||||
title: `${name} bundle raw data`
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const getRollupOptions = (options: BundleOptions): RollupOptions => ({
|
||||
input: options.path,
|
||||
output: {
|
||||
format: "esm"
|
||||
},
|
||||
plugins: createPlugins(pathService, {
|
||||
visualize: options.visualize,
|
||||
visualizations: createVisualizationOutputs(options)
|
||||
}),
|
||||
onwarn: (warning, next) => {
|
||||
if (warning.code === "THIS_IS_UNDEFINED") return
|
||||
next(warning)
|
||||
}
|
||||
})
|
||||
|
||||
const bundle = Effect.fn("Rollup.bundle")(
|
||||
function*(options: BundleOptions) {
|
||||
const bundle = yield* Effect.acquireRelease(
|
||||
Effect.tryPromise({
|
||||
try: () => rollup(getRollupOptions(options)),
|
||||
catch: (cause) => new RollupError({ cause })
|
||||
}),
|
||||
(bundle) => Effect.promise(() => bundle.close())
|
||||
)
|
||||
const fibers = yield* FiberSet.make()
|
||||
|
||||
const { output } = yield* Effect.tryPromise({
|
||||
try: () => bundle.generate({ format: "esm" }),
|
||||
catch: (cause) => new RollupError({ cause })
|
||||
})
|
||||
|
||||
const stream = yield* Stream.fromIterable(output).pipe(
|
||||
Stream.filter((output) => output.type === "chunk"),
|
||||
Stream.map((chunk) => chunk.code),
|
||||
Stream.encodeText,
|
||||
Stream.broadcast({ capacity: 8, replay: 8 })
|
||||
)
|
||||
|
||||
if (options.outputDirectory) {
|
||||
const outputPath = pathService.join(
|
||||
options.outputDirectory,
|
||||
`${pathService.parse(options.path).name}.min.js`
|
||||
)
|
||||
yield* FiberSet.run(
|
||||
fibers,
|
||||
stream.pipe(
|
||||
Stream.run(fs.sink(outputPath))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const sizeInBytes = yield* stream.pipe(
|
||||
NodeStream.pipeThroughDuplex({
|
||||
evaluate: () => createGzip({ level: 9 }),
|
||||
onError: (cause) => new RollupError({ cause })
|
||||
}),
|
||||
Stream.runFold(
|
||||
() => 0,
|
||||
(totalBytes, chunkBytes) => chunkBytes.length + totalBytes
|
||||
)
|
||||
)
|
||||
|
||||
yield* FiberSet.awaitEmpty(fibers)
|
||||
|
||||
yield* Effect.log(`Bundled ${options.path}`).pipe(
|
||||
Effect.annotateLogs({ size: `${(sizeInBytes / 1000).toFixed(2)} kB` })
|
||||
)
|
||||
|
||||
return new BundleStats({ path: options.path, sizeInBytes })
|
||||
},
|
||||
Effect.scoped
|
||||
)
|
||||
|
||||
const bundleAll = Effect.fn("Rollup.bundleAll")(
|
||||
function*(options: BundleAllOptions) {
|
||||
return yield* Effect.forEach(
|
||||
options.paths,
|
||||
(path) => bundle({ path, visualize: options.visualize, outputDirectory: options.outputDirectory }),
|
||||
{ concurrency: options.paths.length }
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
bundle,
|
||||
bundleAll
|
||||
} as const
|
||||
})
|
||||
}
|
||||
) {
|
||||
static readonly layer = Layer.effect(this, this.make)
|
||||
}
|
||||
23
repos/effect/packages/tools/bundle/src/bin.ts
Executable file
23
repos/effect/packages/tools/bundle/src/bin.ts
Executable file
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* @since 4.0.0
|
||||
*/
|
||||
import * as NodeRuntime from "@effect/platform-node/NodeRuntime"
|
||||
import * as NodeServices from "@effect/platform-node/NodeServices"
|
||||
import * as Effect from "effect/Effect"
|
||||
import * as Layer from "effect/Layer"
|
||||
import * as Command from "effect/unstable/cli/Command"
|
||||
import PackageJson from "../package.json" with { type: "json" }
|
||||
import { cli } from "./Cli.ts"
|
||||
import { Fixtures } from "./Fixtures.ts"
|
||||
import { Reporter } from "./Reporter.ts"
|
||||
|
||||
const MainLayer = Layer.mergeAll(
|
||||
Fixtures.layer,
|
||||
Reporter.layer
|
||||
).pipe(Layer.provideMerge(NodeServices.layer))
|
||||
|
||||
Command.run(cli, { version: PackageJson["version"] }).pipe(
|
||||
Effect.provide(MainLayer),
|
||||
NodeRuntime.runMain
|
||||
)
|
||||
74
repos/effect/packages/tools/bundle/test/Plugins.test.ts
Normal file
74
repos/effect/packages/tools/bundle/test/Plugins.test.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { createResolveLocalPackageImports } from "@effect/bundle/Plugins"
|
||||
import { assert, describe, it } from "@effect/vitest"
|
||||
import type * as EffectPath from "effect/Path"
|
||||
import * as path from "node:path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
import type { Plugin } from "rollup"
|
||||
|
||||
type Resolved = {
|
||||
readonly id: string
|
||||
readonly external: false
|
||||
}
|
||||
|
||||
type ResolveId = (
|
||||
this: {
|
||||
readonly resolve: (
|
||||
source: string,
|
||||
importer?: string,
|
||||
options?: { readonly skipSelf?: boolean }
|
||||
) => Promise<Resolved | null>
|
||||
},
|
||||
source: string,
|
||||
importer?: string
|
||||
) => Promise<Resolved | null>
|
||||
|
||||
const packageDir = fileURLToPath(new URL("../../../effect", import.meta.url))
|
||||
const pathService = path as unknown as EffectPath.Path
|
||||
|
||||
const getResolveId = (plugin: Plugin): ResolveId => {
|
||||
assert.strictEqual(typeof plugin.resolveId, "function")
|
||||
return plugin.resolveId as ResolveId
|
||||
}
|
||||
|
||||
const resolved = (id: string): Resolved => ({
|
||||
id,
|
||||
external: false
|
||||
})
|
||||
|
||||
describe("createResolveLocalPackageImports", () => {
|
||||
it("resolves directory package exports to dist index files", async () => {
|
||||
const resolveId = getResolveId(createResolveLocalPackageImports(pathService))
|
||||
const result = await resolveId.call({
|
||||
resolve: async (source) => {
|
||||
switch (source) {
|
||||
case "effect/package.json":
|
||||
return resolved(path.join(packageDir, "package.json"))
|
||||
case "effect/testing":
|
||||
return resolved(path.join(packageDir, "src", "testing", "index.ts"))
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
}, "effect/testing")
|
||||
|
||||
assert.deepStrictEqual(result, resolved(path.join(packageDir, "dist", "testing", "index.js")))
|
||||
})
|
||||
|
||||
it("keeps flat package exports on flat dist files", async () => {
|
||||
const resolveId = getResolveId(createResolveLocalPackageImports(pathService))
|
||||
const result = await resolveId.call({
|
||||
resolve: async (source) => {
|
||||
switch (source) {
|
||||
case "effect/package.json":
|
||||
return resolved(path.join(packageDir, "package.json"))
|
||||
case "effect/Schema":
|
||||
return resolved(path.join(packageDir, "src", "Schema.ts"))
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
}, "effect/Schema")
|
||||
|
||||
assert.deepStrictEqual(result, resolved(path.join(packageDir, "dist", "Schema.js")))
|
||||
})
|
||||
})
|
||||
13
repos/effect/packages/tools/bundle/tsconfig.fixtures.json
Normal file
13
repos/effect/packages/tools/bundle/tsconfig.fixtures.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"$schema": "http://json.schemastore.org/tsconfig",
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"include": ["fixtures"],
|
||||
"compilerOptions": {
|
||||
"rootDir": "fixtures",
|
||||
"noEmit": true
|
||||
},
|
||||
"references": [
|
||||
{ "path": "../../effect" },
|
||||
{ "path": "../../platform-node" }
|
||||
]
|
||||
}
|
||||
9
repos/effect/packages/tools/bundle/tsconfig.json
Normal file
9
repos/effect/packages/tools/bundle/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"$schema": "http://json.schemastore.org/tsconfig",
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"include": [],
|
||||
"references": [
|
||||
{ "path": "tsconfig.fixtures.json" },
|
||||
{ "path": "tsconfig.src.json" }
|
||||
]
|
||||
}
|
||||
13
repos/effect/packages/tools/bundle/tsconfig.src.json
Normal file
13
repos/effect/packages/tools/bundle/tsconfig.src.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"$schema": "http://json.schemastore.org/tsconfig",
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"include": ["src"],
|
||||
"compilerOptions": {
|
||||
"resolveJsonModule": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"references": [
|
||||
{ "path": "../../effect" },
|
||||
{ "path": "../../platform-node" }
|
||||
]
|
||||
}
|
||||
6
repos/effect/packages/tools/bundle/vitest.config.ts
Normal file
6
repos/effect/packages/tools/bundle/vitest.config.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { mergeConfig, type ViteUserConfig } from "vitest/config"
|
||||
import shared from "../../../vitest.shared.ts"
|
||||
|
||||
const config: ViteUserConfig = {}
|
||||
|
||||
export default mergeConfig(shared, config)
|
||||
Reference in New Issue
Block a user