@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 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
+ }
@@ -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
+ }
@@ -0,0 +1,4 @@
1
+ export { svelteLean } from './plugin.js';
2
+ export { scan } from './scanner.js';
3
+ export { BUILTIN_BEHAVIORS } from './behaviors.js';
4
+ export type { SvelteLeanOptions, ScanResult, DynamicMarker, BehaviorManifest, ManifestBehavior } from './types.js';
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>;
@@ -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
+ }
@@ -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[];