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

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,11 +1,10 @@
1
1
  {
2
2
  "name": "@yahoo/uds-create-loader",
3
- "version": "1.11.0-v3-import.1",
3
+ "version": "1.11.0-v3-import.2",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "files": [
7
- "dist",
8
- "src"
7
+ "dist"
9
8
  ],
10
9
  "exports": {
11
10
  ".": {
@@ -21,8 +20,8 @@
21
20
  "default": "./dist/next.js"
22
21
  },
23
22
  "./transform-runner": {
24
- "types": "./src/transform-runner.ts",
25
- "default": "./src/transform-runner.ts"
23
+ "types": "./dist/transform-runner.d.ts",
24
+ "default": "./dist/transform-runner.js"
26
25
  },
27
26
  "./package.json": "./package.json"
28
27
  },
@@ -38,7 +37,7 @@
38
37
  "test:update": "bun test src/ --update-snapshots"
39
38
  },
40
39
  "dependencies": {
41
- "@yahoo/uds-create-config": "1.11.0-v3-import.1",
40
+ "@yahoo/uds-create-config": "1.11.0-v3-import.2",
42
41
  "@babel/parser": "^7.29.0",
43
42
  "@babel/traverse": "^7.29.0",
44
43
  "@babel/types": "^7.29.0",
package/src/index.ts DELETED
@@ -1,12 +0,0 @@
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';
@@ -1,79 +0,0 @@
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
- });
@@ -1,113 +0,0 @@
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
- }
@@ -1,39 +0,0 @@
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
- }
@@ -1,254 +0,0 @@
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
- });
package/src/loader.ts DELETED
@@ -1,209 +0,0 @@
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 DELETED
@@ -1,118 +0,0 @@
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
- }
@@ -1,354 +0,0 @@
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
- }
package/src/unplugin.ts DELETED
@@ -1,94 +0,0 @@
1
- import path from 'node:path';
2
- import { createUnplugin } from 'unplugin';
3
- import transformLoader from './loader';
4
-
5
- export interface UdsPluginOptions {
6
- /**
7
- * Path to `.uds/config.json` (default: `<cwd>/.uds/config.json`)
8
- */
9
- dataPath?: string;
10
- /**
11
- * Enable development-only metadata transforms.
12
- * Defaults to `process.env.NODE_ENV !== 'production'`.
13
- */
14
- isDev?: boolean;
15
- /**
16
- * Include filters for files to transform.
17
- * Defaults to JS/TS module extensions.
18
- */
19
- include?: RegExp | RegExp[];
20
- /**
21
- * Exclude filters for files to skip.
22
- */
23
- exclude?: RegExp | RegExp[];
24
- }
25
-
26
- const DEFAULT_INCLUDE = /\.[cm]?[jt]sx?$/;
27
-
28
- function toArray(value: RegExp | RegExp[] | undefined): RegExp[] {
29
- if (!value) return [];
30
- return Array.isArray(value) ? value : [value];
31
- }
32
-
33
- function sanitizeId(id: string): string {
34
- const queryStart = id.indexOf('?');
35
- const hashStart = id.indexOf('#');
36
- const cutIndex =
37
- queryStart === -1
38
- ? hashStart
39
- : hashStart === -1
40
- ? queryStart
41
- : Math.min(queryStart, hashStart);
42
- return cutIndex === -1 ? id : id.slice(0, cutIndex);
43
- }
44
-
45
- function matchesFilters(
46
- id: string,
47
- includes: RegExp[],
48
- excludes: RegExp[],
49
- ): boolean {
50
- if (excludes.some((pattern) => pattern.test(id))) return false;
51
- if (includes.length === 0) return true;
52
- return includes.some((pattern) => pattern.test(id));
53
- }
54
-
55
- export const udsPlugin = createUnplugin<UdsPluginOptions>((options = {}) => {
56
- const includePatterns = toArray(options.include ?? DEFAULT_INCLUDE);
57
- const excludePatterns = toArray(options.exclude);
58
- const dataPath = path.resolve(
59
- process.cwd(),
60
- options.dataPath ?? '.uds/config.json',
61
- );
62
- const isDev = options.isDev ?? process.env.NODE_ENV !== 'production';
63
-
64
- return {
65
- name: 'uds-unplugin',
66
- enforce: 'pre',
67
- transformInclude(id) {
68
- return matchesFilters(sanitizeId(id), includePatterns, excludePatterns);
69
- },
70
- transform(code, id) {
71
- const resourcePath = sanitizeId(id);
72
- if (!matchesFilters(resourcePath, includePatterns, excludePatterns)) {
73
- return null;
74
- }
75
-
76
- const transformedCode = (
77
- transformLoader as unknown as (this: unknown, source: string) => string
78
- ).call(
79
- {
80
- getOptions: () => ({ dataPath, isDev }),
81
- addDependency: () => {},
82
- resourcePath,
83
- },
84
- code,
85
- );
86
-
87
- if (transformedCode === code) return null;
88
- return {
89
- code: transformedCode,
90
- map: null,
91
- };
92
- },
93
- };
94
- });