Skip to content

Commit acf3460

Browse files
authored
Provide transform for preact signals (#743)
1 parent 35afec9 commit acf3460

File tree

10 files changed

+451
-2
lines changed

10 files changed

+451
-2
lines changed

.changeset/many-hornets-breathe.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@preact/signals-preact-transform": minor
3+
---
4+
5+
Provide transform to name signals in Preact/signals

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"private": true,
44
"scripts": {
55
"prebuild": "shx rm -rf packages/*/dist/",
6-
"build": "pnpm build:core && pnpm build:debug && pnpm build:preact && pnpm build:preact-utils && pnpm build:react-runtime && pnpm build:react && pnpm build:react-transform && pnpm build:react-utils",
6+
"build": "pnpm build:core && pnpm build:debug && pnpm build:preact && pnpm build:preact-transform && pnpm build:preact-utils && pnpm build:react-runtime && pnpm build:react && pnpm build:react-transform && pnpm build:react-utils",
77
"_build": "microbundle --raw --globals @preact/signals-core=preactSignalsCore,preact/hooks=preactHooks,@preact/signals-react/runtime=reactSignalsRuntime",
88
"build:core": "pnpm _build --cwd packages/core && pnpm postbuild:core",
99
"build:debug": "pnpm _build --cwd packages/debug && pnpm postbuild:debug",
@@ -13,6 +13,7 @@
1313
"build:react-utils": "pnpm _build --cwd packages/react/utils && pnpm postbuild:react-utils",
1414
"build:react-runtime": "pnpm _build --cwd packages/react/runtime && pnpm postbuild:react-runtime",
1515
"build:react-transform": "pnpm _build --no-compress --cwd packages/react-transform",
16+
"build:preact-transform": "pnpm _build --no-compress --cwd packages/preact-transform",
1617
"postbuild:core": "cd packages/core/dist && shx mv -f index.d.ts signals-core.d.ts",
1718
"postbuild:debug": "cd packages/debug/dist && shx mv -f debug/src/index.d.ts signals-debug.d.ts",
1819
"postbuild:preact": "cd packages/preact/dist && shx mv -f preact/src/index.d.ts signals.d.ts && shx rm -rf preact",
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# @preact/signals-preact-transform

packages/preact-transform/README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Signals Preact Transform
2+
3+
A Babel plugin to provide names to all your `useComputed` and `useSignal` invocations.
4+
5+
## Installation:
6+
7+
```sh
8+
npm i --save-dev @preact/signals-preact-transform
9+
```
10+
11+
## Usage
12+
13+
To setup the transform plugin, add the following to your Babel config:
14+
15+
```js
16+
// babel.config.js
17+
module.exports = {
18+
plugins: [["module:@preact/signals-preact-transform"]],
19+
};
20+
```
21+
22+
As this is a development plugin it is advised to remove it when you
23+
go to production to remove some bundle size.
24+
25+
## License
26+
27+
`MIT`, see the [LICENSE](../../LICENSE) file.
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
{
2+
"name": "@preact/signals-preact-transform",
3+
"version": "0.0.0",
4+
"license": "MIT",
5+
"description": "Manage state with style in Preact",
6+
"keywords": [
7+
"babel",
8+
"signals",
9+
"preact"
10+
],
11+
"authors": [
12+
"The Preact Authors (https://github.com/preactjs/signals/contributors)"
13+
],
14+
"repository": {
15+
"type": "git",
16+
"url": "https://github.com/preactjs/signals",
17+
"directory": "packages/preact-transform"
18+
},
19+
"bugs": "https://github.com/preactjs/signals/issues",
20+
"homepage": "https://preactjs.com",
21+
"funding": {
22+
"type": "opencollective",
23+
"url": "https://opencollective.com/preact"
24+
},
25+
"main": "dist/signals-transform.js",
26+
"module": "dist/signals-transform.module.js",
27+
"types": "dist/signals-transform.d.ts",
28+
"source": "src/index.ts",
29+
"exports": {
30+
".": {
31+
"types": "./dist/signals-transform.d.ts",
32+
"import": "./dist/signals-transform.mjs",
33+
"require": "./dist/signals-transform.js"
34+
}
35+
},
36+
"mangle": "../../mangle.json",
37+
"files": [
38+
"dist",
39+
"src",
40+
"CHANGELOG.md",
41+
"LICENSE",
42+
"README.md"
43+
],
44+
"scripts": {
45+
"prepublishOnly": "cd ../.. && pnpm build:preact-transform"
46+
},
47+
"dependencies": {
48+
"@babel/helper-module-imports": "^7.22.5",
49+
"@babel/helper-plugin-utils": "^7.22.5",
50+
"debug": "^4.3.4"
51+
},
52+
"peerDependencies": {
53+
"@babel/core": "^7.0.0"
54+
},
55+
"devDependencies": {
56+
"@babel/core": "^7.22.8",
57+
"@types/babel__core": "^7.20.1",
58+
"@types/babel__helper-module-imports": "^7.18.0",
59+
"@types/babel__helper-plugin-utils": "^7.10.0",
60+
"@types/debug": "^4.1.12",
61+
"@types/prettier": "^2.7.3",
62+
"assert": "^2.0.0",
63+
"buffer": "^6.0.3",
64+
"path": "^0.12.7",
65+
"prettier": "^2.7.1"
66+
},
67+
"publishConfig": {
68+
"provenance": true
69+
}
70+
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import { types as BabelTypes, PluginObj, NodePath } from "@babel/core";
2+
3+
interface PluginArgs {
4+
types: typeof BabelTypes;
5+
}
6+
7+
export interface PluginOptions {
8+
enabled?: boolean;
9+
}
10+
11+
/**
12+
* Simple "best effort" to get the base name of a file path. Not fool proof but
13+
* works in browsers and servers. Good enough for our purposes.
14+
*/
15+
function basename(filename: string | undefined): string | undefined {
16+
return filename?.split(/[\\/]/).pop();
17+
}
18+
19+
function isSignalCall(path: NodePath<BabelTypes.CallExpression>): boolean {
20+
const callee = path.get("callee");
21+
22+
// Check direct function calls like signal(), computed(), useSignal(), useComputed()
23+
if (callee.isIdentifier()) {
24+
const name = callee.node.name;
25+
return (
26+
name === "signal" ||
27+
name === "computed" ||
28+
name === "useSignal" ||
29+
name === "useComputed"
30+
);
31+
}
32+
33+
return false;
34+
}
35+
36+
function getVariableNameFromDeclarator(
37+
path: NodePath<BabelTypes.CallExpression>
38+
): string | null {
39+
// Walk up the AST to find a variable declarator
40+
let currentPath: NodePath | null = path;
41+
while (currentPath) {
42+
if (
43+
currentPath.isVariableDeclarator() &&
44+
currentPath.node.id.type === "Identifier"
45+
) {
46+
return currentPath.node.id.name;
47+
}
48+
currentPath = currentPath.parentPath;
49+
}
50+
return null;
51+
}
52+
53+
function hasNameInOptions(
54+
t: typeof BabelTypes,
55+
args: NodePath<
56+
| BabelTypes.Expression
57+
| BabelTypes.SpreadElement
58+
| BabelTypes.JSXNamespacedName
59+
| BabelTypes.ArgumentPlaceholder
60+
>[]
61+
): boolean {
62+
// Check if there's a second argument with a name property
63+
if (args.length >= 2) {
64+
const optionsArg = args[1];
65+
if (optionsArg.isObjectExpression()) {
66+
return optionsArg.node.properties.some(prop => {
67+
if (t.isObjectProperty(prop) && !prop.computed) {
68+
if (t.isIdentifier(prop.key, { name: "name" })) {
69+
return true;
70+
}
71+
if (t.isStringLiteral(prop.key) && prop.key.value === "name") {
72+
return true;
73+
}
74+
}
75+
return false;
76+
});
77+
}
78+
}
79+
return false;
80+
}
81+
82+
function injectSignalName(
83+
t: typeof BabelTypes,
84+
path: NodePath<BabelTypes.CallExpression>,
85+
variableName: string,
86+
filename: string | undefined
87+
): void {
88+
const args = path.get("arguments");
89+
90+
// Create enhanced name with filename and line number
91+
let nameValue = variableName;
92+
if (filename) {
93+
const baseName = basename(filename);
94+
const lineNumber = path.node.loc?.start.line;
95+
if (baseName && lineNumber) {
96+
nameValue = `${variableName} (${baseName}:${lineNumber})`;
97+
}
98+
}
99+
100+
const name = t.stringLiteral(nameValue);
101+
102+
if (args.length === 0) {
103+
// No arguments, add both value and options
104+
const nameOption = t.objectExpression([
105+
t.objectProperty(t.identifier("name"), name),
106+
]);
107+
path.node.arguments.push(t.identifier("undefined"), nameOption);
108+
} else if (args.length === 1) {
109+
// One argument (value), add options object
110+
const nameOption = t.objectExpression([
111+
t.objectProperty(t.identifier("name"), name),
112+
]);
113+
path.node.arguments.push(nameOption);
114+
} else if (args.length >= 2) {
115+
// Two or more arguments, modify existing options object
116+
const optionsArg = args[1];
117+
if (optionsArg.isObjectExpression()) {
118+
// Add name property to existing options object
119+
optionsArg.node.properties.push(
120+
t.objectProperty(t.identifier("name"), name)
121+
);
122+
} else {
123+
// Replace second argument with options object containing name
124+
const nameOption = t.objectExpression([
125+
t.objectProperty(t.identifier("name"), name),
126+
]);
127+
args[1].replaceWith(nameOption);
128+
}
129+
}
130+
}
131+
132+
export default function signalsTransform(
133+
{ types: t }: PluginArgs,
134+
options: PluginOptions
135+
): PluginObj {
136+
const isEnabled = options.enabled !== false;
137+
return {
138+
name: "@preact/signals-transform",
139+
visitor: {
140+
CallExpression(path, state) {
141+
// Handle signal naming
142+
if (isEnabled && isSignalCall(path)) {
143+
const args = path.get("arguments");
144+
145+
// Only inject name if it doesn't already have one
146+
if (!hasNameInOptions(t, args)) {
147+
const variableName = getVariableNameFromDeclarator(path);
148+
if (variableName) {
149+
injectSignalName(t, path, variableName, this.filename);
150+
}
151+
}
152+
}
153+
},
154+
},
155+
};
156+
}

0 commit comments

Comments
 (0)