Skip to content

Commit 651c5f3

Browse files
committed
refactor(monorepo): centralize extension tsconfig and tsup base config
Move duplicated build config from each of the five @docs.plus extension packages to root-level base files: tsconfig.base.json (shared compilerOptions) and tsup.base.ts (defineTiptapExtensionConfig factory). Per-package tsconfig.json and tsup.config.ts shrink to a few lines plus genuine per-package overrides. Side-effect: extension-hypermultimedia's Logger now emits console.warn / console.error in production builds (previously stripped by drop: ['console']) — restores the author's intent. The other three silent extensions produce byte-identical dist trees. LICENSE moves to the repo root as the single committed copy; publishable packages will sync it in via prepack (separate commit). AGENTS.md and bun-monorepo.mdc document the new conventions, override semantics (shallow shadow, not deep merge), and the release routine that landed alongside this work. Made-with: Cursor
1 parent 0162cc9 commit 651c5f3

15 files changed

Lines changed: 280 additions & 168 deletions

File tree

.cursor/rules/bun-monorepo.mdc

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,124 @@ bun run --filter '*' build
3737
- Node >= 24.11.0
3838
- Bun >= 1.3.7
3939

40+
## Publishing packages to npm
41+
42+
- Always publish with `bun publish` (or `bun pm pack` for inspection).
43+
Never use `npm publish`, `yarn publish`, or `pnpm publish`.
44+
- Reason: `peerDependencies` use the `catalog:` protocol so every package
45+
tracks the same Tiptap / Hocuspocus / etc. version pinned in the root
46+
`package.json`'s `catalog` block. Only Bun resolves `catalog:` to a
47+
concrete semver range at pack time. `npm publish` would ship a literal
48+
`"catalog:"` string in the published `package.json` and break every
49+
consumer install with `Invalid Version: catalog:`.
50+
- Publishable packages enforce this with a `prepublishOnly` preflight
51+
(e.g. `packages/extension-hyperlink/scripts/preflight.ts`) that fails
52+
fast if the publisher is not Bun. When you add a new publishable
53+
package, copy that preflight or move it into a shared package.
54+
55+
## Shared config / single source of truth
56+
57+
The monorepo prefers root-level shared config files referenced by relative
58+
path over per-package duplication. Three patterns are in active use:
59+
60+
### `LICENSE` — root file + `prepack` sync
61+
62+
- The canonical `LICENSE` lives at the **monorepo root** (one committed
63+
file, one SPDX identity).
64+
- Each **publishable** package adds `/LICENSE` (anchored to the package
65+
root, so a vendored dep's nested `LICENSE` isn't silently ignored) to
66+
its `.gitignore` and a `scripts/prepack.ts` that copies the root
67+
`LICENSE` into the package directory before `bun publish` /
68+
`bun pm pack`. Wire it via:
69+
70+
```jsonc
71+
// package.json
72+
"scripts": { "prepack": "bun run scripts/prepack.ts" }
73+
```
74+
75+
- Symlinks and hard links **do not work** here:
76+
- Bun's pack silently drops symlinks from the tarball.
77+
- Hard links work locally but git stores them as two independent files,
78+
so contributors who clone get two copies that drift silently.
79+
- The `prepack` lifecycle (not `prepublishOnly`) is the right hook
80+
because both `bun publish` AND `bun pm pack` honor it, so dry-run
81+
packs still produce a realistic tarball.
82+
- Currently only `extension-hyperlink` is wired this way; copy the same
83+
three pieces (`/LICENSE` in `.gitignore`, `scripts/prepack.ts`, the
84+
`prepack` script entry) when a second package is prepared for publish.
85+
86+
### `tsconfig.base.json` — root base + per-package `extends`
87+
88+
- `tsconfig.base.json` at the monorepo root holds the shared
89+
`compilerOptions` (target, module, strict, etc.). Per-package
90+
`tsconfig.json` files only declare what's actually different
91+
(`outDir`, `rootDir`, `include`, `exclude`):
92+
93+
```jsonc
94+
{
95+
"extends": "../../tsconfig.base.json",
96+
"compilerOptions": { "outDir": "dist" },
97+
"include": ["src/**/*"],
98+
"exclude": ["node_modules", "dist"]
99+
}
100+
```
101+
102+
### `tsup.base.ts` — root factory + per-package call
103+
104+
- `tsup.base.ts` at the monorepo root exports a
105+
`defineTiptapExtensionConfig()` factory with the shared
106+
Tiptap-extension build shape (ESM+CJS, dts, `@tiptap/core` /
107+
`@tiptap/pm` external, prod sourcemaps/minify,
108+
`console.log/debug` stripped via `esbuildOptions.pure` while
109+
`console.warn/error` are preserved to match the eslint allowlist
110+
— do **not** use `drop: ['console']` which strips warn/error too).
111+
- The name follows the tsup ecosystem `define*` verb convention and is
112+
honestly scoped: the factory hardcodes Tiptap externals, so it is
113+
**not** a generic library factory. A future non-Tiptap library
114+
package should not reach for this factory; either add its own config
115+
or refactor the factory to take externals as a parameter.
116+
- A package's `tsup.config.ts` becomes a four-line file:
117+
118+
```ts
119+
import { defineConfig } from 'tsup'
120+
import { defineTiptapExtensionConfig } from '../../tsup.base'
121+
122+
export default defineConfig(defineTiptapExtensionConfig())
123+
```
124+
125+
- Pass overrides for genuinely package-specific concerns (e.g. an
126+
`onSuccess` hook to copy `styles.css` into `dist`).
127+
- **Override semantics — shallow spread, NOT deep merge.** Function-valued
128+
options (`esbuildOptions`, `external`, `dts`) passed to
129+
`defineTiptapExtensionConfig({...})` fully replace the base. If you
130+
ever override `esbuildOptions`, you must call the base's `pure` policy
131+
yourself or `console.log`/`debug` will silently survive into the
132+
production bundle. JSDoc on the factory spells out the recipe; add a
133+
deep-merge layer the day a second caller actually needs it, not
134+
before.
135+
- **Behavior change at adoption:** the prior per-package configs used
136+
`drop: ['console']` which stripped warn/error too. Switching to the
137+
factory restores `console.warn`/`console.error` in production.
138+
- 3 packages (`extension-indent`, `extension-inline-code`,
139+
`extension-placeholder`): no console calls in source → byte-identical
140+
dist trees, no observable change.
141+
- 1 package (`extension-hypermultimedia`): the `Logger` class in
142+
`src/utils/floating-toolbar.ts` wraps `console.warn`/`console.error`
143+
— those calls now reach consumer consoles in production. This was
144+
the author's clear intent; the prior behavior was an accidental
145+
over-strip. Note this in the next `extension-hypermultimedia`
146+
CHANGELOG entry when it's republished.
147+
148+
### What we deliberately do **not** share
149+
150+
- `README.md`, `CHANGELOG.md`, package source — these are inherently
151+
per-package.
152+
- `eslint.config.js` per package — already a 3-line shim importing from
153+
`@docs.plus/eslint-config`; further dedup adds no value.
154+
- `package.json` itself — generators that synthesize `package.json`
155+
fields fight semver tooling, registries, and contributor expectations.
156+
Keep these manual.
157+
40158
## Docker base image (single source of truth)
41159

42160
- All Dockerfiles use **one tag**: `oven/bun:1-slim` (latest Bun 1.x, floating; no hardcoded patch version).

AGENTS.md

Lines changed: 9 additions & 4 deletions
Large diffs are not rendered by default.

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2023-2026 Hossein Marzban
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

packages/extension-hyperlink/tsconfig.json

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,8 @@
11
{
2+
"extends": "../../tsconfig.base.json",
23
"compilerOptions": {
3-
"target": "ES2020",
4-
"module": "ESNext",
5-
"moduleResolution": "bundler",
6-
"esModuleInterop": true,
7-
"declaration": true,
84
"rootDir": "src",
9-
"outDir": "dist",
10-
"strict": true,
11-
"skipLibCheck": true,
12-
"forceConsistentCasingInFileNames": true,
13-
"types": []
5+
"outDir": "dist"
146
},
157
"include": ["src/**/*"],
168
"exclude": ["node_modules", "dist"]

packages/extension-hyperlink/tsup.config.ts

Lines changed: 7 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,31 +2,12 @@ import { copyFileSync } from 'node:fs'
22
import { resolve } from 'node:path'
33
import { defineConfig } from 'tsup'
44

5-
const isProduction = process.env.NODE_ENV === 'production'
5+
import { defineTiptapExtensionConfig } from '../../tsup.base'
66

7-
export default defineConfig({
8-
entry: ['src/index.ts'],
9-
outDir: 'dist',
10-
format: ['esm', 'cjs'],
11-
external: ['@tiptap/core', '@tiptap/pm'],
12-
dts: {
13-
entry: './src/index.ts',
14-
resolve: true
15-
},
16-
sourcemap: isProduction,
17-
clean: isProduction,
18-
minify: isProduction,
19-
outExtension({ format }) {
20-
return {
21-
js: format === 'esm' ? '.js' : '.cjs'
7+
export default defineConfig(
8+
defineTiptapExtensionConfig({
9+
async onSuccess() {
10+
copyFileSync(resolve('src/styles.css'), resolve('dist/styles.css'))
2211
}
23-
},
24-
esbuildOptions(options) {
25-
if (isProduction) {
26-
options.pure = ['console.log', 'console.debug']
27-
}
28-
},
29-
async onSuccess() {
30-
copyFileSync(resolve('src/styles.css'), resolve('dist/styles.css'))
31-
}
32-
})
12+
})
13+
)

packages/extension-hypermultimedia/tsconfig.json

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,7 @@
11
{
2+
"extends": "../../tsconfig.base.json",
23
"compilerOptions": {
3-
"target": "ES2020",
4-
"module": "ESNext",
5-
"moduleResolution": "bundler",
6-
"esModuleInterop": true,
7-
"declaration": true,
8-
"outDir": "dist",
9-
"strict": true,
10-
"skipLibCheck": true,
11-
"forceConsistentCasingInFileNames": true,
12-
"types": []
4+
"outDir": "dist"
135
},
146
"include": ["src/**/*"],
157
"exclude": ["node_modules", "dist"]
Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,5 @@
11
import { defineConfig } from 'tsup'
22

3-
const isProduction = process.env.NODE_ENV === 'production'
3+
import { defineTiptapExtensionConfig } from '../../tsup.base'
44

5-
export default defineConfig({
6-
entry: ['src/index.ts'],
7-
outDir: 'dist',
8-
format: ['cjs', 'esm'],
9-
external: ['@tiptap/core', '@tiptap/pm'],
10-
dts: {
11-
entry: './src/index.ts',
12-
resolve: true
13-
},
14-
sourcemap: isProduction,
15-
clean: isProduction,
16-
minify: isProduction,
17-
outExtension({ format }) {
18-
return {
19-
js: format === 'esm' ? '.js' : '.cjs'
20-
}
21-
},
22-
esbuildOptions(options) {
23-
options.drop = isProduction ? ['console'] : []
24-
}
25-
})
5+
export default defineConfig(defineTiptapExtensionConfig())

packages/extension-indent/tsconfig.json

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,7 @@
11
{
2+
"extends": "../../tsconfig.base.json",
23
"compilerOptions": {
3-
"target": "ES2020",
4-
"module": "ESNext",
5-
"moduleResolution": "bundler",
6-
"esModuleInterop": true,
7-
"declaration": true,
8-
"outDir": "dist",
9-
"strict": true,
10-
"skipLibCheck": true,
11-
"forceConsistentCasingInFileNames": true,
12-
"types": []
4+
"outDir": "dist"
135
},
146
"include": ["src/**/*"],
157
"exclude": ["node_modules", "dist"]
Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,5 @@
11
import { defineConfig } from 'tsup'
22

3-
const isProduction = process.env.NODE_ENV === 'production'
3+
import { defineTiptapExtensionConfig } from '../../tsup.base'
44

5-
export default defineConfig({
6-
entry: ['src/index.ts'],
7-
outDir: 'dist',
8-
format: ['esm', 'cjs'],
9-
external: ['@tiptap/core', '@tiptap/pm'],
10-
dts: {
11-
entry: './src/index.ts',
12-
resolve: true
13-
},
14-
sourcemap: isProduction,
15-
clean: isProduction,
16-
minify: isProduction,
17-
outExtension({ format }) {
18-
return {
19-
js: format === 'esm' ? '.js' : '.cjs'
20-
}
21-
},
22-
esbuildOptions(options) {
23-
options.drop = isProduction ? ['console'] : []
24-
}
25-
})
5+
export default defineConfig(defineTiptapExtensionConfig())

packages/extension-inline-code/tsconfig.json

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,7 @@
11
{
2+
"extends": "../../tsconfig.base.json",
23
"compilerOptions": {
3-
"target": "ES2020",
4-
"module": "ESNext",
5-
"moduleResolution": "bundler",
6-
"esModuleInterop": true,
7-
"declaration": true,
8-
"outDir": "dist",
9-
"strict": true,
10-
"skipLibCheck": true,
11-
"forceConsistentCasingInFileNames": true,
12-
"types": []
4+
"outDir": "dist"
135
},
146
"include": ["src/**/*"],
157
"exclude": ["node_modules", "dist"]

0 commit comments

Comments
 (0)