Skip to content
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

feat(api): addition of a new uninstall command #776

Merged
merged 16 commits into from
Oct 20, 2023
Merged
Show file tree
Hide file tree
Changes from 15 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
39 changes: 22 additions & 17 deletions docs/how-it-works.md
Copy link
Member

Choose a reason for hiding this comment

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

sorry for all the feedback in this file — i definitely plan on doing a full comb through our docs before we release v7

Original file line number Diff line number Diff line change
Expand Up @@ -22,23 +22,25 @@ The `.api/` directory is where the CLI installation process stores all of its in

```
.api/
├── api.json // The `package-lock.json` equivalent that records
| // everything that's installed, when it was installed,
| // what the original source was, and what version of
| // `api` was used.
├── api.json // The `package-lock.json` equivalent that records metadata about the
| // installed SDKs.
└── apis/
├── hoot/
├── readme/
| ├── node_modules/
│ ├── index.js
│ ├── index.d.ts // All types for the SDK, ready to use in your IDE.
│ ├── dist/ // The compiled source for your SDK. For JS, we offer a single SDK that's
| | // compatible with both ESM and CJS.
erunion marked this conversation as resolved.
Show resolved Hide resolved
│ ├── src/ // The raw source for your SDK.
│ | |── schemas/ // If your SDK has documented parameters or responses this is a directory
| | | // containing those as represented in JSON Schema. These schemas power
| | | // your SDK and the `types.ts` file.
│ | |── index.ts
│ | |── schemas.ts
│ | └── types.ts
│ |── openapi.json
│ └── package.json
│ |── package.json
│ |── README.md
│ └── tsconfig.json
└── petstore/
├── node_modules/
├── index.ts
├── index.d.ts
├── openapi.json
└── package.json
```

#### `api.json`
Expand All @@ -47,13 +49,16 @@ The `api.json` file within `.api/` is where the CLI keeps track of everything th

```json
{
"version": "1.0",
"$schema": "https://unpkg.com/api@7/schema.json",
"apis": [
{
"identifier": "developers",
"source": "@developers/v2.0#nysezql0wwo236",
"private": true,
"identifier": "petstore",
"source": "@petstore/v1.0#tl1e4kl1cl8eg8",
"integrity": "sha512-lQeYVerukls0IYy3Ys9J6Hri9nucH2zBZk6ehO1EI9a/0K3p/egoIw/Yz9A93KtB1KUUArjGK6ebqsZkHFxguA==",
"installerVersion": "5.0.0-beta.0"
"installerVersion": "7.0.0",
"language": "js",
"createdAt": "2023-10-19T23:13:04.939Z"
}
]
}
Expand Down
6 changes: 6 additions & 0 deletions packages/api/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@
"sha512-ld+djZk8uRWmzXC+JYla1PTBScg0NjP/8x9vOOKRW+DuJ3NNMRjrpfbY7T77Jgnc87dZZsU49robbQfYe3ukug=="
]
},
"language": {
Copy link
Member Author

Choose a reason for hiding this comment

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

Keeping track of what language we generated the SDK for will allow us to know how to uninstall it from that languages package manager. And because this didn't exist before this we'll default to js everywhere we access this.

"type": "string",
"description": "The language that this SDK was generated for.",
"default": "js",
"enum": ["js"]
},
"private": {
"type": "boolean",
"description": "Was this SDK installed as a private, unpublished, package to the filesystem?"
Expand Down
26 changes: 13 additions & 13 deletions packages/api/src/codegen/codegenerator.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,9 @@
import type { InstallerOptions } from './factory.js';
import type Storage from '../storage.js';
import type Oas from 'oas';

import { PACKAGE_NAME, PACKAGE_VERSION } from '../packageInfo.js';

export interface InstallerOptions {
/**
* Will initiate a dry run install process. Used for simulating installations within a unit test.
*/
dryRun?: boolean;

/**
* Used for stubbing out the logger that we use within the installation process so it can be
* easily introspected without having to mock out `console.*`.
*/
logger?: (msg: string) => void;
}

export default abstract class CodeGenerator {
spec: Oas;

Expand Down Expand Up @@ -67,6 +55,18 @@ export default abstract class CodeGenerator {

abstract install(storage: Storage, opts?: InstallerOptions): Promise<void>;

/**
* It would be better if this were an abstract function but TS/JS doesn't have support for that so
* we instead have to rely on throwing a `TypeError` if it's not been implemented instead of a
* build-time error.
*
* @see {@link https://github.com/microsoft/TypeScript/issues/34516}
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
static async uninstall(storage: Storage, opts?: InstallerOptions): Promise<void> {
throw new TypeError('The uninstallation step for this language has not been implemented');
}

abstract compile(storage: Storage, opts?: InstallerOptions): Promise<void>;

hasRequiredPackages() {
Expand Down
29 changes: 28 additions & 1 deletion packages/api/src/codegen/factory.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,26 @@
import type CodeGenerator from './codegenerator.js';
import type Storage from '../storage.js';
import type Oas from 'oas';

import TSGenerator from './languages/typescript/index.js';

export enum SupportedLanguages {
JS = 'js',
}
export interface InstallerOptions {
/**
* Will initiate a dry run install process. Used for simulating installations within a unit test.
*/
dryRun?: boolean;

export default function codegenFactory(
/**
* Used for stubbing out the logger that we use within the installation process so it can be
* easily introspected without having to mock out `console.*`.
*/
logger?: (msg: string) => void;
}

export function codegenFactory(
language: SupportedLanguages,
spec: Oas,
specPath: string,
Expand All @@ -21,3 +34,17 @@ export default function codegenFactory(
throw new TypeError(`Unsupported language supplied: ${language}`);
}
}

export function uninstallerFactory(
language: SupportedLanguages,
storage: Storage,
opts: InstallerOptions = {},
): Promise<void> {
switch (language) {
case SupportedLanguages.JS:
return TSGenerator.uninstall(storage, opts);

default:
throw new TypeError(`Unsupported language supplied: ${language}`);
}
}
20 changes: 18 additions & 2 deletions packages/api/src/codegen/languages/typescript/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { InstallerOptions } from '../../codegenerator.js';
import type { InstallerOptions } from '../../factory.js';
import type Oas from 'oas';
import type Operation from 'oas/operation';
import type { HttpMethods, SchemaObject } from 'oas/rmoas.types';
Expand Down Expand Up @@ -136,7 +136,7 @@ export default class TSGenerator extends CodeGenerator {
// This will install the installed SDK as a dependency within the current working directory,
// adding `@api/<sdk identifier>` as a dependency there so you can load it with
// `require('@api/<sdk identifier>)`.
await execa('npm', [...npmInstall, installDir].filter(Boolean))
return execa('npm', [...npmInstall, installDir].filter(Boolean))
.then(res => {
if (opts.dryRun) {
(opts.logger ? opts.logger : logger)(res.command);
Expand Down Expand Up @@ -164,6 +164,22 @@ export default class TSGenerator extends CodeGenerator {
});
}

static async uninstall(storage: Storage, opts: InstallerOptions = {}): Promise<void> {
const pkgName = storage.getPackageName() as string;

const args = ['uninstall', pkgName, opts.dryRun ? '--dry-run' : ''].filter(Boolean);
return execa('npm', args)
.then(res => {
if (opts.dryRun) {
(opts.logger ? opts.logger : logger)(res.command);
(opts.logger ? opts.logger : logger)(res.stdout);
}
})
.catch(err => {
throw err;
});
}

/**
* Compile the TS code we generated into JS for use in CJS and ESM environments.
*
Expand Down
2 changes: 2 additions & 0 deletions packages/api/src/commands/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import installCommand from './install.js';
import listCommand from './list.js';
import uninstallCommand from './uninstall.js';

export default {
install: installCommand,
list: listCommand,
uninstall: uninstallCommand,
};
10 changes: 5 additions & 5 deletions packages/api/src/commands/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Oas from 'oas';
import ora from 'ora';
import uslug from 'uslug';

import codegenFactory, { SupportedLanguages } from '../codegen/factory.js';
import { SupportedLanguages, codegenFactory } from '../codegen/factory.js';
import Fetcher from '../fetcher.js';
import promptTerminal from '../lib/prompt.js';
import logger from '../logger.js';
Expand Down Expand Up @@ -45,7 +45,7 @@ cmd
}

let spinner = ora('Fetching your API definition').start();
const storage = new Storage(uri);
const storage = new Storage(uri, language);

const oas = await storage
.load(false)
Expand Down Expand Up @@ -188,9 +188,9 @@ cmd
'after',
`
Examples:
$ api install @developers/v2.0#nysezql0wwo236
$ api install https://raw.githubusercontent.com/readmeio/oas-examples/main/3.0/json/petstore-simple.json
$ api install ./petstore.json`,
$ npx api install @developers/v2.0#nysezql0wwo236
$ npx api install https://raw.githubusercontent.com/readmeio/oas-examples/main/3.0/json/petstore-simple.json
$ npx api install ./petstore.json`,
);

export default cmd;
3 changes: 2 additions & 1 deletion packages/api/src/commands/list.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import chalk from 'chalk';
import { Command } from 'commander';

import { SupportedLanguages } from '../codegen/factory.js';
import logger from '../logger.js';
import Storage from '../storage.js';

Expand All @@ -14,7 +15,6 @@ cmd
Storage.setStorageDir();

const lockfile = Storage.getLockfile();

if (!lockfile.apis.length) {
logger('😔 You do not have any SDKs installed.');
return;
Expand All @@ -30,6 +30,7 @@ cmd
logger(`package name (${chalk.red('private')}): ${chalk.grey(`@api/${api.identifier}`)}`);
}

logger(`language: ${chalk.grey(api.language || SupportedLanguages.JS)}`);
Copy link
Member Author

Choose a reason for hiding this comment

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

Screen Shot 2023-10-19 at 11 35 45 PM

logger(`source: ${chalk.grey(api.source)}`);
logger(`installer version: ${chalk.grey(api.installerVersion)}`);
logger(`created at: ${chalk.grey(api.createdAt || 'n/a')}`);
Expand Down
94 changes: 94 additions & 0 deletions packages/api/src/commands/uninstall.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import path from 'node:path';

import chalk from 'chalk';
import { Command, Option } from 'commander';
import ora from 'ora';

import { SupportedLanguages, uninstallerFactory } from '../codegen/factory.js';
import promptTerminal from '../lib/prompt.js';
import logger from '../logger.js';
import Storage from '../storage.js';

interface Options {
yes?: boolean;
}

const cmd = new Command();
cmd
.name('uninstall')
.description('uninstall an SDK from your codebase')
.argument('<identifier>', 'the SDK to uninstall')
.addOption(new Option('-y, --yes', 'Automatically answer "yes" to any prompts printed'))
.action(async (identifier: string, options: Options) => {
// We don't know if we have `identifier` in the storage system yet, we just need to preload the
// system so we can access lockfiles.
const storage = new Storage('', SupportedLanguages.JS, identifier);

const entry = Storage.getFromLockfile(identifier);
if (!entry) {
logger(
`You do not appear to have ${identifier} installed. You can run \`npx api list\` to see what SDKs are present.`,
Copy link
Member

Choose a reason for hiding this comment

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

this is great 🫶🏽

true,
);
process.exit(1);
}

storage.setLanguage(entry?.language);
storage.setIdentifier(identifier);

const directory = path.relative(process.cwd(), storage.getIdentifierStorageDir());
if (!options.yes) {
await promptTerminal({
type: 'confirm',
name: 'value',
message: `Are you sure you want to uninstall ${chalk.yellow(identifier)}? This will delete the ${chalk.yellow(
directory,
)} directory and potentially any changes you may have made there.`,
initial: true,
}).then(({ value }) => {
if (!value) {
process.exit(1);
}
});
}

let spinner = ora(`Uninstalling ${chalk.grey(identifier)}`).start();

// If we have a known package name for this then we can uninstall it from within cooresponding
// package manager.
const pkgName = storage.getPackageName();
if (pkgName) {
const language = storage.getSDKLanguage();
await uninstallerFactory(language, storage)
.then(() => {
spinner.succeed(spinner.text);
})
.catch(err => {
spinner.fail(spinner.text);
logger(err.message, true);
process.exit(1);
});
}

spinner = ora(`Removing ${chalk.grey(directory)}`).start();
await storage
.remove()
.then(() => {
spinner.succeed(spinner.text);
})
.catch(err => {
spinner.fail(spinner.text);
logger(err.message, true);
process.exit(1);
});

logger('🚀 All done!');
})
.addHelpText(
'after',
`
Examples:
$ npx api uninstall petstore`,
);

export default cmd;
Loading