Skip to content
Open
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
54 changes: 27 additions & 27 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -1,64 +1,64 @@
import eslint from "@eslint/js";
import { globalIgnores } from "eslint/config";
import simpleImportSort from "eslint-plugin-simple-import-sort";
import tseslint from "typescript-eslint";
import eslint from '@eslint/js';
import { globalIgnores } from 'eslint/config';
import simpleImportSort from 'eslint-plugin-simple-import-sort';
import tseslint from 'typescript-eslint';

export default [
eslint.configs.recommended,
...tseslint.configs.recommended,
globalIgnores(["website/", "templates/"]),
globalIgnores(['website/', 'templates/', 'packages/templates/']),
{
files: ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"],
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
rules: {
"@typescript-eslint/consistent-type-imports": "error",
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-unused-vars": [
"error",
'@typescript-eslint/consistent-type-imports': 'error',
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-unused-vars': [
'error',
{
argsIgnorePattern: "^_",
caughtErrorsIgnorePattern: "^_",
argsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
},
],
},
},
{
plugins: {
"simple-import-sort": simpleImportSort,
'simple-import-sort': simpleImportSort,
},
rules: {
"simple-import-sort/imports": [
"error",
'simple-import-sort/imports': [
'error',
{
groups: [["^\\u0000", "^node:", "^@?\\w", "^", "^\\."]],
groups: [['^\\u0000', '^node:', '^@?\\w', '^', '^\\.']],
},
],
},
},
{
files: ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"],
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
// Override or add rules here
},
{
files: ["scripts/**/*.mjs"],
files: ['scripts/**/*.mjs'],
// Override or add rules here
rules: {
"no-undef": "off",
'no-undef': 'off',
},
},
{
files: [
"**/*.test.ts",
"**/__tests__/**",
"**/metro.config.js",
"**/vite.e2e.config.js",
'**/*.test.ts',
'**/__tests__/**',
'**/metro.config.js',
'**/vite.e2e.config.js',
],
rules: {
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-require-imports": "off",
"no-undef": "off",
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-require-imports': 'off',
'no-undef': 'off',
},
},
{
ignores: ["**/template/**/*.mjs", "**/dist/**", "**/__fixtures__/**"],
ignores: ['**/template/**/*.mjs', '**/dist/**', '**/__fixtures__/**'],
},
];
2 changes: 1 addition & 1 deletion packages/create-app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@ npx create-lynxjs-app@latest
bun install

# Run dev server
bun dev
bun dev
```
5 changes: 1 addition & 4 deletions packages/create-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,10 @@
},
"files": [
"bin.js",
"dist/**/*",
"templates/**/*"
"dist/**/*"
],
"scripts": {
"prebuild": "npm run copy-templates",
"build": "tsc",
"copy-templates": "rm -rf templates && mkdir -p templates && rsync -av --exclude='node_modules' --exclude='dist' --exclude='.git' --exclude='apple/HelloWorld.xcodeproj/xcuserdata' --exclude='apple/HelloWorld.xcworkspace/xcuserdata' --exclude='android/app/build' --exclude='android/.gradle' --exclude='Pods' --exclude='package-lock.json' ../helloworld/ templates/helloworld/",
"dev": "tsc --watch",
"watch": "tsc --watch",
"lint": "eslint src/",
Expand Down
166 changes: 99 additions & 67 deletions packages/create-app/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
#!/usr/bin/env node

import { execSync } from 'node:child_process';
import path from 'node:path';
import * as p from '@clack/prompts';
import { Command } from 'commander';
import fs from 'fs-extra';
import gradient from 'gradient-string';
import path from 'path';
import pc from 'picocolors';
import { fileURLToPath } from 'url';
import { fetchGitHubFolders } from './utils.js';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const repoUrl = 'https://github.com/lynx-community/cli.git';

function detectPackageManager(): string {
const userAgent = process.env.npm_config_user_agent || '';
Expand All @@ -30,6 +30,15 @@ interface AppConfig {
name: string;
platforms: string[];
directory: string;
useTailwind: boolean;
useGit: boolean;
}

interface CLIOptions {
platforms?: string[];
directory?: string;
useTailwind?: boolean;
useGit?: boolean;
}

function toPascalCase(str: string): string {
Expand Down Expand Up @@ -221,43 +230,40 @@ export async function createApp(): Promise<void> {
.version('0.1.0')
.argument('[project-name]', 'Name of the project')
.option('-p, --platforms <platforms...>', 'Platforms to include')
.option('-t, --tailwind', 'Use Tailwind CSS')
.option('-g, --git', 'Initialize Git repository')
.option('-d, --directory <directory>', 'Target directory')
.action(
async (
projectName?: string,
options?: { platforms?: string[]; directory?: string },
) => {
try {
const config = await gatherProjectInfo(projectName, options);
await scaffoldProject(config);

const packageManager = detectPackageManager();
const installCmd =
packageManager === 'yarn' ? 'yarn' : `${packageManager} install`;
const devCmd = `${packageManager} dev`;

p.outro(pc.cyan('Happy hacking!'));
console.log(pc.white(`Next steps:`));
console.log(pc.gray(` cd ${config.name}`));
console.log(pc.gray(` ${installCmd}`));
console.log(pc.gray(` ${devCmd}`));
} catch (error) {
if (error instanceof Error && error.message === 'cancelled') {
p.cancel('Operation cancelled.');
process.exit(0);
}
p.cancel(pc.red('❌ Error creating project: ' + error));
process.exit(1);
.action(async (projectName?: string, options?: CLIOptions) => {
try {
const config = await gatherProjectInfo(projectName, options);
await scaffoldProject(config);

const packageManager = detectPackageManager();
const installCmd =
packageManager === 'yarn' ? 'yarn' : `${packageManager} install`;
const devCmd = `${packageManager} dev`;

p.outro(pc.cyan('Happy hacking!'));
console.log(pc.white(`Next steps:`));
console.log(pc.gray(` cd ${config.name}`));
console.log(pc.gray(` ${installCmd}`));
console.log(pc.gray(` ${devCmd}`));
} catch (error) {
if (error instanceof Error && error.message === 'cancelled') {
p.cancel('Operation cancelled.');
process.exit(0);
}
},
);
p.cancel(pc.red('❌ Error creating project: ' + error));
process.exit(1);
}
});

await program.parseAsync();
}

async function gatherProjectInfo(
projectName?: string,
options?: { platforms?: string[]; directory?: string },
options?: CLIOptions,
): Promise<AppConfig> {
let name = projectName;
let platforms = options?.platforms;
Expand Down Expand Up @@ -300,62 +306,88 @@ async function gatherProjectInfo(
platforms = platformsResult as string[];
}

let useTailwind = options?.useTailwind;
if (useTailwind === undefined) {
const tailwindResult = await p.confirm({
message: 'Do you want to use Tailwind CSS?',
initialValue: false,
});

if (p.isCancel(tailwindResult)) {
throw new Error('cancelled');
}

useTailwind = tailwindResult;
}

let useGit = options?.useGit;
if (useGit === undefined) {
const gitResult = await p.confirm({
message: 'Do you want to initialize a Git repository?',
initialValue: false,
});

if (p.isCancel(gitResult)) {
throw new Error('cancelled');
}

useGit = gitResult;
}

return {
name: name as string,
platforms: platforms as string[],
directory: options?.directory || process.cwd(),
useTailwind,
useGit,
};
}

async function scaffoldProject(config: AppConfig): Promise<void> {
const targetPath = path.join(config.directory, config.name);

p.spinner({ indicator: 'dots' }).message('hello');
const spinner = p.spinner();
spinner.start(`Creating project in ${targetPath}`);

if (await fs.pathExists(targetPath)) {
spinner.stop();
throw new Error(`Directory ${targetPath} already exists`);
await fs.ensureDir(targetPath);
spinner.message('Adding platform-specific folders and templates...');
const fetchEntries: Array<{ repoPath: string; destPath?: string }> = [];

if (config.platforms.includes('android')) {
fetchEntries.push({
repoPath: 'packages/templates/android',
destPath: 'android',
});
}

if (config.platforms.includes('ios')) {
fetchEntries.push({
repoPath: 'packages/templates/apple',
destPath: 'apple',
});
}

// Use bundled template from templates directory
const templatePath = path.join(__dirname, '../templates/helloworld');
fetchEntries.push({ repoPath: 'packages/templates/react', destPath: '' });

if (!(await fs.pathExists(templatePath))) {
spinner.stop();
throw new Error(
`Template not found at ${templatePath}. Please ensure the helloworld template exists.`,
);
// Tailwind overlays react (so add it after react)
if (config.useTailwind) {
fetchEntries.push({
repoPath: 'packages/templates/react-tailwind',
destPath: '',
});
}

spinner.message('Copying template files...');
await fs.copy(templatePath, targetPath);
spinner.message('Fetching templates...');
await fetchGitHubFolders(repoUrl, fetchEntries, targetPath);

spinner.message('Configuring project files...');

await replaceTemplateStrings(targetPath, config.name);
await renameTemplateFilesAndDirs(targetPath, config.name);

await cleanupUnselectedPlatforms(targetPath, config.platforms);

spinner.stop('Project created successfully!');
}

async function cleanupUnselectedPlatforms(
targetPath: string,
selectedPlatforms: string[],
): Promise<void> {
if (!selectedPlatforms.includes('ios')) {
const applePath = path.join(targetPath, 'apple');
if (await fs.pathExists(applePath)) {
await fs.remove(applePath);
}
if (config.useGit) {
spinner.message('Initializing Git repository...');
execSync('git init', { cwd: targetPath, stdio: 'ignore' });
}

if (!selectedPlatforms.includes('android')) {
const androidPath = path.join(targetPath, 'android');
if (await fs.pathExists(androidPath)) {
await fs.remove(androidPath);
}
}
spinner.stop('Project created successfully!');
}
Loading