-
Notifications
You must be signed in to change notification settings - Fork 36
feat(create-app): add basic functionality #5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
2a71ecb
3d1055d
c0158c3
55fd541
fc4cc78
0b3ebdf
7d00bcc
64c79cc
f17634f
30b2c06
1db653b
d24420f
f9334c9
4f75b17
a0e10d3
41deba1
ff68932
f5802d9
12c11ee
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| templates/** |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,18 @@ | ||
| { | ||
| "name": "@callstack/create-rnef-app", | ||
| "version": "0.0.1", | ||
| "type": "commonjs", | ||
| "main": "./src/index.js", | ||
| "typings": "./src/index.d.ts", | ||
| "bin": { | ||
| "create-rnef-app": "./src/bin.js" | ||
| }, | ||
| "dependencies": { | ||
| "@clack/prompts": "^0.7.0", | ||
| "minimist": "^1.2.8", | ||
| "tslib": "^2.3.0" | ||
| }, | ||
| "type": "commonjs", | ||
| "main": "./src/index.js", | ||
| "typings": "./src/index.d.ts" | ||
| "devDependencies": { | ||
| "@types/minimist": "^1.2.5" | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| #!/usr/bin/env node | ||
| export * from './lib/create-app'; | ||
mdjastrzebski marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| import { parsePackageInfo, parsePackageManagerFromUserAgent } from '../parsers'; | ||
|
|
||
| describe('parsePackageInfo', () => { | ||
| test('handles simple package name: foo', () => { | ||
| const result = parsePackageInfo('foo'); | ||
| expect(result).toEqual({ | ||
| packageName: 'foo', | ||
| targetDir: 'foo', | ||
| }); | ||
| }); | ||
|
|
||
| test('handles nested package path: foo/bar', () => { | ||
| const result = parsePackageInfo('foo/bar'); | ||
| expect(result).toEqual({ | ||
| packageName: 'bar', | ||
| targetDir: 'foo/bar', | ||
| }); | ||
| }); | ||
|
|
||
| test('handles scoped package name: @scope/foo', () => { | ||
| const result = parsePackageInfo('@scope/foo'); | ||
| expect(result).toEqual({ | ||
| packageName: '@scope/foo', | ||
| targetDir: '@scope/foo', | ||
| }); | ||
| }); | ||
|
|
||
| test('handles relative path: ./foo/bar', () => { | ||
| const result = parsePackageInfo('./foo/bar'); | ||
| expect(result).toEqual({ | ||
| packageName: 'bar', | ||
| targetDir: './foo/bar', | ||
| }); | ||
| }); | ||
|
|
||
| test('handles absolute path: /root/path/to/foo', () => { | ||
| const result = parsePackageInfo('/root/path/to/foo'); | ||
| expect(result).toEqual({ | ||
| packageName: 'foo', | ||
| targetDir: '/root/path/to/foo', | ||
| }); | ||
| }); | ||
|
|
||
| test('trims trailing slashes: foo/bar/', () => { | ||
| const result = parsePackageInfo('foo/bar/'); | ||
| expect(result).toEqual({ | ||
| packageName: 'bar', | ||
| targetDir: 'foo/bar', | ||
| }); | ||
| }); | ||
| }); | ||
|
|
||
| describe('parsePackageManagerFromUserAgent', () => { | ||
| it('should return undefined for undefined input', () => { | ||
| expect(parsePackageManagerFromUserAgent(undefined)).toBeUndefined(); | ||
| }); | ||
|
|
||
| it('should parse npm user agent correctly', () => { | ||
| const npmUserAgent = 'npm/10.1.0 node/v20.8.1 linux x64 workspaces/false'; | ||
| const expected = { name: 'npm', version: '10.1.0' }; | ||
| expect(parsePackageManagerFromUserAgent(npmUserAgent)).toEqual(expected); | ||
| }); | ||
|
|
||
| it('should parse yarn user agent correctly', () => { | ||
| const yarnUserAgent = 'yarn/1.22.21 npm/? node/v20.8.1 linux x64'; | ||
| const expected = { name: 'yarn', version: '1.22.21' }; | ||
| expect(parsePackageManagerFromUserAgent(yarnUserAgent)).toEqual(expected); | ||
| }); | ||
|
|
||
| it('should parse pnpm user agent correctly', () => { | ||
| const pnpmUserAgent = 'pnpm/8.10.5 npm/? node/v20.8.1 linux x64'; | ||
| const expected = { name: 'pnpm', version: '8.10.5' }; | ||
| expect(parsePackageManagerFromUserAgent(pnpmUserAgent)).toEqual(expected); | ||
| }); | ||
|
|
||
| it('should parse bun user agent correctly', () => { | ||
| const bunUserAgent = 'bun/0.5.7 (linux-x64) node/v18.0.0'; | ||
| const expected = { name: 'bun', version: '0.5.7' }; | ||
| expect(parsePackageManagerFromUserAgent(bunUserAgent)).toEqual(expected); | ||
| }); | ||
|
|
||
| it('should handle unknown package managers', () => { | ||
| const unknownUserAgent = 'unknown/1.0.0 node/v16.18.0 linux x64'; | ||
| const expected = { name: 'unknown', version: '1.0.0' }; | ||
| expect(parsePackageManagerFromUserAgent(unknownUserAgent)).toEqual( | ||
| expected | ||
| ); | ||
| }); | ||
| }); |
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,72 @@ | ||
| export function createApp(): string { | ||
| return 'create-app'; | ||
| import fs from 'node:fs'; | ||
| import path from 'node:path'; | ||
| import editTemplate from './edit-template'; | ||
| import { parsePackageInfo } from './parsers'; | ||
| import { | ||
| cancelAndExit, | ||
| printHelpMessage, | ||
| printVersionMessage, | ||
| confirmOverrideFiles, | ||
| promptProjectName, | ||
| printWelcomeMessage, | ||
| printByeMessage, | ||
| promptTemplate, | ||
| } from './prompts'; | ||
| import { copyDir, isEmptyDir, removeDir, resolveAbsolutePath } from './fs'; | ||
| import { parseCliOptions } from './parse-cli-options'; | ||
|
|
||
| const TEMPLATES = ['default']; | ||
|
|
||
| async function create() { | ||
| const options = parseCliOptions(process.argv.slice(2)); | ||
|
|
||
| if (options.help) { | ||
| printHelpMessage(TEMPLATES); | ||
| return; | ||
| } | ||
|
|
||
| if (options.version) { | ||
| printVersionMessage(); | ||
| return; | ||
| } | ||
|
|
||
| printWelcomeMessage(); | ||
|
|
||
| const projectName = | ||
| (options.dir || options.name) ?? (await promptProjectName()); | ||
| const { targetDir } = parsePackageInfo(projectName); | ||
| const absoluteTargetDir = resolveAbsolutePath(targetDir); | ||
|
|
||
| if ( | ||
| !options.override && | ||
| fs.existsSync(absoluteTargetDir) && | ||
| !isEmptyDir(absoluteTargetDir) | ||
| ) { | ||
| const confirmOverride = await confirmOverrideFiles(absoluteTargetDir); | ||
| if (!confirmOverride) { | ||
| cancelAndExit(); | ||
| } | ||
| } | ||
|
|
||
| removeDir(absoluteTargetDir); | ||
|
|
||
| const templateName = options.template ?? (await promptTemplate(TEMPLATES)); | ||
| const srcDir = path.join( | ||
| __dirname, | ||
| // Workaround for getting the template from within the monorepo | ||
| // TODO: implement downloading templates from NPM | ||
| '../../../../../templates', | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: To start with, maybe we can use existing React Native template for compatibility and build on top of that? We want some sort of easy migration path for existing users, so having the ability to use community template sounds like a good idea. Otherwise, with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can start with RNC
Pros:
Cons:
|
||
| `rnef-template-${templateName}` | ||
| ); | ||
|
|
||
| if (!fs.existsSync(srcDir)) { | ||
| throw new Error(`Invalid template: template "${templateName}" not found.`); | ||
| } | ||
|
|
||
| copyDir(srcDir, absoluteTargetDir); | ||
| await editTemplate(projectName, absoluteTargetDir); | ||
|
|
||
| printByeMessage(absoluteTargetDir); | ||
| } | ||
|
|
||
| create(); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| import path from 'node:path'; | ||
| import fs from 'node:fs'; | ||
|
|
||
| const DEFAULT_PLACEHOLDER_NAME = 'HelloWorld'; | ||
mdjastrzebski marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| function replaceNameInUTF8File( | ||
| filePath: string, | ||
| projectName: string, | ||
| templateName: string | ||
| ) { | ||
| const fileContent = fs.readFileSync(filePath, 'utf8'); | ||
| const replacedFileContent = fileContent | ||
| .replace(new RegExp(templateName, 'g'), projectName) | ||
| .replace( | ||
| new RegExp(templateName.toLowerCase(), 'g'), | ||
| projectName.toLowerCase() | ||
| ); | ||
|
|
||
| if (fileContent !== replacedFileContent) { | ||
| fs.writeFileSync(filePath, replacedFileContent, 'utf8'); | ||
| } | ||
| } | ||
|
|
||
| async function renameFile(filePath: string, oldName: string, newName: string) { | ||
| const newFileName = path.join( | ||
| path.dirname(filePath), | ||
| path.basename(filePath).replace(new RegExp(oldName, 'g'), newName) | ||
| ); | ||
|
|
||
| fs.renameSync(filePath, newFileName); | ||
| } | ||
|
|
||
| function shouldRenameFile(filePath: string, nameToReplace: string) { | ||
| return path.basename(filePath).includes(nameToReplace); | ||
| } | ||
|
|
||
| function walk(current: string): string[] { | ||
| if (!fs.lstatSync(current).isDirectory()) { | ||
| return [current]; | ||
| } | ||
|
|
||
| const files = fs | ||
| .readdirSync(current) | ||
| .map((child) => walk(path.join(current, child))); | ||
| const result: string[] = []; | ||
| return result.concat.apply([current], files); | ||
| } | ||
|
|
||
|
|
||
| export default async function changePlaceholderInTemplate( | ||
| projectName: string, | ||
| directory: string, | ||
| ) { | ||
| if (projectName === DEFAULT_PLACEHOLDER_NAME) { | ||
| return; | ||
| } | ||
|
|
||
| for (const filePath of walk(directory).reverse()) { | ||
| if (!fs.statSync(filePath).isDirectory()) { | ||
| replaceNameInUTF8File(filePath, projectName, DEFAULT_PLACEHOLDER_NAME); | ||
| } | ||
| if (shouldRenameFile(filePath, DEFAULT_PLACEHOLDER_NAME)) { | ||
| await renameFile(filePath, DEFAULT_PLACEHOLDER_NAME, projectName); | ||
| } else if (shouldRenameFile(filePath, DEFAULT_PLACEHOLDER_NAME.toLowerCase())) { | ||
| await renameFile( | ||
| filePath, | ||
| DEFAULT_PLACEHOLDER_NAME.toLowerCase(), | ||
| projectName.toLowerCase() | ||
| ); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| import fs from 'node:fs'; | ||
| import nodePath from 'node:path'; | ||
|
|
||
| export function isEmptyDir(path: string) { | ||
| const files = fs.readdirSync(path); | ||
| return files.length === 0 || (files.length === 1 && files[0] === '.git'); | ||
| } | ||
|
|
||
| type CopyDirOptions = { | ||
| skipFiles?: string[]; | ||
| }; | ||
|
|
||
| export function copyDir( | ||
| from: string, | ||
| to: string, | ||
| { skipFiles = [] }: CopyDirOptions = {} | ||
| ) { | ||
| fs.mkdirSync(to, { recursive: true }); | ||
|
|
||
| for (const file of fs.readdirSync(from)) { | ||
| const srcFile = nodePath.resolve(from, file); | ||
| const stat = fs.statSync(srcFile); | ||
| const distFile = nodePath.resolve(to, file); | ||
|
|
||
| if (stat.isDirectory()) { | ||
| copyDir(srcFile, distFile, { skipFiles }); | ||
| } else { | ||
| fs.copyFileSync(srcFile, distFile); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| export function removeDir(path: string) { | ||
| if (fs.existsSync(path)) { | ||
| fs.rmSync(path, { recursive: true }); | ||
| } | ||
| } | ||
|
|
||
| export function resolveAbsolutePath(path: string) { | ||
| return nodePath.isAbsolute(path) ? path : nodePath.join(process.cwd(), path); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| import minimist from 'minimist'; | ||
|
|
||
| export type CliOptions = { | ||
| name?: string; | ||
| template?: string; | ||
| help?: boolean; | ||
| version?: boolean; | ||
| dir?: string; | ||
| override?: boolean; | ||
| }; | ||
|
|
||
| export function parseCliOptions(argv: string[]): CliOptions { | ||
| const options = minimist<CliOptions>(argv, { | ||
| alias: { h: 'help', d: 'dir', v: 'version' }, | ||
| }); | ||
|
|
||
| return { | ||
| name: options._[0], | ||
| template: options.template, | ||
| help: options.help, | ||
| version: options.version, | ||
| dir: options.dir, | ||
| override: options.override, | ||
| }; | ||
mdjastrzebski marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.