@assistant-ui/metro 0.0.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/LICENSE +21 -0
- package/README.md +89 -0
- package/dist/index.cjs +48 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +45 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/transformer.cjs +2189 -0
- package/dist/transformer.cjs.map +1 -0
- package/dist/transformer.d.cts +21 -0
- package/dist/transformer.d.cts.map +1 -0
- package/package.json +64 -0
- package/src/__fixtures__/mock-upstream.cjs +10 -0
- package/src/index.ts +68 -0
- package/src/transformer.test.ts +101 -0
- package/src/transformer.ts +116 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll } from "vitest";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { withAui, UPSTREAM_TRANSFORMER_ENV } from "./index";
|
|
5
|
+
|
|
6
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const mockUpstream = join(here, "__fixtures__/mock-upstream.cjs");
|
|
8
|
+
|
|
9
|
+
const GENERATIVE = `"use generative";
|
|
10
|
+
import { defineToolkit } from "@assistant-ui/react";
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
import { db } from "@/db";
|
|
13
|
+
export default defineToolkit({
|
|
14
|
+
weather: {
|
|
15
|
+
description: "Show the weather.",
|
|
16
|
+
parameters: z.object({ city: z.string() }),
|
|
17
|
+
execute: async ({ city }) => db.get(city),
|
|
18
|
+
render: () => null,
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
`;
|
|
22
|
+
|
|
23
|
+
describe("withAui", () => {
|
|
24
|
+
it("points babelTransformerPath at our transformer, preserving the rest", () => {
|
|
25
|
+
const config = withAui({
|
|
26
|
+
projectRoot: "/app",
|
|
27
|
+
transformer: {
|
|
28
|
+
babelTransformerPath: "/up/stream.js",
|
|
29
|
+
assetPlugins: ["x"],
|
|
30
|
+
},
|
|
31
|
+
resolver: { sourceExts: ["ts"] },
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
expect(config.transformer?.babelTransformerPath).toContain("transformer");
|
|
35
|
+
expect(config.transformer?.babelTransformerPath).not.toBe("/up/stream.js");
|
|
36
|
+
expect(config.transformer?.assetPlugins).toEqual(["x"]);
|
|
37
|
+
expect(config.resolver).toEqual({ sourceExts: ["ts"] });
|
|
38
|
+
expect(config.projectRoot).toBe("/app");
|
|
39
|
+
// stashes the upstream so the worker can delegate to it
|
|
40
|
+
expect(process.env[UPSTREAM_TRANSFORMER_ENV]).toBe("/up/stream.js");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("is idempotent: a double-wrap keeps the real upstream, not the wrapper", () => {
|
|
44
|
+
const once = withAui({
|
|
45
|
+
transformer: { babelTransformerPath: "/up/stream.js" },
|
|
46
|
+
});
|
|
47
|
+
const self = once.transformer?.babelTransformerPath;
|
|
48
|
+
|
|
49
|
+
const twice = withAui(once);
|
|
50
|
+
// still points at our transformer, and the env still names the real upstream
|
|
51
|
+
expect(twice.transformer?.babelTransformerPath).toBe(self);
|
|
52
|
+
expect(process.env[UPSTREAM_TRANSFORMER_ENV]).toBe("/up/stream.js");
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("transformer", () => {
|
|
57
|
+
beforeAll(() => {
|
|
58
|
+
process.env[UPSTREAM_TRANSFORMER_ENV] = mockUpstream;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("compiles a use-generative module to the client build by default", async () => {
|
|
62
|
+
const { transform } = await import("./transformer");
|
|
63
|
+
const out = transform({
|
|
64
|
+
filename: "/app/toolkit.tsx",
|
|
65
|
+
src: GENERATIVE,
|
|
66
|
+
options: {},
|
|
67
|
+
}) as { src: string };
|
|
68
|
+
|
|
69
|
+
// client build keeps render + schema, drops the backend execute + its import
|
|
70
|
+
expect(out.src).toContain("render");
|
|
71
|
+
expect(out.src).toContain("z.object");
|
|
72
|
+
expect(out.src).toContain('type: "backend"');
|
|
73
|
+
expect(out.src).not.toContain("db.get");
|
|
74
|
+
expect(out.src).not.toContain("@/db");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("compiles to the server build for a node environment", async () => {
|
|
78
|
+
const { transform } = await import("./transformer");
|
|
79
|
+
const out = transform({
|
|
80
|
+
filename: "/app/toolkit.tsx",
|
|
81
|
+
src: GENERATIVE,
|
|
82
|
+
options: { customTransformOptions: { environment: "node" } },
|
|
83
|
+
}) as { src: string };
|
|
84
|
+
|
|
85
|
+
// server build keeps the backend execute, drops render
|
|
86
|
+
expect(out.src).toContain("db.get");
|
|
87
|
+
expect(out.src).not.toMatch(/render\s*:/);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("passes non-generative modules through untouched", async () => {
|
|
91
|
+
const { transform } = await import("./transformer");
|
|
92
|
+
const src = `export const x = 1;\n`;
|
|
93
|
+
const out = transform({
|
|
94
|
+
filename: "/app/x.ts",
|
|
95
|
+
src,
|
|
96
|
+
options: {},
|
|
97
|
+
}) as { src: string };
|
|
98
|
+
|
|
99
|
+
expect(out.src).toBe(src);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { statSync } from "node:fs";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import {
|
|
5
|
+
compileGenerative,
|
|
6
|
+
isGenerativeModule,
|
|
7
|
+
} from "@assistant-ui/x-generative-compiler";
|
|
8
|
+
import { UPSTREAM_TRANSFORMER_ENV } from "./index";
|
|
9
|
+
|
|
10
|
+
const require = createRequire(import.meta.url);
|
|
11
|
+
|
|
12
|
+
/** Source modules a `"use generative"` directive can appear in. */
|
|
13
|
+
const SOURCE_RE = /\.[cm]?[jt]sx?$/;
|
|
14
|
+
|
|
15
|
+
/** Minimal shape of a Metro babel transformer. */
|
|
16
|
+
type BabelTransformer = {
|
|
17
|
+
transform: (props: {
|
|
18
|
+
filename: string;
|
|
19
|
+
src: string;
|
|
20
|
+
options?:
|
|
21
|
+
| { customTransformOptions?: { environment?: string } | undefined }
|
|
22
|
+
| undefined;
|
|
23
|
+
[key: string]: unknown;
|
|
24
|
+
}) => unknown;
|
|
25
|
+
getCacheKey?: (() => string) | undefined;
|
|
26
|
+
[key: string]: unknown;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The project's upstream babel transformer: what Metro would have used without
|
|
31
|
+
* this wrapper. Taken from {@link UPSTREAM_TRANSFORMER_ENV} (set by `withAui`),
|
|
32
|
+
* else auto-detected: Expo's transformer, then React Native's.
|
|
33
|
+
*/
|
|
34
|
+
function resolveUpstream(): BabelTransformer {
|
|
35
|
+
const fromEnv = process.env[UPSTREAM_TRANSFORMER_ENV];
|
|
36
|
+
if (fromEnv) return require(fromEnv) as BabelTransformer;
|
|
37
|
+
|
|
38
|
+
for (const id of [
|
|
39
|
+
"@expo/metro-config/babel-transformer",
|
|
40
|
+
"@react-native/metro-babel-transformer",
|
|
41
|
+
]) {
|
|
42
|
+
try {
|
|
43
|
+
return require(id) as BabelTransformer;
|
|
44
|
+
} catch {
|
|
45
|
+
// try the next candidate
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
throw new Error(
|
|
50
|
+
"[@assistant-ui/metro] Could not resolve an upstream Metro babel " +
|
|
51
|
+
"transformer. Make sure `transformer.babelTransformerPath` is set before " +
|
|
52
|
+
"`withAui(...)` (Expo's `getDefaultConfig` sets it), or install " +
|
|
53
|
+
"`@expo/metro-config` or `@react-native/metro-babel-transformer`.",
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Resolved lazily on first use, by which point Metro has loaded `metro.config` and
|
|
58
|
+
// `withAui` has populated the env var, and forked workers have inherited it.
|
|
59
|
+
let cachedUpstream: BabelTransformer | undefined;
|
|
60
|
+
function upstreamTransformer(): BabelTransformer {
|
|
61
|
+
return (cachedUpstream ??= resolveUpstream());
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Expo bundles `+api` routes / RSC for a server environment; the app is client. */
|
|
65
|
+
function isServerEnvironment(environment: string | undefined): boolean {
|
|
66
|
+
return environment === "node" || environment === "react-server";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function transform(
|
|
70
|
+
props: Parameters<BabelTransformer["transform"]>[0],
|
|
71
|
+
): unknown {
|
|
72
|
+
const upstream = upstreamTransformer();
|
|
73
|
+
const { filename, src, options } = props;
|
|
74
|
+
|
|
75
|
+
if (SOURCE_RE.test(filename) && isGenerativeModule(src)) {
|
|
76
|
+
const target = isServerEnvironment(
|
|
77
|
+
options?.customTransformOptions?.environment,
|
|
78
|
+
)
|
|
79
|
+
? "server"
|
|
80
|
+
: "client";
|
|
81
|
+
|
|
82
|
+
const { code } = compileGenerative(src, {
|
|
83
|
+
target,
|
|
84
|
+
filename,
|
|
85
|
+
// Metro has no `react-server` layer, so `import "server-only"` would not
|
|
86
|
+
// resolve; the environment split already keeps a server `execute` out of
|
|
87
|
+
// the client bundle.
|
|
88
|
+
injectServerOnly: false,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return upstream.transform({ ...props, src: code });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return upstream.transform(props);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// The bundled compiler lives in this file, so its own mtime moves whenever the
|
|
98
|
+
// transform output could change (a newly published version, or an in-place
|
|
99
|
+
// monorepo rebuild). Deriving the token from it busts Metro's cache
|
|
100
|
+
// automatically.
|
|
101
|
+
let cachedSelfToken: string | undefined;
|
|
102
|
+
function selfCacheToken(): string {
|
|
103
|
+
if (cachedSelfToken !== undefined) return cachedSelfToken;
|
|
104
|
+
try {
|
|
105
|
+
cachedSelfToken = String(statSync(fileURLToPath(import.meta.url)).mtimeMs);
|
|
106
|
+
} catch {
|
|
107
|
+
cachedSelfToken = "0";
|
|
108
|
+
}
|
|
109
|
+
return cachedSelfToken;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function getCacheKey(): string {
|
|
113
|
+
const upstream = upstreamTransformer();
|
|
114
|
+
const base = upstream.getCacheKey ? upstream.getCacheKey() : "";
|
|
115
|
+
return `${base}$aui-metro:${selfCacheToken()}`;
|
|
116
|
+
}
|