From 66de508e3bd96e02eb5989aa181e1a5f615f5311 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Apr 2026 15:30:53 +0000 Subject: [PATCH 1/2] Initial plan From f40afdfe0167db26193e9572156ddc62cbecf137 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Apr 2026 16:26:04 +0000 Subject: [PATCH 2/2] feat(cli): smart default folder paths for Next.js projects Agent-Logs-Url: https://github.com/akii09/pdfx/sessions/4ecb6435-5442-452b-a9ab-7bb2b518a139 Co-authored-by: akii09 <47731376+akii09@users.noreply.github.com> --- packages/cli/src/commands/init.ts | 38 ++++++-- .../src/utils/environment-validator.test.ts | 92 +++++++++++++++++++ .../cli/src/utils/environment-validator.ts | 20 ++++ 3 files changed, 144 insertions(+), 6 deletions(-) create mode 100644 packages/cli/src/utils/environment-validator.test.ts diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index e51276c8..c97c906e 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -10,6 +10,31 @@ import { generateThemeContextFile, generateThemeFile } from '../utils/generate-t import { ensureReactPdfRenderer } from '../utils/install-dependencies.js'; import { displayPreFlightResults, runPreFlightChecks } from '../utils/pre-flight.js'; import { normalizeThemePath, validateThemePath } from '../utils/theme-path.js'; +import { detectNextJs } from '../utils/environment-validator.js'; + +/** + * Return context-aware default paths. + * For Next.js projects without a `src/` directory, drop the `src/` prefix so + * the suggested paths match the project's actual layout. + */ +function getSmartDefaults(cwd: string = process.cwd()) { + const isNextJs = detectNextJs(cwd); + const hasSrcDir = fs.existsSync(path.join(cwd, 'src')); + + if (isNextJs && !hasSrcDir) { + return { + componentDir: './components/pdfx', + blockDir: './blocks/pdfx', + themeFile: './lib/pdfx-theme.ts', + }; + } + + return { + componentDir: DEFAULTS.COMPONENT_DIR, + blockDir: DEFAULTS.BLOCK_DIR, + themeFile: DEFAULTS.THEME_FILE, + }; +} interface InitOptions { /** Skip all prompts and accept defaults. Suitable for CI / non-interactive environments. */ @@ -56,13 +81,14 @@ export async function init(options: InitOptions = {}) { } // In --yes mode, skip all prompts and use sensible defaults. + const defaults = getSmartDefaults(); const answers = options.yes ? { - componentDir: DEFAULTS.COMPONENT_DIR, - blockDir: DEFAULTS.BLOCK_DIR, + componentDir: defaults.componentDir, + blockDir: defaults.blockDir, registry: DEFAULTS.REGISTRY_URL, themePreset: 'professional' as const, - themePath: normalizeThemePath(DEFAULTS.THEME_FILE), + themePath: normalizeThemePath(defaults.themeFile), } : await prompts( [ @@ -70,7 +96,7 @@ export async function init(options: InitOptions = {}) { type: 'text', name: 'componentDir', message: 'Where should we install components?', - initial: DEFAULTS.COMPONENT_DIR, + initial: defaults.componentDir, validate: (value: string) => { if (!value || value.trim().length === 0) { return 'Component directory is required'; @@ -90,7 +116,7 @@ export async function init(options: InitOptions = {}) { type: 'text', name: 'blockDir', message: 'Where should we install blocks?', - initial: DEFAULTS.BLOCK_DIR, + initial: defaults.blockDir, validate: (value: string) => { if (!value || value.trim().length === 0) { return 'Block directory is required'; @@ -143,7 +169,7 @@ export async function init(options: InitOptions = {}) { type: 'text', name: 'themePath', message: 'Where should we create the theme file?', - initial: DEFAULTS.THEME_FILE, + initial: defaults.themeFile, format: normalizeThemePath, validate: validateThemePath, }, diff --git a/packages/cli/src/utils/environment-validator.test.ts b/packages/cli/src/utils/environment-validator.test.ts new file mode 100644 index 00000000..4ee4775b --- /dev/null +++ b/packages/cli/src/utils/environment-validator.test.ts @@ -0,0 +1,92 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { + detectNextJs, + validatePackageJson, + validateReactProject, +} from './environment-validator.js'; + +describe('environment-validator', () => { + let testDir: string; + + beforeEach(() => { + testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pdfx-env-test-')); + }); + + afterEach(() => { + if (fs.existsSync(testDir)) { + fs.rmSync(testDir, { recursive: true, force: true }); + } + }); + + function writePackageJson(content: Record) { + fs.writeFileSync(path.join(testDir, 'package.json'), JSON.stringify(content, null, 2)); + } + + describe('validatePackageJson', () => { + it('returns valid when package.json exists', () => { + writePackageJson({ name: 'test' }); + const result = validatePackageJson(testDir); + expect(result.valid).toBe(true); + }); + + it('returns invalid when package.json is absent', () => { + const result = validatePackageJson(testDir); + expect(result.valid).toBe(false); + expect(result.fixCommand).toBeDefined(); + }); + }); + + describe('validateReactProject', () => { + it('returns valid when react is in dependencies', () => { + writePackageJson({ dependencies: { react: '^18.0.0' } }); + const result = validateReactProject(testDir); + expect(result.valid).toBe(true); + }); + + it('returns valid when react is in devDependencies', () => { + writePackageJson({ devDependencies: { react: '^18.0.0' } }); + const result = validateReactProject(testDir); + expect(result.valid).toBe(true); + }); + + it('returns invalid when react is not present', () => { + writePackageJson({ dependencies: {} }); + const result = validateReactProject(testDir); + expect(result.valid).toBe(false); + }); + + it('returns invalid when package.json is absent', () => { + const result = validateReactProject(testDir); + expect(result.valid).toBe(false); + }); + }); + + describe('detectNextJs', () => { + it('returns true when next is in dependencies', () => { + writePackageJson({ dependencies: { next: '^14.0.0', react: '^18.0.0' } }); + expect(detectNextJs(testDir)).toBe(true); + }); + + it('returns true when next is in devDependencies', () => { + writePackageJson({ devDependencies: { next: '^14.0.0' } }); + expect(detectNextJs(testDir)).toBe(true); + }); + + it('returns false for a plain React project without next', () => { + writePackageJson({ dependencies: { react: '^18.0.0' } }); + expect(detectNextJs(testDir)).toBe(false); + }); + + it('returns false when package.json is absent', () => { + expect(detectNextJs(testDir)).toBe(false); + }); + + it('returns false when dependencies are empty', () => { + writePackageJson({ dependencies: {} }); + expect(detectNextJs(testDir)).toBe(false); + }); + }); +}); diff --git a/packages/cli/src/utils/environment-validator.ts b/packages/cli/src/utils/environment-validator.ts index c5e227b9..def1f654 100644 --- a/packages/cli/src/utils/environment-validator.ts +++ b/packages/cli/src/utils/environment-validator.ts @@ -63,6 +63,26 @@ export function validateReactProject(cwd: string = process.cwd()): EnvironmentVa } } +/** + * Check if this is a Next.js project (has next in dependencies). + */ +export function detectNextJs(cwd: string = process.cwd()): boolean { + const pkgPath = path.join(cwd, 'package.json'); + + if (!checkFileExists(pkgPath)) return false; + + try { + const pkg = readJsonFile(pkgPath) as Record; + const deps = { + ...(pkg.dependencies as Record | undefined), + ...(pkg.devDependencies as Record | undefined), + }; + return 'next' in deps; + } catch { + return false; + } +} + export function validateEnvironment(cwd: string = process.cwd()): EnvironmentCheckResult { return { hasPackageJson: validatePackageJson(cwd),