init project
This commit is contained in:
Vendored
+259
@@ -0,0 +1,259 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { readComposerJson, hasComposerDependency } from './composer.js';
|
||||
import { hasDependency, readPackageJson } from './packageManager.js';
|
||||
import { LaravelAdapter } from './laravel.js';
|
||||
import { CodeIgniterAdapter } from './codeigniter.js';
|
||||
import { ReactAdapter } from './react.js';
|
||||
import { VueAdapter, NuxtAdapter } from './vue.js';
|
||||
import { DjangoAdapter, RailsAdapter, SymfonyAdapter } from './djangoRailsSymfony.js';
|
||||
import { GenericAdapter } from './generic.js';
|
||||
const FRONTEND_SUBDIRS = ['frontend', 'client', 'web', 'resources/js-app'];
|
||||
const MONOREPO_GLOBS = ['apps', 'packages', 'services', 'libs'];
|
||||
function fileExists(root, names) {
|
||||
return names.some((name) => fs.existsSync(path.join(root, name)));
|
||||
}
|
||||
function hasVueSfc(root) {
|
||||
const src = path.join(root, 'src');
|
||||
if (!fs.existsSync(src))
|
||||
return false;
|
||||
try {
|
||||
const stack = [src];
|
||||
let checked = 0;
|
||||
while (stack.length > 0 && checked < 200) {
|
||||
const dir = stack.pop();
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
checked += 1;
|
||||
if (entry.isDirectory() && entry.name !== 'node_modules') {
|
||||
stack.push(path.join(dir, entry.name));
|
||||
}
|
||||
else if (entry.isFile() && entry.name.endsWith('.vue')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function detectPhpOrSymfonyBackend(root) {
|
||||
const composerJson = readComposerJson(root);
|
||||
if (!composerJson)
|
||||
return null;
|
||||
const hasArtisan = fs.existsSync(path.join(root, 'artisan'));
|
||||
const hasSpark = fs.existsSync(path.join(root, 'spark'));
|
||||
if (hasComposerDependency(composerJson, 'laravel/framework') || hasArtisan) {
|
||||
return new LaravelAdapter(root);
|
||||
}
|
||||
if (hasComposerDependency(composerJson, 'codeigniter4/framework') || hasSpark) {
|
||||
return new CodeIgniterAdapter(root);
|
||||
}
|
||||
if (hasComposerDependency(composerJson, 'symfony/framework-bundle') ||
|
||||
hasComposerDependency(composerJson, 'symfony/symfony') ||
|
||||
fileExists(root, ['bin/console', 'symfony.lock'])) {
|
||||
return new SymfonyAdapter(root);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function detectDjangoAt(root) {
|
||||
if (fileExists(root, ['manage.py']))
|
||||
return new DjangoAdapter(root);
|
||||
for (const name of ['requirements.txt', 'requirements-dev.txt', 'pyproject.toml']) {
|
||||
const file = path.join(root, name);
|
||||
if (!fs.existsSync(file))
|
||||
continue;
|
||||
const text = fs.readFileSync(file, 'utf8').toLowerCase();
|
||||
if (/\bdjango\b/.test(text))
|
||||
return new DjangoAdapter(root);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function detectRailsAt(root) {
|
||||
const gemfile = path.join(root, 'Gemfile');
|
||||
if (fs.existsSync(gemfile)) {
|
||||
const text = fs.readFileSync(gemfile, 'utf8').toLowerCase();
|
||||
if (/\brails\b/.test(text) || fileExists(root, ['bin/rails', 'config/application.rb'])) {
|
||||
return new RailsAdapter(root);
|
||||
}
|
||||
}
|
||||
if (fileExists(root, ['bin/rails', 'config/application.rb'])) {
|
||||
return new RailsAdapter(root);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function detectBackend(root) {
|
||||
return detectPhpOrSymfonyBackend(root) ?? detectDjangoAt(root) ?? detectRailsAt(root);
|
||||
}
|
||||
function detectFrontendAt(root) {
|
||||
const pkg = readPackageJson(root);
|
||||
if (hasDependency(pkg, 'nuxt') ||
|
||||
fileExists(root, ['nuxt.config.js', 'nuxt.config.ts', 'nuxt.config.mjs'])) {
|
||||
return new NuxtAdapter(root);
|
||||
}
|
||||
if (hasDependency(pkg, 'vue') ||
|
||||
(hasVueSfc(root) && fileExists(root, ['vite.config.js', 'vite.config.ts', 'vite.config.mjs']))) {
|
||||
return new VueAdapter(root);
|
||||
}
|
||||
if (hasDependency(pkg, 'react'))
|
||||
return new ReactAdapter(root);
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* Inspects `workspaceRoot` and returns the detected backend/frontend
|
||||
* adapter(s). Supports PHP (Laravel/CI/Symfony), Django, Rails backends and
|
||||
* React/Vue/Nuxt frontends, including sibling frontend folders.
|
||||
*/
|
||||
export function detectProject(workspaceRoot) {
|
||||
const backend = detectBackend(workspaceRoot);
|
||||
let frontend = detectFrontendAt(workspaceRoot);
|
||||
if (backend && !frontend) {
|
||||
for (const subdir of FRONTEND_SUBDIRS) {
|
||||
const candidateRoot = path.join(workspaceRoot, subdir);
|
||||
if (!fs.existsSync(candidateRoot))
|
||||
continue;
|
||||
const candidate = detectFrontendAt(candidateRoot);
|
||||
if (candidate) {
|
||||
frontend = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Pure frontend-only Vue/Nuxt/React at root without backend.
|
||||
if (!backend && !frontend) {
|
||||
frontend = detectFrontendAt(workspaceRoot);
|
||||
}
|
||||
if (backend) {
|
||||
return { backend, frontend, primary: backend };
|
||||
}
|
||||
if (frontend) {
|
||||
return { backend: null, frontend, primary: frontend };
|
||||
}
|
||||
const generic = new GenericAdapter(workspaceRoot);
|
||||
return { backend: null, frontend: null, primary: generic };
|
||||
}
|
||||
export function summarizeDetection(detected) {
|
||||
return {
|
||||
backend: detected.backend?.describe() ?? null,
|
||||
frontend: detected.frontend?.describe() ?? null,
|
||||
primary: detected.primary.kind,
|
||||
combined: Boolean(detected.backend && detected.frontend),
|
||||
};
|
||||
}
|
||||
function readWorkspaceGlobs(root) {
|
||||
const globs = new Set([...MONOREPO_GLOBS, ...FRONTEND_SUBDIRS.map((d) => d.split('/')[0]).filter(Boolean)]);
|
||||
const pkg = readPackageJson(root);
|
||||
const workspaces = pkg?.workspaces;
|
||||
if (Array.isArray(workspaces)) {
|
||||
for (const entry of workspaces) {
|
||||
if (typeof entry === 'string' && entry.includes('/*')) {
|
||||
globs.add(entry.replace(/\/\*$/, ''));
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (workspaces && typeof workspaces === 'object' && Array.isArray(workspaces.packages)) {
|
||||
for (const entry of workspaces.packages) {
|
||||
if (entry.includes('/*'))
|
||||
globs.add(entry.replace(/\/\*$/, ''));
|
||||
}
|
||||
}
|
||||
const pnpm = path.join(root, 'pnpm-workspace.yaml');
|
||||
if (fs.existsSync(pnpm)) {
|
||||
const text = fs.readFileSync(pnpm, 'utf8');
|
||||
for (const match of text.matchAll(/['"]?([A-Za-z0-9_-]+)\/\*['"]?/g)) {
|
||||
if (match[1])
|
||||
globs.add(match[1]);
|
||||
}
|
||||
}
|
||||
return [...globs];
|
||||
}
|
||||
function listImmediateSubdirs(dir) {
|
||||
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory())
|
||||
return [];
|
||||
return fs
|
||||
.readdirSync(dir, { withFileTypes: true })
|
||||
.filter((entry) => entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules' && entry.name !== 'vendor')
|
||||
.map((entry) => entry.name);
|
||||
}
|
||||
function packageLooksLikeProject(dir) {
|
||||
return (fs.existsSync(path.join(dir, 'package.json')) ||
|
||||
fs.existsSync(path.join(dir, 'composer.json')) ||
|
||||
fs.existsSync(path.join(dir, 'artisan')) ||
|
||||
fs.existsSync(path.join(dir, 'spark')) ||
|
||||
fs.existsSync(path.join(dir, 'manage.py')) ||
|
||||
fs.existsSync(path.join(dir, 'Gemfile')) ||
|
||||
fs.existsSync(path.join(dir, 'bin', 'console')));
|
||||
}
|
||||
/**
|
||||
* Scan the workspace for monorepo packages under apps/*, packages/*, services/*,
|
||||
* frontend/client/web, plus the workspace root itself.
|
||||
*/
|
||||
export function detectWorkspace(workspaceRoot) {
|
||||
const absoluteRoot = path.resolve(workspaceRoot);
|
||||
const packages = [];
|
||||
const seen = new Set();
|
||||
const addPackage = (name, absolutePath) => {
|
||||
const normalized = path.resolve(absolutePath);
|
||||
if (seen.has(normalized.toLowerCase()))
|
||||
return;
|
||||
if (!packageLooksLikeProject(normalized) && normalized !== absoluteRoot)
|
||||
return;
|
||||
seen.add(normalized.toLowerCase());
|
||||
const relativePath = path.relative(absoluteRoot, normalized) || '.';
|
||||
packages.push({
|
||||
name,
|
||||
relativePath: relativePath === '' ? '.' : relativePath.replace(/\\/g, '/'),
|
||||
absolutePath: normalized,
|
||||
detected: detectProject(normalized),
|
||||
});
|
||||
};
|
||||
addPackage(path.basename(absoluteRoot) || 'root', absoluteRoot);
|
||||
for (const globRoot of readWorkspaceGlobs(absoluteRoot)) {
|
||||
const base = path.join(absoluteRoot, globRoot);
|
||||
for (const child of listImmediateSubdirs(base)) {
|
||||
addPackage(child, path.join(base, child));
|
||||
}
|
||||
if (FRONTEND_SUBDIRS.some((d) => d === globRoot || d.startsWith(`${globRoot}/`)) && packageLooksLikeProject(base)) {
|
||||
addPackage(globRoot, base);
|
||||
}
|
||||
}
|
||||
return { root: absoluteRoot, packages };
|
||||
}
|
||||
export function summarizeWorkspace(workspace) {
|
||||
return {
|
||||
root: workspace.root,
|
||||
packageCount: workspace.packages.length,
|
||||
packages: workspace.packages.map((pkg) => ({
|
||||
name: pkg.name,
|
||||
path: pkg.relativePath,
|
||||
primary: pkg.detected.primary.kind,
|
||||
combined: Boolean(pkg.detected.backend && pkg.detected.frontend),
|
||||
backend: pkg.detected.backend?.kind ?? null,
|
||||
frontend: pkg.detected.frontend?.kind ?? null,
|
||||
})),
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Resolve a package name or relative path to a DetectedPackage inside the workspace.
|
||||
*/
|
||||
export function resolvePackage(workspaceRoot, packageNameOrPath) {
|
||||
const workspace = detectWorkspace(workspaceRoot);
|
||||
if (!packageNameOrPath) {
|
||||
const rootPkg = workspace.packages.find((p) => p.relativePath === '.') ?? workspace.packages[0];
|
||||
if (!rootPkg)
|
||||
throw new Error('No packages detected in the workspace.');
|
||||
return { workspace, pkg: rootPkg };
|
||||
}
|
||||
const needle = packageNameOrPath.replace(/\\/g, '/').replace(/^\.\//, '');
|
||||
const match = workspace.packages.find((p) => p.name === packageNameOrPath ||
|
||||
p.relativePath === needle ||
|
||||
p.relativePath === packageNameOrPath ||
|
||||
p.absolutePath.replace(/\\/g, '/').endsWith(`/${needle}`));
|
||||
if (!match) {
|
||||
const available = workspace.packages.map((p) => `${p.name} (${p.relativePath})`).join(', ');
|
||||
throw new Error(`Unknown package "${packageNameOrPath}". Available: ${available || '(none)'}.`);
|
||||
}
|
||||
return { workspace, pkg: match };
|
||||
}
|
||||
//# sourceMappingURL=detect.js.map
|
||||
Reference in New Issue
Block a user