@svelte-lean/vite 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/LICENSE +21 -0
- package/README.md +229 -0
- package/dist/behaviors.d.ts +23 -0
- package/dist/behaviors.js +106 -0
- package/dist/filter.d.ts +31 -0
- package/dist/filter.js +103 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +5 -0
- package/dist/manifest.d.ts +13 -0
- package/dist/manifest.js +43 -0
- package/dist/plugin.d.ts +33 -0
- package/dist/plugin.js +214 -0
- package/dist/report.d.ts +13 -0
- package/dist/report.js +70 -0
- package/dist/scanner.d.ts +19 -0
- package/dist/scanner.js +394 -0
- package/dist/types.d.ts +98 -0
- package/dist/types.js +1 -0
- package/package.json +59 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# @svelte-lean/vite
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- eb039db: Initial release: the `svelteLean()` plugin for Vite 6, 7 and 8 discovers static `data-slean="…"` markers in Svelte markup and injects only the registration modules that are used, with a behavior manifest and an optional build report.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies [eb039db]
|
|
12
|
+
- @svelte-lean/primitives@0.1.0
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ömer Say
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# @svelte-lean/vite
|
|
2
|
+
|
|
3
|
+
[Documentation](https://svelte-lean.vercel.app/docs/architecture/build-time-discovery) ·
|
|
4
|
+
[Installation](https://svelte-lean.vercel.app/docs/installation) ·
|
|
5
|
+
[Repository](https://github.com/say16/svelte-lean/tree/main/packages/vite) ·
|
|
6
|
+
[Changelog](https://github.com/say16/svelte-lean/blob/main/packages/vite/CHANGELOG.md)
|
|
7
|
+
|
|
8
|
+
Build-time behavior discovery for Svelte Lean. The plugin reads Svelte markup for static
|
|
9
|
+
`data-slean="…"` markers and adds the matching registration modules to the compiled module, so an
|
|
10
|
+
application ships the behavior runtime it uses and nothing else. Markup that only uses native
|
|
11
|
+
behaviors (button, dialog, popover, …) gets no import and no runtime.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
yarn add -D @svelte-lean/vite
|
|
17
|
+
yarn add @svelte-lean/primitives
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Peer dependencies: `vite` 6, 7 or 8, and `@svelte-lean/primitives` (optional; the built-in
|
|
21
|
+
registration modules come from it). No production dependencies. Node `^20.19.0 || >=22.12.0`, the
|
|
22
|
+
range of Vite 7 and 8.
|
|
23
|
+
|
|
24
|
+
The repository builds and tests with Vite 7 (`docs/compatibility.md`); the nightly workflow runs the
|
|
25
|
+
same suite against `vite@latest`. Vite 8 was also checked by hand before the first release: a
|
|
26
|
+
SvelteKit application on Vite 8.3 and `@sveltejs/vite-plugin-svelte` 7.3 with the packed
|
|
27
|
+
tarballs, through `vite build`, `vite preview` and `vite dev`.
|
|
28
|
+
|
|
29
|
+
## Usage with SvelteKit
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
// vite.config.ts
|
|
33
|
+
import { sveltekit } from '@sveltejs/kit/vite';
|
|
34
|
+
import { svelteLean } from '@svelte-lean/vite';
|
|
35
|
+
import { defineConfig } from 'vite';
|
|
36
|
+
|
|
37
|
+
export default defineConfig({
|
|
38
|
+
plugins: [svelteLean(), sveltekit()]
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Plain Vite with `@sveltejs/vite-plugin-svelte` is the same: `plugins: [svelteLean(), svelte()]`.
|
|
43
|
+
|
|
44
|
+
`svelteLean()` returns two plugins: `svelte-lean:scan` (`enforce: 'pre'`) reads the Svelte source
|
|
45
|
+
before the compiler, and `svelte-lean:inject` (`enforce: 'post'`) adds the imports after it.
|
|
46
|
+
`vite-plugin-svelte` compiles in a normal-phase plugin, so the pair works whether it is placed
|
|
47
|
+
before or after `sveltekit()`; `tests/build.test.ts` builds the fixture app with both orders (plain
|
|
48
|
+
Vite + `vite-plugin-svelte`). Put it first so the scanner sees the source before any preprocessor.
|
|
49
|
+
The ordering against SvelteKit itself is verified by the `apps/playground` build, which uses
|
|
50
|
+
`plugins: [svelteLean(), sveltekit()]`.
|
|
51
|
+
|
|
52
|
+
## What is injected
|
|
53
|
+
|
|
54
|
+
Given `src/routes/settings/+page.svelte` containing `<div data-slean="tabs">…</div>`, the compiled
|
|
55
|
+
module receives, after the compiler output:
|
|
56
|
+
|
|
57
|
+
```js
|
|
58
|
+
import '@svelte-lean/primitives/tabs/register';
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The exact format: one `import '<specifier>';` statement per registration module, single quotes,
|
|
62
|
+
sorted by specifier, de-duplicated, each on its own line, appended after the compiled code and
|
|
63
|
+
terminated by a newline. Appending keeps every existing line and column in place, so the transform
|
|
64
|
+
returns `map: null` and the Svelte compiler's source map stays valid. ES module imports are hoisted,
|
|
65
|
+
so the registration module evaluates before the component code exactly as a prepended import
|
|
66
|
+
would.
|
|
67
|
+
|
|
68
|
+
Bundlers de-duplicate modules: a hundred files that use tabs still produce one copy of the
|
|
69
|
+
registration module in the graph. Registration in `@svelte-lean/core` is idempotent, so mixing
|
|
70
|
+
injected imports with manual ones is safe.
|
|
71
|
+
|
|
72
|
+
## Static-only contract
|
|
73
|
+
|
|
74
|
+
Discovered:
|
|
75
|
+
|
|
76
|
+
```svelte
|
|
77
|
+
<div data-slean="tabs">
|
|
78
|
+
<div data-slean='tabs'>
|
|
79
|
+
<div data-slean=tabs>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Not discovered (dynamic):
|
|
83
|
+
|
|
84
|
+
```svelte
|
|
85
|
+
<div data-slean={kind}>
|
|
86
|
+
<div data-slean="tabs-{suffix}">
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Dynamic markers are recorded in the manifest and reported once per file:
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
Dynamic behavior identifiers cannot be auto-discovered.
|
|
93
|
+
Found:
|
|
94
|
+
src/lib/Panel.svelte:12:2 data-slean={kind}
|
|
95
|
+
Use a static identifier such as data-slean="tabs", or import the registration module
|
|
96
|
+
manually (for example import '@svelte-lean/primitives/tabs/register').
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Manual escape hatch
|
|
100
|
+
|
|
101
|
+
Automatic discovery is never mandatory. Without the plugin, with a dynamic identifier, or in a
|
|
102
|
+
build that is not Vite, register a behavior yourself:
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
import '@svelte-lean/primitives/tabs/register';
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
This is the same module the plugin injects, so the runtime behavior is identical.
|
|
109
|
+
|
|
110
|
+
## Options
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
svelteLean({
|
|
114
|
+
include: ['**/*.svelte'], // files to scan; globs (relative to the Vite root or absolute) or RegExp
|
|
115
|
+
exclude: ['**/node_modules/**'],
|
|
116
|
+
package: '@svelte-lean/primitives', // re-bases the built-in registration specifiers
|
|
117
|
+
behaviors: {}, // name → module specifier, or false for "no runtime"
|
|
118
|
+
manifest: true, // true | false | path; true = node_modules/.svelte-lean/manifest.json
|
|
119
|
+
report: false, // print a build report after the client bundle is written
|
|
120
|
+
warnDynamic: true, // warn about data-slean={…}
|
|
121
|
+
ssr: 'skip' // 'skip' | 'inject'
|
|
122
|
+
});
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
| Option | Default | Notes |
|
|
126
|
+
| ------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
127
|
+
| `include` | `['**/*.svelte']` | Globs support `**`, `*`, `?` and `{a,b}`. Style and `?raw`-style sub-requests are never touched. |
|
|
128
|
+
| `exclude` | `['**/node_modules/**']` | Tested after `include`. |
|
|
129
|
+
| `package` | `'@svelte-lean/primitives'` | Built-in specifiers that start with `@svelte-lean/primitives/` are rewritten to start with this value. |
|
|
130
|
+
| `behaviors` | `{}` | Overrides and extends `BUILTIN_BEHAVIORS`. A string is injected as written (bare, relative to the component file, or absolute); `false` marks a behavior that needs no runtime. |
|
|
131
|
+
| `manifest` | `true` | Written at `buildEnd` of production builds. Not written in dev or after a failed build. |
|
|
132
|
+
| `report` | `false` | Printed at `closeBundle` for the client build only (SSR builds are silent). |
|
|
133
|
+
| `warnDynamic` | `true` | Warnings go through the plugin context (`this.warn`), so they carry file and position. |
|
|
134
|
+
| `ssr` | `'skip'` | Registration modules are SSR-safe (no `document` access at evaluation), so `'inject'` is only a server bundle size choice; the browser-only runtime does nothing on the server. |
|
|
135
|
+
|
|
136
|
+
Built-in behaviors (`BUILTIN_BEHAVIORS`): every Tier 1 and Tier 2 primitive (`tabs`, `menu`,
|
|
137
|
+
`listbox`, `combobox`, `select`, `date-picker`, …) maps to `@svelte-lean/primitives/<name>/register`;
|
|
138
|
+
the Tier 0 primitives (`button`, `dialog`, `popover`, `disclosure`, `checkbox`, `switch`,
|
|
139
|
+
`radio-group`, …) map to `false`. `radio` is also accepted as a native name: the ADR 0001 vocabulary
|
|
140
|
+
for `<input type="radio">`, with no primitive contract of its own. The scanner records no marker for
|
|
141
|
+
a bare `<select data-slean="select">`: that is the Tier 0 styled native select, and only the owned
|
|
142
|
+
select (`<div data-slean="select">`, ADR 0007) needs the runtime.
|
|
143
|
+
A test in the primitives package keeps this table equal to its `BEHAVIORS` map, so a new Tier 1
|
|
144
|
+
primitive cannot be added there without a registration module here. A name outside this table
|
|
145
|
+
and `behaviors` is unknown: it is reported once, listed in the manifest, and nothing is injected
|
|
146
|
+
for it.
|
|
147
|
+
|
|
148
|
+
## Manifest
|
|
149
|
+
|
|
150
|
+
`node_modules/.svelte-lean/manifest.json` (or the configured path) after a production build:
|
|
151
|
+
|
|
152
|
+
```json
|
|
153
|
+
{
|
|
154
|
+
"version": 1,
|
|
155
|
+
"behaviors": {
|
|
156
|
+
"button": { "files": ["src/lib/Toolbar.svelte"], "module": null },
|
|
157
|
+
"tabs": {
|
|
158
|
+
"files": ["src/routes/settings/+page.svelte"],
|
|
159
|
+
"module": "@svelte-lean/primitives/tabs/register"
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
"dynamic": {
|
|
163
|
+
"src/lib/Panel.svelte": [{ "line": 12, "expression": "{kind}" }]
|
|
164
|
+
},
|
|
165
|
+
"unknown": []
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
`files` are relative to the Vite root and sorted; `module` is `null` for native and unknown
|
|
170
|
+
behaviors. The manifest is a build artifact for diagnostics and tooling; it is not shipped to the
|
|
171
|
+
browser.
|
|
172
|
+
|
|
173
|
+
## Build report
|
|
174
|
+
|
|
175
|
+
With `report: true`:
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
svelte-lean build report
|
|
179
|
+
native-only button, dialog
|
|
180
|
+
runtime tabs -> @svelte-lean/primitives/tabs/register
|
|
181
|
+
src/routes/settings/+page.svelte
|
|
182
|
+
unknown none
|
|
183
|
+
dynamic src/lib/Panel.svelte:12 data-slean={kind}
|
|
184
|
+
manifest node_modules/.svelte-lean/manifest.json
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
The report only states what the scan observed. Bundle sizes and listener counts are measured by
|
|
188
|
+
the consumer fixtures in `fixtures/*`, not estimated here.
|
|
189
|
+
|
|
190
|
+
## Scanner scope
|
|
191
|
+
|
|
192
|
+
The scanner is lexical and does not use the Svelte compiler. It skips `<script>` and `<style>`
|
|
193
|
+
content, HTML comments, quoted attribute values (including `{…}` expressions inside them) and
|
|
194
|
+
`{…}` expressions in text, so a marker inside a string literal is not a false positive. Inside an
|
|
195
|
+
expression, string, template and regular-expression literals and comments are skipped, so quotes
|
|
196
|
+
or braces in `s.replace(/"/g, '')` do not unbalance it. Markers inside `{#if}`, `{#each}` and
|
|
197
|
+
other blocks are found like any other markup. If an expression never closes, the scanner stops
|
|
198
|
+
there, records the position in `ScanResult.unterminated` and the plugin warns once per file with
|
|
199
|
+
the line and column, because markup after that point is not discovered. It does not see markup
|
|
200
|
+
produced at runtime (`{@html …}`) and, by default, does not scan files under `node_modules`; a
|
|
201
|
+
library that renders Svelte Lean markup should either be added to `include` or import its
|
|
202
|
+
registration modules itself.
|
|
203
|
+
|
|
204
|
+
In development the scan runs on every transform and warnings are repeated after a file changes.
|
|
205
|
+
The manifest and report are build-only.
|
|
206
|
+
|
|
207
|
+
## Dev server and dependency pre-bundling
|
|
208
|
+
|
|
209
|
+
The registration imports are added after Vite's dependency scan has read the source, so a
|
|
210
|
+
pre-bundled `@svelte-lean/primitives` would be discovered on the first page that uses a behavior,
|
|
211
|
+
re-optimized, and that page fully reloaded. The plugin therefore adds `options.package` and
|
|
212
|
+
`@svelte-lean/core` to `optimizeDeps.exclude` (through its `config` hook). Both are plain ES
|
|
213
|
+
modules without CommonJS or third-party dependencies, so the dev server serves them as they are:
|
|
214
|
+
one copy of the core runtime, and hot updates of a registration module keep the shared listeners.
|
|
215
|
+
A package you list in `optimizeDeps.include` yourself is left to your configuration.
|
|
216
|
+
|
|
217
|
+
## Programmatic API
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
import {
|
|
221
|
+
svelteLean, // (options?) => Plugin[]
|
|
222
|
+
scan, // (source, id?) => { id, behaviors: Set<string>, dynamic: DynamicMarker[], unterminated }
|
|
223
|
+
BUILTIN_BEHAVIORS // name → registration module | false
|
|
224
|
+
} from '@svelte-lean/vite';
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Types: `SvelteLeanOptions`, `ScanResult`, `DynamicMarker`, `BehaviorManifest`, `ManifestBehavior`.
|
|
228
|
+
The option resolver, the manifest builder, the report formatter and the import injector are
|
|
229
|
+
internal; their behavior is observable through the plugin, the manifest file and the report.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ResolvedOptions } from './types.js';
|
|
2
|
+
export declare const DEFAULT_PACKAGE = "@svelte-lean/primitives";
|
|
3
|
+
/**
|
|
4
|
+
* Behaviors known to the plugin without configuration. A string is the registration module that is
|
|
5
|
+
* injected when the behavior is used; `false` marks a native (Tier 0) behavior that ships no
|
|
6
|
+
* runtime, so its marker is recorded for the manifest and report only.
|
|
7
|
+
*/
|
|
8
|
+
export declare const BUILTIN_BEHAVIORS: Readonly<Record<string, string | false>>;
|
|
9
|
+
export type BehaviorResolution = {
|
|
10
|
+
kind: 'runtime';
|
|
11
|
+
module: string;
|
|
12
|
+
} | {
|
|
13
|
+
kind: 'native';
|
|
14
|
+
} | {
|
|
15
|
+
kind: 'unknown';
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Maps a behavior name to what the build should do with it. `options.behaviors` wins over the
|
|
19
|
+
* built-in table; built-in specifiers are re-based onto `options.package`.
|
|
20
|
+
*/
|
|
21
|
+
export declare function resolveBehavior(name: string, options: Pick<ResolvedOptions, 'behaviors' | 'package'>): BehaviorResolution;
|
|
22
|
+
/** Sorted, de-duplicated registration modules for a set of behavior names. */
|
|
23
|
+
export declare function modulesFor(names: Iterable<string>, options: Pick<ResolvedOptions, 'behaviors' | 'package'>): string[];
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
export const DEFAULT_PACKAGE = '@svelte-lean/primitives';
|
|
2
|
+
/**
|
|
3
|
+
* Behaviors known to the plugin without configuration. A string is the registration module that is
|
|
4
|
+
* injected when the behavior is used; `false` marks a native (Tier 0) behavior that ships no
|
|
5
|
+
* runtime, so its marker is recorded for the manifest and report only.
|
|
6
|
+
*/
|
|
7
|
+
export const BUILTIN_BEHAVIORS = Object.freeze({
|
|
8
|
+
button: false,
|
|
9
|
+
dialog: false,
|
|
10
|
+
popover: false,
|
|
11
|
+
disclosure: false,
|
|
12
|
+
checkbox: false,
|
|
13
|
+
switch: false,
|
|
14
|
+
// `radio-group` is the name `@svelte-lean/primitives` uses (its contract and `BEHAVIORS` key).
|
|
15
|
+
// `radio` is a native alias from the ADR 0001 vocabulary with no primitive contract. `select` is
|
|
16
|
+
// the owned select (Tier 2, ADR 0007); the scanner records no marker for a bare
|
|
17
|
+
// `<select data-slean="select">`, the Tier 0 styled native select. Both are documented in
|
|
18
|
+
// README.md and checked by the drift test in packages/primitives/tests/registry.test.ts, which
|
|
19
|
+
// keeps this table equal to `BEHAVIORS`.
|
|
20
|
+
'radio-group': false,
|
|
21
|
+
radio: false,
|
|
22
|
+
select: `${DEFAULT_PACKAGE}/select/register`,
|
|
23
|
+
tabs: `${DEFAULT_PACKAGE}/tabs/register`,
|
|
24
|
+
menu: `${DEFAULT_PACKAGE}/menu/register`,
|
|
25
|
+
listbox: `${DEFAULT_PACKAGE}/listbox/register`,
|
|
26
|
+
combobox: `${DEFAULT_PACKAGE}/combobox/register`,
|
|
27
|
+
'date-field': false,
|
|
28
|
+
calendar: `${DEFAULT_PACKAGE}/calendar/register`,
|
|
29
|
+
'date-picker': `${DEFAULT_PACKAGE}/date-picker/register`,
|
|
30
|
+
input: false,
|
|
31
|
+
field: false,
|
|
32
|
+
segmented: false,
|
|
33
|
+
rating: false,
|
|
34
|
+
slider: false,
|
|
35
|
+
'otp-field': false,
|
|
36
|
+
'file-field': false,
|
|
37
|
+
'color-field': false,
|
|
38
|
+
autocomplete: false,
|
|
39
|
+
'alert-dialog': false,
|
|
40
|
+
drawer: false,
|
|
41
|
+
accordion: false,
|
|
42
|
+
card: false,
|
|
43
|
+
separator: false,
|
|
44
|
+
avatar: false,
|
|
45
|
+
badge: false,
|
|
46
|
+
tag: false,
|
|
47
|
+
kbd: false,
|
|
48
|
+
carousel: false,
|
|
49
|
+
alert: false,
|
|
50
|
+
progress: false,
|
|
51
|
+
meter: false,
|
|
52
|
+
spinner: false,
|
|
53
|
+
skeleton: false,
|
|
54
|
+
breadcrumb: false,
|
|
55
|
+
pagination: false,
|
|
56
|
+
steps: false,
|
|
57
|
+
toggle: `${DEFAULT_PACKAGE}/toggle/register`,
|
|
58
|
+
'toggle-group': `${DEFAULT_PACKAGE}/toggle-group/register`,
|
|
59
|
+
toolbar: `${DEFAULT_PACKAGE}/toolbar/register`,
|
|
60
|
+
'range-slider': `${DEFAULT_PACKAGE}/range-slider/register`,
|
|
61
|
+
'number-field': `${DEFAULT_PACKAGE}/number-field/register`,
|
|
62
|
+
tree: `${DEFAULT_PACKAGE}/tree/register`,
|
|
63
|
+
tooltip: `${DEFAULT_PACKAGE}/tooltip/register`,
|
|
64
|
+
toast: `${DEFAULT_PACKAGE}/toast/register`,
|
|
65
|
+
'button-group': false,
|
|
66
|
+
'scroll-area': false,
|
|
67
|
+
descriptions: false,
|
|
68
|
+
timeline: false,
|
|
69
|
+
empty: false,
|
|
70
|
+
splitter: `${DEFAULT_PACKAGE}/splitter/register`,
|
|
71
|
+
'context-menu': `${DEFAULT_PACKAGE}/context-menu/register`,
|
|
72
|
+
'file-drop': `${DEFAULT_PACKAGE}/file-drop/register`,
|
|
73
|
+
'hover-card': `${DEFAULT_PACKAGE}/hover-card/register`,
|
|
74
|
+
menubar: `${DEFAULT_PACKAGE}/menubar/register`
|
|
75
|
+
});
|
|
76
|
+
/**
|
|
77
|
+
* Maps a behavior name to what the build should do with it. `options.behaviors` wins over the
|
|
78
|
+
* built-in table; built-in specifiers are re-based onto `options.package`.
|
|
79
|
+
*/
|
|
80
|
+
export function resolveBehavior(name, options) {
|
|
81
|
+
const custom = Object.hasOwn(options.behaviors, name) ? options.behaviors[name] : undefined;
|
|
82
|
+
if (custom === false)
|
|
83
|
+
return { kind: 'native' };
|
|
84
|
+
if (typeof custom === 'string')
|
|
85
|
+
return { kind: 'runtime', module: custom };
|
|
86
|
+
const builtin = Object.hasOwn(BUILTIN_BEHAVIORS, name) ? BUILTIN_BEHAVIORS[name] : undefined;
|
|
87
|
+
if (builtin === false)
|
|
88
|
+
return { kind: 'native' };
|
|
89
|
+
if (typeof builtin === 'string') {
|
|
90
|
+
const module = builtin.startsWith(DEFAULT_PACKAGE + '/')
|
|
91
|
+
? options.package + builtin.slice(DEFAULT_PACKAGE.length)
|
|
92
|
+
: builtin;
|
|
93
|
+
return { kind: 'runtime', module };
|
|
94
|
+
}
|
|
95
|
+
return { kind: 'unknown' };
|
|
96
|
+
}
|
|
97
|
+
/** Sorted, de-duplicated registration modules for a set of behavior names. */
|
|
98
|
+
export function modulesFor(names, options) {
|
|
99
|
+
const modules = new Set();
|
|
100
|
+
for (const name of names) {
|
|
101
|
+
const resolution = resolveBehavior(name, options);
|
|
102
|
+
if (resolution.kind === 'runtime')
|
|
103
|
+
modules.add(resolution.module);
|
|
104
|
+
}
|
|
105
|
+
return [...modules].sort();
|
|
106
|
+
}
|
package/dist/filter.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module id filtering. Vite ids are absolute posix paths, optionally followed by a query
|
|
3
|
+
* (`?svelte&type=style&lang.css`) or hash. Only the component request itself is scanned and
|
|
4
|
+
* injected into; style/script sub-requests and `?raw`-style requests are left alone.
|
|
5
|
+
*/
|
|
6
|
+
export declare function normalizePath(path: string): string;
|
|
7
|
+
/** Splits an id into its file path (posix) and query string (without `?`). */
|
|
8
|
+
export declare function parseId(id: string): {
|
|
9
|
+
file: string;
|
|
10
|
+
query: string;
|
|
11
|
+
};
|
|
12
|
+
/** True for the request that carries the component source (not a style/raw/url sub-request). */
|
|
13
|
+
export declare function isComponentRequest(query: string): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Converts a small glob dialect to a RegExp: `**` (any path, `**\/` also matches zero segments),
|
|
16
|
+
* `*` (within one segment), `?` (one character) and `{a,b}` alternatives.
|
|
17
|
+
*/
|
|
18
|
+
export declare function globToRegExp(glob: string): RegExp;
|
|
19
|
+
export type IdFilter = (id: string) => boolean;
|
|
20
|
+
/** Path of `file` relative to `root`, in posix form, or `null` when it is outside the root. */
|
|
21
|
+
export declare function relativeToRoot(root: string, file: string): string | null;
|
|
22
|
+
/**
|
|
23
|
+
* Builds the include/exclude filter. Every pattern is tested against the absolute file path and,
|
|
24
|
+
* when the file lives under `root`, against the root-relative path, so `src/**\/*.svelte` and
|
|
25
|
+
* `**\/*.svelte` both behave as expected.
|
|
26
|
+
*/
|
|
27
|
+
export declare function createIdFilter(options: {
|
|
28
|
+
root: string;
|
|
29
|
+
include: Array<string | RegExp>;
|
|
30
|
+
exclude: Array<string | RegExp>;
|
|
31
|
+
}): IdFilter;
|
package/dist/filter.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module id filtering. Vite ids are absolute posix paths, optionally followed by a query
|
|
3
|
+
* (`?svelte&type=style&lang.css`) or hash. Only the component request itself is scanned and
|
|
4
|
+
* injected into; style/script sub-requests and `?raw`-style requests are left alone.
|
|
5
|
+
*/
|
|
6
|
+
const SKIPPED_QUERY_KEYS = ['type', 'raw', 'url', 'inline', 'direct', 'worker', 'sharedworker'];
|
|
7
|
+
export function normalizePath(path) {
|
|
8
|
+
return path.replace(/\\/g, '/');
|
|
9
|
+
}
|
|
10
|
+
/** Splits an id into its file path (posix) and query string (without `?`). */
|
|
11
|
+
export function parseId(id) {
|
|
12
|
+
const hash = id.indexOf('#');
|
|
13
|
+
const withoutHash = hash === -1 ? id : id.slice(0, hash);
|
|
14
|
+
const q = withoutHash.indexOf('?');
|
|
15
|
+
if (q === -1)
|
|
16
|
+
return { file: normalizePath(withoutHash), query: '' };
|
|
17
|
+
return { file: normalizePath(withoutHash.slice(0, q)), query: withoutHash.slice(q + 1) };
|
|
18
|
+
}
|
|
19
|
+
/** True for the request that carries the component source (not a style/raw/url sub-request). */
|
|
20
|
+
export function isComponentRequest(query) {
|
|
21
|
+
if (query === '')
|
|
22
|
+
return true;
|
|
23
|
+
const params = new URLSearchParams(query);
|
|
24
|
+
return !SKIPPED_QUERY_KEYS.some((key) => params.has(key));
|
|
25
|
+
}
|
|
26
|
+
function escapeRegExp(text) {
|
|
27
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Converts a small glob dialect to a RegExp: `**` (any path, `**\/` also matches zero segments),
|
|
31
|
+
* `*` (within one segment), `?` (one character) and `{a,b}` alternatives.
|
|
32
|
+
*/
|
|
33
|
+
export function globToRegExp(glob) {
|
|
34
|
+
let re = '';
|
|
35
|
+
for (let i = 0; i < glob.length; i++) {
|
|
36
|
+
const c = glob[i];
|
|
37
|
+
if (c === '*') {
|
|
38
|
+
if (glob[i + 1] === '*') {
|
|
39
|
+
if (glob[i + 2] === '/') {
|
|
40
|
+
re += '(?:.*/)?';
|
|
41
|
+
i += 2;
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
re += '.*';
|
|
45
|
+
i += 1;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
re += '[^/]*';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
else if (c === '?') {
|
|
53
|
+
re += '[^/]';
|
|
54
|
+
}
|
|
55
|
+
else if (c === '{') {
|
|
56
|
+
const close = glob.indexOf('}', i);
|
|
57
|
+
if (close === -1) {
|
|
58
|
+
re += '\\{';
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
const alternatives = glob
|
|
62
|
+
.slice(i + 1, close)
|
|
63
|
+
.split(',')
|
|
64
|
+
.map((part) => escapeRegExp(part.trim()));
|
|
65
|
+
re += '(?:' + alternatives.join('|') + ')';
|
|
66
|
+
i = close;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
re += escapeRegExp(c);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return new RegExp('^' + re + '$');
|
|
74
|
+
}
|
|
75
|
+
/** Path of `file` relative to `root`, in posix form, or `null` when it is outside the root. */
|
|
76
|
+
export function relativeToRoot(root, file) {
|
|
77
|
+
const base = root.endsWith('/') ? root : root + '/';
|
|
78
|
+
return file.startsWith(base) ? file.slice(base.length) : null;
|
|
79
|
+
}
|
|
80
|
+
function toMatchers(patterns) {
|
|
81
|
+
return patterns.map((pattern) => (pattern instanceof RegExp ? pattern : globToRegExp(pattern)));
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Builds the include/exclude filter. Every pattern is tested against the absolute file path and,
|
|
85
|
+
* when the file lives under `root`, against the root-relative path, so `src/**\/*.svelte` and
|
|
86
|
+
* `**\/*.svelte` both behave as expected.
|
|
87
|
+
*/
|
|
88
|
+
export function createIdFilter(options) {
|
|
89
|
+
const root = normalizePath(options.root);
|
|
90
|
+
const include = toMatchers(options.include);
|
|
91
|
+
const exclude = toMatchers(options.exclude);
|
|
92
|
+
const matches = (matchers, candidates) => matchers.some((matcher) => candidates.some((candidate) => matcher.test(candidate)));
|
|
93
|
+
return (id) => {
|
|
94
|
+
const { file, query } = parseId(id);
|
|
95
|
+
if (!isComponentRequest(query))
|
|
96
|
+
return false;
|
|
97
|
+
const relative = relativeToRoot(root, file);
|
|
98
|
+
const candidates = relative === null ? [file] : [file, relative];
|
|
99
|
+
if (!matches(include, candidates))
|
|
100
|
+
return false;
|
|
101
|
+
return !matches(exclude, candidates);
|
|
102
|
+
};
|
|
103
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Public entry. `svelteLean()` is the product; `scan()` and `BUILTIN_BEHAVIORS` are exported for
|
|
2
|
+
// tooling that wants the same discovery without the plugin. Everything else in src/ is internal.
|
|
3
|
+
export { svelteLean } from './plugin.js';
|
|
4
|
+
export { scan } from './scanner.js';
|
|
5
|
+
export { BUILTIN_BEHAVIORS } from './behaviors.js';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { BehaviorManifest, DynamicMarker, ResolvedOptions } from './types.js';
|
|
2
|
+
export interface ManifestInput {
|
|
3
|
+
/** Vite root, posix. */
|
|
4
|
+
root: string;
|
|
5
|
+
/** Absolute posix file → static behavior names found in it. */
|
|
6
|
+
usage: ReadonlyMap<string, ReadonlySet<string>>;
|
|
7
|
+
/** Absolute posix file → dynamic markers found in it. */
|
|
8
|
+
dynamic: ReadonlyMap<string, readonly DynamicMarker[]>;
|
|
9
|
+
options: Pick<ResolvedOptions, 'behaviors' | 'package'>;
|
|
10
|
+
}
|
|
11
|
+
/** Builds the manifest from scan state. Pure and deterministic: every list is sorted. */
|
|
12
|
+
export declare function createManifest(input: ManifestInput): BehaviorManifest;
|
|
13
|
+
export declare function writeManifest(path: string, manifest: BehaviorManifest): Promise<void>;
|
package/dist/manifest.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import { resolveBehavior } from './behaviors.js';
|
|
4
|
+
import { relativeToRoot } from './filter.js';
|
|
5
|
+
function displayPath(root, file) {
|
|
6
|
+
return relativeToRoot(root, file) ?? file;
|
|
7
|
+
}
|
|
8
|
+
/** Builds the manifest from scan state. Pure and deterministic: every list is sorted. */
|
|
9
|
+
export function createManifest(input) {
|
|
10
|
+
const filesByBehavior = new Map();
|
|
11
|
+
for (const [file, names] of input.usage) {
|
|
12
|
+
for (const name of names) {
|
|
13
|
+
let files = filesByBehavior.get(name);
|
|
14
|
+
if (!files)
|
|
15
|
+
filesByBehavior.set(name, (files = new Set()));
|
|
16
|
+
files.add(displayPath(input.root, file));
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const behaviors = {};
|
|
20
|
+
const unknown = [];
|
|
21
|
+
for (const name of [...filesByBehavior.keys()].sort()) {
|
|
22
|
+
const resolution = resolveBehavior(name, input.options);
|
|
23
|
+
if (resolution.kind === 'unknown')
|
|
24
|
+
unknown.push(name);
|
|
25
|
+
behaviors[name] = {
|
|
26
|
+
files: [...filesByBehavior.get(name)].sort(),
|
|
27
|
+
module: resolution.kind === 'runtime' ? resolution.module : null
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const dynamic = {};
|
|
31
|
+
const dynamicFiles = [...input.dynamic]
|
|
32
|
+
.filter(([, markers]) => markers.length > 0)
|
|
33
|
+
.map(([file, markers]) => [displayPath(input.root, file), markers])
|
|
34
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
35
|
+
for (const [file, markers] of dynamicFiles) {
|
|
36
|
+
dynamic[file] = markers.map(({ line, expression }) => ({ line, expression }));
|
|
37
|
+
}
|
|
38
|
+
return { version: 1, behaviors, dynamic, unknown };
|
|
39
|
+
}
|
|
40
|
+
export async function writeManifest(path, manifest) {
|
|
41
|
+
await mkdir(dirname(path), { recursive: true });
|
|
42
|
+
await writeFile(path, JSON.stringify(manifest, null, '\t') + '\n');
|
|
43
|
+
}
|
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Plugin } from 'vite';
|
|
2
|
+
import type { ResolvedOptions, SvelteLeanOptions } from './types.js';
|
|
3
|
+
export declare const SCAN_PLUGIN_NAME = "svelte-lean:scan";
|
|
4
|
+
export declare const INJECT_PLUGIN_NAME = "svelte-lean:inject";
|
|
5
|
+
export declare const DEFAULT_MANIFEST_PATH = "node_modules/.svelte-lean/manifest.json";
|
|
6
|
+
export declare function resolveOptions(options?: SvelteLeanOptions): ResolvedOptions;
|
|
7
|
+
/**
|
|
8
|
+
* Appends one side-effect import per registration module. Imports are hoisted by the module
|
|
9
|
+
* system, so evaluation order is identical to a prepended import, while appending keeps every
|
|
10
|
+
* existing line and column in place: `map: null` is then the correct source-map answer ("the
|
|
11
|
+
* transform did not move code"), and the compiler's map for the module stays valid.
|
|
12
|
+
*/
|
|
13
|
+
export declare function appendImports(code: string, modules: readonly string[]): string;
|
|
14
|
+
/**
|
|
15
|
+
* Packages kept out of the dev server's dependency pre-bundling. The registration imports are
|
|
16
|
+
* added by the inject transform, after Vite's dependency scan has read the raw source, so a
|
|
17
|
+
* pre-bundled package would be discovered on the first page that uses a behavior, re-optimized,
|
|
18
|
+
* and the page fully reloaded. Excluded, the packages are served as the plain ES modules they are
|
|
19
|
+
* (no CommonJS, no dependencies outside the scope), core stays a single instance, and HMR of the
|
|
20
|
+
* registration modules behaves as it does for linked workspace packages. A package the user lists
|
|
21
|
+
* in `optimizeDeps.include` is left to the user.
|
|
22
|
+
*/
|
|
23
|
+
export declare function optimizeDepsExclude(options: Pick<ResolvedOptions, 'package'>, userConfig?: {
|
|
24
|
+
optimizeDeps?: {
|
|
25
|
+
include?: string[];
|
|
26
|
+
};
|
|
27
|
+
}): string[];
|
|
28
|
+
/**
|
|
29
|
+
* Creates the two plugins. `svelte-lean:scan` (`enforce: 'pre'`) reads Svelte source before the
|
|
30
|
+
* compiler and records which behaviors each file uses; `svelte-lean:inject` (`enforce: 'post'`)
|
|
31
|
+
* adds the registration imports to the compiled module. They share state through this closure.
|
|
32
|
+
*/
|
|
33
|
+
export declare function svelteLean(userOptions?: SvelteLeanOptions): Plugin[];
|