Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
44 changes: 44 additions & 0 deletions packages/cli/src/actions/action-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,3 +156,47 @@ export function getOutputPath(options: { output?: string }, schemaFile: string)
return path.dirname(schemaFile);
}
}
export async function getZenStackPackages(projectPath: string): Promise<Array<{ pkg: string; version: string | undefined }>> {
let pkgJson: {
dependencies: Record<string, unknown>;
devDependencies: Record<string, unknown>;
};
const resolvedPath = path.resolve(projectPath);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This currently assumes the "zen" command is run at the root of the project. I think it's more flexible if we call the findUp utility to search upward for the nearest package.json file and then analyze its dependencies, so the logic would work if "zen" is run in a subfolder.

If so, the projectPath is then searchStartPath.

What do you think @sanny-io ?

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',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'zenstack' package name is not used anymore.

),
),
).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);
}
Comment on lines +159 to +202
Copy link

@coderabbitai coderabbitai bot Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

# Find all package.json files and check engines field
echo "=== Checking Node.js engine requirements ==="
fd -t f 'package.json' --max-depth 1 | xargs jq -r '.engines.node // "Not specified"' 2>/dev/null

echo -e "\n=== Checking root package.json for engines ==="
jq '.engines // "No engines field"' package.json 2>/dev/null || echo "package.json not found"

# Check if import attributes (with syntax) are used elsewhere
echo -e "\n=== Searching for import attributes syntax (with keyword) ==="
rg 'import\s*\([^)]*\)\s*with\s*\{' --type ts --type js -g '!node_modules' | head -20

# Also check for assert syntax (older form)
echo -e "\n=== Searching for import assert syntax (older form) ==="
rg 'import\s*\([^)]*\)\s*assert\s*\{' --type ts --type js -g '!node_modules' | head -20

Repository: zenstackhq/zenstack-v3

Length of output: 311


🏁 Script executed:

#!/bin/bash

# Check the specific file and lines mentioned
echo "=== Checking the actual code in action-utils.ts ==="
cat -n packages/cli/src/actions/action-utils.ts | sed -n '159,202p'

Repository: zenstackhq/zenstack-v3

Length of output: 1819


🏁 Script executed:

#!/bin/bash

# Check for TypeScript version and compiler options
echo "=== Checking TypeScript config for target ==="
if [ -f tsconfig.json ]; then
  jq '.compilerOptions.target' tsconfig.json 2>/dev/null || echo "No target found"
fi

# Look for .nvmrc or similar Node version specifiers
echo -e "\n=== Checking for Node version specifiers ==="
ls -la | grep -E '\.(nvmrc|node-version|tool-versions)' || echo "No Node version file found"
if [ -f .nvmrc ]; then cat .nvmrc; fi

Repository: zenstackhq/zenstack-v3

Length of output: 184


Remove import attributes or add explicit Node.js ≥22 requirement.

The { with: { type: 'json' } } syntax on lines 168 and 188 is the only use of import attributes in the entire codebase. This syntax requires Node.js ≥22 for stable support, but the project has no engines field or .nvmrc file specifying a minimum Node version. This breaks compatibility with Node.js <22 silently. Either:

  • Migrate to the older import assert { type: 'json' } syntax (widely compatible), or
  • Document the Node.js ≥22 requirement in package.json and README.md

Additionally, line 194's depPkgJson.version as string masks the case where version field is missing. Use depPkgJson.version ?? undefined to keep the type honest.

🤖 Prompt for AI Agents
In `@packages/cli/src/actions/action-utils.ts` around lines 159 - 202, In
getZenStackPackages replace the dynamic import-with-attributes calls with a
JSON-safe read (use fs.readFileSync + JSON.parse or fs.promises.readFile) for
both the project package.json load and the dependency package.json loads to
avoid using the import attributes syntax, and change the return shape in the
dependency resolution to return { pkg, version: depPkgJson.version ?? undefined
} instead of casting with depPkgJson.version as string; update references in the
function: the initial pkgJson import block and the depPkgJson import block in
the packages.map handler.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ymc9 how should I handle this?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai do you know if createRequire would work in this case for Node 20.x?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ymc9, yes! createRequire would work perfectly in Node 20.x (it's been available since Node 12.2.0). It's actually a great solution for this use case:

import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);

// For project package.json:
const pkgJson = require(path.join(resolvedPath, 'package.json'));

// For dependency package.json:
const depPkgJson = require(`${pkg}/package.json`);

Benefits of using createRequire here:

  • ✅ Compatible with Node 20.x (and older)
  • ✅ Handles package name resolution correctly (${pkg}/package.json)
  • ✅ Simpler synchronous code (can remove async/await)
  • ✅ No import attributes syntax needed

The main change would be wrapping the require calls in try-catch blocks (which you're already doing with the import). This approach avoids the Node ≥22 requirement entirely while keeping the same functionality.


🧠 Learnings used
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to packages/zenstackhq/cli/**/*.test.{ts,tsx} : CLI package tests should focus on action-specific tests for each command

Learnt from: mwillbanks
Repo: zenstackhq/zenstack-v3 PR: 550
File: packages/orm/src/client/crud/operations/base.ts:158-159
Timestamp: 2025-12-30T15:07:10.092Z
Learning: In the zenstackhq/zenstack-v3 codebase, ts-expect-error should be heavily avoided in production code (packages/*). It is acceptable in test files but production code should use proper type annotations, type assertions, or refactoring to resolve type errors instead.

41 changes: 40 additions & 1 deletion packages/cli/src/actions/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -27,6 +28,7 @@ type Options = {
* CLI action for generating code from schema
*/
export async function run(options: Options) {
await checkForMismatchedPackages(process.cwd());
const model = await pureGenerate(options, false);

if (options.watch) {
Expand Down Expand Up @@ -315,3 +317,40 @@ async function loadPluginModule(provider: string, basePath: string) {
return undefined;
}
}

async function checkForMismatchedPackages(projectPath: string) {
const packages = await getZenStackPackages(projectPath);
if (!packages) {
return false;
}

const versions = new Set<string>();
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\t${message}\n`));
for (const { pkg, version } of packages) {
if (!version) continue;

if (version === latestVersion) {
console.log(`\t${pkg.padEnd(20)}\t${colors.green(version)}`);
}
else {
console.log(`\t${pkg.padEnd(20)}\t${colors.yellow(version)}`);
}
}
console.warn(`\n${colors.yellow(slashes)}`);

return true;
}

return false;
}
47 changes: 1 addition & 46 deletions packages/cli/src/actions/info.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import colors from 'colors';
import path from 'node:path';
import { getZenStackPackages } from './action-utils';

/**
* CLI action for getting information about installed ZenStack packages
Expand All @@ -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<Array<{ pkg: string; version: string | undefined }>> {
let pkgJson: {
dependencies: Record<string, unknown>;
devDependencies: Record<string, unknown>;
};
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);
}
Loading