@rozenite/vite-plugin 1.11.0 → 1.13.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/package.json +2 -1
- package/src/bundle-dts.ts +125 -0
- package/src/client-plugin.ts +392 -0
- package/src/dev-config-module.ts +29 -0
- package/src/dev-host/App.tsx +567 -0
- package/src/dev-host/components/DispatchForm.tsx +169 -0
- package/src/dev-host/components/FlowList.tsx +142 -0
- package/src/dev-host/components/MessageDetailsPane.tsx +109 -0
- package/src/dev-host/components/MessageLogPane.tsx +84 -0
- package/src/dev-host/components/MessagePayloadDetail.tsx +32 -0
- package/src/dev-host/components/PanelTabs.tsx +22 -0
- package/src/dev-host/components/ResizeHandle.tsx +33 -0
- package/src/dev-host/components/icons.tsx +79 -0
- package/src/dev-host/components/ui/Button.tsx +91 -0
- package/src/dev-host/components/ui/DropdownMenu.tsx +84 -0
- package/src/dev-host/components/ui/IconButton.tsx +41 -0
- package/src/dev-host/components/ui/Input.tsx +73 -0
- package/src/dev-host/components/ui/ScrollArea.tsx +20 -0
- package/src/dev-host/components/ui/Tabs.tsx +166 -0
- package/src/dev-host/components/ui/Textarea.tsx +79 -0
- package/src/dev-host/components/ui/ToggleGroup.tsx +120 -0
- package/src/dev-host/config.ts +107 -0
- package/src/dev-host/constants.ts +31 -0
- package/src/dev-host/flow-runtime.ts +297 -0
- package/src/dev-host/index.html +12 -0
- package/src/dev-host/main.tsx +31 -0
- package/src/dev-host/server.ts +55 -0
- package/src/dev-host/styles.css +969 -0
- package/src/dev-host/types.ts +61 -0
- package/src/dev-host/utils.ts +96 -0
- package/src/dev-host/vite.config.mts +20 -0
- package/src/index.ts +114 -0
- package/src/load-config.ts +117 -0
- package/src/package-json.ts +16 -0
- package/src/react-native-plugin.ts +49 -0
- package/src/require-plugin.ts +221 -0
- package/src/sdk-plugin.ts +44 -0
- package/src/server-plugin.ts +36 -0
- package/src/utils.ts +31 -0
- package/src/virtual-modules.d.ts +7 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { DevFlowEntry } from '../load-config.js';
|
|
2
|
+
|
|
3
|
+
export type DevHostPanelEntry = {
|
|
4
|
+
label: string;
|
|
5
|
+
source: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type DevHostPresetEntry = {
|
|
9
|
+
name: string;
|
|
10
|
+
displayName: string;
|
|
11
|
+
type: string;
|
|
12
|
+
payload: unknown;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type DevHostFlowEntry = {
|
|
16
|
+
name: string;
|
|
17
|
+
displayName: string;
|
|
18
|
+
autoRun: boolean;
|
|
19
|
+
run: DevFlowEntry['run'];
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type DevHostState = {
|
|
23
|
+
packageName: string;
|
|
24
|
+
packageDescription: string;
|
|
25
|
+
panels: DevHostPanelEntry[];
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type MessageEntry = {
|
|
29
|
+
id: string;
|
|
30
|
+
direction: 'in' | 'out';
|
|
31
|
+
date: string;
|
|
32
|
+
type: string;
|
|
33
|
+
payload: unknown;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type DevHostFlowRunStatus = 'running' | 'succeeded' | 'failed' | 'aborted';
|
|
37
|
+
|
|
38
|
+
export type DevHostFlowRunState = {
|
|
39
|
+
id: string;
|
|
40
|
+
flowName: string;
|
|
41
|
+
flowDisplayName: string;
|
|
42
|
+
status: DevHostFlowRunStatus;
|
|
43
|
+
result: unknown;
|
|
44
|
+
error: string | null;
|
|
45
|
+
autoRun: boolean;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type PluginMessage = {
|
|
49
|
+
pluginId: string;
|
|
50
|
+
type: string;
|
|
51
|
+
payload: unknown;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export type ResizeHandleId = 'devtools-height' | 'command-width' | 'details-width';
|
|
55
|
+
|
|
56
|
+
export type ResizeSession = {
|
|
57
|
+
handleId: ResizeHandleId;
|
|
58
|
+
pointerId: number;
|
|
59
|
+
element: HTMLElement;
|
|
60
|
+
onMove: (event: PointerEvent) => void;
|
|
61
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { DEV_HOST_STATE_ELEMENT_ID } from './constants.js';
|
|
2
|
+
import type { DevHostPanelEntry, DevHostState, MessageEntry, PluginMessage } from './types.js';
|
|
3
|
+
|
|
4
|
+
export const cn = (...parts: Array<string | false | null | undefined>) => {
|
|
5
|
+
return parts.filter(Boolean).join(' ');
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const formatPayloadPreview = (payload: unknown) => {
|
|
9
|
+
if (payload == null) {
|
|
10
|
+
return 'null';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (typeof payload === 'string') {
|
|
14
|
+
return payload;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
return JSON.stringify(payload);
|
|
19
|
+
} catch {
|
|
20
|
+
return String(payload);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const isJsonTreeData = (value: unknown): value is Record<string, unknown> | unknown[] => {
|
|
25
|
+
return Array.isArray(value) || (typeof value === 'object' && value !== null);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const formatMessageDate = (date: string) => {
|
|
29
|
+
return new Date(date).toLocaleString();
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const formatMessageTableDate = (date: string) => {
|
|
33
|
+
return new Date(date).toLocaleString([], {
|
|
34
|
+
month: '2-digit',
|
|
35
|
+
day: '2-digit',
|
|
36
|
+
hour: '2-digit',
|
|
37
|
+
minute: '2-digit',
|
|
38
|
+
second: '2-digit',
|
|
39
|
+
hour12: false,
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const formatPayloadForCommandInput = (payload: unknown) => {
|
|
44
|
+
try {
|
|
45
|
+
return JSON.stringify(payload, null, 2);
|
|
46
|
+
} catch {
|
|
47
|
+
return String(payload);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const isPluginMessage = (value: unknown): value is PluginMessage => {
|
|
52
|
+
return (
|
|
53
|
+
typeof value === 'object' &&
|
|
54
|
+
value !== null &&
|
|
55
|
+
'pluginId' in value &&
|
|
56
|
+
'type' in value &&
|
|
57
|
+
'payload' in value
|
|
58
|
+
);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const getInitialPanel = (panels: DevHostPanelEntry[]) => {
|
|
62
|
+
const requestedPanel = new URLSearchParams(window.location.search).get('panel');
|
|
63
|
+
|
|
64
|
+
if (requestedPanel) {
|
|
65
|
+
const matchedPanel = panels.find((panel) => panel.label === requestedPanel);
|
|
66
|
+
if (matchedPanel) {
|
|
67
|
+
return matchedPanel;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return panels[0] ?? null;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const createMessageEntry = (
|
|
75
|
+
input: Omit<MessageEntry, 'id' | 'date'>,
|
|
76
|
+
): MessageEntry => {
|
|
77
|
+
return {
|
|
78
|
+
id: crypto.randomUUID(),
|
|
79
|
+
date: new Date().toISOString(),
|
|
80
|
+
...input,
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export const clamp = (value: number, min: number, max: number) => {
|
|
85
|
+
return Math.min(Math.max(value, min), max);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export const readDevHostState = (): DevHostState => {
|
|
89
|
+
const stateElement = document.getElementById(DEV_HOST_STATE_ELEMENT_ID);
|
|
90
|
+
|
|
91
|
+
if (!stateElement?.textContent) {
|
|
92
|
+
throw new Error('Rozenite dev host failed to initialize.');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return JSON.parse(stateElement.textContent) as DevHostState;
|
|
96
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { defineConfig } from 'vite';
|
|
4
|
+
|
|
5
|
+
const HOST_APP_ROOT = fileURLToPath(new URL('./', import.meta.url));
|
|
6
|
+
const PACKAGE_ROOT = fileURLToPath(new URL('../../', import.meta.url));
|
|
7
|
+
|
|
8
|
+
export default defineConfig({
|
|
9
|
+
root: HOST_APP_ROOT,
|
|
10
|
+
base: './',
|
|
11
|
+
publicDir: false,
|
|
12
|
+
build: {
|
|
13
|
+
outDir: path.join(PACKAGE_ROOT, 'dist', 'dev-host'),
|
|
14
|
+
emptyOutDir: true,
|
|
15
|
+
manifest: 'manifest.json',
|
|
16
|
+
// The dev host is served locally during plugin development, so the
|
|
17
|
+
// default production-oriented chunk warning is just noise here.
|
|
18
|
+
chunkSizeWarningLimit: 1000,
|
|
19
|
+
},
|
|
20
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { PluginOption } from 'vite';
|
|
2
|
+
import react from '@vitejs/plugin-react';
|
|
3
|
+
import reactNativeWeb from 'vite-plugin-react-native-web';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { rozeniteServerPlugin } from './server-plugin.js';
|
|
6
|
+
import { rozeniteClientPlugin } from './client-plugin.js';
|
|
7
|
+
import { rozeniteReactNativePlugin } from './react-native-plugin.js';
|
|
8
|
+
import { rozeniteSdkPlugin } from './sdk-plugin.js';
|
|
9
|
+
import maybeDtsPlugin from 'vite-plugin-dts';
|
|
10
|
+
import requirePlugin from './require-plugin.js';
|
|
11
|
+
import { bundleTargetDeclarations } from './bundle-dts.js';
|
|
12
|
+
|
|
13
|
+
// vite-plugin-dts exports differently in CJS and ESM
|
|
14
|
+
const dtsPlugin =
|
|
15
|
+
'default' in maybeDtsPlugin
|
|
16
|
+
? (maybeDtsPlugin.default as typeof maybeDtsPlugin)
|
|
17
|
+
: maybeDtsPlugin;
|
|
18
|
+
|
|
19
|
+
const getDtsPlugin = (
|
|
20
|
+
target: 'react-native' | 'metro' | 'sdk',
|
|
21
|
+
): PluginOption => {
|
|
22
|
+
const projectRoot = process.cwd();
|
|
23
|
+
const entryRoot =
|
|
24
|
+
target === 'react-native'
|
|
25
|
+
? 'react-native.ts'
|
|
26
|
+
: target === 'metro'
|
|
27
|
+
? 'metro.ts'
|
|
28
|
+
: 'sdk.ts';
|
|
29
|
+
const distRoot = path.join(projectRoot, 'dist');
|
|
30
|
+
const targetRoot = path.join(distRoot, target);
|
|
31
|
+
const publicEntryPath = path.join(projectRoot, 'dist', target, 'index.d.ts');
|
|
32
|
+
const sdkBundleEntryPath = path.join(targetRoot, `${target}.d.ts`);
|
|
33
|
+
const rawSdkEntryPath = path.join(distRoot, `${target}.d.ts`);
|
|
34
|
+
|
|
35
|
+
return dtsPlugin({
|
|
36
|
+
entryRoot,
|
|
37
|
+
include: [entryRoot, 'src/**/*.ts', 'src/**/*.tsx', 'src/**/*.d.ts'],
|
|
38
|
+
outDir: `dist/${target}`,
|
|
39
|
+
strictOutput: false,
|
|
40
|
+
insertTypesEntry: false,
|
|
41
|
+
// Preserve package specifiers in published declarations instead of
|
|
42
|
+
// rewriting workspace imports to source file paths.
|
|
43
|
+
pathsToAliases: false,
|
|
44
|
+
tsconfigPath: path.join(projectRoot, 'tsconfig.json'),
|
|
45
|
+
beforeWriteFile: (filePath, content) => {
|
|
46
|
+
if (!filePath.endsWith('.d.ts')) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (
|
|
51
|
+
filePath === publicEntryPath ||
|
|
52
|
+
(target !== 'sdk' && filePath.endsWith(`/${target}.d.ts`))
|
|
53
|
+
) {
|
|
54
|
+
return {
|
|
55
|
+
filePath: publicEntryPath,
|
|
56
|
+
content,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (
|
|
61
|
+
target === 'sdk' &&
|
|
62
|
+
filePath === rawSdkEntryPath
|
|
63
|
+
) {
|
|
64
|
+
return {
|
|
65
|
+
// vite-plugin-dts emits the SDK root declaration at dist/sdk.d.ts.
|
|
66
|
+
// Move it under dist/sdk/ before API Extractor rolls it up into the
|
|
67
|
+
// published dist/sdk/index.d.ts.
|
|
68
|
+
filePath: sdkBundleEntryPath,
|
|
69
|
+
content,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const relativeToDist = path.relative(distRoot, filePath);
|
|
74
|
+
|
|
75
|
+
if (!relativeToDist.startsWith('src/')) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
filePath: path.join(targetRoot, relativeToDist),
|
|
81
|
+
content,
|
|
82
|
+
};
|
|
83
|
+
},
|
|
84
|
+
afterBuild: async () => {
|
|
85
|
+
await bundleTargetDeclarations(projectRoot, target);
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export const rozenitePlugin = (): PluginOption[] => {
|
|
91
|
+
const isServer = process.env.VITE_ROZENITE_TARGET === 'server';
|
|
92
|
+
const isReactNative = process.env.VITE_ROZENITE_TARGET === 'react-native';
|
|
93
|
+
const isSdk = process.env.VITE_ROZENITE_TARGET === 'sdk';
|
|
94
|
+
|
|
95
|
+
if (isServer) {
|
|
96
|
+
return [rozeniteServerPlugin(), getDtsPlugin('metro')] as PluginOption[];
|
|
97
|
+
} else if (isReactNative) {
|
|
98
|
+
return [
|
|
99
|
+
react(),
|
|
100
|
+
requirePlugin(),
|
|
101
|
+
rozeniteReactNativePlugin(),
|
|
102
|
+
getDtsPlugin('react-native'),
|
|
103
|
+
] as PluginOption[];
|
|
104
|
+
} else if (isSdk) {
|
|
105
|
+
return [rozeniteSdkPlugin(), getDtsPlugin('sdk')] as PluginOption[];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return [
|
|
109
|
+
react(),
|
|
110
|
+
// @ts-expect-error: TypeScript gets confused by the dual export
|
|
111
|
+
reactNativeWeb(),
|
|
112
|
+
rozeniteClientPlugin(),
|
|
113
|
+
];
|
|
114
|
+
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { transformWithEsbuild } from 'vite';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
|
|
5
|
+
export type PanelEntry = {
|
|
6
|
+
name: string;
|
|
7
|
+
source: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type DevPresetEntry = {
|
|
11
|
+
name: string;
|
|
12
|
+
type: string;
|
|
13
|
+
payload: unknown;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type DevFlowMessage = {
|
|
17
|
+
id: string;
|
|
18
|
+
direction: 'in' | 'out';
|
|
19
|
+
date: string;
|
|
20
|
+
type: string;
|
|
21
|
+
payload: unknown;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type DevFlowMessageMatcher =
|
|
25
|
+
| string
|
|
26
|
+
| ((message: DevFlowMessage) => boolean)
|
|
27
|
+
| {
|
|
28
|
+
type?: string;
|
|
29
|
+
direction?: DevFlowMessage['direction'];
|
|
30
|
+
predicate?: (message: DevFlowMessage) => boolean;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type DevFlowSubscription = {
|
|
34
|
+
remove: () => void;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type DevFlowContext = {
|
|
38
|
+
signal: AbortSignal;
|
|
39
|
+
send: (type: string, payload: unknown) => void;
|
|
40
|
+
onMessage: (
|
|
41
|
+
matcher: DevFlowMessageMatcher,
|
|
42
|
+
listener: (message: DevFlowMessage) => void,
|
|
43
|
+
) => DevFlowSubscription;
|
|
44
|
+
waitForMessage: (
|
|
45
|
+
matcher: DevFlowMessageMatcher,
|
|
46
|
+
options?: {
|
|
47
|
+
timeoutMs?: number;
|
|
48
|
+
},
|
|
49
|
+
) => Promise<DevFlowMessage>;
|
|
50
|
+
getMessages: (matcher?: DevFlowMessageMatcher) => DevFlowMessage[];
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export type DevFlowEntry = {
|
|
54
|
+
name?: string;
|
|
55
|
+
autoRun?: boolean;
|
|
56
|
+
run: (context: DevFlowContext) => unknown | Promise<unknown>;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export type DevConfig = {
|
|
60
|
+
presets?: DevPresetEntry[];
|
|
61
|
+
flows?: DevFlowEntry[];
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export type RozeniteConfig = {
|
|
65
|
+
panels: PanelEntry[];
|
|
66
|
+
dev?: DevConfig;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export const loadConfig = async (
|
|
70
|
+
configPath: string
|
|
71
|
+
): Promise<RozeniteConfig> => {
|
|
72
|
+
const absoluteConfigPath = path.resolve(process.cwd(), configPath);
|
|
73
|
+
|
|
74
|
+
if (!fs.existsSync(absoluteConfigPath)) {
|
|
75
|
+
throw new Error(`Configuration file not found: ${absoluteConfigPath}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const configContent = await fs.promises.readFile(
|
|
80
|
+
absoluteConfigPath,
|
|
81
|
+
'utf-8'
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const result = await transformWithEsbuild(
|
|
85
|
+
configContent,
|
|
86
|
+
absoluteConfigPath,
|
|
87
|
+
{
|
|
88
|
+
loader: 'ts',
|
|
89
|
+
format: 'cjs',
|
|
90
|
+
target: 'esnext',
|
|
91
|
+
}
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const moduleExports: { default?: unknown } = {};
|
|
95
|
+
const module = { exports: moduleExports };
|
|
96
|
+
const exports = moduleExports;
|
|
97
|
+
|
|
98
|
+
const moduleFunction = new Function('module', 'exports', result.code);
|
|
99
|
+
moduleFunction(module, exports);
|
|
100
|
+
|
|
101
|
+
const configModule = module.exports;
|
|
102
|
+
const config = configModule.default || configModule;
|
|
103
|
+
|
|
104
|
+
if (!config || typeof config !== 'object') {
|
|
105
|
+
throw new Error('Configuration must export an object');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return config as RozeniteConfig;
|
|
109
|
+
} catch (error) {
|
|
110
|
+
if (error instanceof Error) {
|
|
111
|
+
throw new Error(
|
|
112
|
+
`Failed to load configuration from ${configPath}: ${error.message}`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
throw new Error(`Failed to load configuration from ${configPath}`);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
|
|
4
|
+
export type PackageJSON = {
|
|
5
|
+
name: string;
|
|
6
|
+
description: string;
|
|
7
|
+
version: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const getPackageJSON = async (
|
|
11
|
+
projectRoot: string
|
|
12
|
+
): Promise<PackageJSON> => {
|
|
13
|
+
const packageJSONPath = path.join(projectRoot, 'package.json');
|
|
14
|
+
const packageJSON = await fs.readFile(packageJSONPath, 'utf8');
|
|
15
|
+
return JSON.parse(packageJSON);
|
|
16
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Plugin } from 'vite';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export const rozeniteReactNativePlugin = (): Plugin => {
|
|
5
|
+
return {
|
|
6
|
+
name: 'rozenite-react-native-plugin',
|
|
7
|
+
config(config) {
|
|
8
|
+
const projectRoot = config.root ?? process.cwd();
|
|
9
|
+
|
|
10
|
+
config.build ??= {};
|
|
11
|
+
config.build.rollupOptions ??= {};
|
|
12
|
+
|
|
13
|
+
config.build.lib = {
|
|
14
|
+
entry: path.resolve(projectRoot, 'react-native.ts'),
|
|
15
|
+
fileName: (format) =>
|
|
16
|
+
`react-native/index.${format === 'es' ? 'js' : 'cjs'}`,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
config.build.rollupOptions.external = (id) => {
|
|
20
|
+
if (id.startsWith('node:')) {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return !id.startsWith('.') && !path.isAbsolute(id);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
config.build.rollupOptions.output = [
|
|
28
|
+
{
|
|
29
|
+
format: 'es',
|
|
30
|
+
exports: 'named',
|
|
31
|
+
interop: 'auto',
|
|
32
|
+
entryFileNames: 'react-native/index.js',
|
|
33
|
+
chunkFileNames: 'react-native/chunks/[name].js',
|
|
34
|
+
...(config.build.rollupOptions.output ?? {}),
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
format: 'cjs',
|
|
38
|
+
exports: 'named',
|
|
39
|
+
interop: 'auto',
|
|
40
|
+
entryFileNames: 'react-native/index.cjs',
|
|
41
|
+
chunkFileNames: 'react-native/chunks/[name].cjs',
|
|
42
|
+
...(config.build.rollupOptions.output ?? {}),
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
delete config.build.rollupOptions.input;
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
};
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { normalizePath, Plugin } from 'vite';
|
|
5
|
+
|
|
6
|
+
const REQUIRE_REGEX = /require\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g;
|
|
7
|
+
const IMPORT_PREFIX = '__import_';
|
|
8
|
+
const VIRTUAL_REQUIRE_PREFIX = '\0virtual:rozenite-rn-require:';
|
|
9
|
+
const REQUIRE_WRAPPER_SUFFIX = '.require';
|
|
10
|
+
|
|
11
|
+
interface ModuleInfo {
|
|
12
|
+
referenceId: string;
|
|
13
|
+
virtualId: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface TransformResult {
|
|
17
|
+
code: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const extractModuleName = (filePath: string): string => {
|
|
21
|
+
return path.basename(filePath).replace(/\.[^/.]+$/, '');
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const sanitizeChunkName = (value: string): string => {
|
|
25
|
+
return value.replace(/[^a-zA-Z0-9_-]/g, '-');
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const findRequireStatements = (code: string): Set<string> => {
|
|
29
|
+
const requires = new Set<string>();
|
|
30
|
+
let match: RegExpExecArray | null;
|
|
31
|
+
|
|
32
|
+
REQUIRE_REGEX.lastIndex = 0;
|
|
33
|
+
|
|
34
|
+
while ((match = REQUIRE_REGEX.exec(code)) !== null) {
|
|
35
|
+
const moduleName = match[1];
|
|
36
|
+
if (moduleName && moduleName.trim()) {
|
|
37
|
+
requires.add(moduleName.trim());
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return requires;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const transformRequireToImports = (
|
|
45
|
+
code: string,
|
|
46
|
+
moduleInfoMap: Map<string, ModuleInfo>,
|
|
47
|
+
): TransformResult => {
|
|
48
|
+
const imports: string[] = [];
|
|
49
|
+
const importMap = new Map<string, string>();
|
|
50
|
+
const requires = findRequireStatements(code);
|
|
51
|
+
|
|
52
|
+
requires.forEach((moduleName, index) => {
|
|
53
|
+
const moduleInfo = moduleInfoMap.get(moduleName);
|
|
54
|
+
|
|
55
|
+
if (!moduleInfo) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const importName = `${IMPORT_PREFIX}${index}`;
|
|
60
|
+
importMap.set(moduleName, importName);
|
|
61
|
+
imports.push(`import * as ${importName} from '${moduleInfo.virtualId}';`);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
let transformedCode = code.replace(REQUIRE_REGEX, (match, moduleName) => {
|
|
65
|
+
const importName = importMap.get(moduleName.trim());
|
|
66
|
+
return importName || match;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
if (imports.length > 0) {
|
|
70
|
+
transformedCode = imports.join('\n') + '\n' + transformedCode;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return { code: transformedCode };
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const transformRequireToChunkReferences = (
|
|
77
|
+
code: string,
|
|
78
|
+
moduleInfoMap: Map<string, ModuleInfo>,
|
|
79
|
+
getFileName: (referenceId: string) => string,
|
|
80
|
+
): string => {
|
|
81
|
+
return code.replace(REQUIRE_REGEX, (match, moduleName) => {
|
|
82
|
+
const moduleInfo = moduleInfoMap.get(moduleName.trim());
|
|
83
|
+
|
|
84
|
+
if (!moduleInfo) {
|
|
85
|
+
return match;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const outFileName = getFileName(moduleInfo.referenceId);
|
|
89
|
+
const relPath = normalizePath(
|
|
90
|
+
path.posix.relative('react-native', outFileName),
|
|
91
|
+
);
|
|
92
|
+
const requirePath = relPath.startsWith('.') ? relPath : `./${relPath}`;
|
|
93
|
+
|
|
94
|
+
return `require('${requirePath}')`;
|
|
95
|
+
});
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export default function requirePlugin(): Plugin {
|
|
99
|
+
let input = '';
|
|
100
|
+
let inputName = '';
|
|
101
|
+
let isDevMode = false;
|
|
102
|
+
|
|
103
|
+
const moduleInfoMap = new Map<string, ModuleInfo>();
|
|
104
|
+
const virtualModuleSources = new Map<string, string>();
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
name: 'vite-require-plugin',
|
|
108
|
+
|
|
109
|
+
configResolved(config) {
|
|
110
|
+
isDevMode = config.command === 'serve';
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
resolveId(id) {
|
|
114
|
+
if (virtualModuleSources.has(id)) {
|
|
115
|
+
return id;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return null;
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
load(id) {
|
|
122
|
+
return virtualModuleSources.get(id) ?? null;
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
transform(code, id) {
|
|
126
|
+
if (!isDevMode || id !== input) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
const result = transformRequireToImports(code, moduleInfoMap);
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
code: result.code,
|
|
135
|
+
map: null,
|
|
136
|
+
};
|
|
137
|
+
} catch (error) {
|
|
138
|
+
console.error('Error transforming require statements:', error);
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
async buildStart(options) {
|
|
144
|
+
try {
|
|
145
|
+
assert(Array.isArray(options.input), 'input must be an array');
|
|
146
|
+
assert(
|
|
147
|
+
options.input.length === 1,
|
|
148
|
+
'input must be an array with one entry',
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
input = options.input[0];
|
|
152
|
+
inputName = extractModuleName(input);
|
|
153
|
+
moduleInfoMap.clear();
|
|
154
|
+
virtualModuleSources.clear();
|
|
155
|
+
|
|
156
|
+
const code = readFileSync(input, 'utf-8');
|
|
157
|
+
const requires = findRequireStatements(code);
|
|
158
|
+
|
|
159
|
+
for (const req of requires) {
|
|
160
|
+
try {
|
|
161
|
+
const resolved = await this.resolve(req, input, { skipSelf: true });
|
|
162
|
+
|
|
163
|
+
if (!resolved) {
|
|
164
|
+
console.warn(`Could not resolve module: ${req}`);
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
this.addWatchFile(resolved.id);
|
|
169
|
+
|
|
170
|
+
const exportName = sanitizeChunkName(
|
|
171
|
+
extractModuleName(resolved.id),
|
|
172
|
+
);
|
|
173
|
+
const wrapperName = `${exportName}${REQUIRE_WRAPPER_SUFFIX}`;
|
|
174
|
+
const virtualId = `${VIRTUAL_REQUIRE_PREFIX}${wrapperName}`;
|
|
175
|
+
|
|
176
|
+
virtualModuleSources.set(
|
|
177
|
+
virtualId,
|
|
178
|
+
`export * from ${JSON.stringify(req)};`,
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
const referenceId = this.emitFile({
|
|
182
|
+
type: 'chunk',
|
|
183
|
+
id: virtualId,
|
|
184
|
+
name: wrapperName,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
moduleInfoMap.set(req, {
|
|
188
|
+
referenceId,
|
|
189
|
+
virtualId,
|
|
190
|
+
});
|
|
191
|
+
} catch (error) {
|
|
192
|
+
console.error(`Error resolving module ${req}:`, error);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
} catch (error) {
|
|
196
|
+
console.error('Error in buildStart:', error);
|
|
197
|
+
throw error;
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
renderChunk(code, chunk) {
|
|
202
|
+
try {
|
|
203
|
+
if (chunk.name !== inputName) {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
code: transformRequireToChunkReferences(
|
|
209
|
+
code,
|
|
210
|
+
moduleInfoMap,
|
|
211
|
+
(referenceId) => normalizePath(this.getFileName(referenceId)),
|
|
212
|
+
),
|
|
213
|
+
map: null,
|
|
214
|
+
};
|
|
215
|
+
} catch (error) {
|
|
216
|
+
console.error('Error in renderChunk:', error);
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
}
|