Skip to content

Commit

Permalink
Add expo plugin (#879)
Browse files Browse the repository at this point in the history
  • Loading branch information
jjjjonathan authored Dec 16, 2024
1 parent 9ce4a49 commit 40f7be9
Show file tree
Hide file tree
Showing 22 changed files with 375 additions and 0 deletions.
20 changes: 20 additions & 0 deletions packages/knip/fixtures/plugins/expo/app.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const config = {
name: 'Knip',
updates: {
enabled: true,
},
notification: {
color: '#ffffff',
},
userInterfaceStyle: 'automatic',
ios: {
backgroundColor: '#ffffff',
},
plugins: [
['@config-plugins/detox', { subdomains: '*' }],
'@sentry/react-native/expo',
['expo-splash-screen', { backgroundColor: '#ffffff' }],
],
};

export default config;

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 21 additions & 0 deletions packages/knip/fixtures/plugins/expo/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "@fixtures/expo",
"version": "*",
"scripts": {
"start": "expo start"
},
"dependencies": {
"@config-plugins/detox": "*",
"@sentry/react-native": "*",
"expo": "*",
"expo-atlas": "*",
"expo-dev-client": "*",
"expo-notifications": "*",
"expo-router": "*",
"expo-splash-screen": "*",
"expo-system-ui": "*",
"expo-updates": "*",
"react": "*",
"react-native": "*"
}
}
Empty file.
21 changes: 21 additions & 0 deletions packages/knip/fixtures/plugins/expo2/app.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const config = {
name: 'Knip',
platforms: ['android'],
androidNavigationBar: {
visible: true,
},
android: {
userInterfaceStyle: 'dark',
},
plugins: [
'expo-camera',
[
'expo-router',
{
root: 'src/routes',
},
],
],
};

export default config;

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions packages/knip/fixtures/plugins/expo2/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "@fixtures/expo2",
"version": "*",
"main": "expo-router/entry",
"scripts": {
"start": "expo start"
},
"dependencies": {
"expo": "*",
"expo-dev-client": "*",
"expo-insights": "*",
"expo-navigation-bar": "*",
"expo-router": "*",
"react": "*",
"react-native": "*"
}
}
Empty file.
11 changes: 11 additions & 0 deletions packages/knip/fixtures/plugins/expo3/app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"expo": {
"name": "Knip",
"slug": "knip",
"platforms": ["ios", "web"],
"updates": {
"enabled": false
},
"plugins": ["react-native-ble-plx"]
}
}
Empty file.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions packages/knip/fixtures/plugins/expo3/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "@fixtures/expo3",
"version": "*",
"main": "expo-router/entry",
"scripts": {
"start": "expo start"
},
"dependencies": {
"@expo/metro-runtime": "*",
"expo": "*",
"expo-system-ui": "*",
"expo-updates": "*",
"react": "*",
"react-dom": "*",
"react-native": "*",
"react-native-web": "*"
}
}
4 changes: 4 additions & 0 deletions packages/knip/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,10 @@
"title": "ESLint plugin configuration (https://knip.dev/reference/plugins/eslint)",
"$ref": "#/definitions/plugin"
},
"expo": {
"title": "Expo plugin configuration (https://knip.dev/reference/plugins/expo)",
"$ref": "#/definitions/plugin"
},
"gatsby": {
"title": "Gatsby plugin configuration (https://knip.dev/reference/plugins/gatsby)",
"$ref": "#/definitions/plugin"
Expand Down
78 changes: 78 additions & 0 deletions packages/knip/src/plugins/expo/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import type { PluginOptions } from '../../types/config.js';
import { type Input, toDependency, toProductionDependency } from '../../util/input.js';
import { getPackageNameFromModuleSpecifier } from '../../util/modules.js';
import type { ExpoConfig } from './types.js';

// https://docs.expo.dev/versions/latest/config/app

export const getDependencies = async (expoConfig: ExpoConfig, { manifest }: PluginOptions) => {
const config = 'expo' in expoConfig ? expoConfig.expo : expoConfig;

const platforms = config.platforms ?? ['ios', 'android'];

const pluginPackages =
(config.plugins
?.map(plugin => {
const pluginName = Array.isArray(plugin) ? plugin[0] : plugin;
return getPackageNameFromModuleSpecifier(pluginName);
})
.filter(Boolean) as string[]) ?? [];

const inputs = new Set<Input>(pluginPackages.map(toDependency));

const allowedPackages = ['expo-atlas', 'expo-dev-client'];
const allowedProductionPackages = ['expo-insights'];

const manifestDependencies = Object.keys(manifest.dependencies ?? {});

for (const pkg of allowedPackages) {
if (manifestDependencies.includes(pkg)) {
inputs.add(toDependency(pkg));
}
}

for (const pkg of allowedProductionPackages) {
if (manifestDependencies.includes(pkg)) {
inputs.add(toProductionDependency(pkg));
}
}

if (config.updates?.enabled !== false) {
inputs.add(toProductionDependency('expo-updates'));
}

if (config.notification) {
inputs.add(toProductionDependency('expo-notifications'));
}

const isExpoRouter = manifest.main === 'expo-router/entry';

// https://docs.expo.dev/router/installation/#setup-entry-point
if (isExpoRouter) {
inputs.add(toProductionDependency('expo-router'));
}

// https://docs.expo.dev/workflow/web/#install-web-dependencies
if (platforms.includes('web')) {
inputs.add(toProductionDependency('react-native-web'));
inputs.add(toProductionDependency('react-dom'));

// https://github.com/expo/expo/tree/main/packages/@expo/metro-runtime
if (!isExpoRouter) {
inputs.add(toDependency('@expo/metro-runtime'));
}
}

if (
(platforms.includes('android') && (config.userInterfaceStyle || config.android?.userInterfaceStyle)) ||
(platforms.includes('ios') && (config.backgroundColor || config.ios?.backgroundColor))
) {
inputs.add(toProductionDependency('expo-system-ui'));
}

if (platforms.includes('android') && config.androidNavigationBar) {
inputs.add(toProductionDependency('expo-navigation-bar'));
}

return [...inputs];
};
52 changes: 52 additions & 0 deletions packages/knip/src/plugins/expo/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import type { IsPluginEnabled, Plugin, ResolveConfig, ResolveEntryPaths } from '../../types/config.js';
import { toProductionEntry } from '../../util/input.js';
import { join } from '../../util/path.js';
import { hasDependency } from '../../util/plugin.js';
import { getDependencies } from './helpers.js';
import type { ExpoConfig } from './types.js';

// https://docs.expo.dev/

const title = 'Expo';

const enablers = ['expo'];

const isEnabled: IsPluginEnabled = ({ dependencies }) => hasDependency(dependencies, enablers);

const config: string[] = ['app.json', 'app.config.{ts,js}'];

const resolveEntryPaths: ResolveEntryPaths<ExpoConfig> = async (expoConfig, { manifest }) => {
const config = 'expo' in expoConfig ? expoConfig.expo : expoConfig;

let production: string[] = [];

// https://docs.expo.dev/router/installation/#setup-entry-point
if (manifest.main === 'expo-router/entry') {
production = ['app/**/*.{js,jsx,ts,tsx}', 'src/app/**/*.{js,jsx,ts,tsx}'];

const normalizedPlugins =
config.plugins?.map(plugin => (Array.isArray(plugin) ? plugin : ([plugin] as const))) ?? [];
const expoRouterPlugin = normalizedPlugins.find(([plugin]) => plugin === 'expo-router');

if (expoRouterPlugin) {
const [, options] = expoRouterPlugin;

if (typeof options?.root === 'string') {
production = [join(options.root, '**/*.{js,jsx,ts,tsx}')];
}
}
}

return production.map(entry => toProductionEntry(entry));
};

const resolveConfig: ResolveConfig<ExpoConfig> = async (expoConfig, options) => getDependencies(expoConfig, options);

export default {
title,
enablers,
isEnabled,
config,
resolveEntryPaths,
resolveConfig,
} satisfies Plugin;
21 changes: 21 additions & 0 deletions packages/knip/src/plugins/expo/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// https://github.com/expo/expo/blob/main/packages/%40expo/config-types/src/ExpoConfig.ts

type AppConfig = {
platforms?: ('ios' | 'android' | 'web')[];
notification?: Record<string, unknown>;
updates?: {
enabled?: boolean;
};
backgroundColor?: string;
userInterfaceStyle?: 'automatic' | 'light' | 'dark';
ios?: {
backgroundColor?: string;
};
android?: {
userInterfaceStyle?: 'automatic' | 'light' | 'dark';
};
androidNavigationBar?: Record<string, unknown>;
plugins?: (string | [string, Record<string, unknown>])[];
};

export type ExpoConfig = AppConfig | { expo: AppConfig };
2 changes: 2 additions & 0 deletions packages/knip/src/plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { default as dotenv } from './dotenv/index.js';
import { default as drizzle } from './drizzle/index.js';
import { default as eleventy } from './eleventy/index.js';
import { default as eslint } from './eslint/index.js';
import { default as expo } from './expo/index.js';
import { default as gatsby } from './gatsby/index.js';
import { default as githubActions } from './github-actions/index.js';
import { default as glob } from './glob/index.js';
Expand Down Expand Up @@ -105,6 +106,7 @@ export const Plugins = {
drizzle,
eleventy,
eslint,
expo,
gatsby,
'github-actions': githubActions,
glob,
Expand Down
1 change: 1 addition & 0 deletions packages/knip/src/schema/plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export const pluginsSchema = z.object({
drizzle: pluginSchema,
eleventy: pluginSchema,
eslint: pluginSchema,
expo: pluginSchema,
gatsby: pluginSchema,
'github-actions': pluginSchema,
glob: pluginSchema,
Expand Down
2 changes: 2 additions & 0 deletions packages/knip/src/types/PluginNames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export type PluginName =
| 'drizzle'
| 'eleventy'
| 'eslint'
| 'expo'
| 'gatsby'
| 'github-actions'
| 'glob'
Expand Down Expand Up @@ -106,6 +107,7 @@ export const pluginNames = [
'drizzle',
'eleventy',
'eslint',
'expo',
'gatsby',
'github-actions',
'glob',
Expand Down
27 changes: 27 additions & 0 deletions packages/knip/test/plugins/expo.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { test } from 'bun:test';
import assert from 'node:assert/strict';
import { main } from '../../src/index.js';
import { join, resolve } from '../../src/util/path.js';
import baseArguments from '../helpers/baseArguments.js';
import baseCounters from '../helpers/baseCounters.js';

const cwd = resolve('fixtures/plugins/expo');

test('Find dependencies with the Expo plugin (1)', async () => {
const { issues, counters } = await main({
...baseArguments,
cwd,
});

assert(issues.files.has(join(cwd, 'src/app/index.ts')));

assert(issues.dependencies['package.json']['expo-router']);

assert.deepEqual(counters, {
...baseCounters,
processed: 2,
total: 2,
files: 1,
dependencies: 1,
});
});
Loading

0 comments on commit 40f7be9

Please sign in to comment.