@yahoo/uds-create-loader 1.13.0 → 1.14.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/src/loader.ts ADDED
@@ -0,0 +1,209 @@
1
+ import { parse } from '@babel/parser';
2
+ import _traverse from '@babel/traverse';
3
+ import type { LoaderDefinitionFunction } from 'webpack';
4
+ import {
5
+ getLoaderProjection,
6
+ type LoaderOptions,
7
+ loadUdsConfig,
8
+ } from './loader/artifacts';
9
+
10
+ // Handle CJS/ESM interop for the babel CJS-shipped modules.
11
+ // biome-ignore lint/suspicious/noExplicitAny: untyped babel CJS/ESM interop
12
+ const traverse = (_traverse as any).default ?? _traverse;
13
+
14
+ /**
15
+ * The UDS loader's only build-time job: rewrite alias-prefixed local
16
+ * imports of registered UDS components from `@/uds/components/<seg>`
17
+ * (the authoring file) to `'#uds/components/<seg>'` (the codegen-
18
+ * emitted per-source-file module at `.uds/components/<seg>.tsx`).
19
+ *
20
+ * The authoring file is read only by codegen — production renders the
21
+ * downleveled FC. Per-file targets keep Next.js Fast Refresh surgical:
22
+ * editing one authoring source only invalidates that one downleveled
23
+ * module.
24
+ *
25
+ * No JSX-site work: composite / variant / bool / style-prop resolution
26
+ * and motion wiring are owned by the codegen-emitted FC. The loader
27
+ * only changes import sources.
28
+ */
29
+ const transformLoader: LoaderDefinitionFunction = function (source) {
30
+ const loaderContext =
31
+ (this as
32
+ | {
33
+ getOptions?: () => unknown;
34
+ addDependency?: (dep: string) => void;
35
+ resourcePath?: string;
36
+ }
37
+ | undefined) ?? undefined;
38
+ if (!loaderContext?.getOptions) return source;
39
+
40
+ const options = loaderContext.getOptions() as LoaderOptions;
41
+ const { dataPath, isDev, skipWatchDependencies } = options;
42
+ const resourcePath = loaderContext.resourcePath ?? '';
43
+
44
+ // Skip .uds/ generated files — they are codegen output and must
45
+ // not be re-transformed.
46
+ if (resourcePath.includes('.uds/')) {
47
+ return source;
48
+ }
49
+
50
+ // Tell webpack to watch generated uds artifacts for changes.
51
+ // Skipped when the host app has its own reload mechanism (Studio
52
+ // WebSocket) to avoid full-page reloads on artifact regeneration.
53
+ if (!skipWatchDependencies) {
54
+ loaderContext.addDependency?.(dataPath);
55
+ }
56
+
57
+ // Config is required — without it we don't know which JSX tags
58
+ // belong to UDS. Returning early keeps non-UDS files untouched.
59
+ const config = loadUdsConfig(dataPath, isDev);
60
+ if (!config) return source;
61
+ const projection = getLoaderProjection(config);
62
+
63
+ // Cheap pre-filter: only parse files that look like they import or
64
+ // reference a UDS component. Avoids babel overhead on the bulk of
65
+ // application files.
66
+ let hasComponentReference = false;
67
+ for (const name of projection.componentNames) {
68
+ if (source.includes(name)) {
69
+ hasComponentReference = true;
70
+ break;
71
+ }
72
+ }
73
+ if (!hasComponentReference) {
74
+ return source;
75
+ }
76
+
77
+ // Determine parser plugins from file extension.
78
+ const isTS = resourcePath?.endsWith('.ts') || resourcePath?.endsWith('.tsx');
79
+ const isJSX =
80
+ resourcePath?.endsWith('.tsx') || resourcePath?.endsWith('.jsx');
81
+ const parserPlugins: Array<'jsx' | 'typescript'> = [];
82
+ if (isJSX || !isTS) parserPlugins.push('jsx');
83
+ if (isTS) parserPlugins.push('typescript');
84
+
85
+ let ast: ReturnType<typeof parse>;
86
+ try {
87
+ ast = parse(source, {
88
+ sourceType: 'module',
89
+ plugins: parserPlugins,
90
+ });
91
+ } catch {
92
+ return source;
93
+ }
94
+
95
+ // Collected source-literal rewrites, keyed by character offset.
96
+ // We splice these into the original `source` string rather than
97
+ // regenerating the file: the loader's only job is to swap import
98
+ // source strings, and re-printing the whole file through
99
+ // @babel/generator (esp. with `retainLines`) can mangle dense JSX
100
+ // into invalid output. Surgical splicing keeps every byte outside
101
+ // the rewritten literals identical, so the loader can never turn a
102
+ // valid file into a broken one.
103
+ const edits: Array<{ start: number; end: number; value: string }> = [];
104
+
105
+ traverse(ast, {
106
+ // -----------------------------------------------------------------------
107
+ // Import rewrite: alias-prefixed local imports of registered UDS
108
+ // components → `#uds/components/<seg>` (the per-source-file
109
+ // codegen-emitted module).
110
+ // -----------------------------------------------------------------------
111
+ // biome-ignore lint/suspicious/noExplicitAny: untyped babel traverse visitor
112
+ ImportDeclaration(nodePath: any) {
113
+ const importSource: string | undefined = nodePath.node.source?.value;
114
+ if (!importSource) return;
115
+
116
+ // Only rewrite alias-prefixed local imports (e.g.
117
+ // `@/uds/components/<name>`, `~/uds/components/<name>`).
118
+ // Relative imports (`./text`, `../box`) belong to the authoring
119
+ // chain where each `defineComponent(...)` may reference its
120
+ // siblings (`defineComponent(Text).config(...)`) — rewriting those
121
+ // would break codegen's read of the configs.
122
+ const isAliasedLocalImport =
123
+ !importSource.startsWith('.') && !importSource.startsWith('/');
124
+ if (!isAliasedLocalImport) return;
125
+
126
+ // First-pass match: any specifier name matches a registered
127
+ // component. The common case — `import { Box } from
128
+ // '@/uds/components/box'`.
129
+ let shouldRewriteSource = false;
130
+ for (const specifier of nodePath.node.specifiers) {
131
+ if (specifier.type !== 'ImportSpecifier') continue;
132
+ const importedName: string | undefined =
133
+ specifier.imported?.type === 'Identifier'
134
+ ? specifier.imported.name
135
+ : undefined;
136
+ if (!importedName) continue;
137
+
138
+ if (projection.componentNames.has(importedName)) {
139
+ shouldRewriteSource = true;
140
+ }
141
+ }
142
+
143
+ // Second-pass match: the import path points at the authoring
144
+ // file of a registered component (e.g. `@/uds/components/sidebar`)
145
+ // but the visible specifiers are co-located helpers like
146
+ // `SidebarProvider` / `useSidebar`. If we let this resolve to the
147
+ // source file, the helper sets up a different `SidebarContext`
148
+ // than the codegen-emitted `Sidebar` reads — provider and consumer
149
+ // end up with different context identities and `useSidebar()`
150
+ // throws. Funnel every export from that file through the codegen
151
+ // module so helper AND component share the same module.
152
+ //
153
+ // Constrain to `*/uds/components/<name>` paths so unrelated
154
+ // imports whose tail happens to match a registered name
155
+ // (`next/image` → `Image`) don't get rewritten.
156
+ const segmentMatch = importSource.match(/\/uds\/components\/([^/]+)$/);
157
+ if (!shouldRewriteSource && segmentMatch) {
158
+ const tail = segmentMatch[1];
159
+ const pascal = tail
160
+ .split('-')
161
+ .filter(Boolean)
162
+ .map((s) => s.charAt(0).toUpperCase() + s.slice(1))
163
+ .join('');
164
+ if (projection.componentNames.has(pascal)) {
165
+ shouldRewriteSource = true;
166
+ }
167
+ }
168
+
169
+ if (shouldRewriteSource && segmentMatch) {
170
+ // Per-file rewrite: keep the trailing `<seg>` so each import
171
+ // resolves to its own downleveled module at
172
+ // `.uds/components/<seg>.tsx`. Splice over the literal's
173
+ // character range (quotes included) using the original quote
174
+ // char so the rest of the file is untouched.
175
+ const sourceNode = nodePath.node.source;
176
+ if (
177
+ typeof sourceNode.start !== 'number' ||
178
+ typeof sourceNode.end !== 'number'
179
+ ) {
180
+ return;
181
+ }
182
+
183
+ const quote = source[sourceNode.start] ?? "'";
184
+ edits.push({
185
+ start: sourceNode.start,
186
+ end: sourceNode.end,
187
+ value: `${quote}#uds/components/${segmentMatch[1]}${quote}`,
188
+ });
189
+ }
190
+ },
191
+ });
192
+
193
+ if (edits.length === 0) return source;
194
+
195
+ // Apply edits from the end of the file backwards so earlier offsets
196
+ // stay valid as we splice.
197
+ edits.sort((a, b) => b.start - a.start);
198
+
199
+ let output = source;
200
+ for (const edit of edits) {
201
+ output = output.slice(0, edit.start) + edit.value + output.slice(edit.end);
202
+ }
203
+
204
+ return output;
205
+ };
206
+
207
+ // webpack loader-runner treats ESM loaders as valid when `normal` is exported.
208
+ export const normal = transformLoader;
209
+ export default transformLoader;
package/src/next.ts ADDED
@@ -0,0 +1,118 @@
1
+ import path from 'node:path';
2
+ import type { NextConfig } from 'next';
3
+
4
+ export interface WithUdsOptions {
5
+ /** Output directory for generated files (default: '.uds') */
6
+ outputDir?: string;
7
+ /** Skip addDependency calls to prevent webpack from watching .uds/ artifacts.
8
+ * Use this when the app has its own reload mechanism (e.g., Studio WebSocket). */
9
+ skipWatchDependencies?: boolean;
10
+ }
11
+
12
+ function resolveLoaderPath(): string {
13
+ return require.resolve('@yahoo/uds-create-loader/loader');
14
+ }
15
+
16
+ type NextConfigFn = (
17
+ phase: string,
18
+ ctx: { defaultConfig: NextConfig },
19
+ ) => NextConfig | Promise<NextConfig>;
20
+
21
+ function applyUds(
22
+ nextConfig: NextConfig,
23
+ options?: WithUdsOptions,
24
+ ): NextConfig {
25
+ const outputDir = options?.outputDir ?? '.uds';
26
+ const skipWatch = options?.skipWatchDependencies ?? false;
27
+ const dataPath = path.resolve(process.cwd(), outputDir, 'config.json');
28
+ const loaderPath = resolveLoaderPath();
29
+ const turbopack = nextConfig.turbopack ?? {};
30
+ const existingRules = turbopack.rules ?? {};
31
+
32
+ const udsLoader = {
33
+ loader: loaderPath,
34
+ options: {
35
+ dataPath,
36
+ isDev: true,
37
+ skipWatchDependencies: skipWatch,
38
+ },
39
+ };
40
+
41
+ // Build turbopack rules that coexist with other plugins (e.g., Workflow
42
+ // DevKit). Other plugins may attach a `condition` to their rule entries
43
+ // which would incorrectly gate the UDS loader too. To avoid this, we
44
+ // keep existing rules intact and add the UDS loader as a separate
45
+ // unconditional rule under a distinct key (`**/*.ext`).
46
+ const udsRules: typeof existingRules = {};
47
+ for (const ext of ['tsx', 'ts', 'js'] as const) {
48
+ const key = `*.${ext}`;
49
+ const udsKey = `**/*.${ext}`;
50
+ // If another plugin already owns *.ext, use **/*.ext for UDS
51
+ if (existingRules[key]) {
52
+ udsRules[udsKey] = { loaders: [udsLoader] };
53
+ } else {
54
+ udsRules[key] = { loaders: [udsLoader] };
55
+ }
56
+ }
57
+
58
+ return {
59
+ ...nextConfig,
60
+ turbopack: {
61
+ ...turbopack,
62
+ rules: {
63
+ ...existingRules,
64
+ ...udsRules,
65
+ },
66
+ },
67
+ webpack(config, webpackOptions) {
68
+ config.module?.rules?.push({
69
+ test: /\.(tsx|ts|js)$/,
70
+ use: [
71
+ {
72
+ loader: loaderPath,
73
+ options: {
74
+ dataPath,
75
+ isDev: webpackOptions.dev,
76
+ skipWatchDependencies: skipWatch,
77
+ },
78
+ },
79
+ ],
80
+ });
81
+
82
+ if (skipWatch) {
83
+ const existing = config.watchOptions?.ignored;
84
+ config.watchOptions = {
85
+ ...config.watchOptions,
86
+ ignored: [
87
+ ...(Array.isArray(existing)
88
+ ? existing
89
+ : existing
90
+ ? [existing]
91
+ : []),
92
+ '**/.uds/**',
93
+ '**/uds.config.ts',
94
+ ],
95
+ };
96
+ }
97
+
98
+ if (typeof nextConfig.webpack === 'function') {
99
+ return nextConfig.webpack(config, webpackOptions);
100
+ }
101
+
102
+ return config;
103
+ },
104
+ };
105
+ }
106
+
107
+ export function withUds(
108
+ nextConfigOrFn: NextConfig | NextConfigFn,
109
+ options?: WithUdsOptions,
110
+ ): NextConfig | NextConfigFn {
111
+ if (typeof nextConfigOrFn === 'function') {
112
+ return async (phase, ctx) => {
113
+ const resolved = await nextConfigOrFn(phase, ctx);
114
+ return applyUds(resolved, options);
115
+ };
116
+ }
117
+ return applyUds(nextConfigOrFn, options);
118
+ }
@@ -0,0 +1,354 @@
1
+ import path from 'node:path';
2
+ import { gzipSync } from 'node:zlib';
3
+ import { parse as parseBabel } from '@babel/parser';
4
+ import * as t from '@babel/types';
5
+ import { VISITOR_KEYS } from '@babel/types';
6
+ import type { SerializedConfig } from '@yahoo/uds-create-config';
7
+ import transformLoader from './loader';
8
+ import { seedUdsConfig } from './loader/artifacts';
9
+ import { isCompiledJsxCallee } from './loader/babel-utils';
10
+
11
+ export interface LoaderTransformOutputSpecs {
12
+ outputGzipBytes: number;
13
+ classNameAssignments: number;
14
+ utilityClassTokens: number;
15
+ uniqueUtilityClassTokens: number;
16
+ runtimeStyleParserCalls: number;
17
+ }
18
+
19
+ interface LoaderTransformSuccess {
20
+ output: string;
21
+ specs: LoaderTransformOutputSpecs;
22
+ }
23
+
24
+ interface LoaderTransformFailure {
25
+ error: string;
26
+ }
27
+
28
+ export type LoaderTransformResult =
29
+ | LoaderTransformSuccess
30
+ | LoaderTransformFailure;
31
+
32
+ export interface RunLoaderTransformOptions {
33
+ config: SerializedConfig;
34
+ resourcePath?: string;
35
+ isDev?: boolean;
36
+ }
37
+
38
+ function getClassTokens(value: string): string[] {
39
+ return value
40
+ .split(/\s+/)
41
+ .map((token) => token.trim())
42
+ .filter(Boolean);
43
+ }
44
+
45
+ function collectClassTokensFromNode(
46
+ node: t.Node | null | undefined,
47
+ tokens: string[],
48
+ ): void {
49
+ if (!node) {
50
+ return;
51
+ }
52
+
53
+ if (t.isStringLiteral(node)) {
54
+ tokens.push(...getClassTokens(node.value));
55
+ return;
56
+ }
57
+
58
+ if (t.isTemplateLiteral(node)) {
59
+ for (const quasi of node.quasis) {
60
+ tokens.push(
61
+ ...getClassTokens(quasi.value.cooked ?? quasi.value.raw ?? ''),
62
+ );
63
+ }
64
+ for (const expression of node.expressions) {
65
+ collectClassTokensFromNode(expression, tokens);
66
+ }
67
+ return;
68
+ }
69
+
70
+ if (t.isJSXExpressionContainer(node)) {
71
+ if (!t.isJSXEmptyExpression(node.expression)) {
72
+ collectClassTokensFromNode(node.expression, tokens);
73
+ }
74
+ return;
75
+ }
76
+
77
+ if (
78
+ t.isParenthesizedExpression(node) ||
79
+ t.isTSAsExpression(node) ||
80
+ t.isTSTypeAssertion(node) ||
81
+ t.isTSNonNullExpression(node)
82
+ ) {
83
+ collectClassTokensFromNode(node.expression, tokens);
84
+ return;
85
+ }
86
+
87
+ if (t.isUnaryExpression(node) || t.isUpdateExpression(node)) {
88
+ collectClassTokensFromNode(node.argument, tokens);
89
+ return;
90
+ }
91
+
92
+ if (
93
+ t.isBinaryExpression(node) ||
94
+ t.isLogicalExpression(node) ||
95
+ t.isAssignmentExpression(node)
96
+ ) {
97
+ collectClassTokensFromNode(node.left, tokens);
98
+ collectClassTokensFromNode(node.right, tokens);
99
+ return;
100
+ }
101
+
102
+ if (t.isConditionalExpression(node)) {
103
+ collectClassTokensFromNode(node.test, tokens);
104
+ collectClassTokensFromNode(node.consequent, tokens);
105
+ collectClassTokensFromNode(node.alternate, tokens);
106
+ return;
107
+ }
108
+
109
+ if (t.isSequenceExpression(node)) {
110
+ for (const expression of node.expressions) {
111
+ collectClassTokensFromNode(expression, tokens);
112
+ }
113
+ return;
114
+ }
115
+
116
+ if (t.isArrayExpression(node)) {
117
+ for (const element of node.elements) {
118
+ if (!element) {
119
+ continue;
120
+ }
121
+ if (t.isSpreadElement(element)) {
122
+ collectClassTokensFromNode(element.argument, tokens);
123
+ continue;
124
+ }
125
+ collectClassTokensFromNode(element, tokens);
126
+ }
127
+ return;
128
+ }
129
+
130
+ if (t.isObjectExpression(node)) {
131
+ for (const property of node.properties) {
132
+ if (t.isSpreadElement(property)) {
133
+ collectClassTokensFromNode(property.argument, tokens);
134
+ continue;
135
+ }
136
+ if (t.isObjectProperty(property)) {
137
+ collectClassTokensFromNode(property.value, tokens);
138
+ }
139
+ }
140
+ return;
141
+ }
142
+
143
+ if (t.isCallExpression(node) || t.isOptionalCallExpression(node)) {
144
+ collectClassTokensFromNode(node.callee, tokens);
145
+ for (const argument of node.arguments) {
146
+ if (t.isSpreadElement(argument)) {
147
+ collectClassTokensFromNode(argument.argument, tokens);
148
+ continue;
149
+ }
150
+ collectClassTokensFromNode(argument, tokens);
151
+ }
152
+ return;
153
+ }
154
+
155
+ if (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) {
156
+ collectClassTokensFromNode(node.object, tokens);
157
+ if (node.computed) {
158
+ collectClassTokensFromNode(node.property, tokens);
159
+ }
160
+ return;
161
+ }
162
+
163
+ if (t.isNewExpression(node)) {
164
+ collectClassTokensFromNode(node.callee, tokens);
165
+ for (const argument of node.arguments ?? []) {
166
+ if (t.isSpreadElement(argument)) {
167
+ collectClassTokensFromNode(argument.argument, tokens);
168
+ continue;
169
+ }
170
+ collectClassTokensFromNode(argument, tokens);
171
+ }
172
+ }
173
+ }
174
+
175
+ function isClassNameObjectProperty(node: t.ObjectProperty): boolean {
176
+ if (node.computed) {
177
+ return false;
178
+ }
179
+ return (
180
+ (t.isIdentifier(node.key) && node.key.name === 'className') ||
181
+ (t.isStringLiteral(node.key) && node.key.value === 'className')
182
+ );
183
+ }
184
+
185
+ function isBabelNode(value: unknown): value is t.Node {
186
+ return (
187
+ typeof value === 'object' &&
188
+ value !== null &&
189
+ 'type' in (value as Record<string, unknown>)
190
+ );
191
+ }
192
+
193
+ function isClassNamePropOnCompiledJsxCall(
194
+ node: t.ObjectProperty,
195
+ parent: t.Node | null,
196
+ grandParent: t.Node | null,
197
+ ): boolean {
198
+ if (!isClassNameObjectProperty(node)) {
199
+ return false;
200
+ }
201
+ if (!parent || !t.isObjectExpression(parent)) {
202
+ return false;
203
+ }
204
+ if (!grandParent || !t.isCallExpression(grandParent)) {
205
+ return false;
206
+ }
207
+ if (!isCompiledJsxCallee(grandParent.callee)) {
208
+ return false;
209
+ }
210
+ return grandParent.arguments.some((argument) => argument === parent);
211
+ }
212
+
213
+ function getClassTokenStats(output: string): {
214
+ total: number;
215
+ unique: number;
216
+ assignments: number;
217
+ } {
218
+ try {
219
+ const ast = parseBabel(output, {
220
+ sourceType: 'module',
221
+ plugins: ['jsx', 'typescript'],
222
+ });
223
+
224
+ const uniqueTokens = new Set<string>();
225
+ let total = 0;
226
+ let assignments = 0;
227
+
228
+ const addTokens = (node: t.Node | null | undefined) => {
229
+ const nextTokens: string[] = [];
230
+ collectClassTokensFromNode(node, nextTokens);
231
+ total += nextTokens.length;
232
+ for (const token of nextTokens) {
233
+ uniqueTokens.add(token);
234
+ }
235
+ };
236
+
237
+ const visit = (
238
+ node: t.Node | null | undefined,
239
+ parent: t.Node | null,
240
+ grandParent: t.Node | null,
241
+ ) => {
242
+ if (!node) {
243
+ return;
244
+ }
245
+
246
+ if (
247
+ t.isJSXAttribute(node) &&
248
+ t.isJSXIdentifier(node.name) &&
249
+ node.name.name === 'className'
250
+ ) {
251
+ assignments += 1;
252
+ addTokens(node.value);
253
+ } else if (t.isObjectProperty(node)) {
254
+ if (isClassNamePropOnCompiledJsxCall(node, parent, grandParent)) {
255
+ assignments += 1;
256
+ addTokens(node.value);
257
+ }
258
+ }
259
+
260
+ const visitorKeys = VISITOR_KEYS[node.type] ?? [];
261
+ const typedNode = node as unknown as Record<string, unknown>;
262
+ for (const key of visitorKeys) {
263
+ const childValue = typedNode[key];
264
+ if (Array.isArray(childValue)) {
265
+ for (const item of childValue) {
266
+ if (isBabelNode(item)) {
267
+ visit(item, node, parent);
268
+ }
269
+ }
270
+ continue;
271
+ }
272
+ if (isBabelNode(childValue)) {
273
+ visit(childValue, node, parent);
274
+ }
275
+ }
276
+ };
277
+
278
+ visit(ast.program, null, null);
279
+
280
+ return {
281
+ total,
282
+ unique: uniqueTokens.size,
283
+ assignments,
284
+ };
285
+ } catch {
286
+ const fallbackAssignments = (output.match(/className\s*[:=]/g) ?? [])
287
+ .length;
288
+ return {
289
+ total: 0,
290
+ unique: 0,
291
+ assignments: fallbackAssignments,
292
+ };
293
+ }
294
+ }
295
+
296
+ function getRuntimeStyleParserCallCount(output: string): number {
297
+ const patterns = [/extractStyleProps\s*\(/g, /getStyles\s*\(/g];
298
+ return patterns.reduce(
299
+ (count, pattern) => count + (output.match(pattern) ?? []).length,
300
+ 0,
301
+ );
302
+ }
303
+
304
+ export function getLoaderTransformOutputSpecs(
305
+ output: string,
306
+ ): LoaderTransformOutputSpecs {
307
+ const classTokenStats = getClassTokenStats(output);
308
+ return {
309
+ outputGzipBytes: gzipSync(output).byteLength,
310
+ classNameAssignments: classTokenStats.assignments,
311
+ utilityClassTokens: classTokenStats.total,
312
+ uniqueUtilityClassTokens: classTokenStats.unique,
313
+ runtimeStyleParserCalls: getRuntimeStyleParserCallCount(output),
314
+ };
315
+ }
316
+
317
+ const VIRTUAL_CONFIG_PATH = '/virtual/uds-config.json';
318
+
319
+ export function runLoaderTransform(
320
+ source: string,
321
+ options: RunLoaderTransformOptions,
322
+ ): LoaderTransformResult {
323
+ if (typeof source !== 'string') {
324
+ return { error: '`source` must be a string.' };
325
+ }
326
+
327
+ // Seed the loader's in-memory cache so it skips disk I/O
328
+ seedUdsConfig(VIRTUAL_CONFIG_PATH, options.config);
329
+
330
+ const context = {
331
+ getOptions: () => ({
332
+ dataPath: VIRTUAL_CONFIG_PATH,
333
+ isDev: options.isDev ?? false,
334
+ }),
335
+ addDependency: () => {},
336
+ resourcePath:
337
+ options.resourcePath ??
338
+ path.resolve(process.cwd(), 'playground-input.tsx'),
339
+ };
340
+
341
+ try {
342
+ const output = (
343
+ transformLoader as unknown as (this: unknown, code: string) => string
344
+ ).call(context, source);
345
+ return { output, specs: getLoaderTransformOutputSpecs(output) };
346
+ } catch (error) {
347
+ return {
348
+ error:
349
+ error instanceof Error
350
+ ? error.message
351
+ : 'Transform failed unexpectedly.',
352
+ };
353
+ }
354
+ }