@yahoo/uds-create-loader 1.10.0 → 1.11.0-v3-import.1

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/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@yahoo/uds-create-loader",
3
- "version": "1.10.0",
3
+ "version": "1.11.0-v3-import.1",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "files": [
7
- "dist"
7
+ "dist",
8
+ "src"
8
9
  ],
9
10
  "exports": {
10
11
  ".": {
@@ -37,7 +38,7 @@
37
38
  "test:update": "bun test src/ --update-snapshots"
38
39
  },
39
40
  "dependencies": {
40
- "@yahoo/uds-create-config": "1.10.0",
41
+ "@yahoo/uds-create-config": "1.11.0-v3-import.1",
41
42
  "@babel/parser": "^7.29.0",
42
43
  "@babel/traverse": "^7.29.0",
43
44
  "@babel/types": "^7.29.0",
package/src/index.ts ADDED
@@ -0,0 +1,12 @@
1
+ export { default as transformLoader } from './loader';
2
+ export type {
3
+ LoaderTransformOutputSpecs,
4
+ LoaderTransformResult,
5
+ RunLoaderTransformOptions,
6
+ } from './transform-runner';
7
+ export {
8
+ getLoaderTransformOutputSpecs,
9
+ runLoaderTransform,
10
+ } from './transform-runner';
11
+ export type { UdsPluginOptions } from './unplugin';
12
+ export { udsPlugin } from './unplugin';
@@ -0,0 +1,79 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+ import type { Config } from '@yahoo/uds-create-config';
5
+ import { runLoaderTransform } from './transform-runner';
6
+
7
+ function readFlag(flag: string): string | null {
8
+ const index = process.argv.indexOf(flag);
9
+ if (index === -1) return null;
10
+ const value = process.argv[index + 1];
11
+ if (!value || value.startsWith('-')) return null;
12
+ return value;
13
+ }
14
+
15
+ function printUsage(): never {
16
+ console.error(
17
+ [
18
+ 'Usage:',
19
+ ' bun run src/inspect-transform.ts --file <path-to-tsx> --config <path-to-uds.config.ts>',
20
+ '',
21
+ 'Example:',
22
+ ' bun run --filter @yahoo/uds-create-loader inspect:transform -- --file packages/components/src/accordion.tsx --config apps/docs/uds.config.ts',
23
+ ].join('\n'),
24
+ );
25
+ process.exit(1);
26
+ }
27
+
28
+ async function loadUdsConfig(configPath: string): Promise<unknown> {
29
+ const resolvedPath = path.resolve(configPath);
30
+ const configUrl = pathToFileURL(resolvedPath).href;
31
+ const module = (await import(configUrl)) as {
32
+ default?: unknown;
33
+ udsConfig?: unknown;
34
+ };
35
+
36
+ if (module.default !== undefined) {
37
+ return module.default;
38
+ }
39
+ if (module.udsConfig !== undefined) {
40
+ return module.udsConfig;
41
+ }
42
+
43
+ return module;
44
+ }
45
+
46
+ async function main() {
47
+ const sourcePath = readFlag('--file');
48
+ const configPath = readFlag('--config');
49
+ if (!sourcePath || !configPath) {
50
+ printUsage();
51
+ }
52
+
53
+ const sourceAbsolutePath = path.resolve(sourcePath);
54
+ if (!fs.existsSync(sourceAbsolutePath)) {
55
+ throw new Error(`Source file not found: ${sourceAbsolutePath}`);
56
+ }
57
+
58
+ const rawConfig = await loadUdsConfig(configPath);
59
+ const config = (rawConfig as Config).toJSON();
60
+
61
+ const source = fs.readFileSync(sourceAbsolutePath, 'utf8');
62
+ const result = runLoaderTransform(source, {
63
+ config,
64
+ resourcePath: sourceAbsolutePath,
65
+ isDev: false,
66
+ });
67
+
68
+ if ('error' in result) {
69
+ throw new Error(result.error);
70
+ }
71
+
72
+ process.stdout.write(result.output);
73
+ }
74
+
75
+ void main().catch((error) => {
76
+ const message = error instanceof Error ? error.message : String(error);
77
+ console.error(`[inspect-transform] ${message}`);
78
+ process.exit(1);
79
+ });
@@ -0,0 +1,113 @@
1
+ import fs from 'node:fs';
2
+ import type { SerializedConfig } from '@yahoo/uds-create-config';
3
+
4
+ export interface LoaderOptions {
5
+ /** Path to `.uds/manifest.json`. The loader extracts `.config` at the
6
+ * read boundary; the manifest's wrapper fields (`catalogMeta`,
7
+ * `sourceMap`, …) exist for Studio/MCP and aren't read here. */
8
+ dataPath: string;
9
+ isDev: boolean;
10
+ /** Skip addDependency calls to prevent webpack from watching .uds/ artifacts.
11
+ * Use this when the app has its own reload mechanism (e.g., Studio WebSocket). */
12
+ skipWatchDependencies?: boolean;
13
+ }
14
+
15
+ /**
16
+ * Derived projection of one loaded config — built lazily on first use,
17
+ * cached by config object identity so subsequent transforms reuse the
18
+ * same set. Carries the registered-component-name set the loader uses
19
+ * for the pre-filter and the import-rewrite visitor's eligibility check.
20
+ */
21
+ export interface LoaderProjection {
22
+ readonly componentNames: ReadonlySet<string>;
23
+ }
24
+
25
+ /**
26
+ * `.uds/config.json` IS the serialized config — no envelope. Parse and
27
+ * cache by file mtime so subsequent transforms reuse the same object.
28
+ */
29
+ function createCachedJsonLoader(): (
30
+ filePath: string,
31
+ isDev: boolean,
32
+ ) => SerializedConfig | null {
33
+ let cached: SerializedConfig | null = null;
34
+ let cachedPath: string | null = null;
35
+ let cachedMtime = 0;
36
+
37
+ return (filePath: string, isDev: boolean): SerializedConfig | null => {
38
+ try {
39
+ const stat = fs.statSync(filePath);
40
+ const mtime = stat.mtimeMs;
41
+
42
+ if (
43
+ cached &&
44
+ cachedPath === filePath &&
45
+ (!isDev || cachedMtime === mtime)
46
+ ) {
47
+ return cached;
48
+ }
49
+
50
+ const content = fs.readFileSync(filePath, 'utf-8');
51
+ cached = JSON.parse(content) as SerializedConfig;
52
+ cachedPath = filePath;
53
+ cachedMtime = mtime;
54
+ return cached;
55
+ } catch {
56
+ return null;
57
+ }
58
+ };
59
+ }
60
+
61
+ function createCachedJsonLoaderWithSeed(): {
62
+ load: (filePath: string, isDev: boolean) => SerializedConfig | null;
63
+ seed: (filePath: string, config: SerializedConfig) => void;
64
+ } {
65
+ const loader = createCachedJsonLoader();
66
+ let seededPath: string | null = null;
67
+ let seededData: SerializedConfig | null = null;
68
+
69
+ return {
70
+ load: (filePath: string, isDev: boolean): SerializedConfig | null => {
71
+ if (seededPath === filePath && seededData) {
72
+ return seededData;
73
+ }
74
+ return loader(filePath, isDev);
75
+ },
76
+ seed: (filePath: string, config: SerializedConfig) => {
77
+ seededPath = filePath;
78
+ seededData = config;
79
+ },
80
+ };
81
+ }
82
+
83
+ const configLoader = createCachedJsonLoaderWithSeed();
84
+
85
+ export const loadUdsConfig = configLoader.load;
86
+ export const seedUdsConfig = configLoader.seed;
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // Derived projection
90
+ // ---------------------------------------------------------------------------
91
+
92
+ const projectionCache = new WeakMap<SerializedConfig, LoaderProjection>();
93
+
94
+ /**
95
+ * Build (or return a cached) projection of the config for the loader's
96
+ * hot path. Keyed by config object identity. The cache survives across
97
+ * transforms within one process; reloading `.uds/manifest.json`
98
+ * produces a fresh object, which transparently invalidates the
99
+ * projection.
100
+ */
101
+ export function getLoaderProjection(
102
+ config: SerializedConfig,
103
+ ): LoaderProjection {
104
+ const cached = projectionCache.get(config);
105
+ if (cached) return cached;
106
+
107
+ const components = (config.components ?? {}) as Record<string, unknown>;
108
+ const componentNames = new Set<string>(Object.keys(components));
109
+
110
+ const projection: LoaderProjection = { componentNames };
111
+ projectionCache.set(config, projection);
112
+ return projection;
113
+ }
@@ -0,0 +1,39 @@
1
+ import * as t from '@babel/types';
2
+
3
+ /** JSX function names used by compiled JSX (React, etc.) */
4
+ const COMPILED_JSX_NAMES = new Set([
5
+ 'jsx',
6
+ 'jsxs',
7
+ 'jsxDEV',
8
+ '_jsx',
9
+ '_jsxs',
10
+ '_jsxDEV',
11
+ ]);
12
+
13
+ /**
14
+ * Check whether a callee expression is a compiled JSX call.
15
+ *
16
+ * Matches:
17
+ * - Direct identifiers: `jsx(...)`, `_jsxs(...)`, `jsxDEV(...)`, etc.
18
+ * - Member expressions: `React.jsx(...)`, `_reactJsxRuntime.jsxs(...)`, etc.
19
+ */
20
+ export function isCompiledJsxCallee(
21
+ node: t.Expression | t.V8IntrinsicIdentifier | null | undefined,
22
+ ): boolean {
23
+ if (!node) return false;
24
+
25
+ if (t.isIdentifier(node)) {
26
+ return COMPILED_JSX_NAMES.has(node.name);
27
+ }
28
+
29
+ if (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) {
30
+ if (!node.computed && t.isIdentifier(node.property)) {
31
+ return COMPILED_JSX_NAMES.has(node.property.name);
32
+ }
33
+ if (node.computed && t.isStringLiteral(node.property)) {
34
+ return COMPILED_JSX_NAMES.has(node.property.value);
35
+ }
36
+ }
37
+
38
+ return false;
39
+ }
@@ -0,0 +1,254 @@
1
+ /**
2
+ * Unit tests for the trimmed `transformLoader`.
3
+ *
4
+ * Fixtures use the public authoring API (`defineComponent`,
5
+ * `defineStyleProp`, `uds.register*`) and call `Config.toJSON()` so the
6
+ * loader sees exactly the serialized-config shape it would in production.
7
+ * Avoids coupling tests to internal serialized layer / marker shape —
8
+ * those shapes can evolve without touching this file.
9
+ */
10
+
11
+ import { describe, expect, it } from 'bun:test';
12
+ import path from 'node:path';
13
+ import { parse } from '@babel/parser';
14
+ import type { SerializedConfig } from '@yahoo/uds-create-config';
15
+ import {
16
+ Config,
17
+ defineComponent,
18
+ defineStyleProp,
19
+ } from '@yahoo/uds-create-config';
20
+ import transformLoader from './loader';
21
+ import { seedUdsConfig } from './loader/artifacts';
22
+
23
+ const VIRTUAL_CONFIG_PATH = '/virtual/uds-config.json';
24
+
25
+ function runLoader(
26
+ source: string,
27
+ config: SerializedConfig,
28
+ options?: { isDev?: boolean; resourcePath?: string },
29
+ ): string {
30
+ seedUdsConfig(VIRTUAL_CONFIG_PATH, config);
31
+
32
+ const context = {
33
+ getOptions: () => ({
34
+ dataPath: VIRTUAL_CONFIG_PATH,
35
+ isDev: options?.isDev ?? false,
36
+ }),
37
+ addDependency: () => {},
38
+ resourcePath: options?.resourcePath ?? path.join(process.cwd(), 'demo.tsx'),
39
+ };
40
+
41
+ return (
42
+ transformLoader as unknown as (this: unknown, code: string) => string
43
+ ).call(context, source);
44
+ }
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // Shared config factories — authoring-API based
48
+ // ---------------------------------------------------------------------------
49
+
50
+ const bg = defineStyleProp({
51
+ cssProperty: 'background-color',
52
+ classPrefix: 'bg',
53
+ values: [],
54
+ });
55
+
56
+ const padding = defineStyleProp({
57
+ cssProperty: 'padding',
58
+ classPrefix: 'p',
59
+ values: [],
60
+ });
61
+
62
+ /** Single-layer primitive `<Box>` with two style-prop bindings. Lowerable. */
63
+ function makeBoxConfig(): SerializedConfig {
64
+ const Box = defineComponent('div').config(({ styleProp }) => ({
65
+ props: {
66
+ bg: styleProp('bg'),
67
+ padding: styleProp('padding'),
68
+ },
69
+ }));
70
+ const config = new Config()
71
+ .registerStyleProps({ bg, padding })
72
+ .registerComponents({ Box });
73
+ return config.toJSON();
74
+ }
75
+
76
+ /** Empty config — no registered components. */
77
+ function makeEmptyConfig(): SerializedConfig {
78
+ return new Config().toJSON();
79
+ }
80
+
81
+ describe('loader — new-API import rewrite', () => {
82
+ it('rewrites aliased `@/uds/components/<seg>` imports to `#uds/components/<seg>`', () => {
83
+ const config = makeBoxConfig();
84
+ const source = `
85
+ import { Box } from '@/uds/components/box';
86
+
87
+ export function Demo() {
88
+ return <Box bg="brand">hi</Box>;
89
+ }
90
+ `;
91
+ const output = runLoader(source, config);
92
+
93
+ expect(output).toMatch(/from\s+["']#uds\/components\/box["']/);
94
+ expect(output).not.toContain("from '@/uds/components/box'");
95
+ });
96
+
97
+ it('does not rewrite relative imports', () => {
98
+ const config = makeBoxConfig();
99
+ const source = `
100
+ import { Box } from './box';
101
+
102
+ export function Demo() {
103
+ return <Box bg="brand">hi</Box>;
104
+ }
105
+ `;
106
+ const output = runLoader(source, config);
107
+
108
+ expect(output).toMatch(/from\s+["']\.\/box["']/);
109
+ expect(output).not.toMatch(/from\s+["']#uds\/components/);
110
+ });
111
+
112
+ it('does not rewrite imports of unknown names', () => {
113
+ const config = makeBoxConfig();
114
+ const source = `
115
+ import { Unrelated } from '@/uds/components/unrelated';
116
+
117
+ export function Demo() {
118
+ return <Unrelated />;
119
+ }
120
+ `;
121
+ const output = runLoader(source, config);
122
+
123
+ expect(output).toMatch(/from\s+["']@\/uds\/components\/unrelated["']/);
124
+ expect(output).not.toMatch(/from\s+["']#uds\/components/);
125
+ });
126
+ });
127
+
128
+ describe('loader — JSX passes through unchanged', () => {
129
+ it('leaves JSX call sites alone while rewriting the import', () => {
130
+ const config = makeBoxConfig();
131
+ const source = `
132
+ import { Box } from '@/uds/components/box';
133
+
134
+ export function Demo() {
135
+ return <Box bg="brand" padding="md">hi</Box>;
136
+ }
137
+ `;
138
+ const output = runLoader(source, config);
139
+
140
+ expect(output).toMatch(/from\s+["']#uds\/components\/box["']/);
141
+ expect(output).toContain('<Box');
142
+ expect(output).toContain('bg="brand"');
143
+ expect(output).toContain('padding="md"');
144
+ });
145
+ });
146
+
147
+ describe('loader — non-import bytes are preserved verbatim', () => {
148
+ it('rewrites only the import literal and leaves every other byte identical', () => {
149
+ const config = makeBoxConfig();
150
+ const source = `
151
+ import { Box } from '@/uds/components/box';
152
+
153
+ export function Demo() {
154
+ return <Box bg="brand">hi</Box>;
155
+ }
156
+ `;
157
+ const output = runLoader(source, config);
158
+ const expected = source.replace(
159
+ "'@/uds/components/box'",
160
+ "'#uds/components/box'",
161
+ );
162
+
163
+ expect(output).toBe(expected);
164
+ });
165
+
166
+ // Regression: the loader used to re-print the whole file through
167
+ // @babel/generator with `retainLines`, which mangled dense JSX into
168
+ // invalid output (e.g. a stray `}` in JSX-text position). Splicing
169
+ // only the import literal must leave all JSX — including expression
170
+ // containers, fragments, and nested closing braces — untouched.
171
+ it('does not corrupt dense JSX with nested braces and fragments', () => {
172
+ const config = makeBoxConfig();
173
+ const source = `import { Box } from '@/uds/components/box';
174
+ import { useState } from 'react';
175
+
176
+ const items = ['a', 'b', 'c'];
177
+
178
+ export function LayerTree() {
179
+ const [open, setOpen] = useState(false);
180
+ return (
181
+ <>
182
+ {items.map((item) => (
183
+ <Box key={item} bg="brand" padding="md">
184
+ {item === 'a' ? <span>{\`first: \${item}\`}</span> : null}
185
+ {open && (
186
+ <Box>
187
+ {items
188
+ .filter((i) => i !== item)
189
+ .map((i) => (
190
+ <span key={i}>{i}</span>
191
+ ))}
192
+ </Box>
193
+ )}
194
+ </Box>
195
+ ))}
196
+ </>
197
+ );
198
+ }
199
+ `;
200
+ const output = runLoader(source, config);
201
+ const expected = source.replace(
202
+ "'@/uds/components/box'",
203
+ "'#uds/components/box'",
204
+ );
205
+
206
+ expect(output).toBe(expected);
207
+ // The output must still parse as valid TSX.
208
+ expect(() =>
209
+ parse(output, { sourceType: 'module', plugins: ['jsx', 'typescript'] }),
210
+ ).not.toThrow();
211
+ });
212
+ });
213
+
214
+ describe('loader — bailout paths', () => {
215
+ it('returns source unchanged when the manifest has no components', () => {
216
+ const config = makeEmptyConfig();
217
+ const source = `
218
+ import { Box } from '@/uds/components/box';
219
+
220
+ export function Demo() {
221
+ return <Box>hi</Box>;
222
+ }
223
+ `;
224
+ const output = runLoader(source, config);
225
+ expect(output).toBe(source);
226
+ });
227
+
228
+ it('returns source unchanged for files inside `.uds/`', () => {
229
+ const config = makeBoxConfig();
230
+ const source = `
231
+ import { Box } from '@/uds/components/box';
232
+
233
+ export function Demo() {
234
+ return <Box bg="brand">hi</Box>;
235
+ }
236
+ `;
237
+ const output = runLoader(source, config, {
238
+ resourcePath: path.join(process.cwd(), '.uds/components/box.tsx'),
239
+ });
240
+
241
+ expect(output).toBe(source);
242
+ });
243
+
244
+ it('returns source unchanged when no manifest component is referenced', () => {
245
+ const config = makeBoxConfig();
246
+ const source = `
247
+ export function Demo() {
248
+ return <div>just a div</div>;
249
+ }
250
+ `;
251
+ const output = runLoader(source, config);
252
+ expect(output).toBe(source);
253
+ });
254
+ });