jskelet 0.6.4 → 0.6.6
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.
- package/CHANGELOG.md +10 -0
- package/docs/04-render-ve-sablonlar.md +33 -0
- package/docs/07-yapilandirma.md +41 -0
- package/docs/08-build.md +22 -2
- package/docs/10-dagitim.md +4 -1
- package/docs/en/04-rendering.md +33 -0
- package/docs/en/07-configuration.md +41 -0
- package/docs/en/08-build.md +22 -2
- package/docs/en/10-deployment.md +4 -0
- package/package.json +1 -1
- package/src/build/chunk-groups.mjs +331 -0
- package/src/build/tasks/client.mjs +53 -6
- package/src/config/defaults.js +25 -0
- package/src/config/index.js +125 -0
- package/src/server/og-image.js +621 -25
- package/src/server/og-raster.mjs +21 -0
- package/types/config/defaults.d.ts +24 -0
- package/types/config/index.d.ts +19 -0
- package/types/server/og-image.d.ts +46 -3
package/CHANGELOG.md
CHANGED
|
@@ -28,6 +28,16 @@ one is listed under a **Breaking** heading.
|
|
|
28
28
|
|
|
29
29
|
### Added
|
|
30
30
|
|
|
31
|
+
- `build.client` configures esbuild code splitting and chunk groups:
|
|
32
|
+
`splitting`, declarative `groups` (bare package / path / `@/` matchers), and
|
|
33
|
+
a `chunkGroup(id)` hook. Matched modules are co-located into one shared
|
|
34
|
+
chunk (webpack-`manualChunks` trade-off: weaker tree-shaking inside the
|
|
35
|
+
group). The client build step also prints the total JS file count.
|
|
36
|
+
- `ogImage({ fonts })` registers each `ttf` or `otf` (`path` and `family`)
|
|
37
|
+
with fontconfig so card and raw-SVG text use the site's own files. A slim
|
|
38
|
+
image still needs the `fontconfig` package. `woff` / `woff2` are not drawn
|
|
39
|
+
by the rasterizer (empty boxes); they are embedded only when the response
|
|
40
|
+
is SVG.
|
|
31
41
|
- `robots.txt` responses gain a trailing JSkelet note that disallows framework
|
|
32
42
|
endpoints (`/_jskelet/`, `/__jskelet/`, `/_fragment/`, plus a custom admin,
|
|
33
43
|
image, handoff, or dev path when it sits outside those prefixes). Every
|
|
@@ -582,6 +582,39 @@ app.get("/og/custom.png", async (req, res) => {
|
|
|
582
582
|
});
|
|
583
583
|
```
|
|
584
584
|
|
|
585
|
+
### Fontlar
|
|
586
|
+
|
|
587
|
+
`bookworm-slim` gibi ince imajlarda sistem fontu yoktur. Hazır kart
|
|
588
|
+
`system-ui` istediği için librsvg her sitede aynı boş kareleri basar.
|
|
589
|
+
`fonts`, `ttf` veya `otf` dosyasını fontconfig üzerinden librsvg'ye
|
|
590
|
+
bağlar. `family` kart metninin kullandığı addır; dosyanın içindeki adla
|
|
591
|
+
aynı olmak zorunda değildir. Ham `svg`'de kendi `font-family` değerinizi
|
|
592
|
+
bırakın ve aynı `family` adını buraya verin — `@font-face` bloğu yazmayın.
|
|
593
|
+
librsvg onu yok sayar.
|
|
594
|
+
|
|
595
|
+
```js
|
|
596
|
+
return {
|
|
597
|
+
title: post.title,
|
|
598
|
+
description: post.excerpt,
|
|
599
|
+
siteName: "Blog",
|
|
600
|
+
fonts: [
|
|
601
|
+
{ path: "fonts/Inter-Regular.ttf", family: "Inter" },
|
|
602
|
+
{ path: "fonts/Inter-Bold.ttf", family: "Inter" },
|
|
603
|
+
],
|
|
604
|
+
};
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
`path` uygulama köküne görelidir; mutlak yol da geçerlidir. PNG `ttf` ve
|
|
608
|
+
`otf` okur. librsvg `@font-face`'i yok sayar; `woff` / `woff2` boş kare
|
|
609
|
+
basar (yanıt SVG ise bu dosyalar yine gömülür, tarayıcı kullanabilir).
|
|
610
|
+
`weight` opsiyoneldir; her dosya içindeki ağırlığını korur, başlık gerçekten
|
|
611
|
+
kalın olsun diye regular ve bold dosyalarını birlikte verin. Dosya yoksa
|
|
612
|
+
görsel yine üretilir, o yüz atlanır.
|
|
613
|
+
|
|
614
|
+
Çalışma imajında `fontconfig` paketi yine gerekir: sharp'ın SVG metin çizimi
|
|
615
|
+
onsuz yüzleri yüklemez. Sistem font paketi (`fonts-dejavu` gibi) gerekmez.
|
|
616
|
+
PNG için `sharp` çalışma bağımlılığıdır.
|
|
617
|
+
|
|
585
618
|
Varsayılan başlıklar (süreler HTML ayarına bağlı değildir):
|
|
586
619
|
|
|
587
620
|
```
|
package/docs/07-yapilandirma.md
CHANGED
|
@@ -91,6 +91,11 @@ export default {
|
|
|
91
91
|
icons: { dir: "icons", scan: ["views", "client", "routes", "lib"] },
|
|
92
92
|
images: { widths: [400, 800, 1200], quality: 78, skip: ["indirmeler"] },
|
|
93
93
|
clientEnv: ["PUBLIC_WS_URL"],
|
|
94
|
+
build: {
|
|
95
|
+
client: {
|
|
96
|
+
groups: { charts: ["chart.js"] },
|
|
97
|
+
},
|
|
98
|
+
},
|
|
94
99
|
|
|
95
100
|
async headers() {
|
|
96
101
|
return [
|
|
@@ -585,6 +590,42 @@ images: {
|
|
|
585
590
|
ayarla `src` / `srcset`'i `/_jskelet/image?url=…&w=96` biçimine çevirir.
|
|
586
591
|
Elle URL kurmak için `remoteImageUrl(src, { width })` (`jskelet`).
|
|
587
592
|
|
|
593
|
+
## `build`
|
|
594
|
+
|
|
595
|
+
**Tip:** `{ client?: { splitting?, groups?, chunkGroup? } }` — **Varsayılan:**
|
|
596
|
+
`{ client: { splitting: true, groups: {}, chunkGroup: null } }`
|
|
597
|
+
|
|
598
|
+
Client JS (esbuild) bundling knobs. `fonts` / `icons` / `images` / `clientEnv`
|
|
599
|
+
hâlâ top-level kalır; bu bölüm yalnızca splitting ve chunk gruplaması içindir.
|
|
600
|
+
|
|
601
|
+
| Alan | Tip | Varsayılan | Anlamı |
|
|
602
|
+
| --- | --- | --- | --- |
|
|
603
|
+
| `client.splitting` | `boolean` | `true` | esbuild code splitting. `false` iken `groups` / `chunkGroup` yok sayılır (bir kez uyarı). |
|
|
604
|
+
| `client.groups` | `Record<string, string[]>` | `{}` | İsim → matcher listesi. Bare paket (`chart.js`), yol öneki (`./client/chrome/`) veya `@/` öneki. |
|
|
605
|
+
| `client.chunkGroup` | `(id: string) => string \| null \| undefined` | `null` | Modül kimliğini (mutlak yol veya bare specifier) grup adına map eder. **`groups` ile çakışınca hook kazanır.** |
|
|
606
|
+
|
|
607
|
+
esbuild native `manualChunks` sunmaz. Framework, gruba düşen import'ları bir
|
|
608
|
+
barrel üzerinden aynı paylaşılan chunk'a çeker. Bedel: grup içi tree-shaking
|
|
609
|
+
zayıflar — bir üyeyi kullanan sayfa grubun geri kalanını da indirebilir
|
|
610
|
+
(webpack `manualChunks` ile aynı takas). Çıktı dosya adı genelde
|
|
611
|
+
`chunks/chunk.<hash>.js` kalır; grup adı config anahtarıdır, dosya adı değil.
|
|
612
|
+
Ayrıntı: [08-build.md](./08-build.md).
|
|
613
|
+
|
|
614
|
+
```js
|
|
615
|
+
build: {
|
|
616
|
+
client: {
|
|
617
|
+
splitting: true,
|
|
618
|
+
groups: {
|
|
619
|
+
charts: ["chart.js", "chartjs-adapter-date-fns"],
|
|
620
|
+
chrome: ["./client/chrome/", "@/client/chrome/"],
|
|
621
|
+
},
|
|
622
|
+
chunkGroup(id) {
|
|
623
|
+
if (id.includes("node_modules/lodash")) return "vendor";
|
|
624
|
+
},
|
|
625
|
+
},
|
|
626
|
+
},
|
|
627
|
+
```
|
|
628
|
+
|
|
588
629
|
## `clientEnv`
|
|
589
630
|
|
|
590
631
|
**Tip:** `string[]` — **Varsayılan:** `[]`
|
package/docs/08-build.md
CHANGED
|
@@ -175,7 +175,8 @@ esbuild ayarları:
|
|
|
175
175
|
|
|
176
176
|
| Ayar | Değer | Sebebi |
|
|
177
177
|
| --- | --- | --- |
|
|
178
|
-
| `bundle
|
|
178
|
+
| `bundle` | `true` | — |
|
|
179
|
+
| `splitting` | `config.build.client.splitting` (varsayılan `true`) | Ortak modüller paylaşılan chunk'a çıkar; kapatılınca gruplama da düşer |
|
|
179
180
|
| `format` | `esm` | `type="module"` script'ler |
|
|
180
181
|
| `target` | `chrome111`, `edge111`, `firefox111`, `safari16.4` | ESM + dinamik import + `IntersectionObserver` island modelinin alt sınırı; daha eskisine transpile etmek çıktıyı büyütüp hiçbir ziyaretçi kazandırmıyor |
|
|
181
182
|
| `minify` | `true` | — |
|
|
@@ -185,7 +186,26 @@ esbuild ayarları:
|
|
|
185
186
|
| `legalComments` | `none` | — |
|
|
186
187
|
|
|
187
188
|
Çıktı `public/assets/js/` altına düşer ve her turda önce temizlenir.
|
|
188
|
-
`browserslist` okunmaz; hedef listesi kod içinde sabittir.
|
|
189
|
+
`browserslist` okunmaz; hedef listesi kod içinde sabittir. Adım satırı entry
|
|
190
|
+
sayısına ek olarak toplam JS dosya sayısını yazar (`1 entry, 48 files`) — çok
|
|
191
|
+
chunk teşhisini kolaylaştırır.
|
|
192
|
+
|
|
193
|
+
### Chunk gruplama (`build.client.groups` / `chunkGroup`)
|
|
194
|
+
|
|
195
|
+
esbuild native `manualChunks` sunmaz. `build.client.groups` ve
|
|
196
|
+
`build.client.chunkGroup` bir plugin ile uygulanır:
|
|
197
|
+
|
|
198
|
+
1. Eşleşen import bir re-export sarmalayıcısına yönlenir.
|
|
199
|
+
2. Sarmalayıcı `jskelet-group:<name>` barrel'ını side-effect olarak çeker.
|
|
200
|
+
3. Barrel gruptaki tüm bilinen üyeleri (bare paketler + yol önekindeki
|
|
201
|
+
`.{js,ts,mts}` dosyaları + resolve sırasında keşfedilenler) statik import
|
|
202
|
+
eder — gruba dokunan entry'ler aynı üye kümesine ulaşır ve tek paylaşılan
|
|
203
|
+
chunk oluşur.
|
|
204
|
+
|
|
205
|
+
**Trade-off:** grup içi tree-shaking zayıflar; bir üyeyi kullanan sayfa grubun
|
|
206
|
+
geri kalanını da indirebilir. Grup adı config anahtarıdır; çıktı dosyası
|
|
207
|
+
çoğunlukla `chunks/chunk.<hash>.js` kalır. Config sözlüğü:
|
|
208
|
+
[07-yapilandirma.md](./07-yapilandirma.md#build).
|
|
189
209
|
|
|
190
210
|
### `@/` alias'ı
|
|
191
211
|
|
package/docs/10-dagitim.md
CHANGED
|
@@ -136,7 +136,10 @@ Notlar:
|
|
|
136
136
|
optimizasyonu için. `--omit=dev` ile dışarıda kalır (devDependency olarak
|
|
137
137
|
kurulmuşsa). **`images.remote` açıksa** sharp runtime bağımlılığıdır —
|
|
138
138
|
production `dependencies`'e alın ya da runtime imajında ayrıca kurun; yoksa
|
|
139
|
-
optimizer kaynak URL'ye 302 yönlendirir.
|
|
139
|
+
optimizer kaynak URL'ye 302 yönlendirir. **Dinamik OG PNG** de sharp ister
|
|
140
|
+
ve imajda `fontconfig` paketi gerekir (`apt-get install -y fontconfig`).
|
|
141
|
+
`ogImage({ fonts })` site fontunu gömer; sistem font paketi şart değildir.
|
|
142
|
+
`fontconfig` yoksa metin boş kare basar.
|
|
140
143
|
- `jskelet start`ı `npx` olmadan çağırmak isterseniz
|
|
141
144
|
`CMD ["node", "node_modules/jskelet/bin/jskelet.mjs", "start"]` de çalışır.
|
|
142
145
|
|
package/docs/en/04-rendering.md
CHANGED
|
@@ -588,6 +588,39 @@ app.get("/og/custom.png", async (req, res) => {
|
|
|
588
588
|
});
|
|
589
589
|
```
|
|
590
590
|
|
|
591
|
+
### Fonts
|
|
592
|
+
|
|
593
|
+
A slim image such as `bookworm-slim` has no system fonts. The built-in card
|
|
594
|
+
asks for `system-ui`, so librsvg draws the same empty boxes on every site.
|
|
595
|
+
`fonts` points librsvg at your `ttf` or `otf` through fontconfig. `family` is
|
|
596
|
+
the name the card text uses; it does not have to be the name stored in the
|
|
597
|
+
file. On a raw `svg`, keep your own `font-family` and pass the same `family`
|
|
598
|
+
here — do not write an `@font-face` block. librsvg ignores it.
|
|
599
|
+
|
|
600
|
+
```js
|
|
601
|
+
return {
|
|
602
|
+
title: post.title,
|
|
603
|
+
description: post.excerpt,
|
|
604
|
+
siteName: "Blog",
|
|
605
|
+
fonts: [
|
|
606
|
+
{ path: "fonts/Inter-Regular.ttf", family: "Inter" },
|
|
607
|
+
{ path: "fonts/Inter-Bold.ttf", family: "Inter" },
|
|
608
|
+
],
|
|
609
|
+
};
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
`path` is relative to the application root; an absolute path is accepted too.
|
|
613
|
+
PNG rasterization reads `ttf` and `otf`. librsvg ignores `@font-face` and
|
|
614
|
+
draws empty boxes from `woff` / `woff2` (those files are still embedded when
|
|
615
|
+
the response is SVG, because the browser can use them). `weight` is optional;
|
|
616
|
+
each file keeps the weight stored inside it, so pass regular and bold files
|
|
617
|
+
if the title should be a real bold. A missing file is skipped and the image
|
|
618
|
+
is still produced.
|
|
619
|
+
|
|
620
|
+
The runtime image still needs the `fontconfig` package: sharp's SVG text
|
|
621
|
+
drawing will not load the faces without it. A system font package (such as
|
|
622
|
+
`fonts-dejavu`) is not required. PNG needs `sharp` as a runtime dependency.
|
|
623
|
+
|
|
591
624
|
Default headers (the durations are not tied to the HTML setting):
|
|
592
625
|
|
|
593
626
|
```
|
|
@@ -95,6 +95,11 @@ export default {
|
|
|
95
95
|
icons: { dir: "icons", scan: ["views", "client", "routes", "lib"] },
|
|
96
96
|
images: { widths: [400, 800, 1200], quality: 78, skip: ["downloads"] },
|
|
97
97
|
clientEnv: ["PUBLIC_WS_URL"],
|
|
98
|
+
build: {
|
|
99
|
+
client: {
|
|
100
|
+
groups: { charts: ["chart.js"] },
|
|
101
|
+
},
|
|
102
|
+
},
|
|
98
103
|
|
|
99
104
|
async headers() {
|
|
100
105
|
return [
|
|
@@ -598,6 +603,42 @@ images: {
|
|
|
598
603
|
rewrites `src` / `srcset` to `/_jskelet/image?url=…&w=96`. To build URLs by
|
|
599
604
|
hand, use `remoteImageUrl(src, { width })` from `jskelet`.
|
|
600
605
|
|
|
606
|
+
## `build`
|
|
607
|
+
|
|
608
|
+
**Type:** `{ client?: { splitting?, groups?, chunkGroup? } }` — **Default:**
|
|
609
|
+
`{ client: { splitting: true, groups: {}, chunkGroup: null } }`
|
|
610
|
+
|
|
611
|
+
Client JS (esbuild) bundling knobs. `fonts` / `icons` / `images` / `clientEnv`
|
|
612
|
+
stay top-level; this section is only for splitting and chunk grouping.
|
|
613
|
+
|
|
614
|
+
| Field | Type | Default | Meaning |
|
|
615
|
+
| --- | --- | --- | --- |
|
|
616
|
+
| `client.splitting` | `boolean` | `true` | esbuild code splitting. When `false`, `groups` / `chunkGroup` are ignored (one warning). |
|
|
617
|
+
| `client.groups` | `Record<string, string[]>` | `{}` | Name → matcher list. Bare package (`chart.js`), path prefix (`./client/chrome/`), or `@/` prefix. |
|
|
618
|
+
| `client.chunkGroup` | `(id: string) => string \| null \| undefined` | `null` | Maps a module id (absolute path or bare specifier) to a group name. **When it conflicts with `groups`, the hook wins.** |
|
|
619
|
+
|
|
620
|
+
esbuild has no native `manualChunks`. The framework pulls imports that land in a
|
|
621
|
+
group into the same shared chunk via a barrel. Trade-off: tree-shaking inside a
|
|
622
|
+
group weakens — a page that uses one member may download the rest of the group
|
|
623
|
+
(same trade-off as webpack `manualChunks`). Output filenames usually stay
|
|
624
|
+
`chunks/chunk.<hash>.js`; the group name is a config key, not the file name.
|
|
625
|
+
Details: [08-build.md](./08-build.md).
|
|
626
|
+
|
|
627
|
+
```js
|
|
628
|
+
build: {
|
|
629
|
+
client: {
|
|
630
|
+
splitting: true,
|
|
631
|
+
groups: {
|
|
632
|
+
charts: ["chart.js", "chartjs-adapter-date-fns"],
|
|
633
|
+
chrome: ["./client/chrome/", "@/client/chrome/"],
|
|
634
|
+
},
|
|
635
|
+
chunkGroup(id) {
|
|
636
|
+
if (id.includes("node_modules/lodash")) return "vendor";
|
|
637
|
+
},
|
|
638
|
+
},
|
|
639
|
+
},
|
|
640
|
+
```
|
|
641
|
+
|
|
601
642
|
## `clientEnv`
|
|
602
643
|
|
|
603
644
|
**Type:** `string[]` — **Default:** `[]`
|
package/docs/en/08-build.md
CHANGED
|
@@ -183,7 +183,8 @@ esbuild settings:
|
|
|
183
183
|
|
|
184
184
|
| Setting | Value | Reason |
|
|
185
185
|
| --- | --- | --- |
|
|
186
|
-
| `bundle
|
|
186
|
+
| `bundle` | `true` | — |
|
|
187
|
+
| `splitting` | `config.build.client.splitting` (default `true`) | Shared modules move into a shared chunk; turning it off also disables grouping |
|
|
187
188
|
| `format` | `esm` | `type="module"` scripts |
|
|
188
189
|
| `target` | `chrome111`, `edge111`, `firefox111`, `safari16.4` | The lower bound of the ESM + dynamic import + `IntersectionObserver` island model; transpiling to anything older grows the output without winning a single visitor |
|
|
189
190
|
| `minify` | `true` | — |
|
|
@@ -193,7 +194,26 @@ esbuild settings:
|
|
|
193
194
|
| `legalComments` | `none` | — |
|
|
194
195
|
|
|
195
196
|
The output lands under `public/assets/js/` and is cleaned first on every pass.
|
|
196
|
-
`browserslist` is not read; the target list is hard-coded.
|
|
197
|
+
`browserslist` is not read; the target list is hard-coded. The step line prints
|
|
198
|
+
the total JS file count next to the entry count (`1 entry, 48 files`) so large
|
|
199
|
+
chunk graphs are easier to spot.
|
|
200
|
+
|
|
201
|
+
### Chunk grouping (`build.client.groups` / `chunkGroup`)
|
|
202
|
+
|
|
203
|
+
esbuild has no native `manualChunks`. `build.client.groups` and
|
|
204
|
+
`build.client.chunkGroup` are applied through a plugin:
|
|
205
|
+
|
|
206
|
+
1. A matching import is redirected to a re-export wrapper.
|
|
207
|
+
2. The wrapper side-effect-imports the `jskelet-group:<name>` barrel.
|
|
208
|
+
3. The barrel statically imports every known member of the group (bare packages,
|
|
209
|
+
`.{js,ts,mts}` files under path prefixes, plus members discovered at resolve
|
|
210
|
+
time) — every entry that touches the group reaches the same member set and
|
|
211
|
+
one shared chunk is produced.
|
|
212
|
+
|
|
213
|
+
**Trade-off:** tree-shaking inside a group weakens; a page that uses one member
|
|
214
|
+
may download the rest of the group. The group name is a config key; the output
|
|
215
|
+
file is usually still `chunks/chunk.<hash>.js`. Config reference:
|
|
216
|
+
[07-configuration.md](./07-configuration.md#build).
|
|
197
217
|
|
|
198
218
|
### The `@/` alias
|
|
199
219
|
|
package/docs/en/10-deployment.md
CHANGED
|
@@ -138,6 +138,10 @@ Notes:
|
|
|
138
138
|
as a devDependency). **If `images.remote` is enabled**, sharp is a runtime
|
|
139
139
|
dependency — move it to production `dependencies` or install it in the
|
|
140
140
|
runtime image; otherwise the optimizer 302-redirects to the source URL.
|
|
141
|
+
**Dynamic OG PNG** needs sharp too, and the image needs the `fontconfig`
|
|
142
|
+
package (`apt-get install -y fontconfig`). `ogImage({ fonts })` embeds the
|
|
143
|
+
site's own faces; a system font package is not required. Without
|
|
144
|
+
`fontconfig`, text renders as empty boxes.
|
|
141
145
|
- If you would rather call `jskelet start` without `npx`,
|
|
142
146
|
`CMD ["node", "node_modules/jskelet/bin/jskelet.mjs", "start"]` works too.
|
|
143
147
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jskelet",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.6",
|
|
4
4
|
"description": "A framework that feels like no framework: Express 5 + build-time .jsk SSR (optional EJS peer), vanilla JS islands, Tailwind v4 and an in-process HTML TTL cache.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* esbuild manualChunks karşılığı: `build.client.groups` / `chunkGroup`.
|
|
3
|
+
*
|
|
4
|
+
* esbuild native grup ataması sunmaz. Bir gruba düşen import, re-export
|
|
5
|
+
* sarmalayıcısı üzerinden gider; sarmalayıcı gruba ait barrel'ı side-effect
|
|
6
|
+
* olarak çeker. Barrel tüm bilinen üyeleri statik import eder — gruba
|
|
7
|
+
* dokunan entry'ler aynı üye kümesine ulaşır ve paylaşılan tek chunk oluşur.
|
|
8
|
+
*
|
|
9
|
+
* Bedel: grup içi tree-shaking zayıflar (webpack manualChunks ile aynı
|
|
10
|
+
* takas). `splitting: false` iken plugin hiç eklenmez.
|
|
11
|
+
*/
|
|
12
|
+
import fs from "node:fs";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
|
|
15
|
+
const GROUP_NS = "jskelet-group";
|
|
16
|
+
const REEXPORT_NS = "jskelet-group-reexport";
|
|
17
|
+
const GROUP_PREFIX = `${GROUP_NS}:`;
|
|
18
|
+
|
|
19
|
+
/** Barrel / re-export sarmalayıcısının kendi import'ları tekrar sarılmasın. */
|
|
20
|
+
const SKIP_NS = new Set([GROUP_NS, REEXPORT_NS]);
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {object} ChunkGroupOptions
|
|
24
|
+
* @property {string} root
|
|
25
|
+
* @property {Record<string, string[]>} groups
|
|
26
|
+
* @property {((id: string) => string | null | undefined) | null} [chunkGroup]
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Grup adı dosya yoluna uygun mu (`chunks/<name>.[hash]`).
|
|
31
|
+
*
|
|
32
|
+
* @param {string} name
|
|
33
|
+
* @returns {boolean}
|
|
34
|
+
*/
|
|
35
|
+
export function isSafeGroupName(name) {
|
|
36
|
+
return typeof name === "string" && /^[A-Za-z0-9_-]+$/.test(name);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Matcher'ın bare paket, `@/` veya göreli yol öneki olup olmadığını ayırır.
|
|
41
|
+
*
|
|
42
|
+
* @param {string} matcher
|
|
43
|
+
* @returns {"bare" | "path"}
|
|
44
|
+
*/
|
|
45
|
+
export function matcherKind(matcher) {
|
|
46
|
+
if (matcher.startsWith("./") || matcher.startsWith("../") || matcher.startsWith("@/")) {
|
|
47
|
+
return "path";
|
|
48
|
+
}
|
|
49
|
+
return "bare";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Yol önekini proje köküne göre mutlak dizin / dosya önekine çevirir.
|
|
54
|
+
*
|
|
55
|
+
* @param {string} root
|
|
56
|
+
* @param {string} matcher
|
|
57
|
+
* @returns {string}
|
|
58
|
+
*/
|
|
59
|
+
export function resolvePathMatcher(root, matcher) {
|
|
60
|
+
if (matcher.startsWith("@/")) {
|
|
61
|
+
return path.resolve(root, matcher.slice(2));
|
|
62
|
+
}
|
|
63
|
+
return path.resolve(root, matcher);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Bare paket: exact veya `pkg/...` alt yolu.
|
|
68
|
+
*
|
|
69
|
+
* @param {string} matcher
|
|
70
|
+
* @param {string} importPath
|
|
71
|
+
* @returns {boolean}
|
|
72
|
+
*/
|
|
73
|
+
export function matchBarePackage(matcher, importPath) {
|
|
74
|
+
return importPath === matcher || importPath.startsWith(`${matcher}/`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Mutlak yol, matcher önekiyle başlıyor mu (dizin sınırı korunur).
|
|
79
|
+
*
|
|
80
|
+
* @param {string} absMatcher
|
|
81
|
+
* @param {string} absPath
|
|
82
|
+
* @returns {boolean}
|
|
83
|
+
*/
|
|
84
|
+
export function matchPathPrefix(absMatcher, absPath) {
|
|
85
|
+
const normalizedMatcher = path.resolve(absMatcher);
|
|
86
|
+
const normalizedPath = path.resolve(absPath);
|
|
87
|
+
if (normalizedPath === normalizedMatcher) return true;
|
|
88
|
+
const prefix = normalizedMatcher.endsWith(path.sep)
|
|
89
|
+
? normalizedMatcher
|
|
90
|
+
: normalizedMatcher + path.sep;
|
|
91
|
+
return normalizedPath.startsWith(prefix);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Declarative path matcher altındaki `.{js,ts,mts}` dosyalarını listeler.
|
|
96
|
+
*
|
|
97
|
+
* @param {string} root
|
|
98
|
+
* @param {string} matcher
|
|
99
|
+
* @returns {string[]}
|
|
100
|
+
*/
|
|
101
|
+
export function listPathMatcherMembers(root, matcher) {
|
|
102
|
+
const base = resolvePathMatcher(root, matcher);
|
|
103
|
+
if (!fs.existsSync(base)) return [];
|
|
104
|
+
|
|
105
|
+
const stat = fs.statSync(base);
|
|
106
|
+
if (stat.isFile()) return [base];
|
|
107
|
+
|
|
108
|
+
/** @type {string[]} */
|
|
109
|
+
const out = [];
|
|
110
|
+
walkJsFiles(base, out);
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* @param {string} dir
|
|
116
|
+
* @param {string[]} out
|
|
117
|
+
*/
|
|
118
|
+
function walkJsFiles(dir, out) {
|
|
119
|
+
let entries;
|
|
120
|
+
try {
|
|
121
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
122
|
+
} catch {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
for (const entry of entries) {
|
|
126
|
+
const full = path.join(dir, entry.name);
|
|
127
|
+
if (entry.isDirectory()) {
|
|
128
|
+
if (entry.name === "node_modules" || entry.name === ".git") continue;
|
|
129
|
+
walkJsFiles(full, out);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (!entry.isFile()) continue;
|
|
133
|
+
const ext = path.extname(entry.name);
|
|
134
|
+
if (ext === ".js" || ext === ".ts" || ext === ".mts" || ext === ".mjs") {
|
|
135
|
+
out.push(full);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Import yolu / çözümlenmiş dosya için grup adını bulur.
|
|
142
|
+
* `chunkGroup` hook'u `groups` map'inden önce gelir.
|
|
143
|
+
*
|
|
144
|
+
* @param {string} importPath
|
|
145
|
+
* @param {string | null} resolvedPath
|
|
146
|
+
* @param {ChunkGroupOptions} options
|
|
147
|
+
* @returns {string | null}
|
|
148
|
+
*/
|
|
149
|
+
export function resolveGroupName(importPath, resolvedPath, options) {
|
|
150
|
+
const { root, groups, chunkGroup } = options;
|
|
151
|
+
|
|
152
|
+
if (typeof chunkGroup === "function") {
|
|
153
|
+
for (const id of [resolvedPath, importPath].filter(Boolean)) {
|
|
154
|
+
try {
|
|
155
|
+
const result = chunkGroup(/** @type {string} */ (id));
|
|
156
|
+
if (typeof result === "string" && isSafeGroupName(result.trim())) {
|
|
157
|
+
return result.trim();
|
|
158
|
+
}
|
|
159
|
+
} catch (error) {
|
|
160
|
+
console.warn(
|
|
161
|
+
"[build] build.client.chunkGroup() threw, ignoring this module",
|
|
162
|
+
error,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
for (const [name, matchers] of Object.entries(groups)) {
|
|
169
|
+
for (const matcher of matchers) {
|
|
170
|
+
if (matcherKind(matcher) === "bare") {
|
|
171
|
+
if (matchBarePackage(matcher, importPath)) return name;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
const absMatcher = resolvePathMatcher(root, matcher);
|
|
175
|
+
if (resolvedPath && matchPathPrefix(absMatcher, resolvedPath)) return name;
|
|
176
|
+
if (
|
|
177
|
+
importPath.startsWith("./") ||
|
|
178
|
+
importPath.startsWith("../") ||
|
|
179
|
+
importPath.startsWith("@/")
|
|
180
|
+
) {
|
|
181
|
+
// resolveDir bilinmeden göreli yolu kesinleştiremeyiz; @/ yeter.
|
|
182
|
+
if (importPath.startsWith("@/")) {
|
|
183
|
+
const absImport = path.resolve(root, importPath.slice(2));
|
|
184
|
+
if (matchPathPrefix(absMatcher, absImport)) return name;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* @param {ChunkGroupOptions} options
|
|
195
|
+
* @returns {import('esbuild').Plugin | null}
|
|
196
|
+
*/
|
|
197
|
+
export function chunkGroupsPlugin(options) {
|
|
198
|
+
const { root, groups, chunkGroup } = options;
|
|
199
|
+
const hasGroups = Object.keys(groups).length > 0;
|
|
200
|
+
if (!hasGroups && typeof chunkGroup !== "function") return null;
|
|
201
|
+
|
|
202
|
+
/** @type {Map<string, Set<string>>} */
|
|
203
|
+
const members = new Map();
|
|
204
|
+
|
|
205
|
+
for (const name of Object.keys(groups)) {
|
|
206
|
+
if (!isSafeGroupName(name)) continue;
|
|
207
|
+
members.set(name, new Set());
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Declarative matcher'lardan bilinen üyeleri önceden doldur.
|
|
211
|
+
for (const [name, matchers] of Object.entries(groups)) {
|
|
212
|
+
if (!isSafeGroupName(name)) continue;
|
|
213
|
+
const set = members.get(name) ?? new Set();
|
|
214
|
+
members.set(name, set);
|
|
215
|
+
for (const matcher of matchers) {
|
|
216
|
+
if (matcherKind(matcher) === "bare") {
|
|
217
|
+
set.add(matcher);
|
|
218
|
+
} else {
|
|
219
|
+
for (const file of listPathMatcherMembers(root, matcher)) {
|
|
220
|
+
set.add(file);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* @param {string} group
|
|
228
|
+
* @param {string} member
|
|
229
|
+
*/
|
|
230
|
+
function addMember(group, member) {
|
|
231
|
+
if (!isSafeGroupName(group)) return;
|
|
232
|
+
let set = members.get(group);
|
|
233
|
+
if (!set) {
|
|
234
|
+
set = new Set();
|
|
235
|
+
members.set(group, set);
|
|
236
|
+
}
|
|
237
|
+
set.add(member);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
name: "jskelet-chunk-groups",
|
|
242
|
+
setup(build) {
|
|
243
|
+
build.onResolve({ filter: /.*/ }, async (args) => {
|
|
244
|
+
if (args.pluginData?.jskeletChunkGroupSkip) return undefined;
|
|
245
|
+
|
|
246
|
+
// Re-export sarmalayıcısı ve entry `jskelet-group:name` ister; namespace
|
|
247
|
+
// SKIP olsa bile bu specifier'ı biz çözmeliyiz.
|
|
248
|
+
if (args.path.startsWith(GROUP_PREFIX)) {
|
|
249
|
+
const name = args.path.slice(GROUP_PREFIX.length);
|
|
250
|
+
if (!isSafeGroupName(name)) return undefined;
|
|
251
|
+
if (!members.has(name)) members.set(name, new Set());
|
|
252
|
+
return { path: name, namespace: GROUP_NS };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Barrel / re-export içindeki gerçek üye import'ları tekrar sarılmasın.
|
|
256
|
+
if (SKIP_NS.has(args.namespace)) return undefined;
|
|
257
|
+
|
|
258
|
+
// data:/http: vb. ve entry-point'leri dokunma.
|
|
259
|
+
if (args.kind === "entry-point") return undefined;
|
|
260
|
+
if (/^[a-zA-Z][a-zA-Z\d+.-]*:/.test(args.path)) return undefined;
|
|
261
|
+
|
|
262
|
+
// Önce hook / bare matcher ile ucuz eşleşme; path önekleri resolve ister.
|
|
263
|
+
let group = resolveGroupName(args.path, null, options);
|
|
264
|
+
|
|
265
|
+
const resolved = await build.resolve(args.path, {
|
|
266
|
+
importer: args.importer,
|
|
267
|
+
namespace: args.namespace,
|
|
268
|
+
resolveDir: args.resolveDir,
|
|
269
|
+
kind: args.kind,
|
|
270
|
+
pluginData: { jskeletChunkGroupSkip: true },
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
if (resolved.errors?.length || !resolved.path) {
|
|
274
|
+
return undefined;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Virtual / zaten bizim namespace.
|
|
278
|
+
if (resolved.namespace && SKIP_NS.has(resolved.namespace)) {
|
|
279
|
+
return undefined;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
group = resolveGroupName(args.path, resolved.path, options) ?? group;
|
|
283
|
+
if (!group) return undefined;
|
|
284
|
+
|
|
285
|
+
// Üye olarak çözümlenmiş yolu yaz — barrel aynı dosyayı çeker.
|
|
286
|
+
addMember(group, resolved.path);
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
path: `${group}::${resolved.path}`,
|
|
290
|
+
namespace: REEXPORT_NS,
|
|
291
|
+
};
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
build.onLoad({ filter: /.*/, namespace: GROUP_NS }, (args) => {
|
|
295
|
+
const set = members.get(args.path) ?? new Set();
|
|
296
|
+
const lines = [...set].map((member) => `import ${JSON.stringify(member)};`);
|
|
297
|
+
return {
|
|
298
|
+
contents: lines.length ? `${lines.join("\n")}\n` : "export {}\n",
|
|
299
|
+
resolveDir: root,
|
|
300
|
+
loader: "js",
|
|
301
|
+
};
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
build.onLoad({ filter: /.*/, namespace: REEXPORT_NS }, (args) => {
|
|
305
|
+
const sep = args.path.indexOf("::");
|
|
306
|
+
if (sep === -1) {
|
|
307
|
+
return { contents: "export {}\n", loader: "js" };
|
|
308
|
+
}
|
|
309
|
+
const group = args.path.slice(0, sep);
|
|
310
|
+
const target = args.path.slice(sep + 2);
|
|
311
|
+
const targetLit = JSON.stringify(target);
|
|
312
|
+
const groupLit = JSON.stringify(`${GROUP_PREFIX}${group}`);
|
|
313
|
+
|
|
314
|
+
// Side-effect barrel + re-export. Named-only modüllerde
|
|
315
|
+
// `__ns.default` undefined olur (uyarı → logOverride ile susturulur);
|
|
316
|
+
// `export { default } from` ise hard error verdiği için kullanılmaz.
|
|
317
|
+
return {
|
|
318
|
+
contents: `
|
|
319
|
+
import ${groupLit};
|
|
320
|
+
import * as __ns from ${targetLit};
|
|
321
|
+
export * from ${targetLit};
|
|
322
|
+
export default __ns.default;
|
|
323
|
+
`,
|
|
324
|
+
resolveDir: root,
|
|
325
|
+
loader: "js",
|
|
326
|
+
};
|
|
327
|
+
});
|
|
328
|
+
},
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|