Skip to content

Commit 83269e6

Browse files
V3RONFilip Jarno
andauthored
refactor(vite-plugin): improve plugin packaging outputs (callstackincubator#212)
## Summary - move plugin build outputs into target-specific `dist/devtools`, `dist/react-native`, and `dist/metro` directories while keeping `dist/rozenite.json` at the root - make the Rozenite CLI own builder-managed `package.json` entry fields so plugin packages stay aligned with generated outputs during build and dev - stabilize React Native `require()` chunk emission and bundle public declaration files per target entry to keep package surfaces cleaner --------- Co-authored-by: Filip Jarno <filip.jarno@callstack.com>
1 parent 45877af commit 83269e6

30 files changed

Lines changed: 11640 additions & 3740 deletions

File tree

.changeset/tall-beans-double.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@rozenite/vite-plugin': patch
3+
'rozenite': patch
4+
---
5+
6+
Restructure plugin packaging so build outputs are grouped under target-specific `dist/devtools`, `dist/react-native`, and `dist/metro` directories.
7+
8+
The CLI now keeps builder-managed `package.json` entry fields in sync with generated outputs, React Native `require()` chunks use stable names, and public declaration files are bundled per target entry.

apps/playground/src/app/App.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@ const Wrapper = () => {
5959
},
6060
});
6161
useMMKVDevTools({
62-
// @ts-expect-error - This is fine as in production MMKV plugin will pick up installed version automatically.
6362
storages: mmkvStorages,
6463
blacklist: /user-storage:sensitiveToken/,
6564
});

commitlint.config.mjs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
export default {
2+
extends: ['@commitlint/config-conventional'],
3+
rules: {
4+
'scope-enum': [
5+
2,
6+
'always',
7+
[
8+
'cli',
9+
'expo-atlas-plugin',
10+
'metro',
11+
'mmkv-plugin',
12+
'network-activity-plugin',
13+
'plugin-bridge',
14+
'runtime',
15+
'tanstack-query-plugin',
16+
'vite-plugin',
17+
'website',
18+
'redux-devtools-plugin',
19+
'playground',
20+
'middleware',
21+
'repack',
22+
'performance-monitor-plugin',
23+
'tools',
24+
'react-navigation-plugin',
25+
'require-profiler-plugin',
26+
'overlay-plugin',
27+
'chrome-extension',
28+
'web',
29+
'storage-plugin',
30+
'controls-plugin',
31+
'agent-bridge',
32+
'agent-shared',
33+
'file-system-plugin',
34+
'sqlite-plugin',
35+
'',
36+
],
37+
],
38+
},
39+
};
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import { afterEach, describe, expect, it } from 'vitest';
2+
import fs from 'node:fs/promises';
3+
import os from 'node:os';
4+
import path from 'node:path';
5+
import { syncPluginPackageJSON } from '../utils/plugin-package-json.js';
6+
7+
const tempDirs: string[] = [];
8+
9+
const createTempDir = async () => {
10+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'rozenite-plugin-'));
11+
tempDirs.push(dir);
12+
return dir;
13+
};
14+
15+
const writeJson = async (filePath: string, value: unknown) => {
16+
await fs.writeFile(filePath, JSON.stringify(value, null, 2) + '\n');
17+
};
18+
19+
afterEach(async () => {
20+
await Promise.all(
21+
tempDirs
22+
.splice(0)
23+
.map((dir) => fs.rm(dir, { recursive: true, force: true })),
24+
);
25+
});
26+
27+
describe('syncPluginPackageJSON', () => {
28+
it('preserves unknown exports while updating managed entries', async () => {
29+
const projectRoot = await createTempDir();
30+
31+
await writeJson(path.join(projectRoot, 'package.json'), {
32+
name: 'demo-plugin',
33+
type: 'module',
34+
main: './dist/react-native.cjs',
35+
module: './dist/react-native.js',
36+
types: './dist/react-native.d.ts',
37+
exports: {
38+
'.': {
39+
types: './dist/react-native.d.ts',
40+
import: './dist/react-native.js',
41+
require: './dist/react-native.cjs',
42+
},
43+
'./metro': {
44+
types: './dist/metro.d.ts',
45+
import: './dist/metro.js',
46+
require: './dist/metro.cjs',
47+
},
48+
'./custom': './src/custom.ts',
49+
},
50+
});
51+
52+
await fs.writeFile(
53+
path.join(projectRoot, 'react-native.ts'),
54+
'export {}\n',
55+
);
56+
await fs.writeFile(path.join(projectRoot, 'metro.ts'), 'export {}\n');
57+
58+
const result = await syncPluginPackageJSON(projectRoot);
59+
const packageJson = JSON.parse(
60+
await fs.readFile(path.join(projectRoot, 'package.json'), 'utf8'),
61+
);
62+
63+
expect(result.updatedFields).toEqual([
64+
'main',
65+
'module',
66+
'types',
67+
'exports',
68+
]);
69+
expect(packageJson.main).toBe('./dist/react-native/index.cjs');
70+
expect(packageJson.module).toBe('./dist/react-native/index.js');
71+
expect(packageJson.types).toBe('./dist/react-native/index.d.ts');
72+
expect(packageJson.exports).toEqual({
73+
'.': {
74+
types: './dist/react-native/index.d.ts',
75+
import: './dist/react-native/index.js',
76+
require: './dist/react-native/index.cjs',
77+
},
78+
'./metro': {
79+
types: './dist/metro/index.d.ts',
80+
import: './dist/metro/index.js',
81+
require: './dist/metro/index.cjs',
82+
},
83+
'./custom': './src/custom.ts',
84+
'./package.json': './package.json',
85+
});
86+
});
87+
88+
it('removes only the managed metro export when no metro target exists', async () => {
89+
const projectRoot = await createTempDir();
90+
91+
await writeJson(path.join(projectRoot, 'package.json'), {
92+
name: 'demo-plugin',
93+
type: 'module',
94+
main: './dist/react-native/index.cjs',
95+
module: './dist/react-native/index.js',
96+
types: './dist/react-native/index.d.ts',
97+
exports: {
98+
'.': {
99+
types: './dist/react-native/index.d.ts',
100+
import: './dist/react-native/index.js',
101+
require: './dist/react-native/index.cjs',
102+
},
103+
'./metro': {
104+
types: './dist/metro/index.d.ts',
105+
import: './dist/metro/index.js',
106+
require: './dist/metro/index.cjs',
107+
},
108+
'./custom': './src/custom.ts',
109+
},
110+
});
111+
112+
await fs.writeFile(
113+
path.join(projectRoot, 'react-native.ts'),
114+
'export {}\n',
115+
);
116+
117+
const result = await syncPluginPackageJSON(projectRoot);
118+
const packageJson = JSON.parse(
119+
await fs.readFile(path.join(projectRoot, 'package.json'), 'utf8'),
120+
);
121+
122+
expect(result.updatedFields).toEqual(['exports']);
123+
expect(packageJson.exports).toEqual({
124+
'.': {
125+
types: './dist/react-native/index.d.ts',
126+
import: './dist/react-native/index.js',
127+
require: './dist/react-native/index.cjs',
128+
},
129+
'./custom': './src/custom.ts',
130+
'./package.json': './package.json',
131+
});
132+
});
133+
134+
it('leaves root entry fields untouched when there is no react native target', async () => {
135+
const projectRoot = await createTempDir();
136+
137+
await writeJson(path.join(projectRoot, 'package.json'), {
138+
name: 'demo-plugin',
139+
type: 'module',
140+
main: './custom-main.cjs',
141+
module: './custom-module.js',
142+
types: './custom-types.d.ts',
143+
exports: {
144+
'.': './custom-entry.js',
145+
'./metro': {
146+
types: './dist/metro/index.d.ts',
147+
import: './dist/metro/index.js',
148+
require: './dist/metro/index.cjs',
149+
},
150+
'./custom': './src/custom.ts',
151+
},
152+
});
153+
154+
const result = await syncPluginPackageJSON(projectRoot);
155+
const packageJson = JSON.parse(
156+
await fs.readFile(path.join(projectRoot, 'package.json'), 'utf8'),
157+
);
158+
159+
expect(result.updatedFields).toEqual(['exports']);
160+
expect(packageJson.main).toBe('./custom-main.cjs');
161+
expect(packageJson.module).toBe('./custom-module.js');
162+
expect(packageJson.types).toBe('./custom-types.d.ts');
163+
expect(packageJson.exports).toEqual({
164+
'.': './custom-entry.js',
165+
'./custom': './src/custom.ts',
166+
});
167+
});
168+
});

packages/cli/src/commands/build-command.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,22 @@ import fs from 'node:fs/promises';
22
import path from 'node:path';
33
import { step } from '../utils/steps.js';
44
import { spawn } from '../utils/spawn.js';
5-
import { fileExists } from '../utils/files.js';
65
import { intro, outro } from '../utils/prompts.js';
6+
import { logger } from '../utils/logger.js';
7+
import { syncPluginPackageJSON } from '../utils/plugin-package-json.js';
78

89
export const buildCommand = async (targetDir: string) => {
910
intro('Rozenite');
1011

11-
const hasReactNativeEntryPoint = await fileExists('react-native.ts');
12-
const hasMetroEntryPoint = await fileExists('metro.ts');
12+
const { updatedFields, targets } = await syncPluginPackageJSON(targetDir);
13+
14+
if (updatedFields.length > 0) {
15+
logger.warn(
16+
`Updated package.json builder-managed fields: ${updatedFields.join(', ')}`,
17+
);
18+
}
19+
20+
const { hasReactNativeEntryPoint, hasMetroEntryPoint } = targets;
1321

1422
await step(
1523
{
@@ -22,7 +30,7 @@ export const buildCommand = async (targetDir: string) => {
2230
recursive: true,
2331
force: true,
2432
});
25-
}
33+
},
2634
);
2735

2836
await step(
@@ -35,7 +43,7 @@ export const buildCommand = async (targetDir: string) => {
3543
await spawn('vite', ['build'], {
3644
cwd: targetDir,
3745
});
38-
}
46+
},
3947
);
4048

4149
if (hasMetroEntryPoint) {
@@ -52,7 +60,7 @@ export const buildCommand = async (targetDir: string) => {
5260
VITE_ROZENITE_TARGET: 'server',
5361
},
5462
});
55-
}
63+
},
5664
);
5765
}
5866

@@ -70,7 +78,7 @@ export const buildCommand = async (targetDir: string) => {
7078
VITE_ROZENITE_TARGET: 'react-native',
7179
},
7280
});
73-
}
81+
},
7482
);
7583
}
7684

packages/cli/src/commands/dev-command.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
import { spawn, Subprocess } from '../utils/spawn.js';
2-
import { fileExists } from '../utils/files.js';
32
import { intro, outro } from '../utils/prompts.js';
43
import { logger } from '../utils/logger.js';
4+
import { syncPluginPackageJSON } from '../utils/plugin-package-json.js';
55

66
export const devCommand = async (targetDir: string) => {
77
intro('Rozenite');
88

9-
const hasReactNativeEntryPoint = await fileExists('react-native.ts');
10-
const hasMetroEntryPoint = await fileExists('metro.ts');
9+
const { updatedFields, targets } = await syncPluginPackageJSON(targetDir);
10+
11+
if (updatedFields.length > 0) {
12+
logger.warn(
13+
`Updated package.json builder-managed fields: ${updatedFields.join(', ')}`,
14+
);
15+
}
16+
17+
const { hasReactNativeEntryPoint, hasMetroEntryPoint } = targets;
1118

1219
try {
1320
const processes: Subprocess[] = [];

0 commit comments

Comments
 (0)