@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/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -3
- package/src/index.ts +12 -0
- package/src/inspect-transform.ts +79 -0
- package/src/loader/artifacts.ts +113 -0
- package/src/loader/babel-utils.ts +39 -0
- package/src/loader.test.ts +254 -0
- package/src/loader.ts +209 -0
- package/src/next.ts +118 -0
- package/src/transform-runner.ts +354 -0
- package/src/unplugin.ts +94 -0
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yahoo/uds-create-loader",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.0",
|
|
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.
|
|
41
|
+
"@yahoo/uds-create-config": "1.14.0",
|
|
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
|
+
});
|