@yahoo/uds-create-loader 1.1.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/_virtual/_rolldown/runtime.js +5 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/inspect-transform.d.ts +1 -0
- package/dist/inspect-transform.js +63 -0
- package/dist/loader/artifacts.d.ts +30 -0
- package/dist/loader/artifacts.js +60 -0
- package/dist/loader/babel-utils.d.ts +9 -0
- package/dist/loader/babel-utils.js +29 -0
- package/dist/loader.d.ts +22 -0
- package/dist/loader.js +87 -0
- package/dist/next.d.ts +16 -0
- package/dist/next.js +77 -0
- package/dist/transform-runner.d.ts +27 -0
- package/dist/transform-runner.js +200 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/unplugin.d.ts +26 -0
- package/dist/unplugin.js +52 -0
- package/package.json +65 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import transformLoader from "./loader.js";
|
|
2
|
+
import { LoaderTransformOutputSpecs, LoaderTransformResult, RunLoaderTransformOptions, getLoaderTransformOutputSpecs, runLoaderTransform } from "./transform-runner.js";
|
|
3
|
+
import { UdsPluginOptions, udsPlugin } from "./unplugin.js";
|
|
4
|
+
export { type LoaderTransformOutputSpecs, type LoaderTransformResult, type RunLoaderTransformOptions, type UdsPluginOptions, getLoaderTransformOutputSpecs, runLoaderTransform, transformLoader, udsPlugin };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
import { runLoaderTransform } from './transform-runner';
|
|
5
|
+
function readFlag(flag) {
|
|
6
|
+
const index = process.argv.indexOf(flag);
|
|
7
|
+
if (index === -1)
|
|
8
|
+
return null;
|
|
9
|
+
const value = process.argv[index + 1];
|
|
10
|
+
if (!value || value.startsWith('-'))
|
|
11
|
+
return null;
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
function printUsage() {
|
|
15
|
+
console.error([
|
|
16
|
+
'Usage:',
|
|
17
|
+
' bun run src/inspect-transform.ts --file <path-to-tsx> --config <path-to-uds.config.ts>',
|
|
18
|
+
'',
|
|
19
|
+
'Example:',
|
|
20
|
+
' bun run --filter @yahoo/uds-create-loader inspect:transform -- --file packages/components/src/accordion.tsx --config apps/docs/uds.config.ts',
|
|
21
|
+
].join('\n'));
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
async function loadUdsConfig(configPath) {
|
|
25
|
+
const resolvedPath = path.resolve(configPath);
|
|
26
|
+
const configUrl = pathToFileURL(resolvedPath).href;
|
|
27
|
+
const module = (await import(configUrl));
|
|
28
|
+
if (module.default !== undefined) {
|
|
29
|
+
return module.default;
|
|
30
|
+
}
|
|
31
|
+
if (module.udsConfig !== undefined) {
|
|
32
|
+
return module.udsConfig;
|
|
33
|
+
}
|
|
34
|
+
return module;
|
|
35
|
+
}
|
|
36
|
+
async function main() {
|
|
37
|
+
const sourcePath = readFlag('--file');
|
|
38
|
+
const configPath = readFlag('--config');
|
|
39
|
+
if (!sourcePath || !configPath) {
|
|
40
|
+
printUsage();
|
|
41
|
+
}
|
|
42
|
+
const sourceAbsolutePath = path.resolve(sourcePath);
|
|
43
|
+
if (!fs.existsSync(sourceAbsolutePath)) {
|
|
44
|
+
throw new Error(`Source file not found: ${sourceAbsolutePath}`);
|
|
45
|
+
}
|
|
46
|
+
const rawConfig = await loadUdsConfig(configPath);
|
|
47
|
+
const config = rawConfig.toJSON();
|
|
48
|
+
const source = fs.readFileSync(sourceAbsolutePath, 'utf8');
|
|
49
|
+
const result = runLoaderTransform(source, {
|
|
50
|
+
config,
|
|
51
|
+
resourcePath: sourceAbsolutePath,
|
|
52
|
+
isDev: false,
|
|
53
|
+
});
|
|
54
|
+
if ('error' in result) {
|
|
55
|
+
throw new Error(result.error);
|
|
56
|
+
}
|
|
57
|
+
process.stdout.write(result.output);
|
|
58
|
+
}
|
|
59
|
+
void main().catch((error) => {
|
|
60
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
61
|
+
console.error(`[inspect-transform] ${message}`);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { SerializedConfig } from '@yahoo/uds-create-config';
|
|
2
|
+
export interface LoaderOptions {
|
|
3
|
+
/** Path to `.uds/manifest.json`. The loader extracts `.config` at the
|
|
4
|
+
* read boundary; the manifest's wrapper fields (`catalogMeta`,
|
|
5
|
+
* `sourceMap`, …) exist for Studio/MCP and aren't read here. */
|
|
6
|
+
dataPath: string;
|
|
7
|
+
isDev: boolean;
|
|
8
|
+
/** Skip addDependency calls to prevent webpack from watching .uds/ artifacts.
|
|
9
|
+
* Use this when the app has its own reload mechanism (e.g., Studio WebSocket). */
|
|
10
|
+
skipWatchDependencies?: boolean;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Derived projection of one loaded config — built lazily on first use,
|
|
14
|
+
* cached by config object identity so subsequent transforms reuse the
|
|
15
|
+
* same set. Carries the registered-component-name set the loader uses
|
|
16
|
+
* for the pre-filter and the import-rewrite visitor's eligibility check.
|
|
17
|
+
*/
|
|
18
|
+
export interface LoaderProjection {
|
|
19
|
+
readonly componentNames: ReadonlySet<string>;
|
|
20
|
+
}
|
|
21
|
+
export declare const loadUdsConfig: (filePath: string, isDev: boolean) => SerializedConfig | null;
|
|
22
|
+
export declare const seedUdsConfig: (filePath: string, config: SerializedConfig) => void;
|
|
23
|
+
/**
|
|
24
|
+
* Build (or return a cached) projection of the config for the loader's
|
|
25
|
+
* hot path. Keyed by config object identity. The cache survives across
|
|
26
|
+
* transforms within one process; reloading `.uds/manifest.json`
|
|
27
|
+
* produces a fresh object, which transparently invalidates the
|
|
28
|
+
* projection.
|
|
29
|
+
*/
|
|
30
|
+
export declare function getLoaderProjection(config: SerializedConfig): LoaderProjection;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
//#region src/loader/artifacts.ts
|
|
3
|
+
/**
|
|
4
|
+
* `.uds/config.json` IS the serialized config — no envelope. Parse and
|
|
5
|
+
* cache by file mtime so subsequent transforms reuse the same object.
|
|
6
|
+
*/
|
|
7
|
+
function createCachedJsonLoader() {
|
|
8
|
+
let cached = null;
|
|
9
|
+
let cachedPath = null;
|
|
10
|
+
let cachedMtime = 0;
|
|
11
|
+
return (filePath, isDev) => {
|
|
12
|
+
try {
|
|
13
|
+
const mtime = fs.statSync(filePath).mtimeMs;
|
|
14
|
+
if (cached && cachedPath === filePath && (!isDev || cachedMtime === mtime)) return cached;
|
|
15
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
16
|
+
cached = JSON.parse(content);
|
|
17
|
+
cachedPath = filePath;
|
|
18
|
+
cachedMtime = mtime;
|
|
19
|
+
return cached;
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function createCachedJsonLoaderWithSeed() {
|
|
26
|
+
const loader = createCachedJsonLoader();
|
|
27
|
+
let seededPath = null;
|
|
28
|
+
let seededData = null;
|
|
29
|
+
return {
|
|
30
|
+
load: (filePath, isDev) => {
|
|
31
|
+
if (seededPath === filePath && seededData) return seededData;
|
|
32
|
+
return loader(filePath, isDev);
|
|
33
|
+
},
|
|
34
|
+
seed: (filePath, config) => {
|
|
35
|
+
seededPath = filePath;
|
|
36
|
+
seededData = config;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
const configLoader = createCachedJsonLoaderWithSeed();
|
|
41
|
+
const loadUdsConfig = configLoader.load;
|
|
42
|
+
const seedUdsConfig = configLoader.seed;
|
|
43
|
+
const projectionCache = /* @__PURE__ */ new WeakMap();
|
|
44
|
+
/**
|
|
45
|
+
* Build (or return a cached) projection of the config for the loader's
|
|
46
|
+
* hot path. Keyed by config object identity. The cache survives across
|
|
47
|
+
* transforms within one process; reloading `.uds/manifest.json`
|
|
48
|
+
* produces a fresh object, which transparently invalidates the
|
|
49
|
+
* projection.
|
|
50
|
+
*/
|
|
51
|
+
function getLoaderProjection(config) {
|
|
52
|
+
const cached = projectionCache.get(config);
|
|
53
|
+
if (cached) return cached;
|
|
54
|
+
const components = config.components ?? {};
|
|
55
|
+
const projection = { componentNames: new Set(Object.keys(components)) };
|
|
56
|
+
projectionCache.set(config, projection);
|
|
57
|
+
return projection;
|
|
58
|
+
}
|
|
59
|
+
//#endregion
|
|
60
|
+
export { getLoaderProjection, loadUdsConfig, seedUdsConfig };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import * as t from '@babel/types';
|
|
2
|
+
/**
|
|
3
|
+
* Check whether a callee expression is a compiled JSX call.
|
|
4
|
+
*
|
|
5
|
+
* Matches:
|
|
6
|
+
* - Direct identifiers: `jsx(...)`, `_jsxs(...)`, `jsxDEV(...)`, etc.
|
|
7
|
+
* - Member expressions: `React.jsx(...)`, `_reactJsxRuntime.jsxs(...)`, etc.
|
|
8
|
+
*/
|
|
9
|
+
export declare function isCompiledJsxCallee(node: t.Expression | t.V8IntrinsicIdentifier | null | undefined): boolean;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import * as t from "@babel/types";
|
|
2
|
+
//#region src/loader/babel-utils.ts
|
|
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
|
+
* Check whether a callee expression is a compiled JSX call.
|
|
14
|
+
*
|
|
15
|
+
* Matches:
|
|
16
|
+
* - Direct identifiers: `jsx(...)`, `_jsxs(...)`, `jsxDEV(...)`, etc.
|
|
17
|
+
* - Member expressions: `React.jsx(...)`, `_reactJsxRuntime.jsxs(...)`, etc.
|
|
18
|
+
*/
|
|
19
|
+
function isCompiledJsxCallee(node) {
|
|
20
|
+
if (!node) return false;
|
|
21
|
+
if (t.isIdentifier(node)) return COMPILED_JSX_NAMES.has(node.name);
|
|
22
|
+
if (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) {
|
|
23
|
+
if (!node.computed && t.isIdentifier(node.property)) return COMPILED_JSX_NAMES.has(node.property.name);
|
|
24
|
+
if (node.computed && t.isStringLiteral(node.property)) return COMPILED_JSX_NAMES.has(node.property.value);
|
|
25
|
+
}
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
//#endregion
|
|
29
|
+
export { isCompiledJsxCallee };
|
package/dist/loader.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { LoaderDefinitionFunction } from "webpack";
|
|
2
|
+
|
|
3
|
+
//#region src/loader.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* The UDS loader's only build-time job: rewrite alias-prefixed local
|
|
6
|
+
* imports of registered UDS components from `@/uds/components/<seg>`
|
|
7
|
+
* (the authoring file) to `'#uds/components/<seg>'` (the codegen-
|
|
8
|
+
* emitted per-source-file module at `.uds/components/<seg>.tsx`).
|
|
9
|
+
*
|
|
10
|
+
* The authoring file is read only by codegen — production renders the
|
|
11
|
+
* downleveled FC. Per-file targets keep Next.js Fast Refresh surgical:
|
|
12
|
+
* editing one authoring source only invalidates that one downleveled
|
|
13
|
+
* module.
|
|
14
|
+
*
|
|
15
|
+
* No JSX-site work: composite / variant / bool / style-prop resolution
|
|
16
|
+
* and motion wiring are owned by the codegen-emitted FC. The loader
|
|
17
|
+
* only changes import sources.
|
|
18
|
+
*/
|
|
19
|
+
declare const transformLoader: LoaderDefinitionFunction;
|
|
20
|
+
declare const normal: LoaderDefinitionFunction<{}, {}>;
|
|
21
|
+
//#endregion
|
|
22
|
+
export { transformLoader as default, normal };
|
package/dist/loader.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { getLoaderProjection, loadUdsConfig } from "./loader/artifacts.js";
|
|
2
|
+
import { parse } from "@babel/parser";
|
|
3
|
+
import _traverse from "@babel/traverse";
|
|
4
|
+
//#region src/loader.ts
|
|
5
|
+
const traverse = _traverse.default ?? _traverse;
|
|
6
|
+
/**
|
|
7
|
+
* The UDS loader's only build-time job: rewrite alias-prefixed local
|
|
8
|
+
* imports of registered UDS components from `@/uds/components/<seg>`
|
|
9
|
+
* (the authoring file) to `'#uds/components/<seg>'` (the codegen-
|
|
10
|
+
* emitted per-source-file module at `.uds/components/<seg>.tsx`).
|
|
11
|
+
*
|
|
12
|
+
* The authoring file is read only by codegen — production renders the
|
|
13
|
+
* downleveled FC. Per-file targets keep Next.js Fast Refresh surgical:
|
|
14
|
+
* editing one authoring source only invalidates that one downleveled
|
|
15
|
+
* module.
|
|
16
|
+
*
|
|
17
|
+
* No JSX-site work: composite / variant / bool / style-prop resolution
|
|
18
|
+
* and motion wiring are owned by the codegen-emitted FC. The loader
|
|
19
|
+
* only changes import sources.
|
|
20
|
+
*/
|
|
21
|
+
const transformLoader = function(source) {
|
|
22
|
+
const loaderContext = this ?? void 0;
|
|
23
|
+
if (!loaderContext?.getOptions) return source;
|
|
24
|
+
const { dataPath, isDev, skipWatchDependencies } = loaderContext.getOptions();
|
|
25
|
+
const resourcePath = loaderContext.resourcePath ?? "";
|
|
26
|
+
if (resourcePath.includes(".uds/")) return source;
|
|
27
|
+
if (!skipWatchDependencies) loaderContext.addDependency?.(dataPath);
|
|
28
|
+
const config = loadUdsConfig(dataPath, isDev);
|
|
29
|
+
if (!config) return source;
|
|
30
|
+
const projection = getLoaderProjection(config);
|
|
31
|
+
let hasComponentReference = false;
|
|
32
|
+
for (const name of projection.componentNames) if (source.includes(name)) {
|
|
33
|
+
hasComponentReference = true;
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
if (!hasComponentReference) return source;
|
|
37
|
+
const isTS = resourcePath?.endsWith(".ts") || resourcePath?.endsWith(".tsx");
|
|
38
|
+
const isJSX = resourcePath?.endsWith(".tsx") || resourcePath?.endsWith(".jsx");
|
|
39
|
+
const parserPlugins = [];
|
|
40
|
+
if (isJSX || !isTS) parserPlugins.push("jsx");
|
|
41
|
+
if (isTS) parserPlugins.push("typescript");
|
|
42
|
+
let ast;
|
|
43
|
+
try {
|
|
44
|
+
ast = parse(source, {
|
|
45
|
+
sourceType: "module",
|
|
46
|
+
plugins: parserPlugins
|
|
47
|
+
});
|
|
48
|
+
} catch {
|
|
49
|
+
return source;
|
|
50
|
+
}
|
|
51
|
+
const edits = [];
|
|
52
|
+
traverse(ast, { ImportDeclaration(nodePath) {
|
|
53
|
+
const importSource = nodePath.node.source?.value;
|
|
54
|
+
if (!importSource) return;
|
|
55
|
+
if (!(!importSource.startsWith(".") && !importSource.startsWith("/"))) return;
|
|
56
|
+
let shouldRewriteSource = false;
|
|
57
|
+
for (const specifier of nodePath.node.specifiers) {
|
|
58
|
+
if (specifier.type !== "ImportSpecifier") continue;
|
|
59
|
+
const importedName = specifier.imported?.type === "Identifier" ? specifier.imported.name : void 0;
|
|
60
|
+
if (!importedName) continue;
|
|
61
|
+
if (projection.componentNames.has(importedName)) shouldRewriteSource = true;
|
|
62
|
+
}
|
|
63
|
+
const segmentMatch = importSource.match(/\/uds\/components\/([^/]+)$/);
|
|
64
|
+
if (!shouldRewriteSource && segmentMatch) {
|
|
65
|
+
const pascal = segmentMatch[1].split("-").filter(Boolean).map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join("");
|
|
66
|
+
if (projection.componentNames.has(pascal)) shouldRewriteSource = true;
|
|
67
|
+
}
|
|
68
|
+
if (shouldRewriteSource && segmentMatch) {
|
|
69
|
+
const sourceNode = nodePath.node.source;
|
|
70
|
+
if (typeof sourceNode.start !== "number" || typeof sourceNode.end !== "number") return;
|
|
71
|
+
const quote = source[sourceNode.start] ?? "'";
|
|
72
|
+
edits.push({
|
|
73
|
+
start: sourceNode.start,
|
|
74
|
+
end: sourceNode.end,
|
|
75
|
+
value: `${quote}#uds/components/${segmentMatch[1]}${quote}`
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
} });
|
|
79
|
+
if (edits.length === 0) return source;
|
|
80
|
+
edits.sort((a, b) => b.start - a.start);
|
|
81
|
+
let output = source;
|
|
82
|
+
for (const edit of edits) output = output.slice(0, edit.start) + edit.value + output.slice(edit.end);
|
|
83
|
+
return output;
|
|
84
|
+
};
|
|
85
|
+
const normal = transformLoader;
|
|
86
|
+
//#endregion
|
|
87
|
+
export { transformLoader as default, normal };
|
package/dist/next.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { NextConfig } from "next";
|
|
2
|
+
|
|
3
|
+
//#region src/next.d.ts
|
|
4
|
+
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
|
+
type NextConfigFn = (phase: string, ctx: {
|
|
12
|
+
defaultConfig: NextConfig;
|
|
13
|
+
}) => NextConfig | Promise<NextConfig>;
|
|
14
|
+
declare function withUds(nextConfigOrFn: NextConfig | NextConfigFn, options?: WithUdsOptions): NextConfig | NextConfigFn;
|
|
15
|
+
//#endregion
|
|
16
|
+
export { WithUdsOptions, withUds };
|
package/dist/next.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { __require } from "./_virtual/_rolldown/runtime.js";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
//#region src/next.ts
|
|
4
|
+
function resolveLoaderPath() {
|
|
5
|
+
return __require.resolve("@yahoo/uds-create-loader/loader");
|
|
6
|
+
}
|
|
7
|
+
function applyUds(nextConfig, options) {
|
|
8
|
+
const outputDir = options?.outputDir ?? ".uds";
|
|
9
|
+
const skipWatch = options?.skipWatchDependencies ?? false;
|
|
10
|
+
const dataPath = path.resolve(process.cwd(), outputDir, "config.json");
|
|
11
|
+
const loaderPath = resolveLoaderPath();
|
|
12
|
+
const turbopack = nextConfig.turbopack ?? {};
|
|
13
|
+
const existingRules = turbopack.rules ?? {};
|
|
14
|
+
const udsLoader = {
|
|
15
|
+
loader: loaderPath,
|
|
16
|
+
options: {
|
|
17
|
+
dataPath,
|
|
18
|
+
isDev: true,
|
|
19
|
+
skipWatchDependencies: skipWatch
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
const udsRules = {};
|
|
23
|
+
for (const ext of [
|
|
24
|
+
"tsx",
|
|
25
|
+
"ts",
|
|
26
|
+
"js"
|
|
27
|
+
]) {
|
|
28
|
+
const key = `*.${ext}`;
|
|
29
|
+
const udsKey = `**/*.${ext}`;
|
|
30
|
+
if (existingRules[key]) udsRules[udsKey] = { loaders: [udsLoader] };
|
|
31
|
+
else udsRules[key] = { loaders: [udsLoader] };
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
...nextConfig,
|
|
35
|
+
turbopack: {
|
|
36
|
+
...turbopack,
|
|
37
|
+
rules: {
|
|
38
|
+
...existingRules,
|
|
39
|
+
...udsRules
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
webpack(config, webpackOptions) {
|
|
43
|
+
config.module?.rules?.push({
|
|
44
|
+
test: /\.(tsx|ts|js)$/,
|
|
45
|
+
use: [{
|
|
46
|
+
loader: loaderPath,
|
|
47
|
+
options: {
|
|
48
|
+
dataPath,
|
|
49
|
+
isDev: webpackOptions.dev,
|
|
50
|
+
skipWatchDependencies: skipWatch
|
|
51
|
+
}
|
|
52
|
+
}]
|
|
53
|
+
});
|
|
54
|
+
if (skipWatch) {
|
|
55
|
+
const existing = config.watchOptions?.ignored;
|
|
56
|
+
config.watchOptions = {
|
|
57
|
+
...config.watchOptions,
|
|
58
|
+
ignored: [
|
|
59
|
+
...Array.isArray(existing) ? existing : existing ? [existing] : [],
|
|
60
|
+
"**/.uds/**",
|
|
61
|
+
"**/uds.config.ts"
|
|
62
|
+
]
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (typeof nextConfig.webpack === "function") return nextConfig.webpack(config, webpackOptions);
|
|
66
|
+
return config;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function withUds(nextConfigOrFn, options) {
|
|
71
|
+
if (typeof nextConfigOrFn === "function") return async (phase, ctx) => {
|
|
72
|
+
return applyUds(await nextConfigOrFn(phase, ctx), options);
|
|
73
|
+
};
|
|
74
|
+
return applyUds(nextConfigOrFn, options);
|
|
75
|
+
}
|
|
76
|
+
//#endregion
|
|
77
|
+
export { withUds };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { SerializedConfig } from "@yahoo/uds-create-config";
|
|
2
|
+
|
|
3
|
+
//#region src/transform-runner.d.ts
|
|
4
|
+
interface LoaderTransformOutputSpecs {
|
|
5
|
+
outputGzipBytes: number;
|
|
6
|
+
classNameAssignments: number;
|
|
7
|
+
utilityClassTokens: number;
|
|
8
|
+
uniqueUtilityClassTokens: number;
|
|
9
|
+
runtimeStyleParserCalls: number;
|
|
10
|
+
}
|
|
11
|
+
interface LoaderTransformSuccess {
|
|
12
|
+
output: string;
|
|
13
|
+
specs: LoaderTransformOutputSpecs;
|
|
14
|
+
}
|
|
15
|
+
interface LoaderTransformFailure {
|
|
16
|
+
error: string;
|
|
17
|
+
}
|
|
18
|
+
type LoaderTransformResult = LoaderTransformSuccess | LoaderTransformFailure;
|
|
19
|
+
interface RunLoaderTransformOptions {
|
|
20
|
+
config: SerializedConfig;
|
|
21
|
+
resourcePath?: string;
|
|
22
|
+
isDev?: boolean;
|
|
23
|
+
}
|
|
24
|
+
declare function getLoaderTransformOutputSpecs(output: string): LoaderTransformOutputSpecs;
|
|
25
|
+
declare function runLoaderTransform(source: string, options: RunLoaderTransformOptions): LoaderTransformResult;
|
|
26
|
+
//#endregion
|
|
27
|
+
export { LoaderTransformOutputSpecs, LoaderTransformResult, RunLoaderTransformOptions, getLoaderTransformOutputSpecs, runLoaderTransform };
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { seedUdsConfig } from "./loader/artifacts.js";
|
|
2
|
+
import transformLoader from "./loader.js";
|
|
3
|
+
import { isCompiledJsxCallee } from "./loader/babel-utils.js";
|
|
4
|
+
import { parse } from "@babel/parser";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { gzipSync } from "node:zlib";
|
|
7
|
+
import * as t from "@babel/types";
|
|
8
|
+
import { VISITOR_KEYS } from "@babel/types";
|
|
9
|
+
//#region src/transform-runner.ts
|
|
10
|
+
function getClassTokens(value) {
|
|
11
|
+
return value.split(/\s+/).map((token) => token.trim()).filter(Boolean);
|
|
12
|
+
}
|
|
13
|
+
function collectClassTokensFromNode(node, tokens) {
|
|
14
|
+
if (!node) return;
|
|
15
|
+
if (t.isStringLiteral(node)) {
|
|
16
|
+
tokens.push(...getClassTokens(node.value));
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (t.isTemplateLiteral(node)) {
|
|
20
|
+
for (const quasi of node.quasis) tokens.push(...getClassTokens(quasi.value.cooked ?? quasi.value.raw ?? ""));
|
|
21
|
+
for (const expression of node.expressions) collectClassTokensFromNode(expression, tokens);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (t.isJSXExpressionContainer(node)) {
|
|
25
|
+
if (!t.isJSXEmptyExpression(node.expression)) collectClassTokensFromNode(node.expression, tokens);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (t.isParenthesizedExpression(node) || t.isTSAsExpression(node) || t.isTSTypeAssertion(node) || t.isTSNonNullExpression(node)) {
|
|
29
|
+
collectClassTokensFromNode(node.expression, tokens);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (t.isUnaryExpression(node) || t.isUpdateExpression(node)) {
|
|
33
|
+
collectClassTokensFromNode(node.argument, tokens);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (t.isBinaryExpression(node) || t.isLogicalExpression(node) || t.isAssignmentExpression(node)) {
|
|
37
|
+
collectClassTokensFromNode(node.left, tokens);
|
|
38
|
+
collectClassTokensFromNode(node.right, tokens);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (t.isConditionalExpression(node)) {
|
|
42
|
+
collectClassTokensFromNode(node.test, tokens);
|
|
43
|
+
collectClassTokensFromNode(node.consequent, tokens);
|
|
44
|
+
collectClassTokensFromNode(node.alternate, tokens);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (t.isSequenceExpression(node)) {
|
|
48
|
+
for (const expression of node.expressions) collectClassTokensFromNode(expression, tokens);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (t.isArrayExpression(node)) {
|
|
52
|
+
for (const element of node.elements) {
|
|
53
|
+
if (!element) continue;
|
|
54
|
+
if (t.isSpreadElement(element)) {
|
|
55
|
+
collectClassTokensFromNode(element.argument, tokens);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
collectClassTokensFromNode(element, tokens);
|
|
59
|
+
}
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (t.isObjectExpression(node)) {
|
|
63
|
+
for (const property of node.properties) {
|
|
64
|
+
if (t.isSpreadElement(property)) {
|
|
65
|
+
collectClassTokensFromNode(property.argument, tokens);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (t.isObjectProperty(property)) collectClassTokensFromNode(property.value, tokens);
|
|
69
|
+
}
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (t.isCallExpression(node) || t.isOptionalCallExpression(node)) {
|
|
73
|
+
collectClassTokensFromNode(node.callee, tokens);
|
|
74
|
+
for (const argument of node.arguments) {
|
|
75
|
+
if (t.isSpreadElement(argument)) {
|
|
76
|
+
collectClassTokensFromNode(argument.argument, tokens);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
collectClassTokensFromNode(argument, tokens);
|
|
80
|
+
}
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) {
|
|
84
|
+
collectClassTokensFromNode(node.object, tokens);
|
|
85
|
+
if (node.computed) collectClassTokensFromNode(node.property, tokens);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (t.isNewExpression(node)) {
|
|
89
|
+
collectClassTokensFromNode(node.callee, tokens);
|
|
90
|
+
for (const argument of node.arguments ?? []) {
|
|
91
|
+
if (t.isSpreadElement(argument)) {
|
|
92
|
+
collectClassTokensFromNode(argument.argument, tokens);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
collectClassTokensFromNode(argument, tokens);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function isClassNameObjectProperty(node) {
|
|
100
|
+
if (node.computed) return false;
|
|
101
|
+
return t.isIdentifier(node.key) && node.key.name === "className" || t.isStringLiteral(node.key) && node.key.value === "className";
|
|
102
|
+
}
|
|
103
|
+
function isBabelNode(value) {
|
|
104
|
+
return typeof value === "object" && value !== null && "type" in value;
|
|
105
|
+
}
|
|
106
|
+
function isClassNamePropOnCompiledJsxCall(node, parent, grandParent) {
|
|
107
|
+
if (!isClassNameObjectProperty(node)) return false;
|
|
108
|
+
if (!parent || !t.isObjectExpression(parent)) return false;
|
|
109
|
+
if (!grandParent || !t.isCallExpression(grandParent)) return false;
|
|
110
|
+
if (!isCompiledJsxCallee(grandParent.callee)) return false;
|
|
111
|
+
return grandParent.arguments.some((argument) => argument === parent);
|
|
112
|
+
}
|
|
113
|
+
function getClassTokenStats(output) {
|
|
114
|
+
try {
|
|
115
|
+
const ast = parse(output, {
|
|
116
|
+
sourceType: "module",
|
|
117
|
+
plugins: ["jsx", "typescript"]
|
|
118
|
+
});
|
|
119
|
+
const uniqueTokens = /* @__PURE__ */ new Set();
|
|
120
|
+
let total = 0;
|
|
121
|
+
let assignments = 0;
|
|
122
|
+
const addTokens = (node) => {
|
|
123
|
+
const nextTokens = [];
|
|
124
|
+
collectClassTokensFromNode(node, nextTokens);
|
|
125
|
+
total += nextTokens.length;
|
|
126
|
+
for (const token of nextTokens) uniqueTokens.add(token);
|
|
127
|
+
};
|
|
128
|
+
const visit = (node, parent, grandParent) => {
|
|
129
|
+
if (!node) return;
|
|
130
|
+
if (t.isJSXAttribute(node) && t.isJSXIdentifier(node.name) && node.name.name === "className") {
|
|
131
|
+
assignments += 1;
|
|
132
|
+
addTokens(node.value);
|
|
133
|
+
} else if (t.isObjectProperty(node)) {
|
|
134
|
+
if (isClassNamePropOnCompiledJsxCall(node, parent, grandParent)) {
|
|
135
|
+
assignments += 1;
|
|
136
|
+
addTokens(node.value);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const visitorKeys = VISITOR_KEYS[node.type] ?? [];
|
|
140
|
+
const typedNode = node;
|
|
141
|
+
for (const key of visitorKeys) {
|
|
142
|
+
const childValue = typedNode[key];
|
|
143
|
+
if (Array.isArray(childValue)) {
|
|
144
|
+
for (const item of childValue) if (isBabelNode(item)) visit(item, node, parent);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (isBabelNode(childValue)) visit(childValue, node, parent);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
visit(ast.program, null, null);
|
|
151
|
+
return {
|
|
152
|
+
total,
|
|
153
|
+
unique: uniqueTokens.size,
|
|
154
|
+
assignments
|
|
155
|
+
};
|
|
156
|
+
} catch {
|
|
157
|
+
return {
|
|
158
|
+
total: 0,
|
|
159
|
+
unique: 0,
|
|
160
|
+
assignments: (output.match(/className\s*[:=]/g) ?? []).length
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function getRuntimeStyleParserCallCount(output) {
|
|
165
|
+
return [/extractStyleProps\s*\(/g, /getStyles\s*\(/g].reduce((count, pattern) => count + (output.match(pattern) ?? []).length, 0);
|
|
166
|
+
}
|
|
167
|
+
function getLoaderTransformOutputSpecs(output) {
|
|
168
|
+
const classTokenStats = getClassTokenStats(output);
|
|
169
|
+
return {
|
|
170
|
+
outputGzipBytes: gzipSync(output).byteLength,
|
|
171
|
+
classNameAssignments: classTokenStats.assignments,
|
|
172
|
+
utilityClassTokens: classTokenStats.total,
|
|
173
|
+
uniqueUtilityClassTokens: classTokenStats.unique,
|
|
174
|
+
runtimeStyleParserCalls: getRuntimeStyleParserCallCount(output)
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
const VIRTUAL_CONFIG_PATH = "/virtual/uds-config.json";
|
|
178
|
+
function runLoaderTransform(source, options) {
|
|
179
|
+
if (typeof source !== "string") return { error: "`source` must be a string." };
|
|
180
|
+
seedUdsConfig(VIRTUAL_CONFIG_PATH, options.config);
|
|
181
|
+
const context = {
|
|
182
|
+
getOptions: () => ({
|
|
183
|
+
dataPath: VIRTUAL_CONFIG_PATH,
|
|
184
|
+
isDev: options.isDev ?? false
|
|
185
|
+
}),
|
|
186
|
+
addDependency: () => {},
|
|
187
|
+
resourcePath: options.resourcePath ?? path.resolve(process.cwd(), "playground-input.tsx")
|
|
188
|
+
};
|
|
189
|
+
try {
|
|
190
|
+
const output = transformLoader.call(context, source);
|
|
191
|
+
return {
|
|
192
|
+
output,
|
|
193
|
+
specs: getLoaderTransformOutputSpecs(output)
|
|
194
|
+
};
|
|
195
|
+
} catch (error) {
|
|
196
|
+
return { error: error instanceof Error ? error.message : "Transform failed unexpectedly." };
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
//#endregion
|
|
200
|
+
export { getLoaderTransformOutputSpecs, runLoaderTransform };
|