@nicerice/svelte-reactive-params 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/LICENSE +21 -0
- package/README.md +52 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +228 -0
- package/dist/plugin/createSvelteReactiveParamsPlugin.d.ts +2 -0
- package/dist/plugin.d.ts +2 -0
- package/dist/scope/collectDeclaredNames.d.ts +1 -0
- package/dist/scope/collectExternalImportNames.d.ts +1 -0
- package/dist/scope/isExternalReactiveModule.d.ts +1 -0
- package/dist/scope/visitDeclarations.d.ts +2 -0
- package/dist/scope/visitImports.d.ts +2 -0
- package/dist/scope.d.ts +2 -0
- package/dist/transform/extractScriptBlocks.d.ts +4 -0
- package/dist/transform/getRootIdentifier.d.ts +2 -0
- package/dist/transform/getSetterTarget.d.ts +2 -0
- package/dist/transform/isAssignableExpression.d.ts +2 -0
- package/dist/transform/isExternalCall.d.ts +2 -0
- package/dist/transform/renderAccessorPair.d.ts +1 -0
- package/dist/transform/renderProperty.d.ts +2 -0
- package/dist/transform/renderReactiveArgument.d.ts +2 -0
- package/dist/transform/renderReactiveObjectLiteral.d.ts +2 -0
- package/dist/transform/transformCallExpressions.d.ts +1 -0
- package/dist/transform/transformSvelteReactiveParams.d.ts +1 -0
- package/dist/transformCallExpressions.d.ts +1 -0
- package/dist/transformReactiveObjectLiteral.d.ts +2 -0
- package/dist/transformSvelteReactiveParams.d.ts +1 -0
- package/package.json +43 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 nicerice
|
|
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,52 @@
|
|
|
1
|
+
# svelte-reactive-params
|
|
2
|
+
|
|
3
|
+
Makes any params passed to external functions reactive when changed and accessed there.
|
|
4
|
+
|
|
5
|
+
## Why?
|
|
6
|
+
I like to use script tags for declarations exclusively, hiding logic until I need it; and I still like using svelte 4 syntax for simple projects, which badly needs such a mechanism in the absence of $state() runes.
|
|
7
|
+
|
|
8
|
+
## Transforms
|
|
9
|
+
|
|
10
|
+
Rewrites arguments passed to imported external calls into accessor-backed values, so reactive values stay live when they cross module boundaries.
|
|
11
|
+
|
|
12
|
+
Input:
|
|
13
|
+
|
|
14
|
+
```svelte
|
|
15
|
+
<script lang="ts">
|
|
16
|
+
import { countUp } from './lib/countUp.svelte.ts'
|
|
17
|
+
|
|
18
|
+
let { counter = 0 } = $props()
|
|
19
|
+
|
|
20
|
+
countUp(counter)
|
|
21
|
+
</script>
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Output:
|
|
25
|
+
|
|
26
|
+
The plugin rewrites that call so `countUp` receives a live wrapper around `counter`.
|
|
27
|
+
|
|
28
|
+
Usage:
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
export function countUp(args) {
|
|
32
|
+
const { value } = args
|
|
33
|
+
value++ // updates in template
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
bun add svelte-reactive-params
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Use
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
// vite.config.ts
|
|
47
|
+
import { svelteReactiveParams } from 'svelte-reactive-params'
|
|
48
|
+
|
|
49
|
+
export default {
|
|
50
|
+
plugins: [svelteReactiveParams()],
|
|
51
|
+
}
|
|
52
|
+
```
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
// src/scope/collectDeclaredNames.ts
|
|
2
|
+
import * as ts2 from "typescript";
|
|
3
|
+
|
|
4
|
+
// src/scope/visitDeclarations.ts
|
|
5
|
+
import * as ts from "typescript";
|
|
6
|
+
function visitDeclarations(node, names) {
|
|
7
|
+
if (ts.isFunctionDeclaration(node) && node.name) {
|
|
8
|
+
names.add(node.name.text);
|
|
9
|
+
}
|
|
10
|
+
if (ts.isClassDeclaration(node) && node.name) {
|
|
11
|
+
names.add(node.name.text);
|
|
12
|
+
}
|
|
13
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name)) {
|
|
14
|
+
names.add(node.name.text);
|
|
15
|
+
}
|
|
16
|
+
if (ts.isParameter(node) && ts.isIdentifier(node.name)) {
|
|
17
|
+
names.add(node.name.text);
|
|
18
|
+
}
|
|
19
|
+
if (ts.isBindingElement(node) && ts.isIdentifier(node.name)) {
|
|
20
|
+
names.add(node.name.text);
|
|
21
|
+
}
|
|
22
|
+
ts.forEachChild(node, (child) => visitDeclarations(child, names));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// src/scope/collectDeclaredNames.ts
|
|
26
|
+
function collectDeclaredNames(code) {
|
|
27
|
+
const names = new Set;
|
|
28
|
+
const sourceFile = ts2.createSourceFile("file.ts", code, ts2.ScriptTarget.Latest, true, ts2.ScriptKind.TS);
|
|
29
|
+
visitDeclarations(sourceFile, names);
|
|
30
|
+
return names;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// src/scope/collectExternalImportNames.ts
|
|
34
|
+
import * as ts4 from "typescript";
|
|
35
|
+
|
|
36
|
+
// src/scope/visitImports.ts
|
|
37
|
+
import * as ts3 from "typescript";
|
|
38
|
+
|
|
39
|
+
// src/scope/isExternalReactiveModule.ts
|
|
40
|
+
function isExternalReactiveModule(modulePath) {
|
|
41
|
+
return modulePath.endsWith(".svelte") || modulePath.endsWith(".svelte.ts");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// src/scope/visitImports.ts
|
|
45
|
+
function visitImports(node, names) {
|
|
46
|
+
if (ts3.isImportDeclaration(node) && ts3.isStringLiteral(node.moduleSpecifier)) {
|
|
47
|
+
const modulePath = node.moduleSpecifier.text;
|
|
48
|
+
if (isExternalReactiveModule(modulePath)) {
|
|
49
|
+
const importClause = node.importClause;
|
|
50
|
+
if (importClause?.name) {
|
|
51
|
+
names.add(importClause.name.text);
|
|
52
|
+
}
|
|
53
|
+
if (importClause?.namedBindings && ts3.isNamedImports(importClause.namedBindings)) {
|
|
54
|
+
for (const element of importClause.namedBindings.elements) {
|
|
55
|
+
names.add(element.name.text);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (importClause?.namedBindings && ts3.isNamespaceImport(importClause.namedBindings)) {
|
|
59
|
+
names.add(importClause.namedBindings.name.text);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
ts3.forEachChild(node, (child) => visitImports(child, names));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// src/scope/collectExternalImportNames.ts
|
|
67
|
+
function collectExternalImportNames(code) {
|
|
68
|
+
const names = new Set;
|
|
69
|
+
const sourceFile = ts4.createSourceFile("file.ts", code, ts4.ScriptTarget.Latest, true, ts4.ScriptKind.TS);
|
|
70
|
+
visitImports(sourceFile, names);
|
|
71
|
+
return names;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// src/transform/extractScriptBlocks.ts
|
|
75
|
+
function extractScriptBlocks(code) {
|
|
76
|
+
const blocks = [];
|
|
77
|
+
const scriptRegex = /<script\b[^>]*>([\s\S]*?)<\/script>/g;
|
|
78
|
+
let match = null;
|
|
79
|
+
while ((match = scriptRegex.exec(code)) !== null) {
|
|
80
|
+
const scriptStart = match.index + match[0].indexOf(">") + 1;
|
|
81
|
+
const scriptEnd = match.index + match[0].lastIndexOf("<");
|
|
82
|
+
blocks.push({ start: scriptStart, end: scriptEnd });
|
|
83
|
+
}
|
|
84
|
+
return blocks;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// src/transform/transformCallExpressions.ts
|
|
88
|
+
import MagicString from "magic-string";
|
|
89
|
+
import * as ts9 from "typescript";
|
|
90
|
+
|
|
91
|
+
// src/transform/getRootIdentifier.ts
|
|
92
|
+
import * as ts5 from "typescript";
|
|
93
|
+
function getRootIdentifier(expression) {
|
|
94
|
+
if (ts5.isIdentifier(expression)) {
|
|
95
|
+
return expression.text;
|
|
96
|
+
}
|
|
97
|
+
if (ts5.isPropertyAccessExpression(expression) || ts5.isElementAccessExpression(expression)) {
|
|
98
|
+
return getRootIdentifier(expression.expression);
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// src/transform/isExternalCall.ts
|
|
104
|
+
function isExternalCall(expression, externalNames, localNames) {
|
|
105
|
+
const rootName = getRootIdentifier(expression);
|
|
106
|
+
return rootName !== null && externalNames.has(rootName) && !localNames.has(rootName);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// src/transform/renderReactiveArgument.ts
|
|
110
|
+
import * as ts8 from "typescript";
|
|
111
|
+
|
|
112
|
+
// src/transform/isAssignableExpression.ts
|
|
113
|
+
import * as ts6 from "typescript";
|
|
114
|
+
function isAssignableExpression(node) {
|
|
115
|
+
if (ts6.isIdentifier(node) || ts6.isPropertyAccessExpression(node) || ts6.isElementAccessExpression(node)) {
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
if (ts6.isParenthesizedExpression(node)) {
|
|
119
|
+
return isAssignableExpression(node.expression);
|
|
120
|
+
}
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/transform/getSetterTarget.ts
|
|
125
|
+
function getSetterTarget(node, sourceFile) {
|
|
126
|
+
if (isAssignableExpression(node)) {
|
|
127
|
+
return node.getText(sourceFile);
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// src/transform/renderAccessorPair.ts
|
|
133
|
+
function renderAccessorPair(name, getterValue, setterTarget) {
|
|
134
|
+
if (!setterTarget) {
|
|
135
|
+
return `get ${name}() { return ${getterValue} }`;
|
|
136
|
+
}
|
|
137
|
+
return `get ${name}() { return ${getterValue} }, set ${name}(value) { ${setterTarget} = value }`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// src/transform/renderProperty.ts
|
|
141
|
+
import * as ts7 from "typescript";
|
|
142
|
+
function renderProperty(property, sourceFile) {
|
|
143
|
+
if (ts7.isSpreadAssignment(property)) {
|
|
144
|
+
return property.getText(sourceFile);
|
|
145
|
+
}
|
|
146
|
+
if (ts7.isShorthandPropertyAssignment(property)) {
|
|
147
|
+
const name = property.name.getText(sourceFile);
|
|
148
|
+
return renderAccessorPair(name, name, name);
|
|
149
|
+
}
|
|
150
|
+
if (ts7.isPropertyAssignment(property) && ts7.isIdentifier(property.name)) {
|
|
151
|
+
const name = property.name.text;
|
|
152
|
+
const value = property.initializer.getText(sourceFile);
|
|
153
|
+
const setterTarget = getSetterTarget(property.initializer, sourceFile);
|
|
154
|
+
return renderAccessorPair(name, value, setterTarget);
|
|
155
|
+
}
|
|
156
|
+
return property.getText(sourceFile);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// src/transform/renderReactiveObjectLiteral.ts
|
|
160
|
+
function renderReactiveObjectLiteral(node, sourceFile) {
|
|
161
|
+
const properties = node.properties.map((property) => renderProperty(property, sourceFile)).filter(Boolean);
|
|
162
|
+
return `{ ${properties.join(", ")} }`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// src/transform/renderReactiveArgument.ts
|
|
166
|
+
function renderReactiveArgument(node, sourceFile) {
|
|
167
|
+
if (ts8.isObjectLiteralExpression(node)) {
|
|
168
|
+
return renderReactiveObjectLiteral(node, sourceFile);
|
|
169
|
+
}
|
|
170
|
+
const value = node.getText(sourceFile);
|
|
171
|
+
const setterTarget = getSetterTarget(node, sourceFile);
|
|
172
|
+
return `{ ${renderAccessorPair("value", value, setterTarget)} }`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// src/transform/transformCallExpressions.ts
|
|
176
|
+
function transformCallExpressions(code, externalNames, localNames, filePath) {
|
|
177
|
+
const sourceFile = ts9.createSourceFile(filePath, code, ts9.ScriptTarget.Latest, true, ts9.ScriptKind.TS);
|
|
178
|
+
const string = new MagicString(code);
|
|
179
|
+
visit(sourceFile);
|
|
180
|
+
return string.toString();
|
|
181
|
+
function visit(node) {
|
|
182
|
+
if (ts9.isCallExpression(node) && isExternalCall(node.expression, externalNames, localNames)) {
|
|
183
|
+
for (const arg of node.arguments) {
|
|
184
|
+
if (ts9.isSpreadElement(arg)) {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
string.overwrite(arg.getStart(sourceFile), arg.getEnd(), renderReactiveArgument(arg, sourceFile));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
ts9.forEachChild(node, visit);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// src/transform/transformSvelteReactiveParams.ts
|
|
195
|
+
function transformSvelteReactiveParams(code, filePath = "file.svelte") {
|
|
196
|
+
const scripts = extractScriptBlocks(code);
|
|
197
|
+
if (scripts.length === 0) {
|
|
198
|
+
return code;
|
|
199
|
+
}
|
|
200
|
+
let transformedCode = code;
|
|
201
|
+
for (const script of [...scripts].reverse()) {
|
|
202
|
+
const scriptContent = code.slice(script.start, script.end);
|
|
203
|
+
const externalNames = collectExternalImportNames(scriptContent);
|
|
204
|
+
const localNames = collectDeclaredNames(scriptContent);
|
|
205
|
+
const transformedScript = transformCallExpressions(scriptContent, externalNames, localNames, filePath);
|
|
206
|
+
transformedCode = `${transformedCode.slice(0, script.start)}${transformedScript}${transformedCode.slice(script.end)}`;
|
|
207
|
+
}
|
|
208
|
+
return transformedCode;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// src/plugin/createSvelteReactiveParamsPlugin.ts
|
|
212
|
+
function createSvelteReactiveParamsPlugin() {
|
|
213
|
+
return {
|
|
214
|
+
name: "svelte-reactive-params",
|
|
215
|
+
enforce: "pre",
|
|
216
|
+
transform(code, id) {
|
|
217
|
+
if (!id.endsWith(".svelte")) {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
const contents = transformSvelteReactiveParams(code, id);
|
|
221
|
+
return contents === code ? null : contents;
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
export {
|
|
226
|
+
transformSvelteReactiveParams,
|
|
227
|
+
createSvelteReactiveParamsPlugin as svelteReactiveParams
|
|
228
|
+
};
|
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function collectDeclaredNames(code: string): Set<string>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function collectExternalImportNames(code: string): Set<string>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function isExternalReactiveModule(modulePath: string): boolean;
|
package/dist/scope.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function renderAccessorPair(name: string, getterValue: string, setterTarget: string | null): string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function transformCallExpressions(code: string, externalNames: Set<string>, localNames: Set<string>, filePath: string): string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function transformSvelteReactiveParams(code: string, filePath?: string): string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function transformCallExpressions(code: string, externalNames: Set<string>, localNames: Set<string>, filePath: string): string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function transformSvelteReactiveParams(code: string, filePath?: string): string;
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nicerice/svelte-reactive-params",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Makes any params passed to external functions reactive when changed and accessed there.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md",
|
|
18
|
+
"LICENSE"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"test": "bun test",
|
|
22
|
+
"build:types": "bun x tsc -p tsconfig.build.json",
|
|
23
|
+
"build:js": "bun build src/index.ts --outdir dist --format esm --external typescript --external magic-string",
|
|
24
|
+
"build": "bun run build:types && bun run build:js",
|
|
25
|
+
"prepack": "bun run build"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"svelte",
|
|
29
|
+
"vite",
|
|
30
|
+
"plugin",
|
|
31
|
+
"reactivity"
|
|
32
|
+
],
|
|
33
|
+
"author": "nicerice",
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"magic-string": "^0.30.21",
|
|
37
|
+
"typescript": "^5.9.3"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/bun": "^1.3.14",
|
|
41
|
+
"vite": "^8.0.14"
|
|
42
|
+
}
|
|
43
|
+
}
|