@inspecto-dev/plugin 0.3.4 → 0.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +179 -44
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +179 -44
- package/dist/index.js.map +1 -1
- package/dist/legacy/rspack/index.cjs +108 -18
- package/dist/legacy/rspack/index.cjs.map +1 -1
- package/dist/legacy/rspack/index.js +108 -18
- package/dist/legacy/rspack/index.js.map +1 -1
- package/dist/legacy/rspack/loader.cjs +49 -4
- package/dist/legacy/rspack/loader.cjs.map +1 -1
- package/dist/legacy/rspack/loader.js +49 -4
- package/dist/legacy/rspack/loader.js.map +1 -1
- package/dist/legacy/webpack4/index.cjs +120 -20
- package/dist/legacy/webpack4/index.cjs.map +1 -1
- package/dist/legacy/webpack4/index.d.cts +2 -0
- package/dist/legacy/webpack4/index.d.ts +2 -0
- package/dist/legacy/webpack4/index.js +120 -20
- package/dist/legacy/webpack4/index.js.map +1 -1
- package/dist/legacy/webpack4/loader.cjs +49 -4
- package/dist/legacy/webpack4/loader.cjs.map +1 -1
- package/dist/legacy/webpack4/loader.js +49 -4
- package/dist/legacy/webpack4/loader.js.map +1 -1
- package/dist/rollup.cjs +179 -44
- package/dist/rollup.cjs.map +1 -1
- package/dist/rollup.js +179 -44
- package/dist/rollup.js.map +1 -1
- package/dist/rspack.cjs +179 -44
- package/dist/rspack.cjs.map +1 -1
- package/dist/rspack.js +179 -44
- package/dist/rspack.js.map +1 -1
- package/dist/vite.cjs +179 -44
- package/dist/vite.cjs.map +1 -1
- package/dist/vite.js +179 -44
- package/dist/vite.js.map +1 -1
- package/dist/webpack.cjs +179 -44
- package/dist/webpack.cjs.map +1 -1
- package/dist/webpack.js +179 -44
- package/dist/webpack.js.map +1 -1
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/legacy/rspack/loader.ts","../../../src/transform/index.ts","../../../src/transform/transform-jsx.ts","../../../src/transform/utils.ts","../../../src/transform/transform-vue.ts"],"sourcesContent":["import { transformRouter } from '../../transform/index.js'\nimport { shouldTransform } from '../../transform/utils.js'\nimport type { UnpluginOptions } from '@inspecto-dev/types'\n\nexport default function legacyRspackLoader(this: any, source: string) {\n const id = this.resourcePath\n const options = this.getOptions() as Required<UnpluginOptions>\n\n if (!shouldTransform(id, options)) {\n return source\n }\n\n const result = transformRouter({\n filePath: id,\n source,\n projectRoot: process.cwd(),\n pluginOptions: options,\n })\n\n if (result && result.changed) {\n this.callback(null, result.code, result.map)\n return\n }\n\n return source\n}\n","import path from 'node:path'\nimport type { UnpluginOptions } from '@inspecto-dev/types'\nimport { transformJsx } from './transform-jsx.js'\nimport { transformVue } from './transform-vue.js'\nimport { JSX_EXTENSIONS, type TransformResult } from './utils.js'\n\nexport interface RouterOptions {\n filePath: string\n source: string\n projectRoot: string\n pluginOptions: Required<UnpluginOptions>\n}\n\n/**\n * Route a file to the appropriate transform based on extension.\n * Returns null if no transform applies.\n */\nexport function transformRouter(options: RouterOptions): TransformResult | null {\n const { filePath, source, projectRoot, pluginOptions } = options\n const ext = path.extname(filePath).toLowerCase()\n\n if (JSX_EXTENSIONS.has(ext)) {\n return transformJsx({\n filePath,\n source,\n projectRoot,\n escapeTags: pluginOptions.escapeTags,\n pathType: pluginOptions.pathType,\n attributeName: pluginOptions.attributeName,\n })\n }\n\n // ── Vue SFC ──────────────────────────────────────────────────────────────\n if (ext === '.vue') {\n return transformVue({\n filePath,\n source,\n projectRoot,\n escapeTags: pluginOptions.escapeTags,\n pathType: pluginOptions.pathType,\n attributeName: pluginOptions.attributeName,\n })\n }\n\n return null\n}\n// Export transforms for testing\nexport { transformJsx, transformVue }\n","import * as parser from '@babel/parser'\nimport traverse_ from '@babel/traverse'\n// Support both ESM default and CommonJS module.exports\nconst traverse =\n typeof traverse_ === 'function' ? traverse_ : (traverse_ as any).default || traverse_\nimport type { NodePath } from '@babel/traverse'\nimport type { JSXOpeningElement } from '@babel/types'\nimport MagicString from 'magic-string'\nimport path from 'node:path'\nimport type { UnpluginOptions, PathType } from '@inspecto-dev/types'\nimport { buildEscapeTagsSet, formatAttrValue, type TransformResult } from './utils.js'\n\nexport interface TransformJsxOptions {\n filePath: string\n source: string\n projectRoot: string\n escapeTags?: string[]\n pathType?: PathType\n attributeName?: string\n}\n\n/**\n * Transform JSX/TSX source code by injecting data-inspecto attributes.\n */\nexport function transformJsx(options: TransformJsxOptions): TransformResult {\n const {\n filePath,\n source,\n projectRoot,\n escapeTags,\n pathType = 'absolute',\n attributeName = 'data-inspecto',\n } = options\n\n const escapeTagsSet = buildEscapeTagsSet(escapeTags)\n\n // Resolve the file path based on pathType config\n const resolvedPath =\n pathType === 'absolute'\n ? path.resolve(filePath)\n : path.relative(projectRoot, path.resolve(filePath))\n\n // Normalize path separators on Windows\n const normalizedPath = resolvedPath.replace(/\\\\/g, '/')\n\n let ast: ReturnType<typeof parser.parse>\n try {\n ast = parser.parse(source, {\n sourceType: 'module',\n plugins: [\n 'jsx',\n 'typescript',\n 'decorators-legacy',\n 'classProperties',\n 'optionalChaining',\n 'nullishCoalescingOperator',\n 'importMeta',\n ],\n errorRecovery: true,\n })\n } catch {\n // If parsing fails, return source unchanged\n return { code: source, map: null, changed: false }\n }\n\n const ms = new MagicString(source)\n let changed = false\n\n traverse(ast, {\n JSXOpeningElement(nodePath: NodePath<JSXOpeningElement>) {\n const node = nodePath.node\n\n // Skip elements that already have the attribute\n const alreadyHasAttr = node.attributes.some(\n attr =>\n attr.type === 'JSXAttribute' &&\n attr.name.type === 'JSXIdentifier' &&\n attr.name.name === attributeName,\n )\n if (alreadyHasAttr) return\n\n // Get element tag name\n const nameNode = node.name\n let tagName: string\n if (nameNode.type === 'JSXIdentifier') {\n tagName = nameNode.name\n } else if (nameNode.type === 'JSXMemberExpression') {\n const objName = nameNode.object.type === 'JSXIdentifier' ? nameNode.object.name : ''\n const propName = nameNode.property.type === 'JSXIdentifier' ? nameNode.property.name : ''\n tagName = objName && propName ? `${objName}.${propName}` : objName\n } else {\n tagName = ''\n }\n\n // Skip escaped tags\n if (escapeTagsSet.has(tagName)) return\n\n // Get position from AST location\n const loc = node.loc\n if (!loc) return\n\n const { line, column } = loc.start\n // Babel uses 0-based columns, convert to 1-based\n const attrValue = formatAttrValue(normalizedPath, line, column + 1)\n\n // Determine the best insertion position for the attribute\n // When a JSX element has type arguments (e.g. <Component<string> />),\n // inserting after `node.name.end` might inject inside the generic bracket `<`.\n // The safest place to insert is right before the first attribute,\n // or right before the closing slash/bracket if there are no attributes.\n let insertPos: number | null | undefined = null\n if (node.attributes && node.attributes.length > 0) {\n const firstAttr = node.attributes[0]\n if (firstAttr && firstAttr.start != null) {\n insertPos = firstAttr.start\n }\n }\n\n if (insertPos == null) {\n // Find the start of the closing bracket or self-closing slash\n // We know node.end is the index right after the '>'\n // So we look backwards. But Babel AST doesn't give us exact token positions\n // for the closing tag easily.\n // For a safe fallback, we use node.typeParameters?.end || node.name.end\n if (node.typeParameters && node.typeParameters.end != null) {\n insertPos = node.typeParameters.end\n } else if (node.name.end != null) {\n insertPos = node.name.end\n }\n }\n\n if (insertPos == null) return\n\n ms.appendLeft(\n insertPos,\n ` ${attributeName}=\"${attrValue}\"${node.attributes && node.attributes.length > 0 ? '' : ' '}`,\n )\n changed = true\n },\n })\n\n if (!changed) {\n return { code: source, map: null, changed: false }\n }\n\n return {\n code: ms.toString(),\n map: ms.generateMap({ hires: true, source: filePath }),\n changed: true,\n }\n}\n","import type { UnpluginOptions } from '@inspecto-dev/types'\nimport type MagicString from 'magic-string'\n\nexport interface TransformResult {\n code: string\n map: ReturnType<MagicString['generateMap']> | null\n changed: boolean\n}\n\n/** Default tags whose JSX elements should NOT receive data-inspecto attributes */\nexport const DEFAULT_ESCAPE_TAGS = new Set([\n 'template',\n 'script',\n 'style',\n // React special elements\n 'Fragment',\n 'React.Fragment',\n 'StrictMode',\n 'React.StrictMode',\n 'Suspense',\n 'React.Suspense',\n 'Profiler',\n 'React.Profiler',\n // React transitions\n 'Transition',\n 'TransitionGroup',\n // Vue built-in components\n 'KeepAlive',\n 'Teleport',\n 'Suspense',\n // Vue router built-ins\n 'RouterView',\n 'RouterLink',\n 'NuxtPage',\n 'NuxtLink',\n])\n\n/** File extensions that contain JSX/TSX syntax */\nexport const JSX_EXTENSIONS = new Set(['.jsx', '.tsx', '.js', '.ts', '.mjs', '.mts'])\n\n/**\n * Determine if a file should be transformed.\n * Always skips node_modules and dist directories.\n */\nexport function shouldTransform(filePath: string, options: Required<UnpluginOptions>): boolean {\n // Never transform in production\n if (process.env['NODE_ENV'] === 'production') return false\n\n // Skip node_modules always\n if (filePath.includes('node_modules')) return false\n\n // Skip virtual modules\n if (filePath.startsWith('\\x00')) return false\n\n // Skip dist/build directories\n if (/[/\\\\](dist|build|\\.next|\\.nuxt)[/\\\\]/.test(filePath)) return false\n\n // Skip non-code files (like .html, .css)\n const ext = filePath.split('.').pop()?.toLowerCase()\n if (ext && !['js', 'jsx', 'ts', 'tsx', 'mjs', 'mts', 'vue'].includes(ext)) {\n return false\n }\n\n // Check user-defined exclude patterns\n // (picomatch integration — see index.ts for how options.exclude is applied)\n\n return true\n}\n\n/**\n * Build the escape tags set from user options merged with defaults.\n */\nexport function buildEscapeTagsSet(escapeTags?: string[]): Set<string> {\n const merged = new Set(DEFAULT_ESCAPE_TAGS)\n if (escapeTags) {\n for (const tag of escapeTags) {\n merged.add(tag)\n }\n }\n return merged\n}\n\n/**\n * Format a source location value for the data-inspecto attribute.\n * Format: \"filepath:line:column\"\n */\nexport function formatAttrValue(file: string, line: number, column: number): string {\n return `${file}:${line}:${column}`\n}\n","import * as vueCompiler from '@vue/compiler-dom'\nimport { parse as parseSFC } from '@vue/compiler-sfc'\nimport type { ElementNode, AttributeNode } from '@vue/compiler-core'\nimport { NodeTypes } from '@vue/compiler-core'\nimport MagicString from 'magic-string'\nimport path from 'node:path'\nimport type { PathType } from '@inspecto-dev/types'\nimport { buildEscapeTagsSet, formatAttrValue, type TransformResult } from './utils.js'\n\nexport interface TransformVueOptions {\n filePath: string\n source: string\n projectRoot: string\n escapeTags?: string[]\n pathType?: PathType\n attributeName?: string\n}\n\n/**\n * Transform Vue SFC source by injecting data-inspecto attributes\n * into template elements.\n *\n * Strategy:\n * 1. Locate the <template> block in the SFC source\n * 2. Parse only the template block with @vue/compiler-dom\n * 3. Walk ElementNode nodes in the AST\n * 4. For each eligible element, inject the attribute using MagicString\n * at the exact offset within the original source\n */\nexport function transformVue(options: TransformVueOptions): TransformResult {\n const {\n filePath,\n source,\n projectRoot,\n escapeTags,\n pathType = 'absolute',\n attributeName = 'data-inspecto',\n } = options\n\n const escapeTagsSet = buildEscapeTagsSet(escapeTags)\n\n // Resolve path\n const resolvedPath =\n pathType === 'absolute'\n ? path.resolve(filePath)\n : path.relative(projectRoot, path.resolve(filePath))\n\n const normalizedPath = resolvedPath.replace(/\\\\/g, '/')\n\n // ── Find <template> block boundaries ──────────────────────────────────────\n // Use @vue/compiler-sfc to parse the file and extract the template block.\n // This is much safer than regex for handling nested templates.\n const { descriptor, errors } = parseSFC(source, {\n filename: filePath,\n sourceMap: false,\n ignoreEmpty: true,\n })\n\n if (errors.length > 0 || !descriptor.template) {\n return { code: source, map: null, changed: false }\n }\n\n const templateContent = descriptor.template.content\n const templateBlockStart = descriptor.template.loc.start.offset\n\n // ── Parse template block ───────────────────────────────────────────────────\n let ast: vueCompiler.RootNode\n try {\n ast = vueCompiler.parse(templateContent, {\n parseMode: 'html',\n // Preserve source locations relative to templateContent\n onError: () => {\n /* ignore non-fatal parse errors */\n },\n })\n } catch {\n return { code: source, map: null, changed: false }\n }\n\n const ms = new MagicString(source)\n let changed = false\n\n // ── Walk AST ───────────────────────────────────────────────────────────────\n walkElement(ast, node => {\n // Skip non-element nodes\n if (node.type !== NodeTypes.ELEMENT) return\n\n const tagName = node.tag\n\n // Skip escaped tags\n if (escapeTagsSet.has(tagName)) return\n\n // Skip <template> wrapper itself (it's the root, not a real element)\n if (tagName === 'template' && node === ast.children[0]) return\n\n // Skip elements that already have the attribute (idempotency)\n const alreadyHasAttr = node.props.some(\n (p): p is AttributeNode => p.type === NodeTypes.ATTRIBUTE && p.name === attributeName,\n )\n if (alreadyHasAttr) return\n\n // node.loc is relative to templateContent — add templateBlockStart offset\n const loc = node.loc\n if (!loc) return\n\n const { line, column } = loc.start\n\n // Calculate absolute line and column in the original source\n // @vue/compiler-dom uses 1-based line and 1-based column\n const templateStartLoc = descriptor.template!.loc.start\n const absoluteLine = templateStartLoc.line + line - 1\n const absoluteColumn = line === 1 ? templateStartLoc.column + column - 1 : column\n\n const attrValue = formatAttrValue(normalizedPath, absoluteLine, absoluteColumn)\n\n // Find insert position: right after the tag name in the original source\n // node.loc.start.offset is 0-based offset within templateContent\n const tagNameEnd = loc.start.offset + tagName.length + 1 // +1 for '<'\n const absoluteOffset = templateBlockStart + tagNameEnd\n\n ms.appendLeft(absoluteOffset, ` ${attributeName}=\"${attrValue}\"`)\n changed = true\n })\n\n if (!changed) {\n return { code: source, map: null, changed: false }\n }\n\n return {\n code: ms.toString(),\n map: ms.generateMap({ hires: true, source: filePath }),\n changed: true,\n }\n}\n\n// ── AST walker ────────────────────────────────────────────────────────────────\n\ntype AnyNode = vueCompiler.RootNode | vueCompiler.TemplateChildNode\n\nfunction walkElement(node: AnyNode, visitor: (node: ElementNode) => void): void {\n if (node.type === NodeTypes.ELEMENT) {\n visitor(node)\n for (const child of node.children) {\n walkElement(child as AnyNode, visitor)\n }\n } else if ('children' in node && Array.isArray(node.children)) {\n for (const child of node.children) {\n walkElement(child as AnyNode, visitor)\n }\n }\n}\n"],"mappings":"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","names":["import_node_path","traverse_","path","MagicString","import_magic_string","import_node_path","path","parseSFC","MagicString","path"]}
|
|
1
|
+
{"version":3,"sources":["../../../src/legacy/rspack/loader.ts","../../../src/transform/index.ts","../../../src/transform/transform-jsx.ts","../../../src/transform/utils.ts","../../../src/transform/transform-vue.ts"],"sourcesContent":["import { transformRouter } from '../../transform/index.js'\nimport { shouldTransform } from '../../transform/utils.js'\nimport type { UnpluginOptions } from '@inspecto-dev/types'\n\nexport default function legacyRspackLoader(this: any, source: string) {\n const id = this.resourcePath\n const options = this.getOptions() as Required<UnpluginOptions>\n\n if (!shouldTransform(id, options)) {\n return source\n }\n\n const result = transformRouter({\n filePath: id,\n source,\n projectRoot: process.cwd(),\n pluginOptions: options,\n })\n\n if (result && result.changed) {\n this.callback(null, result.code, result.map)\n return\n }\n\n return source\n}\n","import path from 'node:path'\nimport type { UnpluginOptions } from '@inspecto-dev/types'\nimport { transformJsx } from './transform-jsx.js'\nimport { transformVue } from './transform-vue.js'\nimport { JSX_EXTENSIONS, type TransformResult } from './utils.js'\n\nexport interface RouterOptions {\n filePath: string\n source: string\n projectRoot: string\n pluginOptions: Required<UnpluginOptions>\n}\n\n/**\n * Route a file to the appropriate transform based on extension.\n * Returns null if no transform applies.\n */\nexport function transformRouter(options: RouterOptions): TransformResult | null {\n const { filePath, source, projectRoot, pluginOptions } = options\n const ext = path.extname(filePath).toLowerCase()\n\n if (JSX_EXTENSIONS.has(ext)) {\n return transformJsx({\n filePath,\n source,\n projectRoot,\n escapeTags: pluginOptions.escapeTags,\n pathType: pluginOptions.pathType,\n attributeName: pluginOptions.attributeName,\n })\n }\n\n // ── Vue SFC ──────────────────────────────────────────────────────────────\n if (ext === '.vue') {\n return transformVue({\n filePath,\n source,\n projectRoot,\n escapeTags: pluginOptions.escapeTags,\n pathType: pluginOptions.pathType,\n attributeName: pluginOptions.attributeName,\n })\n }\n\n return null\n}\n// Export transforms for testing\nexport { transformJsx, transformVue }\n","import * as parser from '@babel/parser'\nimport traverse_ from '@babel/traverse'\n// Support both ESM default and CommonJS module.exports\nconst traverse =\n typeof traverse_ === 'function' ? traverse_ : (traverse_ as any).default || traverse_\nimport type { NodePath } from '@babel/traverse'\nimport type { JSXOpeningElement } from '@babel/types'\nimport MagicString from 'magic-string'\nimport path from 'node:path'\nimport type { UnpluginOptions, PathType } from '@inspecto-dev/types'\nimport { buildEscapeTagsSet, formatAttrValue, type TransformResult } from './utils.js'\n\nexport interface TransformJsxOptions {\n filePath: string\n source: string\n projectRoot: string\n escapeTags?: string[]\n pathType?: PathType\n attributeName?: string\n}\n\n/**\n * Transform JSX/TSX source code by injecting data-inspecto attributes.\n */\nexport function transformJsx(options: TransformJsxOptions): TransformResult {\n const {\n filePath,\n source,\n projectRoot,\n escapeTags,\n pathType = 'absolute',\n attributeName = 'data-inspecto',\n } = options\n\n const escapeTagsSet = buildEscapeTagsSet(escapeTags)\n\n // Resolve the file path based on pathType config\n const resolvedPath =\n pathType === 'absolute'\n ? path.resolve(filePath)\n : path.relative(projectRoot, path.resolve(filePath))\n\n // Normalize path separators on Windows\n const normalizedPath = resolvedPath.replace(/\\\\/g, '/')\n\n let ast: ReturnType<typeof parser.parse>\n try {\n ast = parser.parse(source, {\n sourceType: 'module',\n plugins: [\n 'jsx',\n 'typescript',\n 'decorators-legacy',\n 'classProperties',\n 'optionalChaining',\n 'nullishCoalescingOperator',\n 'importMeta',\n ],\n errorRecovery: true,\n })\n } catch {\n // If parsing fails, return source unchanged\n return { code: source, map: null, changed: false }\n }\n\n const ms = new MagicString(source)\n let changed = false\n\n traverse(ast, {\n JSXOpeningElement(nodePath: NodePath<JSXOpeningElement>) {\n const node = nodePath.node\n\n // Skip elements that already have the attribute\n const alreadyHasAttr = node.attributes.some(\n attr =>\n attr.type === 'JSXAttribute' &&\n attr.name.type === 'JSXIdentifier' &&\n attr.name.name === attributeName,\n )\n if (alreadyHasAttr) return\n\n // Get element tag name\n const nameNode = node.name\n let tagName: string\n if (nameNode.type === 'JSXIdentifier') {\n tagName = nameNode.name\n } else if (nameNode.type === 'JSXMemberExpression') {\n const objName = nameNode.object.type === 'JSXIdentifier' ? nameNode.object.name : ''\n const propName = nameNode.property.type === 'JSXIdentifier' ? nameNode.property.name : ''\n tagName = objName && propName ? `${objName}.${propName}` : objName\n } else {\n tagName = ''\n }\n\n // Skip escaped tags\n if (escapeTagsSet.has(tagName)) return\n\n // Get position from AST location\n const loc = node.loc\n if (!loc) return\n\n const { line, column } = loc.start\n // Babel uses 0-based columns, convert to 1-based\n const attrValue = formatAttrValue(normalizedPath, line, column + 1)\n\n // Determine the best insertion position for the attribute\n // When a JSX element has type arguments (e.g. <Component<string> />),\n // inserting after `node.name.end` might inject inside the generic bracket `<`.\n // The safest place to insert is right before the first attribute,\n // or right before the closing slash/bracket if there are no attributes.\n let insertPos: number | null | undefined = null\n if (node.attributes && node.attributes.length > 0) {\n const firstAttr = node.attributes[0]\n if (firstAttr && firstAttr.start != null) {\n insertPos = firstAttr.start\n }\n }\n\n if (insertPos == null) {\n // Find the start of the closing bracket or self-closing slash\n // We know node.end is the index right after the '>'\n // So we look backwards. But Babel AST doesn't give us exact token positions\n // for the closing tag easily.\n // For a safe fallback, we use node.typeParameters?.end || node.name.end\n if (node.typeParameters && node.typeParameters.end != null) {\n insertPos = node.typeParameters.end\n } else if (node.name.end != null) {\n insertPos = node.name.end\n }\n }\n\n if (insertPos == null) return\n\n ms.appendLeft(\n insertPos,\n ` ${attributeName}=\"${attrValue}\"${node.attributes && node.attributes.length > 0 ? '' : ' '}`,\n )\n changed = true\n },\n })\n\n if (!changed) {\n return { code: source, map: null, changed: false }\n }\n\n return {\n code: ms.toString(),\n map: ms.generateMap({ hires: true, source: filePath }),\n changed: true,\n }\n}\n","import type { UnpluginOptions } from '@inspecto-dev/types'\nimport type MagicString from 'magic-string'\n\nexport interface TransformResult {\n code: string\n map: ReturnType<MagicString['generateMap']> | null\n changed: boolean\n}\n\nexport interface NormalizedTransformTarget {\n requestId: string\n filePath: string\n wrapped: boolean\n}\n\n/** Default tags whose JSX elements should NOT receive data-inspecto attributes */\nexport const DEFAULT_ESCAPE_TAGS = new Set([\n 'template',\n 'script',\n 'style',\n // React special elements\n 'Fragment',\n 'React.Fragment',\n 'StrictMode',\n 'React.StrictMode',\n 'Suspense',\n 'React.Suspense',\n 'Profiler',\n 'React.Profiler',\n // React transitions\n 'Transition',\n 'TransitionGroup',\n // Vue built-in components\n 'KeepAlive',\n 'Teleport',\n 'Suspense',\n // Vue router built-ins\n 'RouterView',\n 'RouterLink',\n 'NuxtPage',\n 'NuxtLink',\n])\n\n/** File extensions that contain JSX/TSX syntax */\nexport const JSX_EXTENSIONS = new Set(['.jsx', '.tsx', '.js', '.ts', '.mjs', '.mts'])\n\nfunction normalizeWebpackModuleRequest(id: string): string {\n return id.replace(/!+$/, '').replace(/^\\((?:app-pages-browser|rsc|ssr)\\)\\/\\.\\//, '')\n}\n\nfunction extractNextModuleRequest(id: string): string | undefined {\n if (!id.includes('next-flight-client-entry-loader.js?')) {\n return undefined\n }\n\n const queryIndex = id.indexOf('?')\n if (queryIndex === -1) {\n return undefined\n }\n\n const params = new URLSearchParams(id.slice(queryIndex + 1).replace(/!+$/, ''))\n for (const entry of params.getAll('modules')) {\n try {\n const parsed = JSON.parse(entry) as { request?: unknown }\n if (typeof parsed.request === 'string' && parsed.request.length > 0) {\n return parsed.request\n }\n } catch {\n continue\n }\n }\n\n return undefined\n}\n\nexport function extractTransformFilePath(requestId: string): NormalizedTransformTarget {\n const normalizedRequestId = normalizeWebpackModuleRequest(requestId)\n const nextModuleRequest = extractNextModuleRequest(normalizedRequestId)\n if (nextModuleRequest) {\n return {\n requestId,\n filePath: nextModuleRequest,\n wrapped: true,\n }\n }\n\n const lastLoaderSeparator = normalizedRequestId.lastIndexOf('!')\n const resourceRequest =\n lastLoaderSeparator >= 0\n ? normalizedRequestId.slice(lastLoaderSeparator + 1)\n : normalizedRequestId\n const queryIndex = resourceRequest.indexOf('?')\n const filePath = queryIndex >= 0 ? resourceRequest.slice(0, queryIndex) : resourceRequest\n\n return {\n requestId,\n filePath,\n wrapped: filePath !== requestId,\n }\n}\n\n/**\n * Determine if a file should be transformed.\n * Always skips node_modules and dist directories.\n */\nexport function shouldTransform(filePath: string, options: Required<UnpluginOptions>): boolean {\n const resolvedFilePath = extractTransformFilePath(filePath).filePath\n\n // Never transform in production\n if (process.env['NODE_ENV'] === 'production') return false\n\n // Skip node_modules always\n if (resolvedFilePath.includes('node_modules')) return false\n\n // Skip virtual modules\n if (resolvedFilePath.startsWith('\\x00')) return false\n\n // Skip dist/build directories\n if (/[/\\\\](dist|build|\\.next|\\.nuxt)[/\\\\]/.test(resolvedFilePath)) return false\n\n // Skip non-code files (like .html, .css)\n const ext = resolvedFilePath.split('.').pop()?.toLowerCase()\n if (ext && !['js', 'jsx', 'ts', 'tsx', 'mjs', 'mts', 'vue'].includes(ext)) {\n return false\n }\n\n // Check user-defined exclude patterns\n // (picomatch integration — see index.ts for how options.exclude is applied)\n\n return true\n}\n\n/**\n * Build the escape tags set from user options merged with defaults.\n */\nexport function buildEscapeTagsSet(escapeTags?: string[]): Set<string> {\n const merged = new Set(DEFAULT_ESCAPE_TAGS)\n if (escapeTags) {\n for (const tag of escapeTags) {\n merged.add(tag)\n }\n }\n return merged\n}\n\n/**\n * Format a source location value for the data-inspecto attribute.\n * Format: \"filepath:line:column\"\n */\nexport function formatAttrValue(file: string, line: number, column: number): string {\n return `${file}:${line}:${column}`\n}\n","import * as vueCompiler from '@vue/compiler-dom'\nimport { parse as parseSFC } from '@vue/compiler-sfc'\nimport type { ElementNode, AttributeNode } from '@vue/compiler-core'\nimport { NodeTypes } from '@vue/compiler-core'\nimport MagicString from 'magic-string'\nimport path from 'node:path'\nimport type { PathType } from '@inspecto-dev/types'\nimport { buildEscapeTagsSet, formatAttrValue, type TransformResult } from './utils.js'\n\nexport interface TransformVueOptions {\n filePath: string\n source: string\n projectRoot: string\n escapeTags?: string[]\n pathType?: PathType\n attributeName?: string\n}\n\n/**\n * Transform Vue SFC source by injecting data-inspecto attributes\n * into template elements.\n *\n * Strategy:\n * 1. Locate the <template> block in the SFC source\n * 2. Parse only the template block with @vue/compiler-dom\n * 3. Walk ElementNode nodes in the AST\n * 4. For each eligible element, inject the attribute using MagicString\n * at the exact offset within the original source\n */\nexport function transformVue(options: TransformVueOptions): TransformResult {\n const {\n filePath,\n source,\n projectRoot,\n escapeTags,\n pathType = 'absolute',\n attributeName = 'data-inspecto',\n } = options\n\n const escapeTagsSet = buildEscapeTagsSet(escapeTags)\n\n // Resolve path\n const resolvedPath =\n pathType === 'absolute'\n ? path.resolve(filePath)\n : path.relative(projectRoot, path.resolve(filePath))\n\n const normalizedPath = resolvedPath.replace(/\\\\/g, '/')\n\n // ── Find <template> block boundaries ──────────────────────────────────────\n // Use @vue/compiler-sfc to parse the file and extract the template block.\n // This is much safer than regex for handling nested templates.\n const { descriptor, errors } = parseSFC(source, {\n filename: filePath,\n sourceMap: false,\n ignoreEmpty: true,\n })\n\n if (errors.length > 0 || !descriptor.template) {\n return { code: source, map: null, changed: false }\n }\n\n const templateContent = descriptor.template.content\n const templateBlockStart = descriptor.template.loc.start.offset\n\n // ── Parse template block ───────────────────────────────────────────────────\n let ast: vueCompiler.RootNode\n try {\n ast = vueCompiler.parse(templateContent, {\n parseMode: 'html',\n // Preserve source locations relative to templateContent\n onError: () => {\n /* ignore non-fatal parse errors */\n },\n })\n } catch {\n return { code: source, map: null, changed: false }\n }\n\n const ms = new MagicString(source)\n let changed = false\n\n // ── Walk AST ───────────────────────────────────────────────────────────────\n walkElement(ast, node => {\n // Skip non-element nodes\n if (node.type !== NodeTypes.ELEMENT) return\n\n const tagName = node.tag\n\n // Skip escaped tags\n if (escapeTagsSet.has(tagName)) return\n\n // Skip <template> wrapper itself (it's the root, not a real element)\n if (tagName === 'template' && node === ast.children[0]) return\n\n // Skip elements that already have the attribute (idempotency)\n const alreadyHasAttr = node.props.some(\n (p): p is AttributeNode => p.type === NodeTypes.ATTRIBUTE && p.name === attributeName,\n )\n if (alreadyHasAttr) return\n\n // node.loc is relative to templateContent — add templateBlockStart offset\n const loc = node.loc\n if (!loc) return\n\n const { line, column } = loc.start\n\n // Calculate absolute line and column in the original source\n // @vue/compiler-dom uses 1-based line and 1-based column\n const templateStartLoc = descriptor.template!.loc.start\n const absoluteLine = templateStartLoc.line + line - 1\n const absoluteColumn = line === 1 ? templateStartLoc.column + column - 1 : column\n\n const attrValue = formatAttrValue(normalizedPath, absoluteLine, absoluteColumn)\n\n // Find insert position: right after the tag name in the original source\n // node.loc.start.offset is 0-based offset within templateContent\n const tagNameEnd = loc.start.offset + tagName.length + 1 // +1 for '<'\n const absoluteOffset = templateBlockStart + tagNameEnd\n\n ms.appendLeft(absoluteOffset, ` ${attributeName}=\"${attrValue}\"`)\n changed = true\n })\n\n if (!changed) {\n return { code: source, map: null, changed: false }\n }\n\n return {\n code: ms.toString(),\n map: ms.generateMap({ hires: true, source: filePath }),\n changed: true,\n }\n}\n\n// ── AST walker ────────────────────────────────────────────────────────────────\n\ntype AnyNode = vueCompiler.RootNode | vueCompiler.TemplateChildNode\n\nfunction walkElement(node: AnyNode, visitor: (node: ElementNode) => void): void {\n if (node.type === NodeTypes.ELEMENT) {\n visitor(node)\n for (const child of node.children) {\n walkElement(child as AnyNode, visitor)\n }\n } else if ('children' in node && Array.isArray(node.children)) {\n for (const child of node.children) {\n walkElement(child as AnyNode, visitor)\n }\n }\n}\n"],"mappings":"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","names":["import_node_path","traverse_","path","MagicString","import_magic_string","import_node_path","path","parseSFC","MagicString","path"]}
|
|
@@ -35,12 +35,57 @@ var DEFAULT_ESCAPE_TAGS = /* @__PURE__ */ new Set([
|
|
|
35
35
|
"NuxtLink"
|
|
36
36
|
]);
|
|
37
37
|
var JSX_EXTENSIONS = /* @__PURE__ */ new Set([".jsx", ".tsx", ".js", ".ts", ".mjs", ".mts"]);
|
|
38
|
+
function normalizeWebpackModuleRequest(id) {
|
|
39
|
+
return id.replace(/!+$/, "").replace(/^\((?:app-pages-browser|rsc|ssr)\)\/\.\//, "");
|
|
40
|
+
}
|
|
41
|
+
function extractNextModuleRequest(id) {
|
|
42
|
+
if (!id.includes("next-flight-client-entry-loader.js?")) {
|
|
43
|
+
return void 0;
|
|
44
|
+
}
|
|
45
|
+
const queryIndex = id.indexOf("?");
|
|
46
|
+
if (queryIndex === -1) {
|
|
47
|
+
return void 0;
|
|
48
|
+
}
|
|
49
|
+
const params = new URLSearchParams(id.slice(queryIndex + 1).replace(/!+$/, ""));
|
|
50
|
+
for (const entry of params.getAll("modules")) {
|
|
51
|
+
try {
|
|
52
|
+
const parsed = JSON.parse(entry);
|
|
53
|
+
if (typeof parsed.request === "string" && parsed.request.length > 0) {
|
|
54
|
+
return parsed.request;
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return void 0;
|
|
61
|
+
}
|
|
62
|
+
function extractTransformFilePath(requestId) {
|
|
63
|
+
const normalizedRequestId = normalizeWebpackModuleRequest(requestId);
|
|
64
|
+
const nextModuleRequest = extractNextModuleRequest(normalizedRequestId);
|
|
65
|
+
if (nextModuleRequest) {
|
|
66
|
+
return {
|
|
67
|
+
requestId,
|
|
68
|
+
filePath: nextModuleRequest,
|
|
69
|
+
wrapped: true
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
const lastLoaderSeparator = normalizedRequestId.lastIndexOf("!");
|
|
73
|
+
const resourceRequest = lastLoaderSeparator >= 0 ? normalizedRequestId.slice(lastLoaderSeparator + 1) : normalizedRequestId;
|
|
74
|
+
const queryIndex = resourceRequest.indexOf("?");
|
|
75
|
+
const filePath = queryIndex >= 0 ? resourceRequest.slice(0, queryIndex) : resourceRequest;
|
|
76
|
+
return {
|
|
77
|
+
requestId,
|
|
78
|
+
filePath,
|
|
79
|
+
wrapped: filePath !== requestId
|
|
80
|
+
};
|
|
81
|
+
}
|
|
38
82
|
function shouldTransform(filePath, options) {
|
|
83
|
+
const resolvedFilePath = extractTransformFilePath(filePath).filePath;
|
|
39
84
|
if (process.env["NODE_ENV"] === "production") return false;
|
|
40
|
-
if (
|
|
41
|
-
if (
|
|
42
|
-
if (/[/\\](dist|build|\.next|\.nuxt)[/\\]/.test(
|
|
43
|
-
const ext =
|
|
85
|
+
if (resolvedFilePath.includes("node_modules")) return false;
|
|
86
|
+
if (resolvedFilePath.startsWith("\0")) return false;
|
|
87
|
+
if (/[/\\](dist|build|\.next|\.nuxt)[/\\]/.test(resolvedFilePath)) return false;
|
|
88
|
+
const ext = resolvedFilePath.split(".").pop()?.toLowerCase();
|
|
44
89
|
if (ext && !["js", "jsx", "ts", "tsx", "mjs", "mts", "vue"].includes(ext)) {
|
|
45
90
|
return false;
|
|
46
91
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/transform/index.ts","../../../src/transform/transform-jsx.ts","../../../src/transform/utils.ts","../../../src/transform/transform-vue.ts","../../../src/legacy/rspack/loader.ts"],"sourcesContent":["import path from 'node:path'\nimport type { UnpluginOptions } from '@inspecto-dev/types'\nimport { transformJsx } from './transform-jsx.js'\nimport { transformVue } from './transform-vue.js'\nimport { JSX_EXTENSIONS, type TransformResult } from './utils.js'\n\nexport interface RouterOptions {\n filePath: string\n source: string\n projectRoot: string\n pluginOptions: Required<UnpluginOptions>\n}\n\n/**\n * Route a file to the appropriate transform based on extension.\n * Returns null if no transform applies.\n */\nexport function transformRouter(options: RouterOptions): TransformResult | null {\n const { filePath, source, projectRoot, pluginOptions } = options\n const ext = path.extname(filePath).toLowerCase()\n\n if (JSX_EXTENSIONS.has(ext)) {\n return transformJsx({\n filePath,\n source,\n projectRoot,\n escapeTags: pluginOptions.escapeTags,\n pathType: pluginOptions.pathType,\n attributeName: pluginOptions.attributeName,\n })\n }\n\n // ── Vue SFC ──────────────────────────────────────────────────────────────\n if (ext === '.vue') {\n return transformVue({\n filePath,\n source,\n projectRoot,\n escapeTags: pluginOptions.escapeTags,\n pathType: pluginOptions.pathType,\n attributeName: pluginOptions.attributeName,\n })\n }\n\n return null\n}\n// Export transforms for testing\nexport { transformJsx, transformVue }\n","import * as parser from '@babel/parser'\nimport traverse_ from '@babel/traverse'\n// Support both ESM default and CommonJS module.exports\nconst traverse =\n typeof traverse_ === 'function' ? traverse_ : (traverse_ as any).default || traverse_\nimport type { NodePath } from '@babel/traverse'\nimport type { JSXOpeningElement } from '@babel/types'\nimport MagicString from 'magic-string'\nimport path from 'node:path'\nimport type { UnpluginOptions, PathType } from '@inspecto-dev/types'\nimport { buildEscapeTagsSet, formatAttrValue, type TransformResult } from './utils.js'\n\nexport interface TransformJsxOptions {\n filePath: string\n source: string\n projectRoot: string\n escapeTags?: string[]\n pathType?: PathType\n attributeName?: string\n}\n\n/**\n * Transform JSX/TSX source code by injecting data-inspecto attributes.\n */\nexport function transformJsx(options: TransformJsxOptions): TransformResult {\n const {\n filePath,\n source,\n projectRoot,\n escapeTags,\n pathType = 'absolute',\n attributeName = 'data-inspecto',\n } = options\n\n const escapeTagsSet = buildEscapeTagsSet(escapeTags)\n\n // Resolve the file path based on pathType config\n const resolvedPath =\n pathType === 'absolute'\n ? path.resolve(filePath)\n : path.relative(projectRoot, path.resolve(filePath))\n\n // Normalize path separators on Windows\n const normalizedPath = resolvedPath.replace(/\\\\/g, '/')\n\n let ast: ReturnType<typeof parser.parse>\n try {\n ast = parser.parse(source, {\n sourceType: 'module',\n plugins: [\n 'jsx',\n 'typescript',\n 'decorators-legacy',\n 'classProperties',\n 'optionalChaining',\n 'nullishCoalescingOperator',\n 'importMeta',\n ],\n errorRecovery: true,\n })\n } catch {\n // If parsing fails, return source unchanged\n return { code: source, map: null, changed: false }\n }\n\n const ms = new MagicString(source)\n let changed = false\n\n traverse(ast, {\n JSXOpeningElement(nodePath: NodePath<JSXOpeningElement>) {\n const node = nodePath.node\n\n // Skip elements that already have the attribute\n const alreadyHasAttr = node.attributes.some(\n attr =>\n attr.type === 'JSXAttribute' &&\n attr.name.type === 'JSXIdentifier' &&\n attr.name.name === attributeName,\n )\n if (alreadyHasAttr) return\n\n // Get element tag name\n const nameNode = node.name\n let tagName: string\n if (nameNode.type === 'JSXIdentifier') {\n tagName = nameNode.name\n } else if (nameNode.type === 'JSXMemberExpression') {\n const objName = nameNode.object.type === 'JSXIdentifier' ? nameNode.object.name : ''\n const propName = nameNode.property.type === 'JSXIdentifier' ? nameNode.property.name : ''\n tagName = objName && propName ? `${objName}.${propName}` : objName\n } else {\n tagName = ''\n }\n\n // Skip escaped tags\n if (escapeTagsSet.has(tagName)) return\n\n // Get position from AST location\n const loc = node.loc\n if (!loc) return\n\n const { line, column } = loc.start\n // Babel uses 0-based columns, convert to 1-based\n const attrValue = formatAttrValue(normalizedPath, line, column + 1)\n\n // Determine the best insertion position for the attribute\n // When a JSX element has type arguments (e.g. <Component<string> />),\n // inserting after `node.name.end` might inject inside the generic bracket `<`.\n // The safest place to insert is right before the first attribute,\n // or right before the closing slash/bracket if there are no attributes.\n let insertPos: number | null | undefined = null\n if (node.attributes && node.attributes.length > 0) {\n const firstAttr = node.attributes[0]\n if (firstAttr && firstAttr.start != null) {\n insertPos = firstAttr.start\n }\n }\n\n if (insertPos == null) {\n // Find the start of the closing bracket or self-closing slash\n // We know node.end is the index right after the '>'\n // So we look backwards. But Babel AST doesn't give us exact token positions\n // for the closing tag easily.\n // For a safe fallback, we use node.typeParameters?.end || node.name.end\n if (node.typeParameters && node.typeParameters.end != null) {\n insertPos = node.typeParameters.end\n } else if (node.name.end != null) {\n insertPos = node.name.end\n }\n }\n\n if (insertPos == null) return\n\n ms.appendLeft(\n insertPos,\n ` ${attributeName}=\"${attrValue}\"${node.attributes && node.attributes.length > 0 ? '' : ' '}`,\n )\n changed = true\n },\n })\n\n if (!changed) {\n return { code: source, map: null, changed: false }\n }\n\n return {\n code: ms.toString(),\n map: ms.generateMap({ hires: true, source: filePath }),\n changed: true,\n }\n}\n","import type { UnpluginOptions } from '@inspecto-dev/types'\nimport type MagicString from 'magic-string'\n\nexport interface TransformResult {\n code: string\n map: ReturnType<MagicString['generateMap']> | null\n changed: boolean\n}\n\n/** Default tags whose JSX elements should NOT receive data-inspecto attributes */\nexport const DEFAULT_ESCAPE_TAGS = new Set([\n 'template',\n 'script',\n 'style',\n // React special elements\n 'Fragment',\n 'React.Fragment',\n 'StrictMode',\n 'React.StrictMode',\n 'Suspense',\n 'React.Suspense',\n 'Profiler',\n 'React.Profiler',\n // React transitions\n 'Transition',\n 'TransitionGroup',\n // Vue built-in components\n 'KeepAlive',\n 'Teleport',\n 'Suspense',\n // Vue router built-ins\n 'RouterView',\n 'RouterLink',\n 'NuxtPage',\n 'NuxtLink',\n])\n\n/** File extensions that contain JSX/TSX syntax */\nexport const JSX_EXTENSIONS = new Set(['.jsx', '.tsx', '.js', '.ts', '.mjs', '.mts'])\n\n/**\n * Determine if a file should be transformed.\n * Always skips node_modules and dist directories.\n */\nexport function shouldTransform(filePath: string, options: Required<UnpluginOptions>): boolean {\n // Never transform in production\n if (process.env['NODE_ENV'] === 'production') return false\n\n // Skip node_modules always\n if (filePath.includes('node_modules')) return false\n\n // Skip virtual modules\n if (filePath.startsWith('\\x00')) return false\n\n // Skip dist/build directories\n if (/[/\\\\](dist|build|\\.next|\\.nuxt)[/\\\\]/.test(filePath)) return false\n\n // Skip non-code files (like .html, .css)\n const ext = filePath.split('.').pop()?.toLowerCase()\n if (ext && !['js', 'jsx', 'ts', 'tsx', 'mjs', 'mts', 'vue'].includes(ext)) {\n return false\n }\n\n // Check user-defined exclude patterns\n // (picomatch integration — see index.ts for how options.exclude is applied)\n\n return true\n}\n\n/**\n * Build the escape tags set from user options merged with defaults.\n */\nexport function buildEscapeTagsSet(escapeTags?: string[]): Set<string> {\n const merged = new Set(DEFAULT_ESCAPE_TAGS)\n if (escapeTags) {\n for (const tag of escapeTags) {\n merged.add(tag)\n }\n }\n return merged\n}\n\n/**\n * Format a source location value for the data-inspecto attribute.\n * Format: \"filepath:line:column\"\n */\nexport function formatAttrValue(file: string, line: number, column: number): string {\n return `${file}:${line}:${column}`\n}\n","import * as vueCompiler from '@vue/compiler-dom'\nimport { parse as parseSFC } from '@vue/compiler-sfc'\nimport type { ElementNode, AttributeNode } from '@vue/compiler-core'\nimport { NodeTypes } from '@vue/compiler-core'\nimport MagicString from 'magic-string'\nimport path from 'node:path'\nimport type { PathType } from '@inspecto-dev/types'\nimport { buildEscapeTagsSet, formatAttrValue, type TransformResult } from './utils.js'\n\nexport interface TransformVueOptions {\n filePath: string\n source: string\n projectRoot: string\n escapeTags?: string[]\n pathType?: PathType\n attributeName?: string\n}\n\n/**\n * Transform Vue SFC source by injecting data-inspecto attributes\n * into template elements.\n *\n * Strategy:\n * 1. Locate the <template> block in the SFC source\n * 2. Parse only the template block with @vue/compiler-dom\n * 3. Walk ElementNode nodes in the AST\n * 4. For each eligible element, inject the attribute using MagicString\n * at the exact offset within the original source\n */\nexport function transformVue(options: TransformVueOptions): TransformResult {\n const {\n filePath,\n source,\n projectRoot,\n escapeTags,\n pathType = 'absolute',\n attributeName = 'data-inspecto',\n } = options\n\n const escapeTagsSet = buildEscapeTagsSet(escapeTags)\n\n // Resolve path\n const resolvedPath =\n pathType === 'absolute'\n ? path.resolve(filePath)\n : path.relative(projectRoot, path.resolve(filePath))\n\n const normalizedPath = resolvedPath.replace(/\\\\/g, '/')\n\n // ── Find <template> block boundaries ──────────────────────────────────────\n // Use @vue/compiler-sfc to parse the file and extract the template block.\n // This is much safer than regex for handling nested templates.\n const { descriptor, errors } = parseSFC(source, {\n filename: filePath,\n sourceMap: false,\n ignoreEmpty: true,\n })\n\n if (errors.length > 0 || !descriptor.template) {\n return { code: source, map: null, changed: false }\n }\n\n const templateContent = descriptor.template.content\n const templateBlockStart = descriptor.template.loc.start.offset\n\n // ── Parse template block ───────────────────────────────────────────────────\n let ast: vueCompiler.RootNode\n try {\n ast = vueCompiler.parse(templateContent, {\n parseMode: 'html',\n // Preserve source locations relative to templateContent\n onError: () => {\n /* ignore non-fatal parse errors */\n },\n })\n } catch {\n return { code: source, map: null, changed: false }\n }\n\n const ms = new MagicString(source)\n let changed = false\n\n // ── Walk AST ───────────────────────────────────────────────────────────────\n walkElement(ast, node => {\n // Skip non-element nodes\n if (node.type !== NodeTypes.ELEMENT) return\n\n const tagName = node.tag\n\n // Skip escaped tags\n if (escapeTagsSet.has(tagName)) return\n\n // Skip <template> wrapper itself (it's the root, not a real element)\n if (tagName === 'template' && node === ast.children[0]) return\n\n // Skip elements that already have the attribute (idempotency)\n const alreadyHasAttr = node.props.some(\n (p): p is AttributeNode => p.type === NodeTypes.ATTRIBUTE && p.name === attributeName,\n )\n if (alreadyHasAttr) return\n\n // node.loc is relative to templateContent — add templateBlockStart offset\n const loc = node.loc\n if (!loc) return\n\n const { line, column } = loc.start\n\n // Calculate absolute line and column in the original source\n // @vue/compiler-dom uses 1-based line and 1-based column\n const templateStartLoc = descriptor.template!.loc.start\n const absoluteLine = templateStartLoc.line + line - 1\n const absoluteColumn = line === 1 ? templateStartLoc.column + column - 1 : column\n\n const attrValue = formatAttrValue(normalizedPath, absoluteLine, absoluteColumn)\n\n // Find insert position: right after the tag name in the original source\n // node.loc.start.offset is 0-based offset within templateContent\n const tagNameEnd = loc.start.offset + tagName.length + 1 // +1 for '<'\n const absoluteOffset = templateBlockStart + tagNameEnd\n\n ms.appendLeft(absoluteOffset, ` ${attributeName}=\"${attrValue}\"`)\n changed = true\n })\n\n if (!changed) {\n return { code: source, map: null, changed: false }\n }\n\n return {\n code: ms.toString(),\n map: ms.generateMap({ hires: true, source: filePath }),\n changed: true,\n }\n}\n\n// ── AST walker ────────────────────────────────────────────────────────────────\n\ntype AnyNode = vueCompiler.RootNode | vueCompiler.TemplateChildNode\n\nfunction walkElement(node: AnyNode, visitor: (node: ElementNode) => void): void {\n if (node.type === NodeTypes.ELEMENT) {\n visitor(node)\n for (const child of node.children) {\n walkElement(child as AnyNode, visitor)\n }\n } else if ('children' in node && Array.isArray(node.children)) {\n for (const child of node.children) {\n walkElement(child as AnyNode, visitor)\n }\n }\n}\n","import { transformRouter } from '../../transform/index.js'\nimport { shouldTransform } from '../../transform/utils.js'\nimport type { UnpluginOptions } from '@inspecto-dev/types'\n\nexport default function legacyRspackLoader(this: any, source: string) {\n const id = this.resourcePath\n const options = this.getOptions() as Required<UnpluginOptions>\n\n if (!shouldTransform(id, options)) {\n return source\n }\n\n const result = transformRouter({\n filePath: id,\n source,\n projectRoot: process.cwd(),\n pluginOptions: options,\n })\n\n if (result && result.changed) {\n this.callback(null, result.code, result.map)\n return\n }\n\n return source\n}\n"],"mappings":"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","names":["path","MagicString","path","path","MagicString","path"]}
|
|
1
|
+
{"version":3,"sources":["../../../src/transform/index.ts","../../../src/transform/transform-jsx.ts","../../../src/transform/utils.ts","../../../src/transform/transform-vue.ts","../../../src/legacy/rspack/loader.ts"],"sourcesContent":["import path from 'node:path'\nimport type { UnpluginOptions } from '@inspecto-dev/types'\nimport { transformJsx } from './transform-jsx.js'\nimport { transformVue } from './transform-vue.js'\nimport { JSX_EXTENSIONS, type TransformResult } from './utils.js'\n\nexport interface RouterOptions {\n filePath: string\n source: string\n projectRoot: string\n pluginOptions: Required<UnpluginOptions>\n}\n\n/**\n * Route a file to the appropriate transform based on extension.\n * Returns null if no transform applies.\n */\nexport function transformRouter(options: RouterOptions): TransformResult | null {\n const { filePath, source, projectRoot, pluginOptions } = options\n const ext = path.extname(filePath).toLowerCase()\n\n if (JSX_EXTENSIONS.has(ext)) {\n return transformJsx({\n filePath,\n source,\n projectRoot,\n escapeTags: pluginOptions.escapeTags,\n pathType: pluginOptions.pathType,\n attributeName: pluginOptions.attributeName,\n })\n }\n\n // ── Vue SFC ──────────────────────────────────────────────────────────────\n if (ext === '.vue') {\n return transformVue({\n filePath,\n source,\n projectRoot,\n escapeTags: pluginOptions.escapeTags,\n pathType: pluginOptions.pathType,\n attributeName: pluginOptions.attributeName,\n })\n }\n\n return null\n}\n// Export transforms for testing\nexport { transformJsx, transformVue }\n","import * as parser from '@babel/parser'\nimport traverse_ from '@babel/traverse'\n// Support both ESM default and CommonJS module.exports\nconst traverse =\n typeof traverse_ === 'function' ? traverse_ : (traverse_ as any).default || traverse_\nimport type { NodePath } from '@babel/traverse'\nimport type { JSXOpeningElement } from '@babel/types'\nimport MagicString from 'magic-string'\nimport path from 'node:path'\nimport type { UnpluginOptions, PathType } from '@inspecto-dev/types'\nimport { buildEscapeTagsSet, formatAttrValue, type TransformResult } from './utils.js'\n\nexport interface TransformJsxOptions {\n filePath: string\n source: string\n projectRoot: string\n escapeTags?: string[]\n pathType?: PathType\n attributeName?: string\n}\n\n/**\n * Transform JSX/TSX source code by injecting data-inspecto attributes.\n */\nexport function transformJsx(options: TransformJsxOptions): TransformResult {\n const {\n filePath,\n source,\n projectRoot,\n escapeTags,\n pathType = 'absolute',\n attributeName = 'data-inspecto',\n } = options\n\n const escapeTagsSet = buildEscapeTagsSet(escapeTags)\n\n // Resolve the file path based on pathType config\n const resolvedPath =\n pathType === 'absolute'\n ? path.resolve(filePath)\n : path.relative(projectRoot, path.resolve(filePath))\n\n // Normalize path separators on Windows\n const normalizedPath = resolvedPath.replace(/\\\\/g, '/')\n\n let ast: ReturnType<typeof parser.parse>\n try {\n ast = parser.parse(source, {\n sourceType: 'module',\n plugins: [\n 'jsx',\n 'typescript',\n 'decorators-legacy',\n 'classProperties',\n 'optionalChaining',\n 'nullishCoalescingOperator',\n 'importMeta',\n ],\n errorRecovery: true,\n })\n } catch {\n // If parsing fails, return source unchanged\n return { code: source, map: null, changed: false }\n }\n\n const ms = new MagicString(source)\n let changed = false\n\n traverse(ast, {\n JSXOpeningElement(nodePath: NodePath<JSXOpeningElement>) {\n const node = nodePath.node\n\n // Skip elements that already have the attribute\n const alreadyHasAttr = node.attributes.some(\n attr =>\n attr.type === 'JSXAttribute' &&\n attr.name.type === 'JSXIdentifier' &&\n attr.name.name === attributeName,\n )\n if (alreadyHasAttr) return\n\n // Get element tag name\n const nameNode = node.name\n let tagName: string\n if (nameNode.type === 'JSXIdentifier') {\n tagName = nameNode.name\n } else if (nameNode.type === 'JSXMemberExpression') {\n const objName = nameNode.object.type === 'JSXIdentifier' ? nameNode.object.name : ''\n const propName = nameNode.property.type === 'JSXIdentifier' ? nameNode.property.name : ''\n tagName = objName && propName ? `${objName}.${propName}` : objName\n } else {\n tagName = ''\n }\n\n // Skip escaped tags\n if (escapeTagsSet.has(tagName)) return\n\n // Get position from AST location\n const loc = node.loc\n if (!loc) return\n\n const { line, column } = loc.start\n // Babel uses 0-based columns, convert to 1-based\n const attrValue = formatAttrValue(normalizedPath, line, column + 1)\n\n // Determine the best insertion position for the attribute\n // When a JSX element has type arguments (e.g. <Component<string> />),\n // inserting after `node.name.end` might inject inside the generic bracket `<`.\n // The safest place to insert is right before the first attribute,\n // or right before the closing slash/bracket if there are no attributes.\n let insertPos: number | null | undefined = null\n if (node.attributes && node.attributes.length > 0) {\n const firstAttr = node.attributes[0]\n if (firstAttr && firstAttr.start != null) {\n insertPos = firstAttr.start\n }\n }\n\n if (insertPos == null) {\n // Find the start of the closing bracket or self-closing slash\n // We know node.end is the index right after the '>'\n // So we look backwards. But Babel AST doesn't give us exact token positions\n // for the closing tag easily.\n // For a safe fallback, we use node.typeParameters?.end || node.name.end\n if (node.typeParameters && node.typeParameters.end != null) {\n insertPos = node.typeParameters.end\n } else if (node.name.end != null) {\n insertPos = node.name.end\n }\n }\n\n if (insertPos == null) return\n\n ms.appendLeft(\n insertPos,\n ` ${attributeName}=\"${attrValue}\"${node.attributes && node.attributes.length > 0 ? '' : ' '}`,\n )\n changed = true\n },\n })\n\n if (!changed) {\n return { code: source, map: null, changed: false }\n }\n\n return {\n code: ms.toString(),\n map: ms.generateMap({ hires: true, source: filePath }),\n changed: true,\n }\n}\n","import type { UnpluginOptions } from '@inspecto-dev/types'\nimport type MagicString from 'magic-string'\n\nexport interface TransformResult {\n code: string\n map: ReturnType<MagicString['generateMap']> | null\n changed: boolean\n}\n\nexport interface NormalizedTransformTarget {\n requestId: string\n filePath: string\n wrapped: boolean\n}\n\n/** Default tags whose JSX elements should NOT receive data-inspecto attributes */\nexport const DEFAULT_ESCAPE_TAGS = new Set([\n 'template',\n 'script',\n 'style',\n // React special elements\n 'Fragment',\n 'React.Fragment',\n 'StrictMode',\n 'React.StrictMode',\n 'Suspense',\n 'React.Suspense',\n 'Profiler',\n 'React.Profiler',\n // React transitions\n 'Transition',\n 'TransitionGroup',\n // Vue built-in components\n 'KeepAlive',\n 'Teleport',\n 'Suspense',\n // Vue router built-ins\n 'RouterView',\n 'RouterLink',\n 'NuxtPage',\n 'NuxtLink',\n])\n\n/** File extensions that contain JSX/TSX syntax */\nexport const JSX_EXTENSIONS = new Set(['.jsx', '.tsx', '.js', '.ts', '.mjs', '.mts'])\n\nfunction normalizeWebpackModuleRequest(id: string): string {\n return id.replace(/!+$/, '').replace(/^\\((?:app-pages-browser|rsc|ssr)\\)\\/\\.\\//, '')\n}\n\nfunction extractNextModuleRequest(id: string): string | undefined {\n if (!id.includes('next-flight-client-entry-loader.js?')) {\n return undefined\n }\n\n const queryIndex = id.indexOf('?')\n if (queryIndex === -1) {\n return undefined\n }\n\n const params = new URLSearchParams(id.slice(queryIndex + 1).replace(/!+$/, ''))\n for (const entry of params.getAll('modules')) {\n try {\n const parsed = JSON.parse(entry) as { request?: unknown }\n if (typeof parsed.request === 'string' && parsed.request.length > 0) {\n return parsed.request\n }\n } catch {\n continue\n }\n }\n\n return undefined\n}\n\nexport function extractTransformFilePath(requestId: string): NormalizedTransformTarget {\n const normalizedRequestId = normalizeWebpackModuleRequest(requestId)\n const nextModuleRequest = extractNextModuleRequest(normalizedRequestId)\n if (nextModuleRequest) {\n return {\n requestId,\n filePath: nextModuleRequest,\n wrapped: true,\n }\n }\n\n const lastLoaderSeparator = normalizedRequestId.lastIndexOf('!')\n const resourceRequest =\n lastLoaderSeparator >= 0\n ? normalizedRequestId.slice(lastLoaderSeparator + 1)\n : normalizedRequestId\n const queryIndex = resourceRequest.indexOf('?')\n const filePath = queryIndex >= 0 ? resourceRequest.slice(0, queryIndex) : resourceRequest\n\n return {\n requestId,\n filePath,\n wrapped: filePath !== requestId,\n }\n}\n\n/**\n * Determine if a file should be transformed.\n * Always skips node_modules and dist directories.\n */\nexport function shouldTransform(filePath: string, options: Required<UnpluginOptions>): boolean {\n const resolvedFilePath = extractTransformFilePath(filePath).filePath\n\n // Never transform in production\n if (process.env['NODE_ENV'] === 'production') return false\n\n // Skip node_modules always\n if (resolvedFilePath.includes('node_modules')) return false\n\n // Skip virtual modules\n if (resolvedFilePath.startsWith('\\x00')) return false\n\n // Skip dist/build directories\n if (/[/\\\\](dist|build|\\.next|\\.nuxt)[/\\\\]/.test(resolvedFilePath)) return false\n\n // Skip non-code files (like .html, .css)\n const ext = resolvedFilePath.split('.').pop()?.toLowerCase()\n if (ext && !['js', 'jsx', 'ts', 'tsx', 'mjs', 'mts', 'vue'].includes(ext)) {\n return false\n }\n\n // Check user-defined exclude patterns\n // (picomatch integration — see index.ts for how options.exclude is applied)\n\n return true\n}\n\n/**\n * Build the escape tags set from user options merged with defaults.\n */\nexport function buildEscapeTagsSet(escapeTags?: string[]): Set<string> {\n const merged = new Set(DEFAULT_ESCAPE_TAGS)\n if (escapeTags) {\n for (const tag of escapeTags) {\n merged.add(tag)\n }\n }\n return merged\n}\n\n/**\n * Format a source location value for the data-inspecto attribute.\n * Format: \"filepath:line:column\"\n */\nexport function formatAttrValue(file: string, line: number, column: number): string {\n return `${file}:${line}:${column}`\n}\n","import * as vueCompiler from '@vue/compiler-dom'\nimport { parse as parseSFC } from '@vue/compiler-sfc'\nimport type { ElementNode, AttributeNode } from '@vue/compiler-core'\nimport { NodeTypes } from '@vue/compiler-core'\nimport MagicString from 'magic-string'\nimport path from 'node:path'\nimport type { PathType } from '@inspecto-dev/types'\nimport { buildEscapeTagsSet, formatAttrValue, type TransformResult } from './utils.js'\n\nexport interface TransformVueOptions {\n filePath: string\n source: string\n projectRoot: string\n escapeTags?: string[]\n pathType?: PathType\n attributeName?: string\n}\n\n/**\n * Transform Vue SFC source by injecting data-inspecto attributes\n * into template elements.\n *\n * Strategy:\n * 1. Locate the <template> block in the SFC source\n * 2. Parse only the template block with @vue/compiler-dom\n * 3. Walk ElementNode nodes in the AST\n * 4. For each eligible element, inject the attribute using MagicString\n * at the exact offset within the original source\n */\nexport function transformVue(options: TransformVueOptions): TransformResult {\n const {\n filePath,\n source,\n projectRoot,\n escapeTags,\n pathType = 'absolute',\n attributeName = 'data-inspecto',\n } = options\n\n const escapeTagsSet = buildEscapeTagsSet(escapeTags)\n\n // Resolve path\n const resolvedPath =\n pathType === 'absolute'\n ? path.resolve(filePath)\n : path.relative(projectRoot, path.resolve(filePath))\n\n const normalizedPath = resolvedPath.replace(/\\\\/g, '/')\n\n // ── Find <template> block boundaries ──────────────────────────────────────\n // Use @vue/compiler-sfc to parse the file and extract the template block.\n // This is much safer than regex for handling nested templates.\n const { descriptor, errors } = parseSFC(source, {\n filename: filePath,\n sourceMap: false,\n ignoreEmpty: true,\n })\n\n if (errors.length > 0 || !descriptor.template) {\n return { code: source, map: null, changed: false }\n }\n\n const templateContent = descriptor.template.content\n const templateBlockStart = descriptor.template.loc.start.offset\n\n // ── Parse template block ───────────────────────────────────────────────────\n let ast: vueCompiler.RootNode\n try {\n ast = vueCompiler.parse(templateContent, {\n parseMode: 'html',\n // Preserve source locations relative to templateContent\n onError: () => {\n /* ignore non-fatal parse errors */\n },\n })\n } catch {\n return { code: source, map: null, changed: false }\n }\n\n const ms = new MagicString(source)\n let changed = false\n\n // ── Walk AST ───────────────────────────────────────────────────────────────\n walkElement(ast, node => {\n // Skip non-element nodes\n if (node.type !== NodeTypes.ELEMENT) return\n\n const tagName = node.tag\n\n // Skip escaped tags\n if (escapeTagsSet.has(tagName)) return\n\n // Skip <template> wrapper itself (it's the root, not a real element)\n if (tagName === 'template' && node === ast.children[0]) return\n\n // Skip elements that already have the attribute (idempotency)\n const alreadyHasAttr = node.props.some(\n (p): p is AttributeNode => p.type === NodeTypes.ATTRIBUTE && p.name === attributeName,\n )\n if (alreadyHasAttr) return\n\n // node.loc is relative to templateContent — add templateBlockStart offset\n const loc = node.loc\n if (!loc) return\n\n const { line, column } = loc.start\n\n // Calculate absolute line and column in the original source\n // @vue/compiler-dom uses 1-based line and 1-based column\n const templateStartLoc = descriptor.template!.loc.start\n const absoluteLine = templateStartLoc.line + line - 1\n const absoluteColumn = line === 1 ? templateStartLoc.column + column - 1 : column\n\n const attrValue = formatAttrValue(normalizedPath, absoluteLine, absoluteColumn)\n\n // Find insert position: right after the tag name in the original source\n // node.loc.start.offset is 0-based offset within templateContent\n const tagNameEnd = loc.start.offset + tagName.length + 1 // +1 for '<'\n const absoluteOffset = templateBlockStart + tagNameEnd\n\n ms.appendLeft(absoluteOffset, ` ${attributeName}=\"${attrValue}\"`)\n changed = true\n })\n\n if (!changed) {\n return { code: source, map: null, changed: false }\n }\n\n return {\n code: ms.toString(),\n map: ms.generateMap({ hires: true, source: filePath }),\n changed: true,\n }\n}\n\n// ── AST walker ────────────────────────────────────────────────────────────────\n\ntype AnyNode = vueCompiler.RootNode | vueCompiler.TemplateChildNode\n\nfunction walkElement(node: AnyNode, visitor: (node: ElementNode) => void): void {\n if (node.type === NodeTypes.ELEMENT) {\n visitor(node)\n for (const child of node.children) {\n walkElement(child as AnyNode, visitor)\n }\n } else if ('children' in node && Array.isArray(node.children)) {\n for (const child of node.children) {\n walkElement(child as AnyNode, visitor)\n }\n }\n}\n","import { transformRouter } from '../../transform/index.js'\nimport { shouldTransform } from '../../transform/utils.js'\nimport type { UnpluginOptions } from '@inspecto-dev/types'\n\nexport default function legacyRspackLoader(this: any, source: string) {\n const id = this.resourcePath\n const options = this.getOptions() as Required<UnpluginOptions>\n\n if (!shouldTransform(id, options)) {\n return source\n }\n\n const result = transformRouter({\n filePath: id,\n source,\n projectRoot: process.cwd(),\n pluginOptions: options,\n })\n\n if (result && result.changed) {\n this.callback(null, result.code, result.map)\n return\n }\n\n return source\n}\n"],"mappings":"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","names":["path","MagicString","path","path","MagicString","path"]}
|
|
@@ -72,7 +72,7 @@ var resolveClientModule = () => {
|
|
|
72
72
|
|
|
73
73
|
// src/server/index.ts
|
|
74
74
|
var import_node_http = __toESM(require("http"), 1);
|
|
75
|
-
var
|
|
75
|
+
var import_node_fs4 = __toESM(require("fs"), 1);
|
|
76
76
|
var import_node_path4 = __toESM(require("path"), 1);
|
|
77
77
|
var import_node_os2 = __toESM(require("os"), 1);
|
|
78
78
|
var import_node_crypto2 = __toESM(require("crypto"), 1);
|
|
@@ -592,6 +592,7 @@ function hasOverrides(overrides) {
|
|
|
592
592
|
|
|
593
593
|
// src/server/path-guards.ts
|
|
594
594
|
var import_node_path2 = __toESM(require("path"), 1);
|
|
595
|
+
var import_node_fs2 = __toESM(require("fs"), 1);
|
|
595
596
|
function isWindowsAbsolutePath(file) {
|
|
596
597
|
return /^[a-zA-Z]:[\\/]/.test(file) || /^\\\\[^\\]+\\[^\\]+/.test(file);
|
|
597
598
|
}
|
|
@@ -602,9 +603,94 @@ function resolveWorkspacePath(file, cwd) {
|
|
|
602
603
|
return import_node_path2.default.isAbsolute(file) ? import_node_path2.default.resolve(file) : import_node_path2.default.resolve(cwd, file);
|
|
603
604
|
}
|
|
604
605
|
function assertPathWithinProject(file, projectRoot) {
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
606
|
+
let realFile = file;
|
|
607
|
+
let realProjectRoot = projectRoot;
|
|
608
|
+
try {
|
|
609
|
+
if (import_node_fs2.default.existsSync(file)) {
|
|
610
|
+
realFile = import_node_fs2.default.realpathSync(file);
|
|
611
|
+
}
|
|
612
|
+
} catch {
|
|
613
|
+
}
|
|
614
|
+
try {
|
|
615
|
+
if (import_node_fs2.default.existsSync(projectRoot)) {
|
|
616
|
+
realProjectRoot = import_node_fs2.default.realpathSync(projectRoot);
|
|
617
|
+
}
|
|
618
|
+
} catch {
|
|
619
|
+
}
|
|
620
|
+
if (isWithinPath(file, projectRoot) || isWithinPath(realFile, realProjectRoot)) {
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
throw new Error(
|
|
624
|
+
`Access denied: File ${normalizeForComparison(realFile)} is outside of project workspace ${normalizeForComparison(realProjectRoot)}`
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
function tryReadPackageName(packageRoot) {
|
|
628
|
+
try {
|
|
629
|
+
const packageJsonPath = import_node_path2.default.join(packageRoot, "package.json");
|
|
630
|
+
if (!import_node_fs2.default.existsSync(packageJsonPath)) return void 0;
|
|
631
|
+
const packageJson = JSON.parse(import_node_fs2.default.readFileSync(packageJsonPath, "utf8"));
|
|
632
|
+
return typeof packageJson.name === "string" ? packageJson.name : void 0;
|
|
633
|
+
} catch {
|
|
634
|
+
return void 0;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
function findNearestPackageRoot(file) {
|
|
638
|
+
let current = import_node_path2.default.dirname(file);
|
|
639
|
+
while (true) {
|
|
640
|
+
if (import_node_fs2.default.existsSync(import_node_path2.default.join(current, "package.json"))) {
|
|
641
|
+
return current;
|
|
642
|
+
}
|
|
643
|
+
const parent = import_node_path2.default.dirname(current);
|
|
644
|
+
if (parent === current) {
|
|
645
|
+
return void 0;
|
|
646
|
+
}
|
|
647
|
+
current = parent;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
function normalizeForComparison(file) {
|
|
651
|
+
return isWindowsAbsolutePath(file) ? import_node_path2.default.win32.normalize(file) : import_node_path2.default.normalize(file);
|
|
652
|
+
}
|
|
653
|
+
function pathSeparatorFor(file) {
|
|
654
|
+
return isWindowsAbsolutePath(file) ? import_node_path2.default.win32.sep : import_node_path2.default.sep;
|
|
655
|
+
}
|
|
656
|
+
function isWithinPath(file, root) {
|
|
657
|
+
const normalizedFile = normalizeForComparison(file);
|
|
658
|
+
const normalizedRoot = normalizeForComparison(root);
|
|
659
|
+
const separator = pathSeparatorFor(normalizedRoot);
|
|
660
|
+
const rootWithSep = normalizedRoot.endsWith(separator) ? normalizedRoot : normalizedRoot + separator;
|
|
661
|
+
return normalizedFile === normalizedRoot || normalizedFile.startsWith(rootWithSep);
|
|
662
|
+
}
|
|
663
|
+
function resolveLinkedDependencyEntry(projectRoot, packageName) {
|
|
664
|
+
const packageSegments = packageName.split("/");
|
|
665
|
+
const dependencyPath = import_node_path2.default.join(projectRoot, "node_modules", ...packageSegments);
|
|
666
|
+
if (!import_node_fs2.default.existsSync(dependencyPath)) return void 0;
|
|
667
|
+
try {
|
|
668
|
+
return import_node_fs2.default.realpathSync(dependencyPath);
|
|
669
|
+
} catch {
|
|
670
|
+
return dependencyPath;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
function isLinkedDependencyPath(file, projectRoot, packageName) {
|
|
674
|
+
const linkedDependencyRoot = resolveLinkedDependencyEntry(projectRoot, packageName);
|
|
675
|
+
if (!linkedDependencyRoot) return false;
|
|
676
|
+
return isWithinPath(file, linkedDependencyRoot);
|
|
677
|
+
}
|
|
678
|
+
function isLinkedDependencySourcePath(file, projectRoot) {
|
|
679
|
+
const packageRoot = findNearestPackageRoot(file);
|
|
680
|
+
if (!packageRoot) return false;
|
|
681
|
+
const packageName = tryReadPackageName(packageRoot);
|
|
682
|
+
if (!packageName) return false;
|
|
683
|
+
return isLinkedDependencyPath(file, projectRoot, packageName);
|
|
684
|
+
}
|
|
685
|
+
function assertPathWithinIdeOpenScope(file, projectRoot) {
|
|
686
|
+
try {
|
|
687
|
+
assertPathWithinProject(file, projectRoot);
|
|
688
|
+
return;
|
|
689
|
+
} catch {
|
|
690
|
+
if (isLinkedDependencySourcePath(file, projectRoot)) {
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
throw new Error(`Access denied: File is outside of project workspace`);
|
|
608
694
|
}
|
|
609
695
|
}
|
|
610
696
|
|
|
@@ -809,7 +895,7 @@ var VSCODE_FAMILY_SCHEMES = [
|
|
|
809
895
|
];
|
|
810
896
|
function handleOpenFileRequest(body, serverState2) {
|
|
811
897
|
const absolutePath = resolveWorkspacePath(body.file, serverState2.cwd);
|
|
812
|
-
|
|
898
|
+
assertPathWithinIdeOpenScope(absolutePath, serverState2.projectRoot);
|
|
813
899
|
const userConfig = loadUserConfigSync(false, serverState2.cwd, serverState2.configRoot);
|
|
814
900
|
const configuredIde = userConfig.ide;
|
|
815
901
|
const activeIde = serverState2.ideInfo?.ide;
|
|
@@ -867,7 +953,7 @@ function handleOpenFileRequest(body, serverState2) {
|
|
|
867
953
|
}
|
|
868
954
|
|
|
869
955
|
// src/server/project-root.ts
|
|
870
|
-
var
|
|
956
|
+
var import_node_fs3 = __toESM(require("fs"), 1);
|
|
871
957
|
var import_node_path3 = __toESM(require("path"), 1);
|
|
872
958
|
var import_node_child_process3 = require("child_process");
|
|
873
959
|
var serverLogger3 = createLogger("inspecto:server", { logLevel: getGlobalLogLevel() });
|
|
@@ -885,7 +971,7 @@ function resolveProjectRoot() {
|
|
|
885
971
|
let current = start;
|
|
886
972
|
while (!visited.has(current)) {
|
|
887
973
|
visited.add(current);
|
|
888
|
-
if (
|
|
974
|
+
if (import_node_fs3.default.existsSync(import_node_path3.default.join(current, ".inspecto"))) return current;
|
|
889
975
|
if (current === stop) break;
|
|
890
976
|
const parent = import_node_path3.default.dirname(current);
|
|
891
977
|
if (parent === current) break;
|
|
@@ -957,28 +1043,28 @@ async function startServer() {
|
|
|
957
1043
|
const portFile = import_node_path4.default.join(import_node_os2.default.tmpdir(), "inspecto.port.json");
|
|
958
1044
|
try {
|
|
959
1045
|
let portData = {};
|
|
960
|
-
if (
|
|
1046
|
+
if (import_node_fs4.default.existsSync(portFile)) {
|
|
961
1047
|
try {
|
|
962
|
-
portData = JSON.parse(
|
|
1048
|
+
portData = JSON.parse(import_node_fs4.default.readFileSync(portFile, "utf-8"));
|
|
963
1049
|
} catch (e) {
|
|
964
1050
|
}
|
|
965
1051
|
}
|
|
966
1052
|
const rootHash = import_node_crypto2.default.createHash("md5").update(serverState.projectRoot).digest("hex");
|
|
967
1053
|
portData[rootHash] = port;
|
|
968
|
-
|
|
1054
|
+
import_node_fs4.default.writeFileSync(portFile, JSON.stringify(portData, null, 2), "utf-8");
|
|
969
1055
|
} catch (e) {
|
|
970
1056
|
serverLogger4.warn("Failed to write port file:", e);
|
|
971
1057
|
}
|
|
972
1058
|
process.once("exit", () => {
|
|
973
1059
|
try {
|
|
974
|
-
if (
|
|
975
|
-
const portData = JSON.parse(
|
|
1060
|
+
if (import_node_fs4.default.existsSync(portFile)) {
|
|
1061
|
+
const portData = JSON.parse(import_node_fs4.default.readFileSync(portFile, "utf-8"));
|
|
976
1062
|
const rootHash = import_node_crypto2.default.createHash("md5").update(serverState.projectRoot).digest("hex");
|
|
977
1063
|
delete portData[rootHash];
|
|
978
1064
|
if (Object.keys(portData).length === 0) {
|
|
979
|
-
|
|
1065
|
+
import_node_fs4.default.unlinkSync(portFile);
|
|
980
1066
|
} else {
|
|
981
|
-
|
|
1067
|
+
import_node_fs4.default.writeFileSync(portFile, JSON.stringify(portData, null, 2), "utf-8");
|
|
982
1068
|
}
|
|
983
1069
|
}
|
|
984
1070
|
} catch {
|
|
@@ -1047,8 +1133,10 @@ async function handleRequest(url, req, res) {
|
|
|
1047
1133
|
}
|
|
1048
1134
|
try {
|
|
1049
1135
|
handleOpenFileRequest(body, serverState);
|
|
1050
|
-
} catch {
|
|
1051
|
-
serverLogger4.warn(
|
|
1136
|
+
} catch (err) {
|
|
1137
|
+
serverLogger4.warn(
|
|
1138
|
+
`Security: Blocked path traversal attempt in IDE_OPEN: ${body.file}. Reason: ${err.message}`
|
|
1139
|
+
);
|
|
1052
1140
|
res.writeHead(403, { "Content-Type": "application/json" });
|
|
1053
1141
|
res.end(JSON.stringify({ error: "Access denied: File is outside of project workspace" }));
|
|
1054
1142
|
return;
|
|
@@ -1066,8 +1154,10 @@ async function handleRequest(url, req, res) {
|
|
|
1066
1154
|
const absolutePath = resolveWorkspacePath(file, serverState.cwd);
|
|
1067
1155
|
try {
|
|
1068
1156
|
assertPathWithinProject(absolutePath, serverState.projectRoot);
|
|
1069
|
-
} catch {
|
|
1070
|
-
serverLogger4.warn(
|
|
1157
|
+
} catch (err) {
|
|
1158
|
+
serverLogger4.warn(
|
|
1159
|
+
`Security: Blocked path traversal attempt in PROJECT_SNIPPET: ${file}. Reason: ${err.message}`
|
|
1160
|
+
);
|
|
1071
1161
|
res.writeHead(403, { "Content-Type": "application/json" });
|
|
1072
1162
|
res.end(
|
|
1073
1163
|
JSON.stringify({
|
|
@@ -1163,10 +1253,20 @@ function getBatchDispatchStatusCode(errorCode, success) {
|
|
|
1163
1253
|
var import_node_path5 = __toESM(require("path"), 1);
|
|
1164
1254
|
var InspectoWebpack4Plugin = class {
|
|
1165
1255
|
constructor(options = {}) {
|
|
1256
|
+
this.serverPort = null;
|
|
1166
1257
|
this.options = options;
|
|
1167
1258
|
}
|
|
1259
|
+
async ensureServer() {
|
|
1260
|
+
if (this.serverPort === null) {
|
|
1261
|
+
this.serverPort = await startServer();
|
|
1262
|
+
}
|
|
1263
|
+
return this.serverPort;
|
|
1264
|
+
}
|
|
1168
1265
|
apply(compiler) {
|
|
1169
1266
|
const clientPath = resolveClientModule();
|
|
1267
|
+
compiler.hooks.beforeCompile.tapPromise("InspectoWebpack4Plugin", async () => {
|
|
1268
|
+
await this.ensureServer();
|
|
1269
|
+
});
|
|
1170
1270
|
compiler.hooks.afterEnvironment.tap("InspectoWebpack4Plugin", () => {
|
|
1171
1271
|
const inspectoLoader = import_node_path5.default.resolve(__dirname, "loader.cjs");
|
|
1172
1272
|
compiler.options.module.rules.push({
|
|
@@ -1193,7 +1293,7 @@ var InspectoWebpack4Plugin = class {
|
|
|
1193
1293
|
HtmlWebpackPlugin.getHooks(compilation).alterAssetTagGroups.tapAsync(
|
|
1194
1294
|
"InspectoWebpack4Plugin",
|
|
1195
1295
|
async (data, cb) => {
|
|
1196
|
-
const port = await
|
|
1296
|
+
const port = await this.ensureServer();
|
|
1197
1297
|
data.headTags.unshift({
|
|
1198
1298
|
tagName: "script",
|
|
1199
1299
|
voidTag: false,
|
|
@@ -1207,7 +1307,7 @@ var InspectoWebpack4Plugin = class {
|
|
|
1207
1307
|
compilation.hooks.htmlWebpackPluginAlterAssetTags.tapAsync(
|
|
1208
1308
|
"InspectoWebpack4Plugin",
|
|
1209
1309
|
async (data, cb) => {
|
|
1210
|
-
const port = await
|
|
1310
|
+
const port = await this.ensureServer();
|
|
1211
1311
|
data.head.unshift({
|
|
1212
1312
|
tagName: "script",
|
|
1213
1313
|
voidTag: false,
|