Skip to content

feat: let zenstack init install exact versions for zenstack package… #313

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Mar 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
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
12 changes: 8 additions & 4 deletions packages/schema/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,13 @@
"publish-dev": "pnpm publish --tag dev",
"postinstall": "node bin/post-install.js"
},
"peerDependencies": {
"prisma": "^4.0.0"
},
"dependencies": {
"@prisma/generator-helper": "^4.7.1",
"@prisma/internals": "^4.7.1",
"@prisma/generator-helper": "^4.0.0",
"@prisma/internals": "^4.0.0",
"@zenstackhq/language": "workspace:*",
"@zenstackhq/runtime": "workspace:*",
"@zenstackhq/sdk": "workspace:*",
"async-exit-hook": "^2.0.1",
"change-case": "^4.1.2",
Expand All @@ -95,7 +97,6 @@
"node-machine-id": "^1.1.12",
"ora": "^5.4.1",
"pluralize": "^8.0.0",
"prisma": "~4.7.0",
"promisify": "^0.0.3",
"semver": "^7.3.8",
"sleep-promise": "^9.1.0",
Expand All @@ -119,15 +120,18 @@
"@types/vscode": "^1.56.0",
"@typescript-eslint/eslint-plugin": "^5.42.0",
"@typescript-eslint/parser": "^5.42.0",
"@zenstackhq/runtime": "workspace:*",
"@zenstackhq/testtools": "workspace:*",
"concurrently": "^7.4.0",
"copyfiles": "^2.4.1",
"dotenv": "^16.0.3",
"esbuild": "^0.15.12",
"eslint": "^8.27.0",
"eslint-plugin-jest": "^27.1.7",
"get-latest-version": "^5.0.1",
"jest": "^29.2.1",
"langium-cli": "^1.0.0",
"prisma": "^4.0.0",
"renamer": "^4.0.0",
"rimraf": "^3.0.2",
"tmp": "^0.2.1",
Expand Down
59 changes: 58 additions & 1 deletion packages/schema/src/cli/cli-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@ import { isPlugin, Model } from '@zenstackhq/language/ast';
import { getLiteral, PluginError } from '@zenstackhq/sdk';
import colors from 'colors';
import fs from 'fs';
import getLatestVersion from 'get-latest-version';
import { LangiumDocument } from 'langium';
import { NodeFileSystem } from 'langium/node';
import ora from 'ora';
import path from 'path';
import semver from 'semver';
import { URI } from 'vscode-uri';
import { PLUGIN_MODULE_NAME, STD_LIB_MODULE_NAME } from '../language-server/constants';
import { createZModelServices, ZModelServices } from '../language-server/zmodel-module';
import { Context } from '../types';
import { ensurePackage, installPackage, PackageManagers } from '../utils/pkg-utils';
import { getVersion } from '../utils/version-utils';
import { CliError } from './cli-error';
import { PluginRunner } from './plugin-runner';

Expand All @@ -20,7 +24,7 @@ export async function initProject(
projectPath: string,
prismaSchema: string | undefined,
packageManager: PackageManagers | undefined,
tag: string
tag?: string
) {
if (!fs.existsSync(projectPath)) {
console.error(`Path does not exist: ${projectPath}`);
Expand Down Expand Up @@ -56,6 +60,8 @@ export async function initProject(

ensurePackage('prisma', true, packageManager, 'latest', projectPath);
ensurePackage('@prisma/client', false, packageManager, 'latest', projectPath);

tag = tag ?? getVersion();
installPackage('zenstack', true, packageManager, tag, projectPath);
installPackage('@zenstackhq/runtime', false, packageManager, tag, projectPath);

Expand Down Expand Up @@ -180,3 +186,54 @@ export async function runPlugins(options: { schema: string; packageManager: Pack
}
}
}

export async function dumpInfo(projectPath: string) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let pkgJson: any;
const resolvedPath = path.resolve(projectPath);
try {
pkgJson = require(path.join(resolvedPath, 'package.json'));
} catch {
console.error('Unable to locate package.json. Are you in a valid project directory?');
return;
}
const packages = [
'zenstack',
...Object.keys(pkgJson.dependencies ?? {}).filter((p) => p.startsWith('@zenstackhq/')),
...Object.keys(pkgJson.devDependencies ?? {}).filter((p) => p.startsWith('@zenstackhq/')),
];

const versions = new Set<string>();
for (const pkg of packages) {
try {
const resolved = require.resolve(`${pkg}/package.json`, { paths: [resolvedPath] });
// eslint-disable-next-line @typescript-eslint/no-var-requires
const version = require(resolved).version;
versions.add(version);
console.log(` ${colors.green(pkg.padEnd(20))}\t${version}`);
} catch {
// noop
}
}

if (versions.size > 1) {
console.warn(colors.yellow('WARNING: Multiple versions of Zenstack packages detected. This may cause issues.'));
} else if (versions.size > 0) {
const spinner = ora('Checking npm registry').start();
const latest = await getLatestVersion('zenstack');

if (!latest) {
spinner.fail('unable to check for latest version');
} else {
spinner.succeed();
const version = [...versions][0];
if (semver.gt(latest, version)) {
console.log(`A newer version of Zenstack is available: ${latest}.`);
} else if (semver.gt(version, latest)) {
console.log('You are using a pre-release version of Zenstack.');
} else {
console.log('You are using the latest version of Zenstack.');
}
}
}
}
26 changes: 19 additions & 7 deletions packages/schema/src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import telemetry from '../telemetry';
import { PackageManagers } from '../utils/pkg-utils';
import { getVersion } from '../utils/version-utils';
import { CliError } from './cli-error';
import { initProject, runPlugins } from './cli-util';
import { dumpInfo, initProject, runPlugins } from './cli-util';

// required minimal version of Prisma
export const requiredPrismaVersion = '4.0.0';
Expand All @@ -17,7 +17,7 @@ export const initAction = async (
options: {
prisma: string | undefined;
packageManager: PackageManagers | undefined;
tag: string;
tag?: string;
}
): Promise<void> => {
await telemetry.trackSpan(
Expand All @@ -29,6 +29,16 @@ export const initAction = async (
);
};

export const infoAction = async (projectPath: string): Promise<void> => {
await telemetry.trackSpan(
'cli:command:start',
'cli:command:complete',
'cli:command:error',
{ command: 'info' },
() => dumpInfo(projectPath)
);
};

export const generateAction = async (options: {
schema: string;
packageManager: PackageManagers | undefined;
Expand Down Expand Up @@ -95,16 +105,18 @@ export function createProgram() {

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

program
.command('info')
.description('Get information of installed ZenStack and related packages.')
.argument('[path]', 'project path', '.')
.action(infoAction);

program
.command('init')
.description('Initialize an existing project for ZenStack.')
.addOption(pmOption)
.addOption(new Option('--prisma <file>', 'location of Prisma schema file to bootstrap from'))
.addOption(
new Option('--tag <tag>', 'the NPM package tag to use when installing dependencies').default(
'<DEFAULT_NPM_TAG>'
)
)
.addOption(new Option('--tag [tag]', 'the NPM package tag to use when installing dependencies'))
.argument('[path]', 'project path', '.')
.action(initAction);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
MemberAccessExpr,
Model,
} from '@zenstackhq/language/ast';
import { PolicyKind, PolicyOperationKind } from '@zenstackhq/runtime';
import type { PolicyKind, PolicyOperationKind } from '@zenstackhq/runtime';
import { getDataModels, getLiteral, GUARD_FIELD_NAME, PluginError, PluginOptions, resolved } from '@zenstackhq/sdk';
import { camelCase } from 'change-case';
import { streamAllContents } from 'langium';
Expand Down
2 changes: 1 addition & 1 deletion packages/schema/src/plugins/model-meta/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
Model,
ReferenceExpr,
} from '@zenstackhq/language/ast';
import { RuntimeAttribute } from '@zenstackhq/runtime';
import type { RuntimeAttribute } from '@zenstackhq/runtime';
import { getAttributeArgs, getDataModels, getLiteral, hasAttribute, PluginOptions, resolved } from '@zenstackhq/sdk';
import { camelCase } from 'change-case';
import path from 'path';
Expand Down
2 changes: 1 addition & 1 deletion packages/schema/src/plugins/plugin-utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { PolicyOperationKind } from '@zenstackhq/runtime';
import type { PolicyOperationKind } from '@zenstackhq/runtime';
import fs from 'fs';
import path from 'path';

Expand Down
2 changes: 1 addition & 1 deletion packages/schema/src/utils/ast-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
Model,
ReferenceExpr,
} from '@zenstackhq/language/ast';
import { PolicyOperationKind } from '@zenstackhq/runtime';
import type { PolicyOperationKind } from '@zenstackhq/runtime';
import { getLiteral } from '@zenstackhq/sdk';
import { isFromStdlib } from '../language-server/utils';

Expand Down
33 changes: 24 additions & 9 deletions packages/schema/src/utils/pkg-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,21 +39,34 @@ export function installPackage(
dev: boolean,
pkgManager: PackageManagers | undefined = undefined,
tag = 'latest',
projectPath = '.'
projectPath = '.',
exactVersion = true
) {
const manager = pkgManager ?? getPackageManager(projectPath);
console.log(`Installing package "${pkg}" with ${manager}`);
console.log(`Installing package "${pkg}@${tag}" with ${manager}`);
switch (manager) {
case 'yarn':
execSync(`yarn --cwd "${projectPath}" add ${pkg}@${tag} ${dev ? ' --dev' : ''} --ignore-engines`);
execSync(
`yarn --cwd "${projectPath}" add ${exactVersion ? '--exact' : ''} ${pkg}@${tag} ${
dev ? ' --dev' : ''
} --ignore-engines`
);
break;

case 'pnpm':
execSync(`pnpm add -C "${projectPath}" ${dev ? ' --save-dev' : ''} ${pkg}@${tag}`);
execSync(
`pnpm add -C "${projectPath}" ${exactVersion ? '--save-exact' : ''} ${
dev ? ' --save-dev' : ''
} ${pkg}@${tag}`
);
break;

default:
execSync(`npm install --prefix "${projectPath}" ${dev ? ' --save-dev' : ''} ${pkg}@${tag}`);
execSync(
`npm install --prefix "${projectPath}" ${exactVersion ? '--save-exact' : ''} ${
dev ? ' --save-dev' : ''
} ${pkg}@${tag}`
);
break;
}
}
Expand All @@ -63,11 +76,13 @@ export function ensurePackage(
dev: boolean,
pkgManager: PackageManagers | undefined = undefined,
tag = 'latest',
projectPath = '.'
projectPath = '.',
exactVersion = false
) {
const resolvePath = path.resolve(projectPath);
try {
require(pkg);
} catch {
installPackage(pkg, dev, pkgManager, tag, projectPath);
require.resolve(pkg, { paths: [resolvePath] });
} catch (err) {
installPackage(pkg, dev, pkgManager, tag, resolvePath, exactVersion);
}
}
62 changes: 59 additions & 3 deletions packages/schema/tests/cli/cli.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
/* eslint-disable @typescript-eslint/no-var-requires */
/// <reference types="@types/jest" />

import { getWorkspaceNpmCacheFolder } from '@zenstackhq/testtools';
import * as fs from 'fs';
import * as path from 'path';
import * as tmp from 'tmp';
import { createProgram } from '../../src/cli';
import { execSync } from '../../src/utils/exec-utils';
Expand All @@ -25,17 +29,52 @@ describe('CLI Tests', () => {
fs.writeFileSync('.npmrc', `cache=${getWorkspaceNpmCacheFolder(__dirname)}`);
}

it('init project t3 std', async () => {
it('init project t3 npm std', async () => {
execSync('npx --yes create-t3-app@latest --prisma --CI --noGit .', 'inherit', {
npm_config_user_agent: 'npm',
npm_config_cache: getWorkspaceNpmCacheFolder(__dirname),
});
createNpmrc();

const program = createProgram();
program.parse(['init', '--tag', 'latest'], { from: 'user' });
program.parse(['init'], { from: 'user' });

expect(fs.readFileSync('schema.zmodel', 'utf-8')).toEqual(fs.readFileSync('prisma/schema.prisma', 'utf-8'));

checkDependency('zenstack', true, true);
checkDependency('@zenstackhq/runtime', false, true);
});

it('init project t3 yarn std', async () => {
execSync('npx --yes create-t3-app@latest --prisma --CI --noGit .', 'inherit', {
npm_config_user_agent: 'yarn',
npm_config_cache: getWorkspaceNpmCacheFolder(__dirname),
});
createNpmrc();

const program = createProgram();
program.parse(['init'], { from: 'user' });

expect(fs.readFileSync('schema.zmodel', 'utf-8')).toEqual(fs.readFileSync('prisma/schema.prisma', 'utf-8'));

checkDependency('zenstack', true, true);
checkDependency('@zenstackhq/runtime', false, true);
});

it('init project t3 pnpm std', async () => {
execSync('npx --yes create-t3-app@latest --prisma --CI --noGit .', 'inherit', {
npm_config_user_agent: 'pnpm',
npm_config_cache: getWorkspaceNpmCacheFolder(__dirname),
});
createNpmrc();

const program = createProgram();
program.parse(['init'], { from: 'user' });

expect(fs.readFileSync('schema.zmodel', 'utf-8')).toEqual(fs.readFileSync('prisma/schema.prisma', 'utf-8'));

checkDependency('zenstack', true, true);
checkDependency('@zenstackhq/runtime', false, true);
});

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

// eslint-disable-next-line jest/no-disabled-tests
it('init project empty project', async () => {
fs.writeFileSync('package.json', JSON.stringify({ name: 'my app', version: '1.0.0' }));
createNpmrc();
Expand All @@ -67,11 +107,27 @@ describe('CLI Tests', () => {
provider = 'sqlite'
url = 'file:./todo.db'
}
`;
`;
fs.writeFileSync('schema.zmodel', origZModelContent);
createNpmrc();
const program = createProgram();
program.parse(['init', '--tag', 'latest'], { from: 'user' });
expect(fs.readFileSync('schema.zmodel', 'utf-8')).toEqual(origZModelContent);
});
});

function checkDependency(pkg: string, isDev: boolean, requireExactVersion = true) {
const pkgJson = require(path.resolve('./package.json'));

if (isDev) {
expect(pkgJson.devDependencies[pkg]).toBeTruthy();
if (requireExactVersion) {
expect(pkgJson.devDependencies[pkg]).not.toMatch(/^[\^~].*/);
}
} else {
expect(pkgJson.dependencies[pkg]).toBeTruthy();
if (requireExactVersion) {
expect(pkgJson.dependencies[pkg]).not.toMatch(/^[\^~].*/);
}
}
}
Loading