diff --git a/.changeset/cyan-swans-serve.md b/.changeset/cyan-swans-serve.md new file mode 100644 index 000000000..123589717 --- /dev/null +++ b/.changeset/cyan-swans-serve.md @@ -0,0 +1,7 @@ +--- +"@preact/signals-react-transform": minor +--- + +Remove support for transforming CJS files + +Removing support for transforming CommonJS files since we have no tests for it currently diff --git a/.changeset/lucky-radios-deny.md b/.changeset/lucky-radios-deny.md new file mode 100644 index 000000000..936cb4ce9 --- /dev/null +++ b/.changeset/lucky-radios-deny.md @@ -0,0 +1,5 @@ +--- +"@preact/signals-react-transform": patch +--- + +Register newly inserted import statement as a scope declaration in Babel's scope tracking diff --git a/package.json b/package.json index 54bccbd87..fe6f53b98 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "@babel/standalone": "^7.22.6", "@changesets/changelog-github": "^0.4.6", "@changesets/cli": "^2.24.2", + "@types/babel__traverse": "^7.18.5", "@types/chai": "^4.3.3", "@types/mocha": "^9.1.1", "@types/node": "^18.6.5", diff --git a/packages/react-transform/src/index.ts b/packages/react-transform/src/index.ts index 322386eb2..ca5443c4f 100644 --- a/packages/react-transform/src/index.ts +++ b/packages/react-transform/src/index.ts @@ -6,7 +6,7 @@ import { NodePath, template, } from "@babel/core"; -import { isModule, addNamed, addNamespace } from "@babel/helper-module-imports"; +import { isModule, addNamed } from "@babel/helper-module-imports"; // TODO: // - how to trigger rerenders on attributes change if transform never sees @@ -263,29 +263,46 @@ function createImportLazily( path: NodePath, importName: string, source: string -) { +): () => BabelTypes.Identifier { return () => { - if (isModule(path)) { - let reference = get(pass, `imports/${importName}`); - if (reference) return types.cloneNode(reference); - reference = addNamed(path, importName, source, { - importedInterop: "uncompiled", - importPosition: "after", - }); - set(pass, `imports/${importName}`, reference); - return reference; - } else { - let reference = get(pass, `requires/${source}`); - if (reference) { - reference = types.cloneNode(reference); - } else { - reference = addNamespace(path, source, { - importedInterop: "uncompiled", - }); - set(pass, `requires/${source}`, reference); + if (!isModule(path)) { + throw new Error( + `Cannot import ${importName} outside of an ESM module file` + ); + } + + let reference: BabelTypes.Identifier = get(pass, `imports/${importName}`); + if (reference) return types.cloneNode(reference); + reference = addNamed(path, importName, source, { + importedInterop: "uncompiled", + importPosition: "after", + }); + set(pass, `imports/${importName}`, reference); + + /** Helper function to determine if an import declaration's specifier matches the given importName */ + const matchesImportName = ( + s: BabelTypes.ImportDeclaration["specifiers"][0] + ) => { + if (s.type !== "ImportSpecifier") return false; + return ( + (s.imported.type === "Identifier" && s.imported.name === importName) || + (s.imported.type === "StringLiteral" && + s.imported.value === importName) + ); + }; + + for (let statement of path.get("body")) { + if ( + statement.isImportDeclaration() && + statement.node.source.value === source && + statement.node.specifiers.some(matchesImportName) + ) { + path.scope.registerDeclaration(statement); + break; } - return types.memberExpression(reference, types.identifier(importName)); } + + return reference; }; } diff --git a/packages/react-transform/test/node/index.test.tsx b/packages/react-transform/test/node/index.test.tsx index 61164a6bc..3374e6263 100644 --- a/packages/react-transform/test/node/index.test.tsx +++ b/packages/react-transform/test/node/index.test.tsx @@ -1,4 +1,6 @@ -import { transform } from "@babel/core"; +import { transform, traverse } from "@babel/core"; +import type { Visitor } from "@babel/core"; +import type { Scope } from "@babel/traverse"; import signalsTransform, { PluginOptions } from "../../src/index"; function dedent(str: string) { @@ -965,4 +967,54 @@ describe("React Signals Babel Transform", () => { runTest(inputCode, expectedOutput, { importSource: "custom-source" }); }); }); + + describe("scope tracking", () => { + interface VisitorState { + programScope?: Scope; + } + + const programScopeVisitor: Visitor = { + Program: { + exit(path, state) { + state.programScope = path.scope; + }, + }, + }; + + function getRootScope(code: string) { + const signalsPluginConfig: any[] = [signalsTransform]; + const result = transform(code, { + ast: true, + plugins: [signalsPluginConfig, "@babel/plugin-syntax-jsx"], + }); + if (!result) { + throw new Error("Could not transform code"); + } + + const state: VisitorState = {}; + traverse(result.ast, programScopeVisitor, undefined, state); + + const scope = state.programScope; + if (!scope) { + throw new Error("Could not find program scope"); + } + + return scope; + } + + it("adds newly inserted import declarations and usages to program scope", () => { + const scope = getRootScope(` + const MyComponent = () => { + signal.value; + return
Hello World
; + }; + `); + + const signalsBinding = scope.bindings["_useSignals"]; + console.log(signalsBinding); + expect(signalsBinding).to.exist; + expect(signalsBinding.kind).to.equal("module"); + expect(signalsBinding.referenced).to.be.true; + }); + }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6fe6fd0a1..36f53f391 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -55,6 +55,9 @@ importers: '@changesets/cli': specifier: ^2.24.2 version: 2.24.2 + '@types/babel__traverse': + specifier: ^7.18.5 + version: 7.18.5 '@types/chai': specifier: ^4.3.3 version: 4.3.3