diff --git a/.changeset/hot-coats-repeat.md b/.changeset/hot-coats-repeat.md new file mode 100644 index 00000000..b0ea4cc5 --- /dev/null +++ b/.changeset/hot-coats-repeat.md @@ -0,0 +1,8 @@ +--- +"@solid-devtools/debugger": minor +"solid-devtools": minor +--- + +Assign component location directly to the owner +Instead of importing setComponentLocation from solid-devtools/setup +Fixes (#299) \ No newline at end of file diff --git a/packages/debugger/src/inspector/inspector.ts b/packages/debugger/src/inspector/inspector.ts index 1bdf3370..ab33c6d2 100644 --- a/packages/debugger/src/inspector/inspector.ts +++ b/packages/debugger/src/inspector/inspector.ts @@ -1,6 +1,6 @@ import {misc} from '@nothing-but/utils' import {untrackedCallback} from '@solid-devtools/shared/primitives' -import {parseLocationString} from '../locator/index.ts' +import {parseLocationString, type SourceLocation} from '../locator/index.ts' import {NodeType, ValueItemType} from '../main/constants.ts' import {ObjectType, getSdtId} from '../main/id.ts' import {observeValueUpdate, removeValueUpdateObserver} from '../main/observe.ts' @@ -309,14 +309,16 @@ export const collectOwnerDetails = /*#__PURE__*/ untrackedCallback(function ( ;({checkProxyProps, props: details.props} = mapProps(owner.props)) - let location = (owner.component as any).location - if ( - // get location from component.location - (typeof location === 'string' && (location = parseLocationString(location))) || - // get location from the babel plugin marks - ((location = setup.get_owner_location(owner)) && - (location = parseLocationString(location))) - ) { + let location: string | SourceLocation | undefined + if (( + (location = owner.sdtLocation) && + typeof location === 'string' && + (location = parseLocationString(location)) + ) || ( + (location = (owner.component as any).location) && + typeof location === 'string' && + (location = parseLocationString(location)) + )) { details.location = location } } else { diff --git a/packages/debugger/src/locator/index.ts b/packages/debugger/src/locator/index.ts index 4c90132e..f8cb723d 100644 --- a/packages/debugger/src/locator/index.ts +++ b/packages/debugger/src/locator/index.ts @@ -26,6 +26,8 @@ import {type HighlightElementPayload, type LocatorOptions} from './types.ts' export {parseLocationString} from './find-components.ts' +export * from './types.ts' + export function createLocator(props: { locatorEnabled: s.Accessor setLocatorEnabledSignal(signal: s.Accessor): void diff --git a/packages/debugger/src/main/constants.ts b/packages/debugger/src/main/constants.ts index f66f9e04..dd5491ee 100644 --- a/packages/debugger/src/main/constants.ts +++ b/packages/debugger/src/main/constants.ts @@ -56,3 +56,5 @@ export enum ValueItemType { } export const UNKNOWN = 'unknown' + +export const OWNER_LOCATION_PROP = 'sdtLocation' diff --git a/packages/debugger/src/main/types.ts b/packages/debugger/src/main/types.ts index b31df7c0..b555ae61 100644 --- a/packages/debugger/src/main/types.ts +++ b/packages/debugger/src/main/types.ts @@ -1,6 +1,6 @@ import type {EncodedValue, PropGetterState} from '../inspector/types.ts' import type {SourceLocation} from '../locator/types.ts' -import {NodeType, ValueItemType} from './constants.ts' +import {NodeType, OWNER_LOCATION_PROP, ValueItemType} from './constants.ts' // // EXPOSED SOLID API @@ -70,6 +70,7 @@ declare module 'solid-js/types/reactive/signal.d.ts' { interface Owner { sdtType?: NodeType sdtSubRoots?: Solid.Owner[] | null + [OWNER_LOCATION_PROP]?: string } } diff --git a/packages/debugger/src/setup.ts b/packages/debugger/src/setup.ts index 6ba1a0eb..be0b2219 100644 --- a/packages/debugger/src/setup.ts +++ b/packages/debugger/src/setup.ts @@ -12,20 +12,6 @@ import {error} from '@solid-devtools/shared/utils' import type {LocatorOptions} from './locator/types.ts' import type {Solid} from './main/types.ts' -const OwnerLocationMap = new WeakMap() - -/** - * Set the location of the owner in source code. - * Used by the babel plugin. - */ -export function setOwnerLocation(location: string) { - const owner = s.getOwner() - owner && OwnerLocationMap.set(owner, location) -} - -export function getOwnerLocation(owner: Solid.Owner) { - return OwnerLocationMap.get(owner) ?? null -} let PassedLocatorOptions: LocatorOptions | null = null /** @deprecated use `setLocatorOptions` */ @@ -73,7 +59,6 @@ declare global { get_solid(): string | null get_expected_solid(): string | null } - get_owner_location(owner: Solid.Owner): string | null } } @@ -116,7 +101,6 @@ if (!s.DEV || !store.DEV) { get_solid() {return SolidVersion}, get_expected_solid() {return ExpectedSolidVersion}, }, - get_owner_location: getOwnerLocation, } s.DEV.hooks.afterCreateOwner = owner => { diff --git a/packages/main/src/babel/babel.test.ts b/packages/main/src/babel/babel.test.ts index 756ea231..d3a6a82c 100644 --- a/packages/main/src/babel/babel.test.ts +++ b/packages/main/src/babel/babel.test.ts @@ -12,14 +12,13 @@ import * as plugin from './babel.ts' const removeExtraSpaces = (str: string): string => { - return str.replace(/ {2,}/g, ' ').replace(/[\t\n] ?/g, '') + return str.replace(/\s+/g, '') } function assertTransform( src: string, expected: string, plugin: babel.PluginObj, - trim = false, ): void { const ast = parser.parse(src, { sourceType: 'module', @@ -28,8 +27,8 @@ function assertTransform( babel.traverse(ast, plugin.visitor, undefined, {filename: `${cwd}/${file}`}) const res = new generator.CodeGenerator(ast).generate() - const output = trim ? removeExtraSpaces(res.code) : res.code - const expectedOutput = trim ? removeExtraSpaces(expected) : expected + const output = removeExtraSpaces(res.code) + const expectedOutput = removeExtraSpaces(expected) test.expect(output).toBe(expectedOutput) } @@ -39,15 +38,14 @@ function testTransform( src: string, expected: string, plugin: babel.PluginObj, - trim = false, ): void { test.test(name, () => { - assertTransform(src, expected, plugin, trim) + assertTransform(src, expected, plugin) }) } -const setLocationImport = `import { ${plugin.SET_COMPONENT_LOC} as ${plugin.SET_COMPONENT_LOC_LOCAL} } from "${plugin.DevtoolsModule.Setup}";` +const setLocationImport = `import { getOwner as ${plugin.SDT_GET_OWNER} } from "solid-js";` test.describe('location', () => { const testData: [ @@ -59,12 +57,12 @@ test.describe('location', () => { [ 'function component', `function Button(props) { - return + return }`, `${setLocationImport} function Button(props) { - ${plugin.SET_COMPONENT_LOC_LOCAL}("${file}:1:0"); - return ; + if (${plugin.SDT_GET_OWNER}()) ${plugin.SDT_GET_OWNER}().${debug.OWNER_LOCATION_PROP} = "${file}:1:0"; + return ; } globalThis.${debug.WINDOW_PROJECTPATH_PROPERTY} = "${cwd}";`, {jsx: false, components: true}, @@ -72,12 +70,12 @@ globalThis.${debug.WINDOW_PROJECTPATH_PROPERTY} = "${cwd}";`, [ 'arrow component', `const Button = props => { - return + return }`, `${setLocationImport} const Button = props => { - ${plugin.SET_COMPONENT_LOC_LOCAL}("${file}:1:6"); - return ; + if (${plugin.SDT_GET_OWNER}()) ${plugin.SDT_GET_OWNER}().${debug.OWNER_LOCATION_PROP} = "${file}:1:6"; + return ; }; globalThis.${debug.WINDOW_PROJECTPATH_PROPERTY} = "${cwd}";`, {jsx: false, components: true}, @@ -85,10 +83,10 @@ globalThis.${debug.WINDOW_PROJECTPATH_PROPERTY} = "${cwd}";`, [ 'jsx', `function Button(props) { - return + return }`, `function Button(props) { - return ; + return ; } globalThis.${debug.WINDOW_PROJECTPATH_PROPERTY} = "${cwd}";`, {jsx: true, components: false}, @@ -132,7 +130,6 @@ test.describe('autoname: returning primitives', () => { name: "signal" });`, plugin.namePlugin, - true, ) testTransform( @@ -144,7 +141,6 @@ test.describe('autoname: returning primitives', () => { name: "signal" });`, plugin.namePlugin, - true, ) testTransform( @@ -164,7 +160,6 @@ const var_foo = 123, }), var_bar = 321;`, plugin.namePlugin, - true, ) testTransform( @@ -180,7 +175,6 @@ const var_foo = 123, });`, plugin.namePlugin, - true, ) testTransform( @@ -198,7 +192,6 @@ const var_foo = 123, });`, plugin.namePlugin, - true, ) testTransform( @@ -216,7 +209,6 @@ const var_foo = 123, });`, plugin.namePlugin, - true, ) testTransform( @@ -230,7 +222,6 @@ const var_foo = 123, });`, plugin.namePlugin, - true, ) testTransform( @@ -244,7 +235,6 @@ const var_foo = 123, });`, plugin.namePlugin, - true, ) }) } @@ -262,14 +252,14 @@ const var_foo = 123, test.test(`no import`, () => { const src = `const signal = ${create}();` - assertTransform(src, src, plugin.namePlugin, true) + assertTransform(src, src, plugin.namePlugin) }) test.test(`incorrect import`, () => { const src = `import { ${create} } from "${module}"; const signal = ${create}();` - assertTransform(src, src, plugin.namePlugin, true) + assertTransform(src, src, plugin.namePlugin) }) }) } @@ -301,7 +291,6 @@ test.describe('autoname: effect primitives', () => { }); }`, plugin.namePlugin, - true, ) }) @@ -318,7 +307,6 @@ test.describe('autoname: effect primitives', () => { }); };`, plugin.namePlugin, - true, ) testTransform( @@ -334,7 +322,6 @@ test.describe('autoname: effect primitives', () => { }); };`, plugin.namePlugin, - true, ) testTransform( @@ -350,7 +337,6 @@ test.describe('autoname: effect primitives', () => { }); });`, plugin.namePlugin, - true, ) } }) @@ -364,7 +350,7 @@ test.describe('autoname: effect primitives', () => { ${create}(() => {}); }` - assertTransform(src, src, plugin.namePlugin, true) + assertTransform(src, src, plugin.namePlugin) }) }) } diff --git a/packages/main/src/babel/babel.ts b/packages/main/src/babel/babel.ts index 7b8bef95..0489fc51 100644 --- a/packages/main/src/babel/babel.ts +++ b/packages/main/src/babel/babel.ts @@ -3,32 +3,11 @@ import * as t from '@babel/types' import * as debug from '@solid-devtools/debugger/types' import * as path from 'node:path' -export const enum DevtoolsModule { - Main = 'solid-devtools', - Setup = 'solid-devtools/setup', -} - -export function getProgram(path: babel.NodePath): babel.NodePath { - while (!path.isProgram()) { - path = path.parentPath! - } - return path -} - -export function importFromRuntime(path: babel.NodePath, name: string, as: string): void { - const program = getProgram(path) - program.unshiftContainer('body', [ - t.importDeclaration( - [t.importSpecifier(t.identifier(as), t.identifier(name))], - t.stringLiteral('solid-devtools/setup'), - ), - ]) -} - const NAME_ID = /* @__PURE__ */ t.identifier('name') const UNDEFINED_ID = /* @__PURE__ */ t.identifier('undefined') + type Comparable = t.Identifier | t.V8IntrinsicIdentifier | t.PrivateName | t.Expression function equal(a: Comparable, b: Comparable): boolean { if (a.type !== b.type) return false @@ -249,35 +228,24 @@ export const namePlugin: babel.PluginObj = { }, } -export const SET_COMPONENT_LOC = 'setComponentLocation' -export const SET_COMPONENT_LOC_LOCAL = `_$${SET_COMPONENT_LOC}` - const isUpperCase = (s: string): boolean => /^[A-Z]/.test(s) const getLocationAttribute = (filePath: string, line: number, column: number): debug.LocationAttr => `${filePath}:${line}:${column}` function getNodeLocationAttribute( - node: t.Node, - state: {filename?: unknown}, + node: t.Node, + filename: string, isJSX = false, ): string | undefined { - if (!node.loc || typeof state.filename !== 'string') return - return getLocationAttribute( - path.relative(process.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: babel.NodePath): void { - if (importedRuntime) return - importFromRuntime(path, SET_COMPONENT_LOC, SET_COMPONENT_LOC_LOCAL) - importedRuntime = true + if (node.loc) { + return getLocationAttribute( + path.relative(process.cwd(), filename), + node.loc.start.line, + // 2 is added to place the caret after the "<" character + node.loc.start.column + (isJSX ? 2 : 0), + ) + } } export type JsxLocationPluginConfig = { @@ -285,83 +253,105 @@ export type JsxLocationPluginConfig = { components: boolean } +export const SDT_GET_OWNER = '$sdt_getOwner' + +const solid_js_str = t.stringLiteral('solid-js') +const get_owner_ident = t.identifier('getOwner') +const sdt_get_owner_ident = t.identifier(SDT_GET_OWNER) +const jsx_location_attr = t.jsxIdentifier(debug.LOCATION_ATTRIBUTE_NAME) + export function jsxLocationPlugin(config: JsxLocationPluginConfig): babel.PluginObj { - const projectPathAst = babel.template( + let file_transform = false + let file_imported = false + let file_filename = '' + let file_program: babel.NodePath | undefined + + const project_path_ast = babel.template( `globalThis.${debug.WINDOW_PROJECTPATH_PROPERTY} = %%loc%%;`)({ loc: t.stringLiteral(process.cwd()), } ) as t.Statement - const buildMarkComponent = babel.template( - `${SET_COMPONENT_LOC_LOCAL}(%%loc%%);` + const buildSetComponentLocationAst = babel.template( + `if (${SDT_GET_OWNER}()) ${SDT_GET_OWNER}().${debug.OWNER_LOCATION_PROP} = %%loc%%;` ) as (...args: Parameters>) => t.Statement + function addLocationToBody( + body: babel.types.BlockStatement, + node: t.Node, + ) { + let location = getNodeLocationAttribute(node, file_filename) + if (location) { + + if (!file_imported) { + file_imported = true + + file_program!.unshiftContainer('body', [ + t.importDeclaration( + [t.importSpecifier(sdt_get_owner_ident, get_owner_ident)], + solid_js_str, + ), + ]) + } + + body.body.unshift(buildSetComponentLocationAst({loc: t.stringLiteral(location)})) + } + } + 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(process.cwd())) return - transformCurrentFile = true - - // inject projectPath variable - path.node.body.push(projectPathAst) + + file_transform = false + file_imported = false + file_filename = '' + file_program = undefined + + if (typeof state.filename === 'string') { + file_transform = true + file_filename = state.filename + file_program = path + + // inject projectPath variable + file_program.node.body.push(project_path_ast) + } }, ...(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(debug.LOCATION_ATTRIBUTE_NAME), - t.stringLiteral(location), - ), - ) + JSXOpeningElement(path) { + let {openingElement} = path.container as t.JSXElement + + if (file_transform && + openingElement.name.type === 'JSXIdentifier' && + // Filter native elements + !isUpperCase(openingElement.name.name) + ) { + let location = getNodeLocationAttribute(openingElement, file_filename, true) + if (location) { + openingElement.attributes.push( + t.jsxAttribute(jsx_location_attr, 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)})) + FunctionDeclaration(path) { + if (file_transform && path.node.id && isUpperCase(path.node.id.name)) { + addLocationToBody(path.node.body, path.node) + } }, - 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 + VariableDeclarator(path) { + if (file_transform && + 'name' in path.node.id && isUpperCase(path.node.id.name) && + path.node.init && + (path.node.init.type === 'FunctionExpression' || + path.node.init.type === 'ArrowFunctionExpression') && + path.node.init.body.type === 'BlockStatement' + ) { + addLocationToBody(path.node.init.body, path.node) } - - 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/setup.ts b/packages/main/src/setup.ts index 7731f812..63a7c4f8 100644 --- a/packages/main/src/setup.ts +++ b/packages/main/src/setup.ts @@ -2,7 +2,6 @@ import '@solid-devtools/debugger/setup' import { setClientVersion, - setOwnerLocation, setSolidVersion, setLocatorOptions, } from '@solid-devtools/debugger/setup' @@ -10,13 +9,4 @@ import { setClientVersion(process.env.CLIENT_VERSION) setSolidVersion(process.env.SOLID_VERSION, process.env.EXPECTED_SOLID_VERSION) -/** - * Set debugger locator module options. - * Used by the `solid-devtools` plugin. - */ -export function setComponentLocation(location: string): void { - if (typeof location !== 'string') return - setOwnerLocation(location) -} - export {setLocatorOptions} diff --git a/packages/main/src/setup_noop.ts b/packages/main/src/setup_noop.ts index 09e9bdd3..71ab069f 100644 --- a/packages/main/src/setup_noop.ts +++ b/packages/main/src/setup_noop.ts @@ -1,9 +1,6 @@ import type * as API from './setup.ts' -export const {setComponentLocation, setLocatorOptions}: typeof API = { - setComponentLocation() { - /**/ - }, +export const {setLocatorOptions}: typeof API = { setLocatorOptions() { /**/ }, diff --git a/packages/main/src/vite/vite.ts b/packages/main/src/vite/vite.ts index 572b67a2..593b44fc 100644 --- a/packages/main/src/vite/vite.ts +++ b/packages/main/src/vite/vite.ts @@ -4,6 +4,11 @@ import {type PluginItem, transformAsync} from '@babel/core' import * as debug from '@solid-devtools/debugger/types' import * as babel from '../babel.ts' +export const enum DevtoolsModule { + Main = 'solid-devtools', + Setup = 'solid-devtools/setup', +} + export type LocatorPluginOptions = { /** Choose in which IDE the component source code should be revealed. */ targetIDE?: Exclude @@ -71,16 +76,16 @@ export const devtoolsPlugin = (_options: DevtoolsPluginOptions = {}): vite.Plugi is_dev = config.command === 'serve' && config.mode !== 'production' }, resolveId(id) { - if (is_dev && id === babel.DevtoolsModule.Main) return babel.DevtoolsModule.Main + if (is_dev && id === DevtoolsModule.Main) return DevtoolsModule.Main }, load(id) { // Inject runtime debugger script - if (!is_dev || id !== babel.DevtoolsModule.Main) return + if (!is_dev || id !== DevtoolsModule.Main) return - let code = `import "${babel.DevtoolsModule.Setup}";` + let code = `import "${DevtoolsModule.Setup}";` if (options.locator) { - code += `\nimport { setLocatorOptions } from "${babel.DevtoolsModule.Setup}"; + code += `\nimport { setLocatorOptions } from "${DevtoolsModule.Setup}"; setLocatorOptions(${JSON.stringify(options.locator)});` }