Skip to content

Commit

Permalink
feat(api): addition of a new uninstall command (#776)
Browse files Browse the repository at this point in the history
* feat(api): creation of a new `list` command

* Update packages/api/src/commands/list.ts

Co-authored-by: Kanad Gupta <[email protected]>

* Update packages/api/src/commands/list.ts

Co-authored-by: Kanad Gupta <[email protected]>

* Update packages/api/src/commands/list.ts

Co-authored-by: Kanad Gupta <[email protected]>

* fix: pr feedback

* feat(api): addition of a new `uninstall` command

* feat(api): outputting `language` to the `list` command

* docs: cleanup

* Update packages/api/src/codegen/codegenerator.ts

Co-authored-by: Kanad Gupta <[email protected]>

* Update docs/how-it-works.md

Co-authored-by: Kanad Gupta <[email protected]>

* Update docs/how-it-works.md

Co-authored-by: Kanad Gupta <[email protected]>

* Update docs/how-it-works.md

Co-authored-by: Kanad Gupta <[email protected]>

* docs: spacing fixes

* Update docs/how-it-works.md

Co-authored-by: Kanad Gupta <[email protected]>

---------

Co-authored-by: Kanad Gupta <[email protected]>
  • Loading branch information
erunion and kanadgupta authored Oct 20, 2023
1 parent 3bbb7a8 commit e44461a
Show file tree
Hide file tree
Showing 12 changed files with 379 additions and 103 deletions.
39 changes: 22 additions & 17 deletions docs/how-it-works.md
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
| | // fully typed and compatible with both ESM and CJS.
│ ├── 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": {
"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)}`);
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.`,
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

0 comments on commit e44461a

Please sign in to comment.