@harness-fe/webpack 3.0.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 MorphixAI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,51 @@
1
+ # @harness-fe/webpack
2
+
3
+ > Webpack plugin for [Harness-FE](https://github.com/Morphicai/harness-fe) — the frontend harness for AI agents.
4
+
5
+ Source-aware transform + runtime injection + MCP bridge for Webpack projects. Tags every JSX element with `data-morphix-loc` / `data-morphix-comp` so AI agents can map any UI element back to a file:line:column.
6
+
7
+ > **Status:** stable for React + Vue 2/3 on Webpack 5. **thread-loader compatible** as of this release.
8
+
9
+ > **Note:** This package is now a hand-written webpack plugin, not a wrapper around `unplugin.webpack`. The change is invisible to users — same import, same options — but unblocks projects that put `thread-loader` anywhere in their loader chain (typical Vue 2 + TypeScript SFC builds). See [`.changeset/webpack-native-plugin.md`](../../.changeset/webpack-native-plugin.md) for the why.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ pnpm add -D @harness-fe/webpack @harness-fe/runtime
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ```js
20
+ // webpack.config.js
21
+ const { harnessFE } = require('@harness-fe/webpack');
22
+
23
+ module.exports = {
24
+ plugins: [harnessFE()],
25
+ };
26
+ ```
27
+
28
+ ESM:
29
+
30
+ ```ts
31
+ import { harnessFE } from '@harness-fe/webpack';
32
+
33
+ export default {
34
+ plugins: [harnessFE()],
35
+ };
36
+ ```
37
+
38
+ The plugin auto-disables in production builds — zero overhead in your shipped bundle.
39
+
40
+ ## Options
41
+
42
+ Same as [`@harness-fe/vite`](https://www.npmjs.com/package/@harness-fe/vite). All bundler-specific plugins share the same option surface via the underlying `unplugin`.
43
+
44
+ ## Docs
45
+
46
+ - [Root README](https://github.com/Morphicai/harness-fe#readme)
47
+ - [Architecture](https://github.com/Morphicai/harness-fe/blob/main/ARCHITECTURE.md)
48
+
49
+ ## License
50
+
51
+ MIT
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @harness-fe/webpack — native webpack plugin for Harness-FE.
3
+ *
4
+ * Usage:
5
+ * const { harnessFE } = require('@harness-fe/webpack')
6
+ * // or
7
+ * import { harnessFE } from '@harness-fe/webpack'
8
+ *
9
+ * module.exports = { plugins: [harnessFE()] }
10
+ *
11
+ * Unlike previous versions (which were thin re-exports of
12
+ * @harness-fe/unplugin/webpack), this package implements a native webpack
13
+ * plugin that is compatible with thread-loader. See plugin.ts for the why.
14
+ */
15
+ export { harnessFE, HarnessFEWebpackPlugin } from './plugin.js';
16
+ export type { HarnessFEOptions } from '@harness-fe/unplugin';
17
+ export { transformJsx, type ComponentMap, type ComponentLocation, type TransformResult, } from '@harness-fe/unplugin';
18
+ import { harnessFE } from './plugin.js';
19
+ export default harnessFE;
package/dist/index.js ADDED
@@ -0,0 +1,20 @@
1
+ /**
2
+ * @harness-fe/webpack — native webpack plugin for Harness-FE.
3
+ *
4
+ * Usage:
5
+ * const { harnessFE } = require('@harness-fe/webpack')
6
+ * // or
7
+ * import { harnessFE } from '@harness-fe/webpack'
8
+ *
9
+ * module.exports = { plugins: [harnessFE()] }
10
+ *
11
+ * Unlike previous versions (which were thin re-exports of
12
+ * @harness-fe/unplugin/webpack), this package implements a native webpack
13
+ * plugin that is compatible with thread-loader. See plugin.ts for the why.
14
+ */
15
+ export { harnessFE, HarnessFEWebpackPlugin } from './plugin.js';
16
+ // Re-export transform utilities for direct usage (preserves the previous
17
+ // public surface of @harness-fe/webpack@2.x).
18
+ export { transformJsx, } from '@harness-fe/unplugin';
19
+ import { harnessFE } from './plugin.js';
20
+ export default harnessFE;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Webpack loader entrypoint for @harness-fe/webpack.
3
+ *
4
+ * IMPORTANT: This file is loaded by webpack's loader runner (potentially
5
+ * inside a thread-loader worker process). The options object passed to this
6
+ * loader MUST be pure JSON-serializable data — no plugin instance, no
7
+ * compiler reference, no closures. The reason this package exists at all is
8
+ * that unplugin's webpack adapter passes the plugin instance in options,
9
+ * which closes over `compiler.root` and breaks thread-loader's JSON.stringify.
10
+ *
11
+ * The loader collects component locations into a per-call temporary
12
+ * componentMap, then writes them to `module.buildMeta.harnessCollected`.
13
+ * The main-process plugin reads buildMeta via `compilation.succeedModule`
14
+ * and merges the entries into the real shared componentMap.
15
+ */
16
+ export interface HarnessLoaderOptions {
17
+ pluginId: string;
18
+ projectRoot: string;
19
+ vueOptions: {
20
+ safeMode: boolean;
21
+ dryRun: boolean;
22
+ };
23
+ disabled: boolean;
24
+ }
25
+ interface LoaderContext {
26
+ async: () => (err: Error | null, content?: string, map?: object) => void;
27
+ getOptions: () => HarnessLoaderOptions;
28
+ resourcePath: string;
29
+ resourceQuery: string;
30
+ _module?: {
31
+ buildMeta?: Record<string, unknown>;
32
+ };
33
+ }
34
+ export default function harnessLoader(this: LoaderContext, source: string): void;
35
+ export declare const raw = false;
36
+ export {};
package/dist/loader.js ADDED
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Webpack loader entrypoint for @harness-fe/webpack.
3
+ *
4
+ * IMPORTANT: This file is loaded by webpack's loader runner (potentially
5
+ * inside a thread-loader worker process). The options object passed to this
6
+ * loader MUST be pure JSON-serializable data — no plugin instance, no
7
+ * compiler reference, no closures. The reason this package exists at all is
8
+ * that unplugin's webpack adapter passes the plugin instance in options,
9
+ * which closes over `compiler.root` and breaks thread-loader's JSON.stringify.
10
+ *
11
+ * The loader collects component locations into a per-call temporary
12
+ * componentMap, then writes them to `module.buildMeta.harnessCollected`.
13
+ * The main-process plugin reads buildMeta via `compilation.succeedModule`
14
+ * and merges the entries into the real shared componentMap.
15
+ */
16
+ import { runTransform } from './transform-runner.js';
17
+ export default function harnessLoader(source) {
18
+ const callback = this.async();
19
+ const opts = this.getOptions();
20
+ if (opts.disabled) {
21
+ callback(null, source);
22
+ return;
23
+ }
24
+ // Fresh map per call — caller-side accumulation lives in main-process
25
+ // shared-state, fed via module.buildMeta.
26
+ const localMap = new Map();
27
+ let out;
28
+ try {
29
+ out = runTransform(source, this.resourcePath, this.resourceQuery, opts.projectRoot, { safeMode: opts.vueOptions.safeMode, dryRun: opts.vueOptions.dryRun }, localMap);
30
+ }
31
+ catch (err) {
32
+ // Never break the host build because of a transform bug.
33
+ callback(null, source);
34
+ return;
35
+ }
36
+ // Forward collected locations to main process via buildMeta.
37
+ if (localMap.size > 0 && this._module) {
38
+ const collected = [];
39
+ for (const [name, locs] of localMap.entries()) {
40
+ for (const location of locs) {
41
+ collected.push({ name, location });
42
+ }
43
+ }
44
+ const buildMeta = (this._module.buildMeta ??= {});
45
+ const existing = buildMeta.harnessCollected ?? [];
46
+ buildMeta.harnessCollected = existing.concat(collected);
47
+ }
48
+ if (!out) {
49
+ callback(null, source);
50
+ return;
51
+ }
52
+ callback(null, out.code, out.map);
53
+ }
54
+ export const raw = false;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * @harness-fe/webpack — native webpack plugin.
3
+ *
4
+ * Why native (vs the unplugin adapter):
5
+ * unplugin's webpack adapter passes the plugin instance through a loader's
6
+ * `options` field. The plugin instance closes over `compiler` (via the
7
+ * `webpack(compiler)` hook), and `compiler.root` self-references the
8
+ * compiler — JSON.stringify chokes on the cycle. thread-loader serializes
9
+ * downstream loader options when dispatching to its worker pool, so any
10
+ * project that puts thread-loader anywhere ahead of harness in the
11
+ * resolved loader chain (e.g. a `.ts` rule that vue-loader inlines for
12
+ * `<script lang="ts">` SFC blocks) breaks the entire build.
13
+ *
14
+ * This native plugin:
15
+ * - Registers a separate `enforce: 'pre'` rule pointing at an independent
16
+ * loader file (loader.ts) whose options are pure JSON-serializable data.
17
+ * - Keeps all stateful work (WebSocket to MCP, log capture, runtime
18
+ * injection, error forwarding, HTML injection) in the main process via
19
+ * compiler hooks.
20
+ * - Aggregates componentMap entries from worker processes via
21
+ * `module.buildMeta.harnessCollected`.
22
+ */
23
+ import { type HarnessFEOptions } from '@harness-fe/unplugin';
24
+ export declare class HarnessFEWebpackPlugin {
25
+ private readonly pluginId;
26
+ private readonly options;
27
+ private projectRoot;
28
+ private projectId;
29
+ private readonly mcpUrl;
30
+ private readonly token;
31
+ private mcpClient?;
32
+ private logCleanup?;
33
+ private identity;
34
+ constructor(options?: HarnessFEOptions);
35
+ apply(compiler: any): void;
36
+ private installRuntimeEntry;
37
+ private installMcpHooks;
38
+ private installComponentMapAggregator;
39
+ private installHtmlInjection;
40
+ private installErrorForwarding;
41
+ private injectConfigScript;
42
+ }
43
+ /** Factory matching the previous unplugin-based call shape. */
44
+ export declare function harnessFE(options?: HarnessFEOptions): HarnessFEWebpackPlugin;
package/dist/plugin.js ADDED
@@ -0,0 +1,233 @@
1
+ /**
2
+ * @harness-fe/webpack — native webpack plugin.
3
+ *
4
+ * Why native (vs the unplugin adapter):
5
+ * unplugin's webpack adapter passes the plugin instance through a loader's
6
+ * `options` field. The plugin instance closes over `compiler` (via the
7
+ * `webpack(compiler)` hook), and `compiler.root` self-references the
8
+ * compiler — JSON.stringify chokes on the cycle. thread-loader serializes
9
+ * downstream loader options when dispatching to its worker pool, so any
10
+ * project that puts thread-loader anywhere ahead of harness in the
11
+ * resolved loader chain (e.g. a `.ts` rule that vue-loader inlines for
12
+ * `<script lang="ts">` SFC blocks) breaks the entire build.
13
+ *
14
+ * This native plugin:
15
+ * - Registers a separate `enforce: 'pre'` rule pointing at an independent
16
+ * loader file (loader.ts) whose options are pure JSON-serializable data.
17
+ * - Keeps all stateful work (WebSocket to MCP, log capture, runtime
18
+ * injection, error forwarding, HTML injection) in the main process via
19
+ * compiler hooks.
20
+ * - Aggregates componentMap entries from worker processes via
21
+ * `module.buildMeta.harnessCollected`.
22
+ */
23
+ import { createRequire } from 'node:module';
24
+ import { fileURLToPath } from 'node:url';
25
+ import { dirname, resolve as resolvePath } from 'node:path';
26
+ import { createMcpClient, createBuildIdentity, installNodeLogCapture, appendTokenQuery, } from '@harness-fe/unplugin';
27
+ import { DEFAULT_WS_PORT } from '@harness-fe/protocol';
28
+ import { getOrCreateComponentMap } from './shared-state.js';
29
+ const require = createRequire(import.meta.url);
30
+ const __filename = fileURLToPath(import.meta.url);
31
+ const __dirname = dirname(__filename);
32
+ function newPluginId() {
33
+ const g = globalThis;
34
+ if (g.crypto?.randomUUID)
35
+ return g.crypto.randomUUID();
36
+ return `harness-${Date.now()}-${Math.random().toString(36).slice(2)}`;
37
+ }
38
+ export class HarnessFEWebpackPlugin {
39
+ pluginId = newPluginId();
40
+ options;
41
+ projectRoot = process.cwd();
42
+ projectId;
43
+ mcpUrl;
44
+ token;
45
+ mcpClient;
46
+ logCleanup;
47
+ identity = createBuildIdentity({
48
+ userBuildId: undefined,
49
+ userDisplayName: undefined,
50
+ });
51
+ constructor(options = {}) {
52
+ this.options = options;
53
+ this.projectId = options.projectId ?? 'unknown-project';
54
+ const baseUrl = options.mcpUrl ??
55
+ process.env.HARNESS_FE_URL ??
56
+ `ws://127.0.0.1:${DEFAULT_WS_PORT}`;
57
+ this.token = options.token ?? process.env.HARNESS_FE_TOKEN;
58
+ this.mcpUrl = appendTokenQuery(baseUrl, this.token);
59
+ this.identity = createBuildIdentity({
60
+ userBuildId: options.buildId,
61
+ userDisplayName: options.displayName,
62
+ });
63
+ }
64
+ apply(compiler) {
65
+ if (this.options.disabled)
66
+ return;
67
+ this.projectRoot = compiler.options?.context ?? process.cwd();
68
+ // Resolve loader path. require.resolve gives an absolute path that
69
+ // webpack can hand to loader-runner regardless of host project's
70
+ // module resolution config.
71
+ const loaderPath = resolvePath(__dirname, 'loader.js');
72
+ const loaderOptions = {
73
+ pluginId: this.pluginId,
74
+ projectRoot: this.projectRoot,
75
+ vueOptions: {
76
+ safeMode: this.options.safeMode !== false,
77
+ dryRun: process.env.HARNESS_FE_DRY_RUN === '1',
78
+ },
79
+ disabled: false,
80
+ };
81
+ // Register the transform loader as a pre-rule.
82
+ compiler.options.module ??= { rules: [] };
83
+ compiler.options.module.rules ??= [];
84
+ compiler.options.module.rules.unshift({
85
+ test: /\.([jt]sx|vue)($|\?)/,
86
+ exclude: /[\\/]node_modules[\\/]/,
87
+ enforce: 'pre',
88
+ use: [
89
+ {
90
+ loader: loaderPath,
91
+ options: loaderOptions,
92
+ ident: `harness-fe-${this.pluginId}`,
93
+ },
94
+ ],
95
+ });
96
+ // Skip the rest in production builds — runtime injection and MCP
97
+ // connection are dev-only conveniences.
98
+ if (compiler.options?.mode === 'production')
99
+ return;
100
+ this.installRuntimeEntry(compiler);
101
+ this.installMcpHooks(compiler);
102
+ this.installComponentMapAggregator(compiler);
103
+ this.installHtmlInjection(compiler);
104
+ this.installErrorForwarding(compiler);
105
+ }
106
+ installRuntimeEntry(compiler) {
107
+ try {
108
+ const webpackPkg = require('webpack');
109
+ const { EntryPlugin } = webpackPkg;
110
+ const runtimeEntry = require.resolve('@harness-fe/runtime');
111
+ new EntryPlugin(compiler.context ?? this.projectRoot, runtimeEntry, {
112
+ name: undefined,
113
+ }).apply(compiler);
114
+ }
115
+ catch (err) {
116
+ console.warn('[harness-fe] failed to register runtime entry via webpack.EntryPlugin:', err);
117
+ }
118
+ }
119
+ installMcpHooks(compiler) {
120
+ const ctx = {
121
+ get projectId() {
122
+ return self.projectId;
123
+ },
124
+ get mcpUrl() {
125
+ return self.mcpUrl;
126
+ },
127
+ get token() {
128
+ return self.token;
129
+ },
130
+ peerRole: 'webpack-plugin',
131
+ get parentProjectId() {
132
+ return self.options.parentProjectId;
133
+ },
134
+ get projectRoot() {
135
+ return self.projectRoot;
136
+ },
137
+ get componentMap() {
138
+ return getOrCreateComponentMap(self.pluginId);
139
+ },
140
+ getBuildId: () => this.identity.getBuildId(this.projectRoot),
141
+ getDisplayName: () => this.identity.getDisplayName(this.projectRoot),
142
+ };
143
+ const self = this;
144
+ compiler.hooks.afterEnvironment.tap('harness-fe', () => {
145
+ const client = createMcpClient(ctx);
146
+ this.mcpClient = client;
147
+ client.connect();
148
+ this.logCleanup = installNodeLogCapture((name, payload) => client.emitEvent(name, payload));
149
+ });
150
+ compiler.hooks.shutdown?.tap('harness-fe', () => {
151
+ this.logCleanup?.();
152
+ this.logCleanup = undefined;
153
+ this.mcpClient?.disconnect();
154
+ this.mcpClient = undefined;
155
+ });
156
+ }
157
+ installComponentMapAggregator(compiler) {
158
+ const pluginId = this.pluginId;
159
+ compiler.hooks.compilation.tap('harness-fe', (compilation) => {
160
+ compilation.hooks.succeedModule.tap('harness-fe', (module) => {
161
+ const collected = module.buildMeta?.harnessCollected;
162
+ if (!collected?.length)
163
+ return;
164
+ const map = getOrCreateComponentMap(pluginId);
165
+ for (const { name, location } of collected) {
166
+ const existing = map.get(name) ?? [];
167
+ existing.push(location);
168
+ map.set(name, existing);
169
+ }
170
+ });
171
+ });
172
+ }
173
+ installHtmlInjection(compiler) {
174
+ compiler.hooks.compilation.tap('harness-fe', (compilation) => {
175
+ // Prefer html-webpack-plugin hooks when available.
176
+ try {
177
+ const HtmlPlugin = require('html-webpack-plugin');
178
+ const hooks = HtmlPlugin.getHooks(compilation);
179
+ hooks.beforeEmit.tapAsync('harness-fe', (data, cb) => {
180
+ data.html = this.injectConfigScript(data.html);
181
+ cb(null, data);
182
+ });
183
+ }
184
+ catch {
185
+ // Fallback: rewrite html assets directly.
186
+ const { Compilation, sources } = require('webpack');
187
+ compilation.hooks.processAssets.tap({
188
+ name: 'harness-fe',
189
+ stage: Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_INLINE,
190
+ }, (assets) => {
191
+ for (const [name, source] of Object.entries(assets)) {
192
+ if (!name.endsWith('.html'))
193
+ continue;
194
+ const html = source.source();
195
+ if (typeof html !== 'string')
196
+ continue;
197
+ compilation.updateAsset(name, new sources.RawSource(this.injectConfigScript(html)));
198
+ }
199
+ });
200
+ }
201
+ });
202
+ }
203
+ installErrorForwarding(compiler) {
204
+ compiler.hooks.done.tap('harness-fe', (stats) => {
205
+ if (!this.mcpClient || !stats.hasErrors())
206
+ return;
207
+ const errors = stats.compilation?.errors ?? [];
208
+ for (const err of errors) {
209
+ this.mcpClient.emitEvent('error', {
210
+ message: err.message ?? String(err),
211
+ file: err.module?.resource ?? undefined,
212
+ });
213
+ }
214
+ });
215
+ }
216
+ injectConfigScript(html) {
217
+ const injection = `<!-- @harness-fe injected (dev only) -->
218
+ <script>
219
+ window.__HARNESS_FE__ = ${JSON.stringify({
220
+ projectId: this.projectId,
221
+ mcpUrl: this.mcpUrl,
222
+ buildId: this.identity.getBuildId(this.projectRoot),
223
+ parentProjectId: this.options.parentProjectId,
224
+ displayName: this.identity.getDisplayName(this.projectRoot),
225
+ })};
226
+ </script>`;
227
+ return html.replace(/<\/head>/i, `${injection}\n</head>`);
228
+ }
229
+ }
230
+ /** Factory matching the previous unplugin-based call shape. */
231
+ export function harnessFE(options = {}) {
232
+ return new HarnessFEWebpackPlugin(options);
233
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Module-level componentMap registry keyed by plugin instance id.
3
+ *
4
+ * Used by the main webpack process to accumulate component locations forwarded
5
+ * from worker processes via `module.buildMeta.harnessCollected`. The MCP
6
+ * client (also main-process only) reads from this map to answer
7
+ * `project.where_is` / `project.module_graph` queries.
8
+ *
9
+ * thread-loader workers fork separate Node processes, so they get their own
10
+ * empty `maps` instance — but the loader in workers doesn't persist anything
11
+ * here. It writes collected locations to `module.buildMeta` and the main
12
+ * process aggregates them via the `succeedModule` hook.
13
+ */
14
+ import type { ComponentMap } from '@harness-fe/unplugin';
15
+ export declare function getOrCreateComponentMap(pluginId: string): ComponentMap;
16
+ export declare function clearComponentMap(pluginId: string): void;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Module-level componentMap registry keyed by plugin instance id.
3
+ *
4
+ * Used by the main webpack process to accumulate component locations forwarded
5
+ * from worker processes via `module.buildMeta.harnessCollected`. The MCP
6
+ * client (also main-process only) reads from this map to answer
7
+ * `project.where_is` / `project.module_graph` queries.
8
+ *
9
+ * thread-loader workers fork separate Node processes, so they get their own
10
+ * empty `maps` instance — but the loader in workers doesn't persist anything
11
+ * here. It writes collected locations to `module.buildMeta` and the main
12
+ * process aggregates them via the `succeedModule` hook.
13
+ */
14
+ const maps = new Map();
15
+ export function getOrCreateComponentMap(pluginId) {
16
+ let m = maps.get(pluginId);
17
+ if (!m) {
18
+ m = new Map();
19
+ maps.set(pluginId, m);
20
+ }
21
+ return m;
22
+ }
23
+ export function clearComponentMap(pluginId) {
24
+ maps.delete(pluginId);
25
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Pure transform dispatcher — picks the right transform function for a
3
+ * given file id (.vue / .vue?type=template / .vue?other-sub-module / .tsx /
4
+ * .jsx) and returns the transformed source + map.
5
+ *
6
+ * The componentMap is supplied per-call so the loader can collect new
7
+ * locations in a temporary map and forward them via module.buildMeta to
8
+ * the main process (where thread-loader is not in the picture).
9
+ */
10
+ import { type ComponentMap, type VueTransformOptions } from '@harness-fe/unplugin';
11
+ export interface RunTransformResult {
12
+ code: string;
13
+ map?: object;
14
+ }
15
+ export declare function runTransform(source: string, resourcePath: string, resourceQuery: string, projectRoot: string, vueOptions: VueTransformOptions, componentMap: ComponentMap): RunTransformResult | null;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Pure transform dispatcher — picks the right transform function for a
3
+ * given file id (.vue / .vue?type=template / .vue?other-sub-module / .tsx /
4
+ * .jsx) and returns the transformed source + map.
5
+ *
6
+ * The componentMap is supplied per-call so the loader can collect new
7
+ * locations in a temporary map and forward them via module.buildMeta to
8
+ * the main process (where thread-loader is not in the picture).
9
+ */
10
+ import { readFileSync } from 'node:fs';
11
+ import { relative } from 'node:path';
12
+ import { transformJsx, transformVueSFC, transformVueTemplate, resolveVueComponentName, getTemplateLineOffset, } from '@harness-fe/unplugin';
13
+ export function runTransform(source, resourcePath, resourceQuery, projectRoot, vueOptions, componentMap) {
14
+ const rel = relative(projectRoot, resourcePath);
15
+ // Vue template virtual sub-module.
16
+ if (resourcePath.endsWith('.vue') &&
17
+ /[?&]vue\b/.test(resourceQuery) &&
18
+ /[?&]type=template\b/.test(resourceQuery)) {
19
+ let componentName;
20
+ let lineOffset = 0;
21
+ try {
22
+ const sfcSource = readFileSync(resourcePath, 'utf-8');
23
+ componentName = resolveVueComponentName(sfcSource, rel);
24
+ lineOffset = getTemplateLineOffset(sfcSource, rel);
25
+ }
26
+ catch {
27
+ /* fall through with no offset / no name */
28
+ }
29
+ const out = transformVueTemplate(source, rel, componentName, componentMap, lineOffset, vueOptions);
30
+ if (!out)
31
+ return null;
32
+ return { code: out.code, map: out.map };
33
+ }
34
+ // Plain .vue request: full SFC transform.
35
+ if (resourcePath.endsWith('.vue') && !resourceQuery) {
36
+ const out = transformVueSFC(source, rel, componentMap, vueOptions);
37
+ if (!out)
38
+ return null;
39
+ return { code: out.code, map: out.map };
40
+ }
41
+ // Every other .vue sub-module (script / style) is a no-op — the
42
+ // information was already collected from the SFC and template variants.
43
+ if (resourcePath.endsWith('.vue'))
44
+ return null;
45
+ // .jsx / .tsx
46
+ const out = transformJsx(source, rel, componentMap);
47
+ if (!out)
48
+ return null;
49
+ return { code: out.code, map: out.map };
50
+ }
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@harness-fe/webpack",
3
+ "version": "3.0.0",
4
+ "description": "Native webpack plugin for Harness-FE: source-aware transforms, runtime injection, and MCP bridge. thread-loader compatible.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/Morphicai/harness-fe.git",
10
+ "directory": "packages/webpack-plugin"
11
+ },
12
+ "homepage": "https://github.com/Morphicai/harness-fe#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/Morphicai/harness-fe/issues"
15
+ },
16
+ "main": "dist/index.js",
17
+ "types": "dist/index.d.ts",
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/index.d.ts",
21
+ "default": "./dist/index.js"
22
+ },
23
+ "./loader": {
24
+ "types": "./dist/loader.d.ts",
25
+ "default": "./dist/loader.js"
26
+ }
27
+ },
28
+ "files": [
29
+ "dist",
30
+ "src",
31
+ "README.md",
32
+ "LICENSE"
33
+ ],
34
+ "dependencies": {
35
+ "@harness-fe/unplugin": "3.0.0",
36
+ "@harness-fe/protocol": "3.0.0"
37
+ },
38
+ "peerDependencies": {
39
+ "webpack": "^5.0.0"
40
+ },
41
+ "devDependencies": {
42
+ "typescript": "^5.6.0",
43
+ "vitest": "^2.1.9",
44
+ "webpack": "^5.107.0"
45
+ },
46
+ "publishConfig": {
47
+ "access": "public"
48
+ },
49
+ "scripts": {
50
+ "build": "tsc",
51
+ "typecheck": "tsc --noEmit",
52
+ "test": "vitest run --passWithNoTests"
53
+ }
54
+ }