Skip to content

Commit 40f7be9

Browse files
authored
Add expo plugin (#879)
1 parent 9ce4a49 commit 40f7be9

File tree

22 files changed

+375
-0
lines changed

22 files changed

+375
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
const config = {
2+
name: 'Knip',
3+
updates: {
4+
enabled: true,
5+
},
6+
notification: {
7+
color: '#ffffff',
8+
},
9+
userInterfaceStyle: 'automatic',
10+
ios: {
11+
backgroundColor: '#ffffff',
12+
},
13+
plugins: [
14+
['@config-plugins/detox', { subdomains: '*' }],
15+
'@sentry/react-native/expo',
16+
['expo-splash-screen', { backgroundColor: '#ffffff' }],
17+
],
18+
};
19+
20+
export default config;

packages/knip/fixtures/plugins/expo/node_modules/expo/package.json

+8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"name": "@fixtures/expo",
3+
"version": "*",
4+
"scripts": {
5+
"start": "expo start"
6+
},
7+
"dependencies": {
8+
"@config-plugins/detox": "*",
9+
"@sentry/react-native": "*",
10+
"expo": "*",
11+
"expo-atlas": "*",
12+
"expo-dev-client": "*",
13+
"expo-notifications": "*",
14+
"expo-router": "*",
15+
"expo-splash-screen": "*",
16+
"expo-system-ui": "*",
17+
"expo-updates": "*",
18+
"react": "*",
19+
"react-native": "*"
20+
}
21+
}

packages/knip/fixtures/plugins/expo/src/app/index.ts

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
const config = {
2+
name: 'Knip',
3+
platforms: ['android'],
4+
androidNavigationBar: {
5+
visible: true,
6+
},
7+
android: {
8+
userInterfaceStyle: 'dark',
9+
},
10+
plugins: [
11+
'expo-camera',
12+
[
13+
'expo-router',
14+
{
15+
root: 'src/routes',
16+
},
17+
],
18+
],
19+
};
20+
21+
export default config;

packages/knip/fixtures/plugins/expo2/node_modules/expo/package.json

+8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"name": "@fixtures/expo2",
3+
"version": "*",
4+
"main": "expo-router/entry",
5+
"scripts": {
6+
"start": "expo start"
7+
},
8+
"dependencies": {
9+
"expo": "*",
10+
"expo-dev-client": "*",
11+
"expo-insights": "*",
12+
"expo-navigation-bar": "*",
13+
"expo-router": "*",
14+
"react": "*",
15+
"react-native": "*"
16+
}
17+
}

packages/knip/fixtures/plugins/expo2/src/routes/index.js

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"expo": {
3+
"name": "Knip",
4+
"slug": "knip",
5+
"platforms": ["ios", "web"],
6+
"updates": {
7+
"enabled": false
8+
},
9+
"plugins": ["react-native-ble-plx"]
10+
}
11+
}

packages/knip/fixtures/plugins/expo3/app/_layout.ts

Whitespace-only changes.

packages/knip/fixtures/plugins/expo3/node_modules/expo/package.json

+8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"name": "@fixtures/expo3",
3+
"version": "*",
4+
"main": "expo-router/entry",
5+
"scripts": {
6+
"start": "expo start"
7+
},
8+
"dependencies": {
9+
"@expo/metro-runtime": "*",
10+
"expo": "*",
11+
"expo-system-ui": "*",
12+
"expo-updates": "*",
13+
"react": "*",
14+
"react-dom": "*",
15+
"react-native": "*",
16+
"react-native-web": "*"
17+
}
18+
}

packages/knip/schema.json

+4
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,10 @@
344344
"title": "ESLint plugin configuration (https://knip.dev/reference/plugins/eslint)",
345345
"$ref": "#/definitions/plugin"
346346
},
347+
"expo": {
348+
"title": "Expo plugin configuration (https://knip.dev/reference/plugins/expo)",
349+
"$ref": "#/definitions/plugin"
350+
},
347351
"gatsby": {
348352
"title": "Gatsby plugin configuration (https://knip.dev/reference/plugins/gatsby)",
349353
"$ref": "#/definitions/plugin"
+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import type { PluginOptions } from '../../types/config.js';
2+
import { type Input, toDependency, toProductionDependency } from '../../util/input.js';
3+
import { getPackageNameFromModuleSpecifier } from '../../util/modules.js';
4+
import type { ExpoConfig } from './types.js';
5+
6+
// https://docs.expo.dev/versions/latest/config/app
7+
8+
export const getDependencies = async (expoConfig: ExpoConfig, { manifest }: PluginOptions) => {
9+
const config = 'expo' in expoConfig ? expoConfig.expo : expoConfig;
10+
11+
const platforms = config.platforms ?? ['ios', 'android'];
12+
13+
const pluginPackages =
14+
(config.plugins
15+
?.map(plugin => {
16+
const pluginName = Array.isArray(plugin) ? plugin[0] : plugin;
17+
return getPackageNameFromModuleSpecifier(pluginName);
18+
})
19+
.filter(Boolean) as string[]) ?? [];
20+
21+
const inputs = new Set<Input>(pluginPackages.map(toDependency));
22+
23+
const allowedPackages = ['expo-atlas', 'expo-dev-client'];
24+
const allowedProductionPackages = ['expo-insights'];
25+
26+
const manifestDependencies = Object.keys(manifest.dependencies ?? {});
27+
28+
for (const pkg of allowedPackages) {
29+
if (manifestDependencies.includes(pkg)) {
30+
inputs.add(toDependency(pkg));
31+
}
32+
}
33+
34+
for (const pkg of allowedProductionPackages) {
35+
if (manifestDependencies.includes(pkg)) {
36+
inputs.add(toProductionDependency(pkg));
37+
}
38+
}
39+
40+
if (config.updates?.enabled !== false) {
41+
inputs.add(toProductionDependency('expo-updates'));
42+
}
43+
44+
if (config.notification) {
45+
inputs.add(toProductionDependency('expo-notifications'));
46+
}
47+
48+
const isExpoRouter = manifest.main === 'expo-router/entry';
49+
50+
// https://docs.expo.dev/router/installation/#setup-entry-point
51+
if (isExpoRouter) {
52+
inputs.add(toProductionDependency('expo-router'));
53+
}
54+
55+
// https://docs.expo.dev/workflow/web/#install-web-dependencies
56+
if (platforms.includes('web')) {
57+
inputs.add(toProductionDependency('react-native-web'));
58+
inputs.add(toProductionDependency('react-dom'));
59+
60+
// https://github.com/expo/expo/tree/main/packages/@expo/metro-runtime
61+
if (!isExpoRouter) {
62+
inputs.add(toDependency('@expo/metro-runtime'));
63+
}
64+
}
65+
66+
if (
67+
(platforms.includes('android') && (config.userInterfaceStyle || config.android?.userInterfaceStyle)) ||
68+
(platforms.includes('ios') && (config.backgroundColor || config.ios?.backgroundColor))
69+
) {
70+
inputs.add(toProductionDependency('expo-system-ui'));
71+
}
72+
73+
if (platforms.includes('android') && config.androidNavigationBar) {
74+
inputs.add(toProductionDependency('expo-navigation-bar'));
75+
}
76+
77+
return [...inputs];
78+
};
+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import type { IsPluginEnabled, Plugin, ResolveConfig, ResolveEntryPaths } from '../../types/config.js';
2+
import { toProductionEntry } from '../../util/input.js';
3+
import { join } from '../../util/path.js';
4+
import { hasDependency } from '../../util/plugin.js';
5+
import { getDependencies } from './helpers.js';
6+
import type { ExpoConfig } from './types.js';
7+
8+
// https://docs.expo.dev/
9+
10+
const title = 'Expo';
11+
12+
const enablers = ['expo'];
13+
14+
const isEnabled: IsPluginEnabled = ({ dependencies }) => hasDependency(dependencies, enablers);
15+
16+
const config: string[] = ['app.json', 'app.config.{ts,js}'];
17+
18+
const resolveEntryPaths: ResolveEntryPaths<ExpoConfig> = async (expoConfig, { manifest }) => {
19+
const config = 'expo' in expoConfig ? expoConfig.expo : expoConfig;
20+
21+
let production: string[] = [];
22+
23+
// https://docs.expo.dev/router/installation/#setup-entry-point
24+
if (manifest.main === 'expo-router/entry') {
25+
production = ['app/**/*.{js,jsx,ts,tsx}', 'src/app/**/*.{js,jsx,ts,tsx}'];
26+
27+
const normalizedPlugins =
28+
config.plugins?.map(plugin => (Array.isArray(plugin) ? plugin : ([plugin] as const))) ?? [];
29+
const expoRouterPlugin = normalizedPlugins.find(([plugin]) => plugin === 'expo-router');
30+
31+
if (expoRouterPlugin) {
32+
const [, options] = expoRouterPlugin;
33+
34+
if (typeof options?.root === 'string') {
35+
production = [join(options.root, '**/*.{js,jsx,ts,tsx}')];
36+
}
37+
}
38+
}
39+
40+
return production.map(entry => toProductionEntry(entry));
41+
};
42+
43+
const resolveConfig: ResolveConfig<ExpoConfig> = async (expoConfig, options) => getDependencies(expoConfig, options);
44+
45+
export default {
46+
title,
47+
enablers,
48+
isEnabled,
49+
config,
50+
resolveEntryPaths,
51+
resolveConfig,
52+
} satisfies Plugin;
+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// https://github.com/expo/expo/blob/main/packages/%40expo/config-types/src/ExpoConfig.ts
2+
3+
type AppConfig = {
4+
platforms?: ('ios' | 'android' | 'web')[];
5+
notification?: Record<string, unknown>;
6+
updates?: {
7+
enabled?: boolean;
8+
};
9+
backgroundColor?: string;
10+
userInterfaceStyle?: 'automatic' | 'light' | 'dark';
11+
ios?: {
12+
backgroundColor?: string;
13+
};
14+
android?: {
15+
userInterfaceStyle?: 'automatic' | 'light' | 'dark';
16+
};
17+
androidNavigationBar?: Record<string, unknown>;
18+
plugins?: (string | [string, Record<string, unknown>])[];
19+
};
20+
21+
export type ExpoConfig = AppConfig | { expo: AppConfig };

packages/knip/src/plugins/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { default as dotenv } from './dotenv/index.js';
1515
import { default as drizzle } from './drizzle/index.js';
1616
import { default as eleventy } from './eleventy/index.js';
1717
import { default as eslint } from './eslint/index.js';
18+
import { default as expo } from './expo/index.js';
1819
import { default as gatsby } from './gatsby/index.js';
1920
import { default as githubActions } from './github-actions/index.js';
2021
import { default as glob } from './glob/index.js';
@@ -105,6 +106,7 @@ export const Plugins = {
105106
drizzle,
106107
eleventy,
107108
eslint,
109+
expo,
108110
gatsby,
109111
'github-actions': githubActions,
110112
glob,

packages/knip/src/schema/plugins.ts

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export const pluginsSchema = z.object({
2929
drizzle: pluginSchema,
3030
eleventy: pluginSchema,
3131
eslint: pluginSchema,
32+
expo: pluginSchema,
3233
gatsby: pluginSchema,
3334
'github-actions': pluginSchema,
3435
glob: pluginSchema,

packages/knip/src/types/PluginNames.ts

+2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export type PluginName =
1616
| 'drizzle'
1717
| 'eleventy'
1818
| 'eslint'
19+
| 'expo'
1920
| 'gatsby'
2021
| 'github-actions'
2122
| 'glob'
@@ -106,6 +107,7 @@ export const pluginNames = [
106107
'drizzle',
107108
'eleventy',
108109
'eslint',
110+
'expo',
109111
'gatsby',
110112
'github-actions',
111113
'glob',
+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { test } from 'bun:test';
2+
import assert from 'node:assert/strict';
3+
import { main } from '../../src/index.js';
4+
import { join, resolve } from '../../src/util/path.js';
5+
import baseArguments from '../helpers/baseArguments.js';
6+
import baseCounters from '../helpers/baseCounters.js';
7+
8+
const cwd = resolve('fixtures/plugins/expo');
9+
10+
test('Find dependencies with the Expo plugin (1)', async () => {
11+
const { issues, counters } = await main({
12+
...baseArguments,
13+
cwd,
14+
});
15+
16+
assert(issues.files.has(join(cwd, 'src/app/index.ts')));
17+
18+
assert(issues.dependencies['package.json']['expo-router']);
19+
20+
assert.deepEqual(counters, {
21+
...baseCounters,
22+
processed: 2,
23+
total: 2,
24+
files: 1,
25+
dependencies: 1,
26+
});
27+
});

0 commit comments

Comments
 (0)