diff --git a/.vscode/launch.json b/.vscode/launch.json index 09ccbd596..df6fbc3c6 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -11,7 +11,7 @@ "skipFiles": ["/**"], "type": "node", "args": ["generate"], - "cwd": "${workspaceFolder}/samples/blog" + "cwd": "${workspaceFolder}/samples/orm" }, { "name": "Debug with TSX", diff --git a/packages/cli/src/actions/action-utils.ts b/packages/cli/src/actions/action-utils.ts index d2e0ca2e9..5c6962ee7 100644 --- a/packages/cli/src/actions/action-utils.ts +++ b/packages/cli/src/actions/action-utils.ts @@ -3,6 +3,7 @@ import { isDataSource } from '@zenstackhq/language/ast'; import { PrismaSchemaGenerator } from '@zenstackhq/sdk'; import colors from 'colors'; import fs from 'node:fs'; +import { createRequire } from 'node:module'; import path from 'node:path'; import { CliError } from '../cli-error'; @@ -125,10 +126,10 @@ function findUp( } const target = names.find((name) => fs.existsSync(path.join(cwd, name))); if (multiple === false && target) { - return path.join(cwd, target) as FindUpResult; + return path.resolve(cwd, target) as FindUpResult; } if (target) { - result.push(path.join(cwd, target)); + result.push(path.resolve(cwd, target)); } const up = path.resolve(cwd, '..'); if (up === cwd) { @@ -156,3 +157,45 @@ export function getOutputPath(options: { output?: string }, schemaFile: string) return path.dirname(schemaFile); } } +export async function getZenStackPackages( + searchPath: string, +): Promise> { + const pkgJsonFile = findUp(['package.json'], searchPath, false); + if (!pkgJsonFile) { + return []; + } + + let pkgJson: { + dependencies?: Record; + devDependencies?: Record; + }; + try { + pkgJson = JSON.parse(fs.readFileSync(pkgJsonFile, 'utf8')); + } catch { + return []; + } + + const packages = Array.from( + new Set( + [...Object.keys(pkgJson.dependencies ?? {}), ...Object.keys(pkgJson.devDependencies ?? {})].filter((p) => + p.startsWith('@zenstackhq/'), + ), + ), + ).sort(); + + const require = createRequire(pkgJsonFile); + + const result = packages.map((pkg) => { + try { + const depPkgJson = require(`${pkg}/package.json`); + if (depPkgJson.private) { + return undefined; + } + return { pkg, version: depPkgJson.version as string }; + } catch { + return { pkg, version: undefined }; + } + }); + + return result.filter((p) => !!p); +} diff --git a/packages/cli/src/actions/generate.ts b/packages/cli/src/actions/generate.ts index 7ac6db6b2..c014c02ef 100644 --- a/packages/cli/src/actions/generate.ts +++ b/packages/cli/src/actions/generate.ts @@ -12,7 +12,8 @@ import { watch } from 'chokidar'; import ora, { type Ora } from 'ora'; import { CliError } from '../cli-error'; import * as corePlugins from '../plugins'; -import { getOutputPath, getSchemaFile, loadSchemaDocument } from './action-utils'; +import { getOutputPath, getSchemaFile, getZenStackPackages, loadSchemaDocument } from './action-utils'; +import semver from 'semver'; type Options = { schema?: string; @@ -27,6 +28,11 @@ type Options = { * CLI action for generating code from schema */ export async function run(options: Options) { + try { + await checkForMismatchedPackages(process.cwd()); + } catch (err) { + console.warn(colors.yellow(`Failed to check for mismatched ZenStack packages: ${err}`)); + } const model = await pureGenerate(options, false); if (options.watch) { @@ -315,3 +321,40 @@ async function loadPluginModule(provider: string, basePath: string) { return undefined; } } + +async function checkForMismatchedPackages(projectPath: string) { + const packages = await getZenStackPackages(projectPath); + if (!packages.length) { + return false; + } + + const versions = new Set(); + for (const { version } of packages) { + if (version) { + versions.add(version); + } + } + + if (versions.size > 1) { + const message = + 'WARNING: Multiple versions of ZenStack packages detected.\n\tThis will probably cause issues and break your types.'; + const slashes = '/'.repeat(73); + const latestVersion = semver.sort(Array.from(versions)).reverse()[0]!; + + console.warn(colors.yellow(`${slashes}\n\n\t${message}\n`)); + for (const { pkg, version } of packages) { + if (!version) continue; + + if (version === latestVersion) { + console.log(`\t${pkg.padEnd(32)}\t${colors.green(version)}`); + } else { + console.log(`\t${pkg.padEnd(32)}\t${colors.yellow(version)}`); + } + } + console.warn(`\n${colors.yellow(slashes)}`); + + return true; + } + + return false; +} diff --git a/packages/cli/src/actions/info.ts b/packages/cli/src/actions/info.ts index bbea51ebb..26e42422f 100644 --- a/packages/cli/src/actions/info.ts +++ b/packages/cli/src/actions/info.ts @@ -1,12 +1,12 @@ import colors from 'colors'; -import path from 'node:path'; +import { getZenStackPackages } from './action-utils'; /** * CLI action for getting information about installed ZenStack packages */ export async function run(projectPath: string) { const packages = await getZenStackPackages(projectPath); - if (!packages) { + if (!packages.length) { console.error('Unable to locate package.json. Are you in a valid project directory?'); return; } @@ -24,48 +24,3 @@ export async function run(projectPath: string) { console.warn(colors.yellow('WARNING: Multiple versions of Zenstack packages detected. This may cause issues.')); } } - -async function getZenStackPackages(projectPath: string): Promise> { - let pkgJson: { - dependencies: Record; - devDependencies: Record; - }; - const resolvedPath = path.resolve(projectPath); - try { - pkgJson = ( - await import(path.join(resolvedPath, 'package.json'), { - with: { type: 'json' }, - }) - ).default; - } catch { - return []; - } - - const packages = Array.from( - new Set( - [...Object.keys(pkgJson.dependencies ?? {}), ...Object.keys(pkgJson.devDependencies ?? {})].filter( - (p) => p.startsWith('@zenstackhq/') || p === 'zenstack', - ), - ), - ).sort(); - - const result = await Promise.all( - packages.map(async (pkg) => { - try { - const depPkgJson = ( - await import(`${pkg}/package.json`, { - with: { type: 'json' }, - }) - ).default; - if (depPkgJson.private) { - return undefined; - } - return { pkg, version: depPkgJson.version as string }; - } catch { - return { pkg, version: undefined }; - } - }), - ); - - return result.filter((p) => !!p); -}