diff --git a/.changeset/cold-games-flash.md b/.changeset/cold-games-flash.md new file mode 100644 index 00000000..7aef0733 --- /dev/null +++ b/.changeset/cold-games-flash.md @@ -0,0 +1,5 @@ +--- +'solid-devtools': minor +--- + +Expose the babel plugins currently used by vite: `devtoolsJsxLocationPlugin` and `devtoolsNamePlugin` diff --git a/.changeset/wicked-clocks-sing.md b/.changeset/wicked-clocks-sing.md new file mode 100644 index 00000000..78e1eac9 --- /dev/null +++ b/.changeset/wicked-clocks-sing.md @@ -0,0 +1,5 @@ +--- +'solid-devtools': minor +--- + +exposed babel plugins diff --git a/package.json b/package.json index 90f790f3..8da10747 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "----------------------BUILD----------------------": "", "build": "turbo run build --filter=./packages/*", "----------------------TEST----------------------": "", - "test:unit": "turbo run test --filter=./packages/*", + "test:unit": "turbo run test:unit --filter=./packages/*", "test:types": "turbo run test:types --filter=./packages/*", "test:lint": "eslint **/*.{js,ts,tsx,jsx} --ignore-path .gitignore --max-warnings 0", "test:e2e": "cross-env PW_CHROMIUM_ATTACH_TO_OTHER=1 playwright test -c e2e/playwright.config.ts", diff --git a/packages/main/package.json b/packages/main/package.json index 9f5e68bc..6b6d04ab 100644 --- a/packages/main/package.json +++ b/packages/main/package.json @@ -80,6 +80,12 @@ "default": "./dist/vite.js" } }, + "./babel": { + "import": { + "types": "./dist/babel.d.ts", + "default": "./dist/babel.js" + } + }, "./package.json": "./package.json" }, "typesVersions": { @@ -89,6 +95,9 @@ ], "vite": [ "./dist/vite.d.ts" + ], + "babel": [ + "./dist/babel.d.ts" ] } }, diff --git a/packages/main/src/babel.ts b/packages/main/src/babel.ts new file mode 100644 index 00000000..3d64f726 --- /dev/null +++ b/packages/main/src/babel.ts @@ -0,0 +1,3 @@ +export {DevtoolsModule} from './babel/shared.js' +export {jsxLocationPlugin, JsxLocationPluginConfig} from './babel/location.js' +export {namePlugin} from './babel/name.js' diff --git a/packages/main/src/vite/test/location.test.ts b/packages/main/src/babel/location.test.ts similarity index 79% rename from packages/main/src/vite/test/location.test.ts rename to packages/main/src/babel/location.test.ts index 2773e2d6..a7fd458d 100644 --- a/packages/main/src/vite/test/location.test.ts +++ b/packages/main/src/babel/location.test.ts @@ -1,18 +1,23 @@ -import {assertTransform, cwd, file} from './setup' +import {assertTransform, cwd, file} from './setup_test' import {LOCATION_ATTRIBUTE_NAME, WINDOW_PROJECTPATH_PROPERTY} from '@solid-devtools/debugger/types' import {describe, test} from 'vitest' -import {Module, SET_COMPONENT_LOC, SET_COMPONENT_LOC_LOCAL} from '../constants' -import getPlugin from '../location' +import {DevtoolsModule} from './shared' +import { + jsxLocationPlugin, + JsxLocationPluginConfig, + SET_COMPONENT_LOC, + SET_COMPONENT_LOC_LOCAL, +} from './location' -const setLocationImport = `import { ${SET_COMPONENT_LOC} as ${SET_COMPONENT_LOC_LOCAL} } from "${Module.Setup}";` +const setLocationImport = `import { ${SET_COMPONENT_LOC} as ${SET_COMPONENT_LOC_LOCAL} } from "${DevtoolsModule.Setup}";` describe('location', () => { const testData: [ name: string, src: string, expected: string, - options: Parameters[0], + options: JsxLocationPluginConfig, ][] = [ [ 'function component', @@ -55,7 +60,7 @@ globalThis.${WINDOW_PROJECTPATH_PROPERTY} = "${cwd}";`, testData.forEach(([name, src, expected, options]) => { test(name, () => { - assertTransform(src, expected, getPlugin(options)) + assertTransform(src, expected, jsxLocationPlugin(options)) }) }) }) diff --git a/packages/main/src/babel/location.ts b/packages/main/src/babel/location.ts new file mode 100644 index 00000000..2e837ec4 --- /dev/null +++ b/packages/main/src/babel/location.ts @@ -0,0 +1,126 @@ +import {PluginObj, template} from '@babel/core' +import {NodePath} from '@babel/traverse' +import * as t from '@babel/types' +import { + LOCATION_ATTRIBUTE_NAME, + LocationAttr, + WINDOW_PROJECTPATH_PROPERTY, +} from '@solid-devtools/debugger/types' +import p from 'path' +import {importFromRuntime} from './shared' + +const cwd = process.cwd() + +export const SET_COMPONENT_LOC = 'setComponentLocation' +export const SET_COMPONENT_LOC_LOCAL = `_$${SET_COMPONENT_LOC}` + +const projectPathAst = template(`globalThis.${WINDOW_PROJECTPATH_PROPERTY} = %%loc%%;`)({ + loc: t.stringLiteral(cwd), +}) as t.Statement + +const buildMarkComponent = template(`${SET_COMPONENT_LOC_LOCAL}(%%loc%%);`) as ( + ...args: Parameters> +) => t.Statement + +const isUpperCase = (s: string): boolean => /^[A-Z]/.test(s) + +const getLocationAttribute = (filePath: string, line: number, column: number): LocationAttr => + `${filePath}:${line}:${column}` + +function getNodeLocationAttribute( + node: t.Node, + state: {filename?: unknown}, + isJSX = false, +): string | undefined { + if (!node.loc || typeof state.filename !== 'string') return + return getLocationAttribute( + p.relative(cwd, state.filename), + node.loc.start.line, + // 2 is added to place the caret after the "<" character + node.loc.start.column + (isJSX ? 2 : 0), + ) +} + +let transformCurrentFile = false +let importedRuntime = false + +function importComponentSetter(path: NodePath): void { + if (importedRuntime) return + importFromRuntime(path, SET_COMPONENT_LOC, SET_COMPONENT_LOC_LOCAL) + importedRuntime = true +} + +export type JsxLocationPluginConfig = { + jsx: boolean + components: boolean +} + +export function jsxLocationPlugin(config: JsxLocationPluginConfig): PluginObj { + return { + name: '@solid-devtools/location', + visitor: { + Program(path, state) { + transformCurrentFile = false + importedRuntime = false + // target only project files + if (typeof state.filename !== 'string' || !state.filename.includes(cwd)) return + transformCurrentFile = true + + // inject projectPath variable + path.node.body.push(projectPathAst) + }, + ...(config.jsx && { + JSXOpeningElement(path, state) { + const {openingElement} = path.container as t.JSXElement + if (!transformCurrentFile || openingElement.name.type !== 'JSXIdentifier') return + + // Filter native elements + if (isUpperCase(openingElement.name.name)) return + + const location = getNodeLocationAttribute(openingElement, state, true) + if (!location) return + + openingElement.attributes.push( + t.jsxAttribute( + t.jsxIdentifier(LOCATION_ATTRIBUTE_NAME), + t.stringLiteral(location), + ), + ) + }, + }), + ...(config.components && { + FunctionDeclaration(path, state) { + if (!transformCurrentFile || !path.node.id || !isUpperCase(path.node.id.name)) + return + + const location = getNodeLocationAttribute(path.node, state) + if (!location) return + + importComponentSetter(path) + + path.node.body.body.unshift(buildMarkComponent({loc: t.stringLiteral(location)})) + }, + VariableDeclarator(path, state) { + const {init, id} = path.node + if ( + !transformCurrentFile || + !('name' in id) || + !isUpperCase(id.name) || + !init || + (init.type !== 'FunctionExpression' && + init.type !== 'ArrowFunctionExpression') || + init.body.type !== 'BlockStatement' + ) + return + + const location = getNodeLocationAttribute(path.node, state) + if (!location) return + + importComponentSetter(path) + + init.body.body.unshift(buildMarkComponent({loc: t.stringLiteral(location)})) + }, + }), + } + } +} diff --git a/packages/main/src/vite/test/name.test.ts b/packages/main/src/babel/name.test.ts similarity index 91% rename from packages/main/src/vite/test/name.test.ts rename to packages/main/src/babel/name.test.ts index 81b8fa56..5f04e081 100644 --- a/packages/main/src/vite/test/name.test.ts +++ b/packages/main/src/babel/name.test.ts @@ -1,6 +1,6 @@ import {describe, test} from 'vitest' -import plugin from '../name' -import {assertTransform, testTransform} from './setup' +import {namePlugin} from './name' +import {assertTransform, testTransform} from './setup_test' describe('returning primitives', () => { // Positive tests @@ -26,7 +26,7 @@ describe('returning primitives', () => { const signal = ${creator}(undefined, ${extraArg}{ name: "signal" });`, - plugin, + namePlugin, true, ) @@ -38,7 +38,7 @@ describe('returning primitives', () => { const signal = ${creator}(5, ${extraArg}{ name: "signal" });`, - plugin, + namePlugin, true, ) @@ -54,7 +54,7 @@ describe('returning primitives', () => { name: "signal" });`, - plugin, + namePlugin, true, ) @@ -72,7 +72,7 @@ describe('returning primitives', () => { ...rest });`, - plugin, + namePlugin, true, ) @@ -90,7 +90,7 @@ describe('returning primitives', () => { ...rest });`, - plugin, + namePlugin, true, ) @@ -104,7 +104,7 @@ describe('returning primitives', () => { name: "signal" });`, - plugin, + namePlugin, true, ) @@ -118,7 +118,7 @@ describe('returning primitives', () => { name: "signal" });`, - plugin, + namePlugin, true, ) }) @@ -137,14 +137,14 @@ describe('returning primitives', () => { test(`no import`, () => { const src = `const signal = ${create}();` - assertTransform(src, src, plugin, true) + assertTransform(src, src, namePlugin, true) }) test(`incorrect import`, () => { const src = `import { ${create} } from "${module}"; const signal = ${create}();` - assertTransform(src, src, plugin, true) + assertTransform(src, src, namePlugin, true) }) }) } @@ -174,7 +174,7 @@ describe('effect primitives', () => { name: "in_${fnName}" }); }`, - plugin, + namePlugin, true, ) }) @@ -191,7 +191,7 @@ describe('effect primitives', () => { name: "in_${fnName}" }); };`, - plugin, + namePlugin, true, ) @@ -207,7 +207,7 @@ describe('effect primitives', () => { name: "in_${fnName}" }); };`, - plugin, + namePlugin, true, ) @@ -223,7 +223,7 @@ describe('effect primitives', () => { name: "to_${fnName}" }); });`, - plugin, + namePlugin, true, ) } @@ -238,7 +238,7 @@ describe('effect primitives', () => { ${create}(() => {}); }` - assertTransform(src, src, plugin, true) + assertTransform(src, src, namePlugin, true) }) }) } diff --git a/packages/main/src/vite/name.ts b/packages/main/src/babel/name.ts similarity index 99% rename from packages/main/src/vite/name.ts rename to packages/main/src/babel/name.ts index 5ca50f71..f2693b16 100644 --- a/packages/main/src/vite/name.ts +++ b/packages/main/src/babel/name.ts @@ -107,7 +107,7 @@ function getTarget( let Sources: Record let FileWithImports: boolean = false -const namePlugin: PluginObj = { +export const namePlugin: PluginObj = { name: '@solid-devtools/name', visitor: { Program() { @@ -229,6 +229,4 @@ const namePlugin: PluginObj = { }) }, }, -} - -export default namePlugin +} \ No newline at end of file diff --git a/packages/main/src/vite/test/setup.ts b/packages/main/src/babel/setup_test.ts similarity index 100% rename from packages/main/src/vite/test/setup.ts rename to packages/main/src/babel/setup_test.ts diff --git a/packages/main/src/vite/utils.ts b/packages/main/src/babel/shared.ts similarity index 84% rename from packages/main/src/vite/utils.ts rename to packages/main/src/babel/shared.ts index b5318639..74c4d19d 100644 --- a/packages/main/src/vite/utils.ts +++ b/packages/main/src/babel/shared.ts @@ -1,6 +1,11 @@ import {NodePath} from '@babel/traverse' import * as t from '@babel/types' +export const enum DevtoolsModule { + Main = 'solid-devtools', + Setup = 'solid-devtools/setup', +} + export function getProgram(path: NodePath): NodePath { while (!path.isProgram()) { path = path.parentPath! diff --git a/packages/main/src/vite/constants.ts b/packages/main/src/vite/constants.ts deleted file mode 100644 index cb2df557..00000000 --- a/packages/main/src/vite/constants.ts +++ /dev/null @@ -1,7 +0,0 @@ -export const enum Module { - Main = 'solid-devtools', - Setup = `${Main}/setup`, -} - -export const SET_COMPONENT_LOC = 'setComponentLocation' -export const SET_COMPONENT_LOC_LOCAL = `_$${SET_COMPONENT_LOC}` diff --git a/packages/main/src/vite/index.ts b/packages/main/src/vite/index.ts index abefab7b..2cb312d1 100644 --- a/packages/main/src/vite/index.ts +++ b/packages/main/src/vite/index.ts @@ -2,9 +2,7 @@ import type {PluginOption} from 'vite' import {PluginItem, transformAsync} from '@babel/core' import {LocatorOptions, TargetURLFunction} from '@solid-devtools/debugger/types' -import {Module} from './constants' -import jsxLocationPlugin from './location' -import namePlugin from './name' +import * as babel from '../babel' export type DevtoolsPluginOptions = { /** Add automatic name when creating signals, memos, stores, or mutables */ @@ -63,16 +61,16 @@ export const devtoolsPlugin = (_options: DevtoolsPluginOptions = {}): PluginOpti enablePlugin = config.command === 'serve' && config.mode !== 'production' }, resolveId(id) { - if (enablePlugin && id === Module.Main) return Module.Main + if (enablePlugin && id === babel.DevtoolsModule.Main) return babel.DevtoolsModule.Main }, load(id) { // Inject runtime debugger script - if (!enablePlugin || id !== Module.Main) return + if (!enablePlugin || id !== babel.DevtoolsModule.Main) return - let code = `import "${Module.Setup}";` + let code = `import "${babel.DevtoolsModule.Setup}";` if (options.locator) { - code += `\nimport { setLocatorOptions } from "${Module.Setup}"; + code += `\nimport { setLocatorOptions } from "${babel.DevtoolsModule.Setup}"; setLocatorOptions(${JSON.stringify(options.locator)});` } @@ -92,14 +90,14 @@ export const devtoolsPlugin = (_options: DevtoolsPluginOptions = {}): PluginOpti // plugins that should only run on .tsx/.jsx files in development if ((enabledJsxLocation || enabledComponentLocation) && isJSX) { plugins.push( - jsxLocationPlugin({ + babel.jsxLocationPlugin({ jsx: enabledJsxLocation, components: enabledComponentLocation, }), ) } if (options.autoname) { - plugins.push(namePlugin) + plugins.push(babel.namePlugin) } if (plugins.length === 0) return {code: source} diff --git a/packages/main/src/vite/location.ts b/packages/main/src/vite/location.ts deleted file mode 100644 index 30ec2d2f..00000000 --- a/packages/main/src/vite/location.ts +++ /dev/null @@ -1,122 +0,0 @@ -import {PluginObj, template} from '@babel/core' -import {NodePath} from '@babel/traverse' -import * as t from '@babel/types' -import { - LOCATION_ATTRIBUTE_NAME, - LocationAttr, - WINDOW_PROJECTPATH_PROPERTY, -} from '@solid-devtools/debugger/types' -import p from 'path' -import {SET_COMPONENT_LOC, SET_COMPONENT_LOC_LOCAL} from './constants' -import {importFromRuntime} from './utils' - -const cwd = process.cwd() - -const projectPathAst = template(`globalThis.${WINDOW_PROJECTPATH_PROPERTY} = %%loc%%;`)({ - loc: t.stringLiteral(cwd), -}) as t.Statement - -const buildMarkComponent = template(`${SET_COMPONENT_LOC_LOCAL}(%%loc%%);`) as ( - ...args: Parameters> -) => t.Statement - -const isUpperCase = (s: string): boolean => /^[A-Z]/.test(s) - -const getLocationAttribute = (filePath: string, line: number, column: number): LocationAttr => - `${filePath}:${line}:${column}` - -function getNodeLocationAttribute( - node: t.Node, - state: {filename?: unknown}, - isJSX = false, -): string | undefined { - if (!node.loc || typeof state.filename !== 'string') return - return getLocationAttribute( - p.relative(cwd, state.filename), - node.loc.start.line, - // 2 is added to place the caret after the "<" character - node.loc.start.column + (isJSX ? 2 : 0), - ) -} - -let transformCurrentFile = false -let importedRuntime = false - -function importComponentSetter(path: NodePath): void { - if (importedRuntime) return - importFromRuntime(path, SET_COMPONENT_LOC, SET_COMPONENT_LOC_LOCAL) - importedRuntime = true -} - -const jsxLocationPlugin: (config: { - jsx: boolean - components: boolean -}) => PluginObj = config => ({ - name: '@solid-devtools/location', - visitor: { - Program(path, state) { - transformCurrentFile = false - importedRuntime = false - // target only project files - if (typeof state.filename !== 'string' || !state.filename.includes(cwd)) return - transformCurrentFile = true - - // inject projectPath variable - path.node.body.push(projectPathAst) - }, - ...(config.jsx && { - JSXOpeningElement(path, state) { - const {openingElement} = path.container as t.JSXElement - if (!transformCurrentFile || openingElement.name.type !== 'JSXIdentifier') return - - // Filter native elements - if (isUpperCase(openingElement.name.name)) return - - const location = getNodeLocationAttribute(openingElement, state, true) - if (!location) return - - openingElement.attributes.push( - t.jsxAttribute( - t.jsxIdentifier(LOCATION_ATTRIBUTE_NAME), - t.stringLiteral(location), - ), - ) - }, - }), - ...(config.components && { - FunctionDeclaration(path, state) { - if (!transformCurrentFile || !path.node.id || !isUpperCase(path.node.id.name)) - return - - const location = getNodeLocationAttribute(path.node, state) - if (!location) return - - importComponentSetter(path) - - path.node.body.body.unshift(buildMarkComponent({loc: t.stringLiteral(location)})) - }, - VariableDeclarator(path, state) { - const {init, id} = path.node - if ( - !transformCurrentFile || - !('name' in id) || - !isUpperCase(id.name) || - !init || - (init.type !== 'FunctionExpression' && - init.type !== 'ArrowFunctionExpression') || - init.body.type !== 'BlockStatement' - ) - return - - const location = getNodeLocationAttribute(path.node, state) - if (!location) return - - importComponentSetter(path) - - init.body.body.unshift(buildMarkComponent({loc: t.stringLiteral(location)})) - }, - }), - }, -}) - -export default jsxLocationPlugin diff --git a/packages/main/tsup.config.ts b/packages/main/tsup.config.ts index 06067b80..fba241f8 100644 --- a/packages/main/tsup.config.ts +++ b/packages/main/tsup.config.ts @@ -18,7 +18,7 @@ export default defineConfig([ }, }, { - entryPoints: ['src/vite.ts'], + entryPoints: ['src/vite.ts', 'src/babel.ts'], format: 'esm', dts: true, }, diff --git a/packages/overlay/tsconfig.json b/packages/overlay/tsconfig.json new file mode 100644 index 00000000..4d7c5623 --- /dev/null +++ b/packages/overlay/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/**/*", "./env.d.ts"] +}