@yahoo/uds-create-loader 5.87.1 → 5.88.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/dist/loader.d.ts +4 -3
- package/dist/loader.js +55 -36
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/loader.test.ts +95 -0
- package/src/loader.ts +115 -94
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yahoo/uds-create-loader",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.88.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"private": false,
|
|
6
6
|
"files": [
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"test:update": "bun test src/ --update-snapshots"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@yahoo/uds-create-config": "5.
|
|
41
|
+
"@yahoo/uds-create-config": "5.88.1",
|
|
42
42
|
"@babel/parser": "^7.29.0",
|
|
43
43
|
"@babel/traverse": "^7.29.0",
|
|
44
44
|
"@babel/types": "^7.29.0",
|
package/src/loader.test.ts
CHANGED
|
@@ -154,6 +154,101 @@ export function Demo() {
|
|
|
154
154
|
});
|
|
155
155
|
});
|
|
156
156
|
|
|
157
|
+
describe('loader — re-export rewrite', () => {
|
|
158
|
+
it("re-exports a linked system's sources as the modules the build emitted", () => {
|
|
159
|
+
const source = `
|
|
160
|
+
export { Button } from '@/uds/linked/yos-v3/button';
|
|
161
|
+
export { createToast, ToastPortal } from "@/uds/linked/yos-v3/toast-portal";
|
|
162
|
+
export * from '@/uds/linked/yos-v3/box';
|
|
163
|
+
`;
|
|
164
|
+
const output = runLoader(source, makeEmptyConfig());
|
|
165
|
+
|
|
166
|
+
expect(output).toBe(`
|
|
167
|
+
export { Button } from '#uds/components/_linked/yos-v3/button';
|
|
168
|
+
export { createToast, ToastPortal } from "#uds/components/_linked/yos-v3/toast-portal";
|
|
169
|
+
export * from '#uds/components/_linked/yos-v3/box';
|
|
170
|
+
`);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('re-exports `@/uds/components/<seg>` as `#uds/components/<seg>`, whatever the case of the segment', () => {
|
|
174
|
+
const source = `
|
|
175
|
+
export { Box } from '@/uds/components/Box';
|
|
176
|
+
export { Box as Panel } from '@/uds/components/box';
|
|
177
|
+
`;
|
|
178
|
+
const output = runLoader(source, makeBoxConfig());
|
|
179
|
+
|
|
180
|
+
expect(output).toBe(`
|
|
181
|
+
export { Box } from '#uds/components/Box';
|
|
182
|
+
export { Box as Panel } from '#uds/components/box';
|
|
183
|
+
`);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("re-exports a component file's helpers through the same generated module", () => {
|
|
187
|
+
const source = `
|
|
188
|
+
export { BoxProvider, useBox } from '@/uds/components/box';
|
|
189
|
+
export { default as BoxDefault } from '@/uds/components/box';
|
|
190
|
+
`;
|
|
191
|
+
const output = runLoader(source, makeBoxConfig());
|
|
192
|
+
|
|
193
|
+
expect(output).toBe(`
|
|
194
|
+
export { BoxProvider, useBox } from '#uds/components/box';
|
|
195
|
+
export { default as BoxDefault } from '#uds/components/box';
|
|
196
|
+
`);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("re-exports everything from a component's file through its generated module", () => {
|
|
200
|
+
const source = `
|
|
201
|
+
export * from '@/uds/components/box';
|
|
202
|
+
export * as box from '@/uds/components/box';
|
|
203
|
+
export * from '@/uds/components/utils';
|
|
204
|
+
`;
|
|
205
|
+
const output = runLoader(source, makeBoxConfig());
|
|
206
|
+
|
|
207
|
+
expect(output).toBe(`
|
|
208
|
+
export * from '#uds/components/box';
|
|
209
|
+
export * as box from '#uds/components/box';
|
|
210
|
+
export * from '@/uds/components/utils';
|
|
211
|
+
`);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('leaves type-only re-exports pointed at the source', () => {
|
|
215
|
+
const source = `
|
|
216
|
+
export type { BoxProps } from '@/uds/components/box';
|
|
217
|
+
export type { ButtonProps } from '@/uds/linked/yos-v3/button';
|
|
218
|
+
export type * from '@/uds/components/box';
|
|
219
|
+
`;
|
|
220
|
+
const output = runLoader(source, makeBoxConfig());
|
|
221
|
+
|
|
222
|
+
expect(output).toBe(source);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('rewrites a re-export that mixes values and types, for its values', () => {
|
|
226
|
+
const source = `
|
|
227
|
+
export { Box, type BoxProps } from '@/uds/components/box';
|
|
228
|
+
export { type Box as BoxType } from '@/uds/components/layout';
|
|
229
|
+
`;
|
|
230
|
+
const output = runLoader(source, makeBoxConfig());
|
|
231
|
+
|
|
232
|
+
expect(output).toBe(`
|
|
233
|
+
export { Box, type BoxProps } from '#uds/components/box';
|
|
234
|
+
export { type Box as BoxType } from '@/uds/components/layout';
|
|
235
|
+
`);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('leaves non-UDS and relative re-exports alone', () => {
|
|
239
|
+
const source = `
|
|
240
|
+
export { Box } from './box';
|
|
241
|
+
export * from '../uds/components/box';
|
|
242
|
+
export { Image } from 'next/image';
|
|
243
|
+
export { Box as Other } from '@/components/box';
|
|
244
|
+
export const local = 1;
|
|
245
|
+
`;
|
|
246
|
+
const output = runLoader(source, makeBoxConfig());
|
|
247
|
+
|
|
248
|
+
expect(output).toBe(source);
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
157
252
|
describe('loader — JSX passes through unchanged', () => {
|
|
158
253
|
it('leaves JSX call sites alone while rewriting the import', () => {
|
|
159
254
|
const config = makeBoxConfig();
|
package/src/loader.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { parse } from '@babel/parser';
|
|
2
2
|
import _traverse from '@babel/traverse';
|
|
3
|
+
import type * as t from '@babel/types';
|
|
3
4
|
import {
|
|
4
5
|
resolveLinkedAlias,
|
|
5
6
|
resolveLinkedDir,
|
|
@@ -19,9 +20,10 @@ const traverse = (_traverse as any).default ?? _traverse;
|
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* The UDS loader's only build-time job: rewrite alias-prefixed local
|
|
22
|
-
* imports of registered UDS components from
|
|
23
|
-
* (the authoring file) to
|
|
24
|
-
* emitted per-source-file module
|
|
23
|
+
* imports and re-exports of registered UDS components from
|
|
24
|
+
* `@/uds/components/<seg>` (the authoring file) to
|
|
25
|
+
* `'#uds/components/<seg>'` (the codegen-emitted per-source-file module
|
|
26
|
+
* at `.uds/components/<seg>.tsx`).
|
|
25
27
|
*
|
|
26
28
|
* The authoring file is read only by codegen — production renders the
|
|
27
29
|
* downleveled FC. Per-file targets keep Next.js Fast Refresh surgical:
|
|
@@ -102,8 +104,10 @@ const transformLoader: LoaderDefinitionFunction = function (source) {
|
|
|
102
104
|
|
|
103
105
|
// Cheap pre-filter: only parse files that look like they import or
|
|
104
106
|
// reference a UDS component. Avoids babel overhead on the bulk of
|
|
105
|
-
// application files.
|
|
106
|
-
|
|
107
|
+
// application files. An `export * from '@/uds/components/box'` names no
|
|
108
|
+
// component, so the path itself also counts.
|
|
109
|
+
let hasComponentReference =
|
|
110
|
+
source.includes(linkedAlias) || source.includes('/uds/components/');
|
|
107
111
|
for (const name of projection.componentNames) {
|
|
108
112
|
if (source.includes(name)) {
|
|
109
113
|
hasComponentReference = true;
|
|
@@ -134,7 +138,7 @@ const transformLoader: LoaderDefinitionFunction = function (source) {
|
|
|
134
138
|
|
|
135
139
|
// Collected source-literal rewrites, keyed by character offset.
|
|
136
140
|
// We splice these into the original `source` string rather than
|
|
137
|
-
// regenerating the file: the loader's only job is to swap
|
|
141
|
+
// regenerating the file: the loader's only job is to swap module
|
|
138
142
|
// source strings, and re-printing the whole file through
|
|
139
143
|
// @babel/generator (esp. with `retainLines`) can mangle dense JSX
|
|
140
144
|
// into invalid output. Surgical splicing keeps every byte outside
|
|
@@ -142,110 +146,127 @@ const transformLoader: LoaderDefinitionFunction = function (source) {
|
|
|
142
146
|
// valid file into a broken one.
|
|
143
147
|
const edits: Array<{ start: number; end: number; value: string }> = [];
|
|
144
148
|
|
|
149
|
+
/**
|
|
150
|
+
* Rewrites one import or re-export source to the generated module it
|
|
151
|
+
* runs as. `names` are the value bindings the statement takes from that
|
|
152
|
+
* module, as the module spells them.
|
|
153
|
+
*/
|
|
154
|
+
const rewriteSource = ({
|
|
155
|
+
sourceNode,
|
|
156
|
+
names,
|
|
157
|
+
}: {
|
|
158
|
+
sourceNode: t.StringLiteral;
|
|
159
|
+
names: string[];
|
|
160
|
+
}) => {
|
|
161
|
+
const { start, end, value: moduleSource } = sourceNode;
|
|
162
|
+
if (typeof start !== 'number' || typeof end !== 'number') return;
|
|
163
|
+
|
|
164
|
+
// Splice over the literal's character range (quotes included) using
|
|
165
|
+
// the original quote char so the rest of the file is untouched.
|
|
166
|
+
const quote = source[start] ?? "'";
|
|
167
|
+
const splice = (target: string) => {
|
|
168
|
+
edits.push({ start, end, value: `${quote}${target}${quote}` });
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// A linked system's source, imported by its alias for the types (`@/uds/linked/yos/avatar`),
|
|
172
|
+
// runs as the module the build emitted from it — the same path under `components/_linked/`.
|
|
173
|
+
if (moduleSource.startsWith(linkedAlias)) {
|
|
174
|
+
splice(
|
|
175
|
+
`${outputAlias}/components/_linked/${moduleSource.slice(linkedAlias.length)}`,
|
|
176
|
+
);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Only rewrite alias-prefixed local imports (e.g.
|
|
181
|
+
// `@/uds/components/<name>`, `~/uds/components/<name>`).
|
|
182
|
+
// Relative imports (`./text`, `../box`) belong to the authoring
|
|
183
|
+
// chain where each `defineComponent(...)` may reference its
|
|
184
|
+
// siblings (`defineComponent(Text).config(...)`) — rewriting those
|
|
185
|
+
// would break codegen's read of the configs.
|
|
186
|
+
if (moduleSource.startsWith('.') || moduleSource.startsWith('/')) return;
|
|
187
|
+
|
|
188
|
+
// Constrain to `*/uds/components/<name>` paths so unrelated
|
|
189
|
+
// imports whose tail happens to match a registered name
|
|
190
|
+
// (`next/image` → `Image`) don't get rewritten.
|
|
191
|
+
const segmentMatch = moduleSource.match(/\/uds\/components\/([^/]+)$/);
|
|
192
|
+
if (!segmentMatch) return;
|
|
193
|
+
|
|
194
|
+
const segment = segmentMatch[1];
|
|
195
|
+
|
|
196
|
+
// First-pass match: any name matches a registered component. The
|
|
197
|
+
// common case — `import { Box } from '@/uds/components/box'`.
|
|
198
|
+
//
|
|
199
|
+
// Second-pass match: the path points at the authoring file of a
|
|
200
|
+
// registered component (e.g. `@/uds/components/sidebar`) but the
|
|
201
|
+
// visible names are co-located helpers like `SidebarProvider` /
|
|
202
|
+
// `useSidebar`. If we let this resolve to the source file, the helper
|
|
203
|
+
// sets up a different `SidebarContext` than the codegen-emitted
|
|
204
|
+
// `Sidebar` reads — provider and consumer end up with different
|
|
205
|
+
// context identities and `useSidebar()` throws. Funnel every export
|
|
206
|
+
// from that file through the codegen module so helper and component
|
|
207
|
+
// share the same module.
|
|
208
|
+
const pascal = segment
|
|
209
|
+
.split('-')
|
|
210
|
+
.filter(Boolean)
|
|
211
|
+
.map((s) => s.charAt(0).toUpperCase() + s.slice(1))
|
|
212
|
+
.join('');
|
|
213
|
+
const namesComponent = names.some((name) =>
|
|
214
|
+
projection.componentNames.has(name),
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
if (!namesComponent && !projection.componentNames.has(pascal)) return;
|
|
218
|
+
|
|
219
|
+
// Per-file rewrite: keep the trailing `<seg>` so each import resolves
|
|
220
|
+
// to its own downleveled module at `.uds/components/<seg>.tsx`.
|
|
221
|
+
splice(`${outputAlias}/components/${segment}`);
|
|
222
|
+
};
|
|
223
|
+
|
|
145
224
|
traverse(ast, {
|
|
146
|
-
// -----------------------------------------------------------------------
|
|
147
|
-
// Import rewrite: alias-prefixed local imports of registered UDS
|
|
148
|
-
// components → `#uds/components/<seg>` (the per-source-file
|
|
149
|
-
// codegen-emitted module).
|
|
150
|
-
// -----------------------------------------------------------------------
|
|
151
225
|
// biome-ignore lint/suspicious/noExplicitAny: untyped babel traverse visitor
|
|
152
226
|
ImportDeclaration(nodePath: any) {
|
|
153
|
-
const
|
|
154
|
-
if (!importSource) return;
|
|
227
|
+
const names: string[] = [];
|
|
155
228
|
|
|
156
|
-
|
|
157
|
-
// runs as the module the build emitted from it — the same path under `components/_linked/`.
|
|
158
|
-
if (importSource.startsWith(linkedAlias)) {
|
|
159
|
-
const sourceNode = nodePath.node.source;
|
|
229
|
+
for (const specifier of nodePath.node.specifiers) {
|
|
160
230
|
if (
|
|
161
|
-
|
|
162
|
-
|
|
231
|
+
specifier.type === 'ImportSpecifier' &&
|
|
232
|
+
specifier.imported?.type === 'Identifier'
|
|
163
233
|
) {
|
|
164
|
-
|
|
234
|
+
names.push(specifier.imported.name);
|
|
165
235
|
}
|
|
166
|
-
const quote = source[sourceNode.start] ?? "'";
|
|
167
|
-
edits.push({
|
|
168
|
-
start: sourceNode.start,
|
|
169
|
-
end: sourceNode.end,
|
|
170
|
-
value: `${quote}${outputAlias}/components/_linked/${importSource.slice(linkedAlias.length)}${quote}`,
|
|
171
|
-
});
|
|
172
|
-
return;
|
|
173
236
|
}
|
|
174
237
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
// Relative imports (`./text`, `../box`) belong to the authoring
|
|
178
|
-
// chain where each `defineComponent(...)` may reference its
|
|
179
|
-
// siblings (`defineComponent(Text).config(...)`) — rewriting those
|
|
180
|
-
// would break codegen's read of the configs.
|
|
181
|
-
const isAliasedLocalImport =
|
|
182
|
-
!importSource.startsWith('.') && !importSource.startsWith('/');
|
|
183
|
-
if (!isAliasedLocalImport) return;
|
|
184
|
-
|
|
185
|
-
// First-pass match: any specifier name matches a registered
|
|
186
|
-
// component. The common case — `import { Box } from
|
|
187
|
-
// '@/uds/components/box'`.
|
|
188
|
-
let shouldRewriteSource = false;
|
|
189
|
-
for (const specifier of nodePath.node.specifiers) {
|
|
190
|
-
if (specifier.type !== 'ImportSpecifier') continue;
|
|
191
|
-
const importedName: string | undefined =
|
|
192
|
-
specifier.imported?.type === 'Identifier'
|
|
193
|
-
? specifier.imported.name
|
|
194
|
-
: undefined;
|
|
195
|
-
if (!importedName) continue;
|
|
238
|
+
rewriteSource({ sourceNode: nodePath.node.source, names });
|
|
239
|
+
},
|
|
196
240
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
241
|
+
// `export { Box } from '…'` runs the same module an import of `Box`
|
|
242
|
+
// would. `export type { … } from '…'` is erased before it runs, and
|
|
243
|
+
// stays pointed at the source its types come from.
|
|
244
|
+
// biome-ignore lint/suspicious/noExplicitAny: untyped babel traverse visitor
|
|
245
|
+
ExportNamedDeclaration(nodePath: any) {
|
|
246
|
+
const { node } = nodePath;
|
|
247
|
+
if (!node.source || node.exportKind === 'type') return;
|
|
201
248
|
|
|
202
|
-
|
|
203
|
-
// file of a registered component (e.g. `@/uds/components/sidebar`)
|
|
204
|
-
// but the visible specifiers are co-located helpers like
|
|
205
|
-
// `SidebarProvider` / `useSidebar`. If we let this resolve to the
|
|
206
|
-
// source file, the helper sets up a different `SidebarContext`
|
|
207
|
-
// than the codegen-emitted `Sidebar` reads — provider and consumer
|
|
208
|
-
// end up with different context identities and `useSidebar()`
|
|
209
|
-
// throws. Funnel every export from that file through the codegen
|
|
210
|
-
// module so helper AND component share the same module.
|
|
211
|
-
//
|
|
212
|
-
// Constrain to `*/uds/components/<name>` paths so unrelated
|
|
213
|
-
// imports whose tail happens to match a registered name
|
|
214
|
-
// (`next/image` → `Image`) don't get rewritten.
|
|
215
|
-
const segmentMatch = importSource.match(/\/uds\/components\/([^/]+)$/);
|
|
216
|
-
if (!shouldRewriteSource && segmentMatch) {
|
|
217
|
-
const tail = segmentMatch[1];
|
|
218
|
-
const pascal = tail
|
|
219
|
-
.split('-')
|
|
220
|
-
.filter(Boolean)
|
|
221
|
-
.map((s) => s.charAt(0).toUpperCase() + s.slice(1))
|
|
222
|
-
.join('');
|
|
223
|
-
if (projection.componentNames.has(pascal)) {
|
|
224
|
-
shouldRewriteSource = true;
|
|
225
|
-
}
|
|
226
|
-
}
|
|
249
|
+
const names: string[] = [];
|
|
227
250
|
|
|
228
|
-
|
|
229
|
-
// Per-file rewrite: keep the trailing `<seg>` so each import
|
|
230
|
-
// resolves to its own downleveled module at
|
|
231
|
-
// `.uds/components/<seg>.tsx`. Splice over the literal's
|
|
232
|
-
// character range (quotes included) using the original quote
|
|
233
|
-
// char so the rest of the file is untouched.
|
|
234
|
-
const sourceNode = nodePath.node.source;
|
|
251
|
+
for (const specifier of node.specifiers) {
|
|
235
252
|
if (
|
|
236
|
-
|
|
237
|
-
|
|
253
|
+
specifier.type === 'ExportSpecifier' &&
|
|
254
|
+
specifier.exportKind !== 'type' &&
|
|
255
|
+
specifier.local?.type === 'Identifier'
|
|
238
256
|
) {
|
|
239
|
-
|
|
257
|
+
names.push(specifier.local.name);
|
|
240
258
|
}
|
|
241
|
-
|
|
242
|
-
const quote = source[sourceNode.start] ?? "'";
|
|
243
|
-
edits.push({
|
|
244
|
-
start: sourceNode.start,
|
|
245
|
-
end: sourceNode.end,
|
|
246
|
-
value: `${quote}${outputAlias}/components/${segmentMatch[1]}${quote}`,
|
|
247
|
-
});
|
|
248
259
|
}
|
|
260
|
+
|
|
261
|
+
rewriteSource({ sourceNode: node.source, names });
|
|
262
|
+
},
|
|
263
|
+
|
|
264
|
+
// biome-ignore lint/suspicious/noExplicitAny: untyped babel traverse visitor
|
|
265
|
+
ExportAllDeclaration(nodePath: any) {
|
|
266
|
+
const { node } = nodePath;
|
|
267
|
+
if (node.exportKind === 'type') return;
|
|
268
|
+
|
|
269
|
+
rewriteSource({ sourceNode: node.source, names: [] });
|
|
249
270
|
},
|
|
250
271
|
});
|
|
251
272
|
|