Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/pages/more/create-expo.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ Uses the default options to create a new project.

Skips installing npm dependencies or CocoaPods.

### `--no-agents-md`

Skips generating `AGENTS.md`, `CLAUDE.md`, and `.claude/settings.json`. By default, `create-expo-app` generates these files so AI coding agents (such as Claude Code) have Expo-specific context and the [`expo` skills plugin](https://expo.dev/expo-skills) configured automatically. The generated `AGENTS.md` points to the versioned Expo docs matching your project's SDK version.

### `--template`

Running `create-expo-app` with a [Node Package Manager](#node-package-managers-support) initializes and sets up a new Expo project using the default template.
Expand Down
2 changes: 2 additions & 0 deletions packages/create-expo/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

### 🎉 New features

- Generate `AGENTS.md`, `CLAUDE.md`, and `.claude/settings.json` for new projects to provide AI coding agents with Expo-specific guidance and the `expo` skills plugin. Use `--no-agents-md` to skip. ([#44618](https://github.com/expo/expo/pull/44618) by [@EvanBacon](https://github.com/EvanBacon))

### 🐛 Bug fixes

### 💡 Others
Expand Down
85 changes: 85 additions & 0 deletions packages/create-expo/src/__tests__/generateAgentFiles.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import fs from 'fs';
import os from 'os';
import path from 'path';

import { generateAgentFiles } from '../generateAgentFiles';

describe(generateAgentFiles, () => {
let tmpDir: string;

beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'create-expo-test-'));
// Write a minimal package.json so the SDK version can be resolved
fs.writeFileSync(
path.join(tmpDir, 'package.json'),
JSON.stringify({ dependencies: { expo: '~55.0.0' } })
);
});

afterEach(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
});

it('generates all three files when none exist', () => {
generateAgentFiles(tmpDir);

expect(fs.existsSync(path.join(tmpDir, 'AGENTS.md'))).toBe(true);
expect(fs.existsSync(path.join(tmpDir, 'CLAUDE.md'))).toBe(true);
expect(fs.existsSync(path.join(tmpDir, '.claude', 'settings.json'))).toBe(true);
});

it('writes correct content to AGENTS.md with versioned docs URL', () => {
generateAgentFiles(tmpDir);

const content = fs.readFileSync(path.join(tmpDir, 'AGENTS.md'), 'utf-8');
expect(content).toContain('# Expo');
expect(content).toContain('https://docs.expo.dev/versions/v55.0.0/');
});

it('falls back to unversioned docs URL when expo version is missing', () => {
fs.writeFileSync(path.join(tmpDir, 'package.json'), JSON.stringify({ dependencies: {} }));

generateAgentFiles(tmpDir);

const content = fs.readFileSync(path.join(tmpDir, 'AGENTS.md'), 'utf-8');
expect(content).toContain('https://docs.expo.dev');
expect(content).not.toContain('/versions/');
});

it('writes correct content to CLAUDE.md', () => {
generateAgentFiles(tmpDir);

const content = fs.readFileSync(path.join(tmpDir, 'CLAUDE.md'), 'utf-8');
expect(content).toBe('@AGENTS.md\n');
});

it('writes correct content to .claude/settings.json', () => {
generateAgentFiles(tmpDir);

const content = JSON.parse(
fs.readFileSync(path.join(tmpDir, '.claude', 'settings.json'), 'utf-8')
);
expect(content).toEqual({ plugins: ['expo'] });
});

it('skips files that already exist', () => {
fs.writeFileSync(path.join(tmpDir, 'AGENTS.md'), 'custom content');
fs.writeFileSync(path.join(tmpDir, 'CLAUDE.md'), 'custom claude');

generateAgentFiles(tmpDir);

expect(fs.readFileSync(path.join(tmpDir, 'AGENTS.md'), 'utf-8')).toBe('custom content');
expect(fs.readFileSync(path.join(tmpDir, 'CLAUDE.md'), 'utf-8')).toBe('custom claude');
// .claude/settings.json should still be created since it didn't exist
expect(fs.existsSync(path.join(tmpDir, '.claude', 'settings.json'))).toBe(true);
});

it('creates .claude/ directory when it does not exist', () => {
expect(fs.existsSync(path.join(tmpDir, '.claude'))).toBe(false);

generateAgentFiles(tmpDir);

expect(fs.existsSync(path.join(tmpDir, '.claude'))).toBe(true);
expect(fs.statSync(path.join(tmpDir, '.claude')).isDirectory()).toBe(true);
});
});
3 changes: 3 additions & 0 deletions packages/create-expo/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ async function run() {
// Types
'--yes': Boolean,
'--no-install': Boolean,
'--no-agents-md': Boolean,
'--help': Boolean,
'--version': Boolean,
// Aliases
Expand All @@ -40,6 +41,7 @@ async function run() {
[
`-y, --yes Use the default options for creating a project`,
` --no-install Skip installing npm packages or CocoaPods`,
` --no-agents-md Skip generating AGENTS.md, CLAUDE.md, and .claude/settings.json`,
chalk`-t, --template {gray [pkg]} NPM template to use: default, blank, blank-typescript, tabs, bare-minimum. Default: default`,
chalk`-e, --example {gray [name]} Example name from {underline https://github.com/expo/examples}.`,
`-v, --version Version number`,
Expand Down Expand Up @@ -86,6 +88,7 @@ async function run() {
template: parsed.args['--template'],
example: parsed.args['--example'],
install: !args['--no-install'],
agentsMd: !args['--no-agents-md'],
});

// Track successful event.
Expand Down
10 changes: 10 additions & 0 deletions packages/create-expo/src/createAsync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
promptExamplesAsync,
} from './Examples';
import * as Template from './Template';
import { generateAgentFiles } from './generateAgentFiles';
import { promptTemplateAsync } from './legacyTemplates';
import { Log } from './log';
import {
Expand All @@ -36,6 +37,7 @@ export type Options = {
template?: string | true;
example?: string | true;
yes: boolean;
agentsMd: boolean;
};

const debug = require('debug')('expo:init:create') as typeof console.log;
Expand Down Expand Up @@ -138,6 +140,10 @@ async function createTemplateAsync(inputPath: string, props: Options): Promise<v

await setupDependenciesAsync(projectRoot, props);

if (props.agentsMd) {
generateAgentFiles(projectRoot);
}

// for now, we will just init a git repo if they have git installed and the
// project is not inside an existing git tree, and do it silently. we should
// at some point check if git is installed and actually bail out if not, because
Expand Down Expand Up @@ -223,6 +229,10 @@ async function createExampleAsync(inputPath: string, props: Options): Promise<vo

await setupDependenciesAsync(projectRoot, props);

if (props.agentsMd) {
generateAgentFiles(projectRoot);
}

// for now, we will just init a git repo if they have git installed and the
// project is not inside an existing git tree, and do it silently. we should
// at some point check if git is installed and actually bail out if not, because
Expand Down
51 changes: 51 additions & 0 deletions packages/create-expo/src/generateAgentFiles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import fs from 'fs';
import path from 'path';

function getExpoSdkVersion(root: string): string | null {
try {
const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf-8'));
const expoVersion = (pkg.dependencies?.expo ?? pkg.devDependencies?.expo) as string | undefined;
if (!expoVersion) return null;
const match = expoVersion.match(/(\d+)\./);
return match?.[1] ?? null;
} catch {
return null;
}
}

function getAgentsMdContent(sdkVersion: string | null): string {
const docsUrl = sdkVersion
? `https://docs.expo.dev/versions/v${sdkVersion}.0.0/`
: 'https://docs.expo.dev';
return `# Expo HAS CHANGED

Read the exact versioned docs at ${docsUrl} before writing any code.
`;
}

const CLAUDE_MD_CONTENT = `@AGENTS.md
`;

const CLAUDE_SETTINGS_CONTENT = `{
"plugins": ["expo"]
}
`;

export function generateAgentFiles(root: string): void {
const sdkVersion = getExpoSdkVersion(root);

const files: { filePath: string; content: string }[] = [
{ filePath: path.join(root, 'AGENTS.md'), content: getAgentsMdContent(sdkVersion) },
{ filePath: path.join(root, 'CLAUDE.md'), content: CLAUDE_MD_CONTENT },
{ filePath: path.join(root, '.claude', 'settings.json'), content: CLAUDE_SETTINGS_CONTENT },
];

for (const { filePath, content } of files) {
if (fs.existsSync(filePath)) {
continue;
}
const dir = path.dirname(filePath);
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(filePath, content);
}
}
Loading