Skip to content

Commit 38c97bd

Browse files
authored
feat: let zenstack init install exact versions for zenstack package… (#313)
1 parent 51484a7 commit 38c97bd

File tree

10 files changed

+323
-36
lines changed

10 files changed

+323
-36
lines changed

packages/schema/package.json

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,11 +78,13 @@
7878
"publish-dev": "pnpm publish --tag dev",
7979
"postinstall": "node bin/post-install.js"
8080
},
81+
"peerDependencies": {
82+
"prisma": "^4.0.0"
83+
},
8184
"dependencies": {
82-
"@prisma/generator-helper": "^4.7.1",
83-
"@prisma/internals": "^4.7.1",
85+
"@prisma/generator-helper": "^4.0.0",
86+
"@prisma/internals": "^4.0.0",
8487
"@zenstackhq/language": "workspace:*",
85-
"@zenstackhq/runtime": "workspace:*",
8688
"@zenstackhq/sdk": "workspace:*",
8789
"async-exit-hook": "^2.0.1",
8890
"change-case": "^4.1.2",
@@ -95,7 +97,6 @@
9597
"node-machine-id": "^1.1.12",
9698
"ora": "^5.4.1",
9799
"pluralize": "^8.0.0",
98-
"prisma": "~4.7.0",
99100
"promisify": "^0.0.3",
100101
"semver": "^7.3.8",
101102
"sleep-promise": "^9.1.0",
@@ -119,15 +120,18 @@
119120
"@types/vscode": "^1.56.0",
120121
"@typescript-eslint/eslint-plugin": "^5.42.0",
121122
"@typescript-eslint/parser": "^5.42.0",
123+
"@zenstackhq/runtime": "workspace:*",
122124
"@zenstackhq/testtools": "workspace:*",
123125
"concurrently": "^7.4.0",
124126
"copyfiles": "^2.4.1",
125127
"dotenv": "^16.0.3",
126128
"esbuild": "^0.15.12",
127129
"eslint": "^8.27.0",
128130
"eslint-plugin-jest": "^27.1.7",
131+
"get-latest-version": "^5.0.1",
129132
"jest": "^29.2.1",
130133
"langium-cli": "^1.0.0",
134+
"prisma": "^4.0.0",
131135
"renamer": "^4.0.0",
132136
"rimraf": "^3.0.2",
133137
"tmp": "^0.2.1",

packages/schema/src/cli/cli-util.ts

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,18 @@ import { isPlugin, Model } from '@zenstackhq/language/ast';
22
import { getLiteral, PluginError } from '@zenstackhq/sdk';
33
import colors from 'colors';
44
import fs from 'fs';
5+
import getLatestVersion from 'get-latest-version';
56
import { LangiumDocument } from 'langium';
67
import { NodeFileSystem } from 'langium/node';
8+
import ora from 'ora';
79
import path from 'path';
10+
import semver from 'semver';
811
import { URI } from 'vscode-uri';
912
import { PLUGIN_MODULE_NAME, STD_LIB_MODULE_NAME } from '../language-server/constants';
1013
import { createZModelServices, ZModelServices } from '../language-server/zmodel-module';
1114
import { Context } from '../types';
1215
import { ensurePackage, installPackage, PackageManagers } from '../utils/pkg-utils';
16+
import { getVersion } from '../utils/version-utils';
1317
import { CliError } from './cli-error';
1418
import { PluginRunner } from './plugin-runner';
1519

@@ -20,7 +24,7 @@ export async function initProject(
2024
projectPath: string,
2125
prismaSchema: string | undefined,
2226
packageManager: PackageManagers | undefined,
23-
tag: string
27+
tag?: string
2428
) {
2529
if (!fs.existsSync(projectPath)) {
2630
console.error(`Path does not exist: ${projectPath}`);
@@ -56,6 +60,8 @@ export async function initProject(
5660

5761
ensurePackage('prisma', true, packageManager, 'latest', projectPath);
5862
ensurePackage('@prisma/client', false, packageManager, 'latest', projectPath);
63+
64+
tag = tag ?? getVersion();
5965
installPackage('zenstack', true, packageManager, tag, projectPath);
6066
installPackage('@zenstackhq/runtime', false, packageManager, tag, projectPath);
6167

@@ -180,3 +186,54 @@ export async function runPlugins(options: { schema: string; packageManager: Pack
180186
}
181187
}
182188
}
189+
190+
export async function dumpInfo(projectPath: string) {
191+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
192+
let pkgJson: any;
193+
const resolvedPath = path.resolve(projectPath);
194+
try {
195+
pkgJson = require(path.join(resolvedPath, 'package.json'));
196+
} catch {
197+
console.error('Unable to locate package.json. Are you in a valid project directory?');
198+
return;
199+
}
200+
const packages = [
201+
'zenstack',
202+
...Object.keys(pkgJson.dependencies ?? {}).filter((p) => p.startsWith('@zenstackhq/')),
203+
...Object.keys(pkgJson.devDependencies ?? {}).filter((p) => p.startsWith('@zenstackhq/')),
204+
];
205+
206+
const versions = new Set<string>();
207+
for (const pkg of packages) {
208+
try {
209+
const resolved = require.resolve(`${pkg}/package.json`, { paths: [resolvedPath] });
210+
// eslint-disable-next-line @typescript-eslint/no-var-requires
211+
const version = require(resolved).version;
212+
versions.add(version);
213+
console.log(` ${colors.green(pkg.padEnd(20))}\t${version}`);
214+
} catch {
215+
// noop
216+
}
217+
}
218+
219+
if (versions.size > 1) {
220+
console.warn(colors.yellow('WARNING: Multiple versions of Zenstack packages detected. This may cause issues.'));
221+
} else if (versions.size > 0) {
222+
const spinner = ora('Checking npm registry').start();
223+
const latest = await getLatestVersion('zenstack');
224+
225+
if (!latest) {
226+
spinner.fail('unable to check for latest version');
227+
} else {
228+
spinner.succeed();
229+
const version = [...versions][0];
230+
if (semver.gt(latest, version)) {
231+
console.log(`A newer version of Zenstack is available: ${latest}.`);
232+
} else if (semver.gt(version, latest)) {
233+
console.log('You are using a pre-release version of Zenstack.');
234+
} else {
235+
console.log('You are using the latest version of Zenstack.');
236+
}
237+
}
238+
}
239+
}

packages/schema/src/cli/index.ts

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import telemetry from '../telemetry';
77
import { PackageManagers } from '../utils/pkg-utils';
88
import { getVersion } from '../utils/version-utils';
99
import { CliError } from './cli-error';
10-
import { initProject, runPlugins } from './cli-util';
10+
import { dumpInfo, initProject, runPlugins } from './cli-util';
1111

1212
// required minimal version of Prisma
1313
export const requiredPrismaVersion = '4.0.0';
@@ -17,7 +17,7 @@ export const initAction = async (
1717
options: {
1818
prisma: string | undefined;
1919
packageManager: PackageManagers | undefined;
20-
tag: string;
20+
tag?: string;
2121
}
2222
): Promise<void> => {
2323
await telemetry.trackSpan(
@@ -29,6 +29,16 @@ export const initAction = async (
2929
);
3030
};
3131

32+
export const infoAction = async (projectPath: string): Promise<void> => {
33+
await telemetry.trackSpan(
34+
'cli:command:start',
35+
'cli:command:complete',
36+
'cli:command:error',
37+
{ command: 'info' },
38+
() => dumpInfo(projectPath)
39+
);
40+
};
41+
3242
export const generateAction = async (options: {
3343
schema: string;
3444
packageManager: PackageManagers | undefined;
@@ -95,16 +105,18 @@ export function createProgram() {
95105

96106
const noDependencyCheck = new Option('--no-dependency-check', 'do not check if dependencies are installed');
97107

108+
program
109+
.command('info')
110+
.description('Get information of installed ZenStack and related packages.')
111+
.argument('[path]', 'project path', '.')
112+
.action(infoAction);
113+
98114
program
99115
.command('init')
100116
.description('Initialize an existing project for ZenStack.')
101117
.addOption(pmOption)
102118
.addOption(new Option('--prisma <file>', 'location of Prisma schema file to bootstrap from'))
103-
.addOption(
104-
new Option('--tag <tag>', 'the NPM package tag to use when installing dependencies').default(
105-
'<DEFAULT_NPM_TAG>'
106-
)
107-
)
119+
.addOption(new Option('--tag [tag]', 'the NPM package tag to use when installing dependencies'))
108120
.argument('[path]', 'project path', '.')
109121
.action(initAction);
110122

packages/schema/src/plugins/access-policy/policy-guard-generator.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
MemberAccessExpr,
1414
Model,
1515
} from '@zenstackhq/language/ast';
16-
import { PolicyKind, PolicyOperationKind } from '@zenstackhq/runtime';
16+
import type { PolicyKind, PolicyOperationKind } from '@zenstackhq/runtime';
1717
import { getDataModels, getLiteral, GUARD_FIELD_NAME, PluginError, PluginOptions, resolved } from '@zenstackhq/sdk';
1818
import { camelCase } from 'change-case';
1919
import { streamAllContents } from 'langium';

packages/schema/src/plugins/model-meta/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
Model,
88
ReferenceExpr,
99
} from '@zenstackhq/language/ast';
10-
import { RuntimeAttribute } from '@zenstackhq/runtime';
10+
import type { RuntimeAttribute } from '@zenstackhq/runtime';
1111
import { getAttributeArgs, getDataModels, getLiteral, hasAttribute, PluginOptions, resolved } from '@zenstackhq/sdk';
1212
import { camelCase } from 'change-case';
1313
import path from 'path';

packages/schema/src/plugins/plugin-utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { PolicyOperationKind } from '@zenstackhq/runtime';
1+
import type { PolicyOperationKind } from '@zenstackhq/runtime';
22
import fs from 'fs';
33
import path from 'path';
44

packages/schema/src/utils/ast-utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
Model,
1414
ReferenceExpr,
1515
} from '@zenstackhq/language/ast';
16-
import { PolicyOperationKind } from '@zenstackhq/runtime';
16+
import type { PolicyOperationKind } from '@zenstackhq/runtime';
1717
import { getLiteral } from '@zenstackhq/sdk';
1818
import { isFromStdlib } from '../language-server/utils';
1919

packages/schema/src/utils/pkg-utils.ts

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,21 +39,34 @@ export function installPackage(
3939
dev: boolean,
4040
pkgManager: PackageManagers | undefined = undefined,
4141
tag = 'latest',
42-
projectPath = '.'
42+
projectPath = '.',
43+
exactVersion = true
4344
) {
4445
const manager = pkgManager ?? getPackageManager(projectPath);
45-
console.log(`Installing package "${pkg}" with ${manager}`);
46+
console.log(`Installing package "${pkg}@${tag}" with ${manager}`);
4647
switch (manager) {
4748
case 'yarn':
48-
execSync(`yarn --cwd "${projectPath}" add ${pkg}@${tag} ${dev ? ' --dev' : ''} --ignore-engines`);
49+
execSync(
50+
`yarn --cwd "${projectPath}" add ${exactVersion ? '--exact' : ''} ${pkg}@${tag} ${
51+
dev ? ' --dev' : ''
52+
} --ignore-engines`
53+
);
4954
break;
5055

5156
case 'pnpm':
52-
execSync(`pnpm add -C "${projectPath}" ${dev ? ' --save-dev' : ''} ${pkg}@${tag}`);
57+
execSync(
58+
`pnpm add -C "${projectPath}" ${exactVersion ? '--save-exact' : ''} ${
59+
dev ? ' --save-dev' : ''
60+
} ${pkg}@${tag}`
61+
);
5362
break;
5463

5564
default:
56-
execSync(`npm install --prefix "${projectPath}" ${dev ? ' --save-dev' : ''} ${pkg}@${tag}`);
65+
execSync(
66+
`npm install --prefix "${projectPath}" ${exactVersion ? '--save-exact' : ''} ${
67+
dev ? ' --save-dev' : ''
68+
} ${pkg}@${tag}`
69+
);
5770
break;
5871
}
5972
}
@@ -63,11 +76,13 @@ export function ensurePackage(
6376
dev: boolean,
6477
pkgManager: PackageManagers | undefined = undefined,
6578
tag = 'latest',
66-
projectPath = '.'
79+
projectPath = '.',
80+
exactVersion = false
6781
) {
82+
const resolvePath = path.resolve(projectPath);
6883
try {
69-
require(pkg);
70-
} catch {
71-
installPackage(pkg, dev, pkgManager, tag, projectPath);
84+
require.resolve(pkg, { paths: [resolvePath] });
85+
} catch (err) {
86+
installPackage(pkg, dev, pkgManager, tag, resolvePath, exactVersion);
7287
}
7388
}

packages/schema/tests/cli/cli.test.ts

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1+
/* eslint-disable @typescript-eslint/no-var-requires */
2+
/// <reference types="@types/jest" />
3+
14
import { getWorkspaceNpmCacheFolder } from '@zenstackhq/testtools';
25
import * as fs from 'fs';
6+
import * as path from 'path';
37
import * as tmp from 'tmp';
48
import { createProgram } from '../../src/cli';
59
import { execSync } from '../../src/utils/exec-utils';
@@ -25,17 +29,52 @@ describe('CLI Tests', () => {
2529
fs.writeFileSync('.npmrc', `cache=${getWorkspaceNpmCacheFolder(__dirname)}`);
2630
}
2731

28-
it('init project t3 std', async () => {
32+
it('init project t3 npm std', async () => {
2933
execSync('npx --yes create-t3-app@latest --prisma --CI --noGit .', 'inherit', {
3034
npm_config_user_agent: 'npm',
3135
npm_config_cache: getWorkspaceNpmCacheFolder(__dirname),
3236
});
3337
createNpmrc();
3438

3539
const program = createProgram();
36-
program.parse(['init', '--tag', 'latest'], { from: 'user' });
40+
program.parse(['init'], { from: 'user' });
41+
42+
expect(fs.readFileSync('schema.zmodel', 'utf-8')).toEqual(fs.readFileSync('prisma/schema.prisma', 'utf-8'));
43+
44+
checkDependency('zenstack', true, true);
45+
checkDependency('@zenstackhq/runtime', false, true);
46+
});
47+
48+
it('init project t3 yarn std', async () => {
49+
execSync('npx --yes create-t3-app@latest --prisma --CI --noGit .', 'inherit', {
50+
npm_config_user_agent: 'yarn',
51+
npm_config_cache: getWorkspaceNpmCacheFolder(__dirname),
52+
});
53+
createNpmrc();
54+
55+
const program = createProgram();
56+
program.parse(['init'], { from: 'user' });
57+
58+
expect(fs.readFileSync('schema.zmodel', 'utf-8')).toEqual(fs.readFileSync('prisma/schema.prisma', 'utf-8'));
59+
60+
checkDependency('zenstack', true, true);
61+
checkDependency('@zenstackhq/runtime', false, true);
62+
});
63+
64+
it('init project t3 pnpm std', async () => {
65+
execSync('npx --yes create-t3-app@latest --prisma --CI --noGit .', 'inherit', {
66+
npm_config_user_agent: 'pnpm',
67+
npm_config_cache: getWorkspaceNpmCacheFolder(__dirname),
68+
});
69+
createNpmrc();
70+
71+
const program = createProgram();
72+
program.parse(['init'], { from: 'user' });
3773

3874
expect(fs.readFileSync('schema.zmodel', 'utf-8')).toEqual(fs.readFileSync('prisma/schema.prisma', 'utf-8'));
75+
76+
checkDependency('zenstack', true, true);
77+
checkDependency('@zenstackhq/runtime', false, true);
3978
});
4079

4180
it('init project t3 non-std prisma schema', async () => {
@@ -52,6 +91,7 @@ describe('CLI Tests', () => {
5291
expect(fs.readFileSync('schema.zmodel', 'utf-8')).toEqual(fs.readFileSync('prisma/my.prisma', 'utf-8'));
5392
});
5493

94+
// eslint-disable-next-line jest/no-disabled-tests
5595
it('init project empty project', async () => {
5696
fs.writeFileSync('package.json', JSON.stringify({ name: 'my app', version: '1.0.0' }));
5797
createNpmrc();
@@ -67,11 +107,27 @@ describe('CLI Tests', () => {
67107
provider = 'sqlite'
68108
url = 'file:./todo.db'
69109
}
70-
`;
110+
`;
71111
fs.writeFileSync('schema.zmodel', origZModelContent);
72112
createNpmrc();
73113
const program = createProgram();
74114
program.parse(['init', '--tag', 'latest'], { from: 'user' });
75115
expect(fs.readFileSync('schema.zmodel', 'utf-8')).toEqual(origZModelContent);
76116
});
77117
});
118+
119+
function checkDependency(pkg: string, isDev: boolean, requireExactVersion = true) {
120+
const pkgJson = require(path.resolve('./package.json'));
121+
122+
if (isDev) {
123+
expect(pkgJson.devDependencies[pkg]).toBeTruthy();
124+
if (requireExactVersion) {
125+
expect(pkgJson.devDependencies[pkg]).not.toMatch(/^[\^~].*/);
126+
}
127+
} else {
128+
expect(pkgJson.dependencies[pkg]).toBeTruthy();
129+
if (requireExactVersion) {
130+
expect(pkgJson.dependencies[pkg]).not.toMatch(/^[\^~].*/);
131+
}
132+
}
133+
}

0 commit comments

Comments
 (0)