Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/many-hornets-breathe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@preact/signals-preact-transform": minor
---

Provide transform to name signals in Preact/signals
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"private": true,
"scripts": {
"prebuild": "shx rm -rf packages/*/dist/",
"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",
"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",
"_build": "microbundle --raw --globals @preact/signals-core=preactSignalsCore,preact/hooks=preactHooks,@preact/signals-react/runtime=reactSignalsRuntime",
"build:core": "pnpm _build --cwd packages/core && pnpm postbuild:core",
"build:debug": "pnpm _build --cwd packages/debug && pnpm postbuild:debug",
Expand All @@ -13,6 +13,7 @@
"build:react-utils": "pnpm _build --cwd packages/react/utils && pnpm postbuild:react-utils",
"build:react-runtime": "pnpm _build --cwd packages/react/runtime && pnpm postbuild:react-runtime",
"build:react-transform": "pnpm _build --no-compress --cwd packages/react-transform",
"build:preact-transform": "pnpm _build --no-compress --cwd packages/preact-transform",
"postbuild:core": "cd packages/core/dist && shx mv -f index.d.ts signals-core.d.ts",
"postbuild:debug": "cd packages/debug/dist && shx mv -f debug/src/index.d.ts signals-debug.d.ts",
"postbuild:preact": "cd packages/preact/dist && shx mv -f preact/src/index.d.ts signals.d.ts && shx rm -rf preact",
Expand Down
1 change: 1 addition & 0 deletions packages/preact-transform/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# @preact/signals-preact-transform
27 changes: 27 additions & 0 deletions packages/preact-transform/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Signals Preact Transform

A Babel plugin to provide names to all your `useComputed` and `useSignal` invocations.

## Installation:

```sh
npm i --save-dev @preact/signals-preact-transform
```

## Usage

To setup the transform plugin, add the following to your Babel config:

```js
// babel.config.js
module.exports = {
plugins: [["module:@preact/signals-preact-transform"]],
};
```

As this is a development plugin it is advised to remove it when you
go to production to remove some bundle size.

## License

`MIT`, see the [LICENSE](../../LICENSE) file.
70 changes: 70 additions & 0 deletions packages/preact-transform/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
{
"name": "@preact/signals-preact-transform",
"version": "0.0.0",
"license": "MIT",
"description": "Manage state with style in Preact",
"keywords": [
"babel",
"signals",
"preact"
],
"authors": [
"The Preact Authors (https://github.com/preactjs/signals/contributors)"
],
"repository": {
"type": "git",
"url": "https://github.com/preactjs/signals",
"directory": "packages/preact-transform"
},
"bugs": "https://github.com/preactjs/signals/issues",
"homepage": "https://preactjs.com",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/preact"
},
"main": "dist/signals-transform.js",
"module": "dist/signals-transform.module.js",
"types": "dist/signals-transform.d.ts",
"source": "src/index.ts",
"exports": {
".": {
"types": "./dist/signals-transform.d.ts",
"import": "./dist/signals-transform.mjs",
"require": "./dist/signals-transform.js"
}
},
"mangle": "../../mangle.json",
"files": [
"dist",
"src",
"CHANGELOG.md",
"LICENSE",
"README.md"
],
"scripts": {
"prepublishOnly": "cd ../.. && pnpm build:preact-transform"
},
"dependencies": {
"@babel/helper-module-imports": "^7.22.5",
"@babel/helper-plugin-utils": "^7.22.5",
"debug": "^4.3.4"
},
"peerDependencies": {
"@babel/core": "^7.0.0"
},
"devDependencies": {
"@babel/core": "^7.22.8",
"@types/babel__core": "^7.20.1",
"@types/babel__helper-module-imports": "^7.18.0",
"@types/babel__helper-plugin-utils": "^7.10.0",
"@types/debug": "^4.1.12",
"@types/prettier": "^2.7.3",
"assert": "^2.0.0",
"buffer": "^6.0.3",
"path": "^0.12.7",
"prettier": "^2.7.1"
},
"publishConfig": {
"provenance": true
}
}
156 changes: 156 additions & 0 deletions packages/preact-transform/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { types as BabelTypes, PluginObj, NodePath } from "@babel/core";

interface PluginArgs {
types: typeof BabelTypes;
}

export interface PluginOptions {
enabled?: boolean;
}

/**
* Simple "best effort" to get the base name of a file path. Not fool proof but
* works in browsers and servers. Good enough for our purposes.
*/
function basename(filename: string | undefined): string | undefined {
return filename?.split(/[\\/]/).pop();
}

function isSignalCall(path: NodePath<BabelTypes.CallExpression>): boolean {
const callee = path.get("callee");

// Check direct function calls like signal(), computed(), useSignal(), useComputed()
if (callee.isIdentifier()) {
const name = callee.node.name;
return (
name === "signal" ||
name === "computed" ||
name === "useSignal" ||
name === "useComputed"
);
}

return false;
}

function getVariableNameFromDeclarator(
path: NodePath<BabelTypes.CallExpression>
): string | null {
// Walk up the AST to find a variable declarator
let currentPath: NodePath | null = path;
while (currentPath) {
if (
currentPath.isVariableDeclarator() &&
currentPath.node.id.type === "Identifier"
) {
return currentPath.node.id.name;
}
currentPath = currentPath.parentPath;
}
return null;
}

function hasNameInOptions(
t: typeof BabelTypes,
args: NodePath<
| BabelTypes.Expression
| BabelTypes.SpreadElement
| BabelTypes.JSXNamespacedName
| BabelTypes.ArgumentPlaceholder
>[]
): boolean {
// Check if there's a second argument with a name property
if (args.length >= 2) {
const optionsArg = args[1];
if (optionsArg.isObjectExpression()) {
return optionsArg.node.properties.some(prop => {
if (t.isObjectProperty(prop) && !prop.computed) {
if (t.isIdentifier(prop.key, { name: "name" })) {
return true;
}
if (t.isStringLiteral(prop.key) && prop.key.value === "name") {
return true;
}
}
return false;
});
}
}
return false;
}

function injectSignalName(
t: typeof BabelTypes,
path: NodePath<BabelTypes.CallExpression>,
variableName: string,
filename: string | undefined
): void {
const args = path.get("arguments");

// Create enhanced name with filename and line number
let nameValue = variableName;
if (filename) {
const baseName = basename(filename);
const lineNumber = path.node.loc?.start.line;
if (baseName && lineNumber) {
nameValue = `${variableName} (${baseName}:${lineNumber})`;
}
}

const name = t.stringLiteral(nameValue);

if (args.length === 0) {
// No arguments, add both value and options
const nameOption = t.objectExpression([
t.objectProperty(t.identifier("name"), name),
]);
path.node.arguments.push(t.identifier("undefined"), nameOption);
} else if (args.length === 1) {
// One argument (value), add options object
const nameOption = t.objectExpression([
t.objectProperty(t.identifier("name"), name),
]);
path.node.arguments.push(nameOption);
} else if (args.length >= 2) {
// Two or more arguments, modify existing options object
const optionsArg = args[1];
if (optionsArg.isObjectExpression()) {
// Add name property to existing options object
optionsArg.node.properties.push(
t.objectProperty(t.identifier("name"), name)
);
} else {
// Replace second argument with options object containing name
const nameOption = t.objectExpression([
t.objectProperty(t.identifier("name"), name),
]);
args[1].replaceWith(nameOption);
}
}
}

export default function signalsTransform(
{ types: t }: PluginArgs,
options: PluginOptions
): PluginObj {
const isEnabled = options.enabled !== false;
return {
name: "@preact/signals-transform",
visitor: {
CallExpression(path, state) {
// Handle signal naming
if (isEnabled && isSignalCall(path)) {
const args = path.get("arguments");

// Only inject name if it doesn't already have one
if (!hasNameInOptions(t, args)) {
const variableName = getVariableNameFromDeclarator(path);
if (variableName) {
injectSignalName(t, path, variableName, this.filename);
}
}
}
},
},
};
}
Loading