@rmc-toolkit/vite 0.1.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.
@@ -0,0 +1,25 @@
1
+ import { type RuntimeCompositionManifest, type RuntimeEnvironment } from "@rmc-toolkit/core";
2
+ import type { Plugin } from "vite";
3
+ export type RuntimeCompositionViteOptions = {
4
+ manifest: RuntimeCompositionManifest;
5
+ environment?: RuntimeEnvironment;
6
+ includeImportMap?: boolean;
7
+ externalize?: boolean;
8
+ };
9
+ export declare const createRollupExternal: (manifest: RuntimeCompositionManifest) => ((source: string) => boolean);
10
+ export declare const externalizeRuntimeComposition: ({ manifest, }: RuntimeCompositionViteOptions) => Plugin;
11
+ export declare const includeRuntimeImportMap: ({ manifest, environment, }: RuntimeCompositionViteOptions) => Plugin;
12
+ export declare const runtimeComposition: (options: RuntimeCompositionViteOptions) => Plugin[];
13
+ export type LocalSliceOverride = {
14
+ name: string;
15
+ port: number;
16
+ };
17
+ export declare const buildLocalImportMapScript: (manifest: RuntimeCompositionManifest, localSlice: LocalSliceOverride) => string;
18
+ export type IncludeHostedImportMapOptions = {
19
+ manifest: RuntimeCompositionManifest;
20
+ path?: string;
21
+ localSlice?: LocalSliceOverride;
22
+ };
23
+ export declare const includeHostedImportMap: ({ manifest, path, localSlice, }: IncludeHostedImportMapOptions) => Plugin;
24
+ export { defineSliceBuild, type SliceBuildOptions } from "./slice-build.js";
25
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,0BAA0B,EAC/B,KAAK,kBAAkB,EACxB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAEnC,MAAM,MAAM,6BAA6B,GAAG;IAC1C,QAAQ,EAAE,0BAA0B,CAAC;IACrC,WAAW,CAAC,EAAE,kBAAkB,CAAC;IACjC,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAC/B,UAAU,0BAA0B,KACnC,CAAC,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAoC,CAAC;AAEpE,eAAO,MAAM,6BAA6B,GAAI,eAE3C,6BAA6B,KAAG,MAwBlC,CAAC;AAEF,eAAO,MAAM,uBAAuB,GAAI,4BAGrC,6BAA6B,KAAG,MAgBjC,CAAC;AAEH,eAAO,MAAM,kBAAkB,GAC7B,SAAS,6BAA6B,KACrC,MAAM,EAYR,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,eAAO,MAAM,yBAAyB,GACpC,UAAU,0BAA0B,EACpC,YAAY,kBAAkB,KAC7B,MAkBF,CAAC;AAEF,MAAM,MAAM,6BAA6B,GAAG;IAC1C,QAAQ,EAAE,0BAA0B,CAAC;IACrC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,kBAAkB,CAAC;CACjC,CAAC;AAEF,eAAO,MAAM,sBAAsB,GAAI,iCAIpC,6BAA6B,KAAG,MAmCjC,CAAC;AAEH,OAAO,EAAE,gBAAgB,EAAE,KAAK,iBAAiB,EAAE,MAAM,kBAAkB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,102 @@
1
+ import { createExternalMatcher, createImportMap, createImportMapBootstrapScript, } from "@rmc-toolkit/core";
2
+ export const createRollupExternal = (manifest) => createExternalMatcher(manifest);
3
+ export const externalizeRuntimeComposition = ({ manifest, }) => {
4
+ const isExternal = createExternalMatcher(manifest);
5
+ return {
6
+ name: "runtime-module-composition-externalize",
7
+ enforce: "pre",
8
+ config() {
9
+ return {
10
+ optimizeDeps: {
11
+ noDiscovery: true,
12
+ },
13
+ };
14
+ },
15
+ resolveId(source) {
16
+ if (!isExternal(source)) {
17
+ return null;
18
+ }
19
+ return {
20
+ id: source,
21
+ external: true,
22
+ };
23
+ },
24
+ };
25
+ };
26
+ export const includeRuntimeImportMap = ({ manifest, environment = "development", }) => ({
27
+ name: "runtime-module-composition-include-import-map",
28
+ transformIndexHtml(html) {
29
+ const importMap = createImportMap(manifest, { environment });
30
+ const script = `<script type="importmap" data-runtime-module-composition>${JSON.stringify(importMap)}</script>`;
31
+ const existingImportMap = /<script[^>]*type=["']importmap["'][^>]*data-runtime-module-composition[^>]*>.*?<\/script>/s;
32
+ if (existingImportMap.test(html)) {
33
+ return html.replace(existingImportMap, script);
34
+ }
35
+ return html.includes("<head>")
36
+ ? html.replace("<head>", `<head>\n ${script}`)
37
+ : `${script}\n${html}`;
38
+ },
39
+ });
40
+ export const runtimeComposition = (options) => {
41
+ const plugins = [];
42
+ if (options.externalize !== false) {
43
+ plugins.push(externalizeRuntimeComposition(options));
44
+ }
45
+ if (options.includeImportMap !== false) {
46
+ plugins.push(includeRuntimeImportMap(options));
47
+ }
48
+ return plugins;
49
+ };
50
+ export const buildLocalImportMapScript = (manifest, localSlice) => {
51
+ const derivedManifest = {
52
+ ...manifest,
53
+ environments: {
54
+ ...manifest.environments,
55
+ development: {
56
+ ...manifest.environments?.development,
57
+ sliceOrigins: {
58
+ ...manifest.environments?.development?.sliceOrigins,
59
+ [localSlice.name]: `http://localhost:${localSlice.port}`,
60
+ },
61
+ },
62
+ },
63
+ };
64
+ return createImportMapBootstrapScript(derivedManifest, {
65
+ environment: "development",
66
+ });
67
+ };
68
+ export const includeHostedImportMap = ({ manifest, path = "/js/importmap.js", localSlice, }) => ({
69
+ name: "runtime-module-composition-hosted-import-map",
70
+ configureServer(server) {
71
+ server.middlewares.use(path, (req, res, next) => {
72
+ // Connect (which Vite's dev server middlewares are built on) mounts
73
+ // handlers by path *prefix*: it matches any request whose pathname
74
+ // starts with `path` and is followed by end-of-string, "/", or ".",
75
+ // then strips the matched prefix from `req.url` before invoking the
76
+ // handler. That means req.url here is *relative to the mount point*,
77
+ // and a request for "/js/importmap.js.map" or
78
+ // "/js/importmap.js/anything" also reaches this handler (as
79
+ // req.url === "/.map" or "/anything"), not just the exact endpoint.
80
+ // Comparing req.url against the outer `path` would never match,
81
+ // since Connect has already removed that prefix — the only way to
82
+ // detect "this is genuinely the exact endpoint" is to check that the
83
+ // remaining, query-stripped req.url is empty or "/".
84
+ const method = req.method ?? "GET";
85
+ if (method !== "GET" && method !== "HEAD") {
86
+ next();
87
+ return;
88
+ }
89
+ const remainder = (req.url ?? "/").split("?")[0] ?? "/";
90
+ if (remainder !== "" && remainder !== "/") {
91
+ next();
92
+ return;
93
+ }
94
+ const script = localSlice
95
+ ? buildLocalImportMapScript(manifest, localSlice)
96
+ : createImportMapBootstrapScript(manifest, { environment: "development" });
97
+ res.setHeader("Content-Type", "text/javascript");
98
+ res.end(script);
99
+ });
100
+ },
101
+ });
102
+ export { defineSliceBuild } from "./slice-build.js";
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=index.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.test.d.ts","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,169 @@
1
+ import { defineManifest } from "@rmc-toolkit/core";
2
+ import { describe, expect, test } from "vitest";
3
+ import { buildLocalImportMapScript, defineSliceBuild, includeHostedImportMap, includeRuntimeImportMap, runtimeComposition, } from "./index.js";
4
+ const manifest = defineManifest({
5
+ namespace: "@acme",
6
+ assetsOrigin: "https://assets.example.com",
7
+ externalDepsOrigin: "https://esm.sh",
8
+ environments: {
9
+ development: {
10
+ sliceOrigins: {
11
+ search: "http://localhost:5174",
12
+ },
13
+ },
14
+ },
15
+ });
16
+ describe("vite adapter", () => {
17
+ test("includes the import map in transformed HTML", () => {
18
+ const plugin = includeRuntimeImportMap({
19
+ manifest,
20
+ environment: "development",
21
+ });
22
+ if (typeof plugin.transformIndexHtml !== "function") {
23
+ throw new TypeError("Expected transformIndexHtml to be a function.");
24
+ }
25
+ const transformIndexHtml = plugin.transformIndexHtml;
26
+ const html = transformIndexHtml("<html><head></head><body></body></html>");
27
+ expect(html).toContain('<script type="importmap" data-runtime-module-composition>');
28
+ expect(html).toContain('"@acme/":"https://assets.example.com/"');
29
+ expect(html).toContain('"@esm.sh/":"https://esm.sh/"');
30
+ expect(html).toContain('"@acme/search/":"http://localhost:5174/"');
31
+ expect(html).toContain("</head>");
32
+ });
33
+ test("can disable import map HTML generation in the combined plugin list", () => {
34
+ const plugins = runtimeComposition({
35
+ manifest,
36
+ includeImportMap: false,
37
+ });
38
+ expect(plugins.map((plugin) => plugin.name)).toEqual([
39
+ "runtime-module-composition-externalize",
40
+ ]);
41
+ });
42
+ test("re-exports defineSliceBuild from the public barrel", () => {
43
+ const config = defineSliceBuild({
44
+ mode: "development",
45
+ devPort: 5301,
46
+ entry: "src/index.ts",
47
+ });
48
+ expect(config).toEqual({ server: { port: 5301 } });
49
+ });
50
+ });
51
+ const createMockServer = () => {
52
+ const middlewares = [];
53
+ const use = (path, handler) => {
54
+ middlewares.push({ path, handler });
55
+ };
56
+ return { server: { middlewares: { use } }, middlewares };
57
+ };
58
+ describe("includeHostedImportMap", () => {
59
+ test("buildLocalImportMapScript overrides only the local slice's origin", () => {
60
+ const script = buildLocalImportMapScript(manifest, {
61
+ name: "search",
62
+ port: 5173,
63
+ });
64
+ expect(script).toContain('"@acme/search/":"http://localhost:5173/"');
65
+ expect(script).toContain('"@acme/":"https://assets.example.com/"');
66
+ });
67
+ test("serves the generated script from the default middleware path", () => {
68
+ const plugin = includeHostedImportMap({ manifest });
69
+ const { server, middlewares } = createMockServer();
70
+ if (typeof plugin.configureServer !== "function") {
71
+ throw new TypeError("Expected configureServer to be a function.");
72
+ }
73
+ plugin.configureServer(server);
74
+ expect(middlewares).toHaveLength(1);
75
+ expect(middlewares[0]?.path).toBe("/js/importmap.js");
76
+ const headers = {};
77
+ let body = "";
78
+ const res = {
79
+ setHeader: (name, value) => {
80
+ headers[name] = value;
81
+ },
82
+ end: (chunk) => {
83
+ body = chunk;
84
+ },
85
+ };
86
+ // Connect strips the matched mount path from req.url before invoking the
87
+ // handler, so an exact match to the mounted path shows up as "/" here.
88
+ middlewares[0]?.handler({ url: "/", method: "GET" }, res, () => { });
89
+ expect(headers["Content-Type"]).toBe("text/javascript");
90
+ expect(body).toContain('"@acme/":"https://assets.example.com/"');
91
+ });
92
+ test("uses a custom middleware path when provided", () => {
93
+ const plugin = includeHostedImportMap({ manifest, path: "/custom.js" });
94
+ const { server, middlewares } = createMockServer();
95
+ if (typeof plugin.configureServer !== "function") {
96
+ throw new TypeError("Expected configureServer to be a function.");
97
+ }
98
+ plugin.configureServer(server);
99
+ expect(middlewares[0]?.path).toBe("/custom.js");
100
+ });
101
+ test("applies the localSlice override when serving the script", () => {
102
+ const plugin = includeHostedImportMap({
103
+ manifest,
104
+ localSlice: { name: "search", port: 5173 },
105
+ });
106
+ const { server, middlewares } = createMockServer();
107
+ if (typeof plugin.configureServer !== "function") {
108
+ throw new TypeError("Expected configureServer to be a function.");
109
+ }
110
+ plugin.configureServer(server);
111
+ let body = "";
112
+ const res = {
113
+ setHeader: () => { },
114
+ end: (chunk) => {
115
+ body = chunk;
116
+ },
117
+ };
118
+ middlewares[0]?.handler({ url: "/", method: "GET" }, res, () => { });
119
+ expect(body).toContain('"@acme/search/":"http://localhost:5173/"');
120
+ });
121
+ test("calls next() instead of end() for a method other than GET/HEAD", () => {
122
+ const plugin = includeHostedImportMap({ manifest });
123
+ const { server, middlewares } = createMockServer();
124
+ if (typeof plugin.configureServer !== "function") {
125
+ throw new TypeError("Expected configureServer to be a function.");
126
+ }
127
+ plugin.configureServer(server);
128
+ let nextCalled = false;
129
+ let endCalled = false;
130
+ const res = {
131
+ setHeader: () => { },
132
+ end: () => {
133
+ endCalled = true;
134
+ },
135
+ };
136
+ // Exact-path match ("/"), but a POST — should fall through, not respond.
137
+ middlewares[0]?.handler({ url: "/", method: "POST" }, res, () => {
138
+ nextCalled = true;
139
+ });
140
+ expect(nextCalled).toBe(true);
141
+ expect(endCalled).toBe(false);
142
+ });
143
+ test("calls next() instead of end() when the matched path is a prefix-extended variant", () => {
144
+ const plugin = includeHostedImportMap({ manifest });
145
+ const { server, middlewares } = createMockServer();
146
+ if (typeof plugin.configureServer !== "function") {
147
+ throw new TypeError("Expected configureServer to be a function.");
148
+ }
149
+ plugin.configureServer(server);
150
+ let nextCalled = false;
151
+ let endCalled = false;
152
+ const res = {
153
+ setHeader: () => { },
154
+ end: () => {
155
+ endCalled = true;
156
+ },
157
+ };
158
+ // Connect mounts by prefix and strips the matched portion from req.url,
159
+ // so "/js/importmap.js.map" and "/js/importmap.js/anything" show up
160
+ // inside the handler as "/.map" and "/anything" respectively (confirmed
161
+ // against the real `connect` package that Vite bundles). Neither is the
162
+ // exact mounted endpoint, so both must fall through via next().
163
+ middlewares[0]?.handler({ url: "/.map", method: "GET" }, res, () => {
164
+ nextCalled = true;
165
+ });
166
+ expect(nextCalled).toBe(true);
167
+ expect(endCalled).toBe(false);
168
+ });
169
+ });
@@ -0,0 +1,9 @@
1
+ import type { UserConfig } from "vite";
2
+ export declare const resolveEntry: (cwd: string, entry?: string) => string;
3
+ export type SliceBuildOptions = {
4
+ mode: string;
5
+ devPort: number;
6
+ entry?: string;
7
+ };
8
+ export declare const defineSliceBuild: (options: SliceBuildOptions) => UserConfig;
9
+ //# sourceMappingURL=slice-build.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slice-build.d.ts","sourceRoot":"","sources":["../src/slice-build.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,MAAM,CAAC;AAEvC,eAAO,MAAM,YAAY,GAAI,KAAK,MAAM,EAAE,QAAQ,MAAM,KAAG,MAgB1D,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,eAAO,MAAM,gBAAgB,GAAI,SAAS,iBAAiB,KAAG,UA2B7D,CAAC"}
@@ -0,0 +1,40 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ export const resolveEntry = (cwd, entry) => {
4
+ if (entry !== undefined) {
5
+ return entry;
6
+ }
7
+ if (existsSync(join(cwd, "src/index.tsx"))) {
8
+ return "src/index.tsx";
9
+ }
10
+ if (existsSync(join(cwd, "src/index.ts"))) {
11
+ return "src/index.ts";
12
+ }
13
+ throw new Error("defineSliceBuild: could not find src/index.tsx or src/index.ts. Pass an explicit `entry` option if this slice uses a non-standard layout.");
14
+ };
15
+ export const defineSliceBuild = (options) => {
16
+ const resolvedEntry = resolveEntry(process.cwd(), options.entry);
17
+ // Any mode other than "development" (production, or a custom mode) is
18
+ // treated as a real build and gets the full library-build config below.
19
+ if (options.mode === "development") {
20
+ return { server: { port: options.devPort } };
21
+ }
22
+ return {
23
+ preview: { cors: true },
24
+ // Vite's library-build mode, unlike its app-build mode, doesn't
25
+ // auto-replace process.env.NODE_ENV, so React/Vue's internal dev/prod
26
+ // checks left a raw `process` reference in bundles loaded directly via
27
+ // native import() in the browser, throwing `ReferenceError: process is
28
+ // not defined` at runtime (no bundler runs afterward to catch it).
29
+ define: {
30
+ "process.env.NODE_ENV": JSON.stringify("production"),
31
+ },
32
+ build: {
33
+ lib: {
34
+ entry: resolvedEntry,
35
+ formats: ["es"],
36
+ fileName: () => "index.mjs",
37
+ },
38
+ },
39
+ };
40
+ };
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=slice-build.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slice-build.test.d.ts","sourceRoot":"","sources":["../src/slice-build.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,71 @@
1
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterEach, describe, expect, test } from "vitest";
5
+ import { defineSliceBuild, resolveEntry } from "./slice-build.js";
6
+ let tempDir = null;
7
+ const createTempSliceDir = () => {
8
+ tempDir = mkdtempSync(join(tmpdir(), "rmc-vite-slice-build-"));
9
+ return tempDir;
10
+ };
11
+ afterEach(() => {
12
+ if (tempDir) {
13
+ rmSync(tempDir, { recursive: true, force: true });
14
+ tempDir = null;
15
+ }
16
+ });
17
+ describe("resolveEntry", () => {
18
+ test("returns the explicit entry without touching the filesystem", () => {
19
+ const dir = createTempSliceDir();
20
+ expect(resolveEntry(dir, "custom/entry.ts")).toBe("custom/entry.ts");
21
+ });
22
+ test("prefers src/index.tsx when both .tsx and .ts exist", () => {
23
+ const dir = createTempSliceDir();
24
+ mkdirSync(join(dir, "src"), { recursive: true });
25
+ writeFileSync(join(dir, "src", "index.tsx"), "export default {};");
26
+ writeFileSync(join(dir, "src", "index.ts"), "export default {};");
27
+ expect(resolveEntry(dir)).toBe("src/index.tsx");
28
+ });
29
+ test("falls back to src/index.ts when only it exists", () => {
30
+ const dir = createTempSliceDir();
31
+ mkdirSync(join(dir, "src"), { recursive: true });
32
+ writeFileSync(join(dir, "src", "index.ts"), "export default {};");
33
+ expect(resolveEntry(dir)).toBe("src/index.ts");
34
+ });
35
+ test("throws a clear error when neither conventional path exists", () => {
36
+ const dir = createTempSliceDir();
37
+ mkdirSync(join(dir, "src"), { recursive: true });
38
+ expect(() => resolveEntry(dir)).toThrow("defineSliceBuild: could not find src/index.tsx or src/index.ts. Pass an explicit `entry` option if this slice uses a non-standard layout.");
39
+ });
40
+ });
41
+ describe("defineSliceBuild", () => {
42
+ test("dev mode returns only the server port", () => {
43
+ const config = defineSliceBuild({
44
+ mode: "development",
45
+ devPort: 5301,
46
+ entry: "src/index.tsx",
47
+ });
48
+ expect(config).toEqual({ server: { port: 5301 } });
49
+ });
50
+ test("build mode returns preview/define/build.lib with the resolved entry", () => {
51
+ const config = defineSliceBuild({
52
+ mode: "production",
53
+ devPort: 5301,
54
+ entry: "src/index.tsx",
55
+ });
56
+ expect(config.preview).toEqual({ cors: true });
57
+ expect(config.define).toEqual({
58
+ "process.env.NODE_ENV": JSON.stringify("production"),
59
+ });
60
+ const lib = config.build?.lib;
61
+ if (!lib) {
62
+ throw new TypeError("Expected config.build.lib to be defined.");
63
+ }
64
+ expect(lib.entry).toBe("src/index.tsx");
65
+ expect(lib.formats).toEqual(["es"]);
66
+ expect(typeof lib.fileName).toBe("function");
67
+ if (typeof lib.fileName === "function") {
68
+ expect(lib.fileName("es", "index")).toBe("index.mjs");
69
+ }
70
+ });
71
+ });
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@rmc-toolkit/vite",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist"
15
+ ],
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "peerDependencies": {
20
+ "@rmc-toolkit/core": "0.1.0",
21
+ "vite": ">=5"
22
+ },
23
+ "dependencies": {
24
+ "@rmc-toolkit/core": "0.1.0"
25
+ },
26
+ "scripts": {
27
+ "build": "tsc -b"
28
+ }
29
+ }
30
+