diff --git a/.eslintrc.js b/.eslintrc.js index 90ad0a4e..f77ea855 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -23,7 +23,7 @@ module.exports = { rules: { 'newline-before-return': 'error', '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], - 'simple-import-sort/sort': 'error', + 'simple-import-sort/imports': 'error', 'sort-imports': 'off', 'import/first': 'error', 'import/newline-after-import': 'error', diff --git a/src/commands/compile.ts b/src/commands/compile.ts index 24266e8a..d194c8af 100644 --- a/src/commands/compile.ts +++ b/src/commands/compile.ts @@ -2,13 +2,12 @@ import { Command, flags } from '@oclif/command'; import { Source } from '@superfaceai/parser'; import * as nodePath from 'path'; -import { userError } from '../common/error'; - import { DOCUMENT_PARSE_FUNCTION, DocumentType, inferDocumentTypeWithFlag, } from '../common/document'; +import { userError } from '../common/error'; import { DocumentTypeFlag, documentTypeFlag } from '../common/flags'; import { lstatPromise, OutputStream, readFilePromise } from '../common/io'; diff --git a/src/commands/create.ts b/src/commands/create.ts index e8b92a2d..78b6793c 100644 --- a/src/commands/create.ts +++ b/src/commands/create.ts @@ -1,12 +1,14 @@ import { Command, flags } from '@oclif/command'; +import { + MAP_EXTENSIONS, + PROFILE_EXTENSIONS, + validateDocumentName, +} from '../common/document'; import { developerError, userError } from '../common/error'; - -import { MAP_EXTENSIONS, PROFILE_EXTENSIONS, validateDocumentName } from '../common/document'; import { OutputStream } from '../common/io'; - -import * as profileTemplate from '../templates/profile'; import * as mapTemplate from '../templates/map'; +import * as profileTemplate from '../templates/profile'; export default class Create extends Command { static description = 'Creates empty map and profile on a local filesystem.'; @@ -36,8 +38,8 @@ export default class Create extends Command { template: flags.string({ options: ['empty', 'pubs'], default: 'empty', - description: 'Template to initialize the usecases and maps with' - }) + description: 'Template to initialize the usecases and maps with', + }), }; async run(): Promise { @@ -46,7 +48,8 @@ export default class Create extends Command { let usecases: string[]; if ( - typeof documentName !== 'string' || !validateDocumentName(documentName) + typeof documentName !== 'string' || + !validateDocumentName(documentName) ) { throw userError('Invalid document name.', 1); } @@ -73,16 +76,32 @@ export default class Create extends Command { break; case 'map': if (!flags.provider) { - throw userError('Provider name must be provided when generating a map.', 2); + throw userError( + 'Provider name must be provided when generating a map.', + 2 + ); } - await this.createMap(documentName, usecases, flags.provider, flags.template); + await this.createMap( + documentName, + usecases, + flags.provider, + flags.template + ); break; case 'both': if (!flags.provider) { - throw userError('Provider name must be provided when generating a map.', 2); + throw userError( + 'Provider name must be provided when generating a map.', + 2 + ); } await this.createProfile(documentName, usecases, flags.template); - await this.createMap(documentName, usecases, flags.provider, flags.template); + await this.createMap( + documentName, + usecases, + flags.provider, + flags.template + ); } } @@ -95,9 +114,10 @@ export default class Create extends Command { const outputStream = new OutputStream(fileName); await outputStream.write( - profileTemplate.header(documentName) + useCaseNames.map( - usecase => profileTemplate.usecase(template, usecase) - ).join('') + profileTemplate.header(documentName) + + useCaseNames + .map(usecase => profileTemplate.usecase(template, usecase)) + .join('') ); this.log( `-> Created ${fileName} (id = "https://example.com/profile/${documentName}")` @@ -116,9 +136,8 @@ export default class Create extends Command { const outputStream = new OutputStream(fileName); await outputStream.write( - mapTemplate.header(documentName, providerName) + useCaseNames.map( - usecase => mapTemplate.map(template, usecase) - ).join('') + mapTemplate.header(documentName, providerName) + + useCaseNames.map(usecase => mapTemplate.map(template, usecase)).join('') ); this.log( `-> Created ${fileName} (provider = ${providerName}, id = "https://example.com/${providerName}/${documentName}")` diff --git a/src/commands/lint.ts b/src/commands/lint.ts index 3f79c334..8780ecda 100644 --- a/src/commands/lint.ts +++ b/src/commands/lint.ts @@ -1,13 +1,12 @@ import { Command, flags } from '@oclif/command'; import { Source, SyntaxError } from '@superfaceai/parser'; -import { userError, developerError } from '../common/error'; - import { DOCUMENT_PARSE_FUNCTION, DocumentType, inferDocumentTypeWithFlag, } from '../common/document'; +import { developerError, userError } from '../common/error'; import { DocumentTypeFlag, documentTypeFlag } from '../common/flags'; import { OutputStream, readFilePromise } from '../common/io'; import { formatWordPlurality } from '../util'; diff --git a/src/commands/play.test.ts b/src/commands/play.test.ts index 6d19c1fb..423af96a 100644 --- a/src/commands/play.test.ts +++ b/src/commands/play.test.ts @@ -1,87 +1,143 @@ -import { stdout } from 'stdout-stderr'; -import { accessPromise, mkdirPromise, rimrafPromise, OutputStream } from '../common/io'; import * as nodePath from 'path'; +import { stdout } from 'stdout-stderr'; import Play from '../commands/play'; +import { + accessPromise, + mkdirPromise, + OutputStream, + rimrafPromise, +} from '../common/io'; describe('Play CLI command', () => { - const baseFixture = nodePath.join('fixtures', 'playgrounds'); - const testPlaygroundName = 'test'; - const testPlaygroundPath = nodePath.join(baseFixture, testPlaygroundName); - - afterEach(async () => { + const baseFixture = nodePath.join('fixtures', 'playgrounds'); + const testPlaygroundName = 'test'; + const testPlaygroundPath = nodePath.join(baseFixture, testPlaygroundName); + + afterEach(async () => { await rimrafPromise(testPlaygroundPath); - - const testFiles = ['package-lock.json', 'node_modules', 'valid.supr.ast.json', 'valid.noop.suma.ast.json', 'valid.noop.js']; + + const testFiles = [ + 'package-lock.json', + 'node_modules', + 'valid.supr.ast.json', + 'valid.noop.suma.ast.json', + 'valid.noop.js', + ]; await Promise.all( - testFiles.map( - file => rimrafPromise(nodePath.join(baseFixture, 'valid', file)) + testFiles.map(file => + rimrafPromise(nodePath.join(baseFixture, 'valid', file)) ) - ) - }); + ); + }); + + it('a valid playground is detected', async () => { + expect( + await Play.run(['clean', nodePath.join(baseFixture, 'valid')]) + ).toBeUndefined(); + }); - it('a valid playground is detected', async () => { - await Play.run(['clean', nodePath.join(baseFixture, 'valid')]) - }); + it('an invalid playground is rejected', async () => { + await expect( + Play.run(['clean', nodePath.join(baseFixture, 'invalid')]) + ).rejects.toThrow('The directory at playground path is not a playground'); + }); - it('an invalid playground is rejected', async () => { - await expect( - Play.run(['clean', nodePath.join(baseFixture, 'invalid')]) - ).rejects.toThrow('The directory at playground path is not a playground') - }); + it('initialize creates a valid playground', async () => { + await Play.run([ + 'initialize', + testPlaygroundPath, + '--providers', + 'foo', + 'bar', + ]); - it('initialize creates a valid playground', async () => { - await Play.run(['initialize', testPlaygroundPath, '--providers', 'foo', 'bar']); + await accessPromise(testPlaygroundPath); - await accessPromise(testPlaygroundPath); + const expectedFiles = [ + 'package.json', + 'test.supr', + 'test.foo.suma', + 'test.bar.suma', + 'test.foo.ts', + 'test.bar.ts', + ]; + for (const file of expectedFiles) { + await accessPromise(nodePath.join(testPlaygroundPath, file)); + } - const expectedFiles = ['package.json', 'test.supr', 'test.foo.suma', 'test.bar.suma', 'test.foo.ts', 'test.bar.ts']; - for (const file of expectedFiles) { - await accessPromise(nodePath.join(testPlaygroundPath, file)) - } + // No exceptions thrown + expect(undefined).toBeUndefined(); }); - + it('execute compiles playground and executes it', async () => { stdout.start(); - await Play.run(['execute', nodePath.join(baseFixture, 'valid'), '--providers', 'noop']); + await Play.run([ + 'execute', + nodePath.join(baseFixture, 'valid'), + '--providers', + 'noop', + ]); stdout.stop(); - expect( - stdout.output - ).toMatch(/pubs\/Noop result: Ok { value: \[\] }\s*$/); + expect(stdout.output).toMatch(/pubs\/Noop result: Ok { value: \[\] }\s*$/); - const expectedFiles = ['package-lock.json', 'node_modules', 'valid.supr.ast.json', 'valid.noop.suma.ast.json', 'valid.noop.js']; + const expectedFiles = [ + 'package-lock.json', + 'node_modules', + 'valid.supr.ast.json', + 'valid.noop.suma.ast.json', + 'valid.noop.js', + ]; await Promise.all( - expectedFiles.map( - file => accessPromise(nodePath.join(baseFixture, 'valid', file)) + expectedFiles.map(file => + accessPromise(nodePath.join(baseFixture, 'valid', file)) ) - ) + ); }, 30000); - it('clean cleans compilation artifacts', async () => { - const deletedFiles = ['package-lock.json', 'node_modules', 'test.supr.ast.json', 'test.foo.suma.ast.json', 'test.bar.suma.ast.json', 'test.foo.js', 'test.bar.js']; - const expectedFiles = ['package.json', 'test.supr', 'test.foo.suma', 'test.bar.suma', 'test.foo.ts', 'test.bar.ts', 'test.baz.ts']; - + it('clean cleans compilation artifacts', async () => { + const deletedFiles = [ + 'package-lock.json', + 'node_modules', + 'test.supr.ast.json', + 'test.foo.suma.ast.json', + 'test.bar.suma.ast.json', + 'test.foo.js', + 'test.bar.js', + ]; + const expectedFiles = [ + 'package.json', + 'test.supr', + 'test.foo.suma', + 'test.bar.suma', + 'test.foo.ts', + 'test.bar.ts', + 'test.baz.ts', + ]; + await mkdirPromise(testPlaygroundPath); await Promise.all( - [...deletedFiles, ...expectedFiles].map( - file => OutputStream.writeOnce(nodePath.join(testPlaygroundPath, file), '') + [...deletedFiles, ...expectedFiles].map(file => + OutputStream.writeOnce(nodePath.join(testPlaygroundPath, file), '') ) ); await Play.run(['clean', testPlaygroundPath]); await Promise.all( - deletedFiles.map( - file => expect(accessPromise(nodePath.join(testPlaygroundPath, file))).rejects.toThrowError('ENOENT') + deletedFiles.map(file => + expect( + accessPromise(nodePath.join(testPlaygroundPath, file)) + ).rejects.toThrowError('ENOENT') ) ); await Promise.all( - expectedFiles.map( - file => accessPromise(nodePath.join(testPlaygroundPath, file)) + expectedFiles.map(file => + accessPromise(nodePath.join(testPlaygroundPath, file)) ) - ) - }); -}); \ No newline at end of file + ); + }); +}); diff --git a/src/commands/play.ts b/src/commands/play.ts index 75b78c3e..2042ab68 100644 --- a/src/commands/play.ts +++ b/src/commands/play.ts @@ -1,74 +1,90 @@ import { Command, flags } from '@oclif/command'; -import * as nodePath from 'path'; -import { userError, developerError } from '../common/error'; -import { accessPromise, readdirPromise, statPromise, rimrafPromise, execFilePromise, resolveSkipFile, mkdirPromise, OutputStream } from '../common/io'; +import chalk from 'chalk'; import * as inquirer from 'inquirer'; import FileTreeSelectionPrompt from 'inquirer-file-tree-selection-prompt'; -import Compile from './compile'; -import chalk from 'chalk'; +import * as nodePath from 'path'; +import * as ts from 'typescript'; -import ts from 'typescript'; -import { skipFileFlag, SkipFileType } from '../common/flags'; import { validateDocumentName } from '../common/document'; - +import { developerError, userError } from '../common/error'; +import { skipFileFlag, SkipFileType } from '../common/flags'; +import { + accessPromise, + execFilePromise, + mkdirPromise, + OutputStream, + readdirPromise, + resolveSkipFile, + rimrafPromise, + statPromise, +} from '../common/io'; +import * as mapTemplate from '../templates/map'; import * as playgroundTemplate from '../templates/playground'; import * as profileTemplate from '../templates/profile'; -import * as mapTemplate from '../templates/map'; +import Compile from './compile'; -inquirer.registerPrompt('file-tree-selection', FileTreeSelectionPrompt) +// just let me use this js library eslint +// eslint-disable-next-line import/namespace +inquirer.registerPrompt('file-tree-selection', FileTreeSelectionPrompt); type ActionType = 'initialize' | 'execute' | 'clean'; interface PlaygroundFolder { - name: string, - path: string, - providers: Set -}; + name: string; + path: string; + providers: Set; +} export default class Play extends Command { static description = `Manages and executes interactive playgrounds. Missing arguments are interactively prompted. Playground is a folder F which contains a profile (\`F.supr\`), maps (\`F.*.suma\`) and glue scripts (\`F.*.ts\`) where \`*\` denotes provider name. initialize: a playground is populated with an example profile, and a pair of a map and a glue script for each provider. execute: the profile, and the selected pairs of a map and a glue script are compiled and the specified provider glue scripts are executed. -clean: the \`node_modules\` folder and compilation artifacts are cleaned.` - ; +clean: the \`node_modules\` folder and compilation artifacts are cleaned.`; static examples = [ 'superface play', 'superface play initialize PubHours --providers osm gmaps', 'superface play execute PubHours --providers osm', - 'superface play clean PubHours' + 'superface play clean PubHours', ]; - static args = [{ - name: 'action', - description: 'Action to take.', - required: false, - options: ['initialize', 'execute', 'clean'] - }, { - name: 'playground', - description: 'Path to the playground to initialize or execute.', - required: false - }]; + static args = [ + { + name: 'action', + description: 'Action to take.', + required: false, + options: ['initialize', 'execute', 'clean'], + }, + { + name: 'playground', + description: 'Path to the playground to initialize or execute.', + required: false, + }, + ]; static flags = { providers: flags.string({ char: 'p', multiple: true, - description: 'Providers to initialize or execute.' + description: 'Providers to initialize or execute.', }), skip: skipFileFlag({ - description: 'Controls the fallback skipping behavior of more specific skip flags.\nSee `--skip-npm`, `--skip-ast`, and `--skip-tsc` for more details.', - default: 'never' + description: + 'Controls the fallback skipping behavior of more specific skip flags.\nSee `--skip-npm`, `--skip-ast`, and `--skip-tsc` for more details.', + default: 'never', }), 'skip-npm': skipFileFlag({ - description: 'Control skipping behavior of the npm install execution step.\n`exists` checks for the presence of `node_modules` directory.' + description: + 'Control skipping behavior of the npm install execution step.\n`exists` checks for the presence of `node_modules` directory.', }), 'skip-ast': skipFileFlag({ - description: 'Control skipping behavior of the superface ast compile execution step.\n`exists` checks for the presence of `..suma.ast.json` files.' + description: + 'Control skipping behavior of the superface ast compile execution step.\n`exists` checks for the presence of `..suma.ast.json` files.', }), 'skip-tsc': skipFileFlag({ - description: 'Control skipping behavior of the tsc compile execution step.\n`exists` checks for the presence of `..js files.' + description: + 'Control skipping behavior of the tsc compile execution step.\n`exists` checks for the presence of `..js files.', }), help: flags.help({ char: 'h' }), @@ -77,34 +93,41 @@ clean: the \`node_modules\` folder and compilation artifacts are cleaned.` async run(): Promise { const { args, flags } = this.parse(Play); + // what even are types + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment let action: ActionType = args.action; if (action === undefined) { - const response = await inquirer.prompt({ + // eslint-disable-next-line import/namespace + const response: { action: ActionType } = await inquirer.prompt({ name: 'action', message: 'Select an action', type: 'list', - choices: [{ - name: 'Initialize a new playground', - value: 'initialize' - }, { - name: 'Execute an existing playground', - value: 'execute' - }, { - name: 'Clean an existing playground', - value: 'clean' - }], + choices: [ + { + name: 'Initialize a new playground', + value: 'initialize', + }, + { + name: 'Execute an existing playground', + value: 'execute', + }, + { + name: 'Clean an existing playground', + value: 'clean', + }, + ], }); action = response.action; - }; - this.debug("Action:", action); + } + this.debug('Action:', action); if (action === 'initialize') { await this.runInitialize(args.playground, flags.providers); } else if (action === 'execute') { await this.runExecute(args.playground, flags.providers, { - npm: flags["skip-npm"] ?? flags.skip, - ast: flags["skip-ast"] ?? flags.skip, - tsc: flags["skip-tsc"] ?? flags.skip + npm: flags['skip-npm'] ?? flags.skip, + ast: flags['skip-ast'] ?? flags.skip, + tsc: flags['skip-tsc'] ?? flags.skip, }); } else if (action === 'clean') { await this.runClean(args.playground); @@ -115,27 +138,38 @@ clean: the \`node_modules\` folder and compilation artifacts are cleaned.` // INITIALIZE // - private async runInitialize(path: string | undefined, providers: string[] | undefined): Promise { + private async runInitialize( + path: string | undefined, + providers: string[] | undefined + ): Promise { if (path === undefined) { + // eslint-disable-next-line import/namespace const response: { playground: string } = await inquirer.prompt({ name: 'playground', message: `Path to playground to initialize`, type: 'input', - validate: async (input: any): Promise => { + validate: async (input: unknown): Promise => { if (typeof input !== 'string') { throw developerError('unexpected argument type', 11); } if (input.trim().length === 0) { - return 'The playground path must not be empty.' + return 'The playground path must not be empty.'; } let exists = true; try { await accessPromise(input); - } catch (e) { - // We are looking for a non-existent path - if (e.code === 'ENOENT') { + } catch (err) { + if (!('code' in err)) { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + throw developerError(`unexpected error: ${err}`, 12); + } + + // we check the 'code` member + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + if (err.code === 'ENOENT') { + // We are looking for a non-existent path exists = false; } } @@ -149,7 +183,7 @@ clean: the \`node_modules\` folder and compilation artifacts are cleaned.` } return true; - } + }, }); path = response.playground; @@ -158,10 +192,14 @@ clean: the \`node_modules\` folder and compilation artifacts are cleaned.` const name = nodePath.basename(playgroundPath); if (!validateDocumentName(name)) { - throw userError('The playground name must be a valid slang identifier', 11); + throw userError( + 'The playground name must be a valid slang identifier', + 11 + ); } if (providers === undefined || providers.length === 0) { + // eslint-disable-next-line import/namespace const response: { providers: string } = await inquirer.prompt({ name: 'providers', message: 'Input space separated list of providers to create', @@ -170,14 +208,14 @@ clean: the \`node_modules\` folder and compilation artifacts are cleaned.` const providers = Play.parseProviderNames(input); return providers.length > 0; - } + }, }); providers = Play.parseProviderNames(response.providers); } - this.debug("Playground path:", playgroundPath); - this.debug("Playground name:", name); - this.debug("Providers:", providers); + this.debug('Playground path:', playgroundPath); + this.debug('Playground name:', name); + this.debug('Providers:', providers); await mkdirPromise(playgroundPath, { recursive: true, mode: 0o744 }); @@ -186,8 +224,8 @@ clean: the \`node_modules\` folder and compilation artifacts are cleaned.` playgroundTemplate.packageJson(name) ); - const gluesPromises = providers.map( - provider => OutputStream.writeOnce( + const gluesPromises = providers.map(provider => + OutputStream.writeOnce( nodePath.join(playgroundPath, `${name}.${provider}.ts`), playgroundTemplate.glueScript('pubs', name, provider) ) @@ -198,8 +236,8 @@ clean: the \`node_modules\` folder and compilation artifacts are cleaned.` profileTemplate.header(name) + profileTemplate.pubs(name) ); - const mapsPromises = providers.map( - provider => OutputStream.writeOnce( + const mapsPromises = providers.map(provider => + OutputStream.writeOnce( nodePath.join(playgroundPath, `${name}.${provider}.suma`), mapTemplate.header(name, provider) + mapTemplate.pubs(name) ) @@ -210,15 +248,13 @@ clean: the \`node_modules\` folder and compilation artifacts are cleaned.` playgroundTemplate.npmRc() ); - await Promise.all( - [ - packageJsonPromise, - ...gluesPromises, - profilePromise, - ...mapsPromises, - npmrcPromise - ] - ) + await Promise.all([ + packageJsonPromise, + ...gluesPromises, + profilePromise, + ...mapsPromises, + npmrcPromise, + ]); } // EXECUTE // @@ -234,29 +270,33 @@ clean: the \`node_modules\` folder and compilation artifacts are cleaned.` const playground = await Play.detectPlayground(playgroundPath); if (providers === undefined || providers.length === 0) { + // eslint-disable-next-line import/namespace const response: { providers: string[] } = await inquirer.prompt({ name: 'providers', message: 'Select a provider to execute', type: 'checkbox', choices: [...playground.providers.values()].map(p => { - return { name: p } + return { name: p }; }), validate: (input: string[]): boolean => { return input.length > 0; - } + }, }); providers = response.providers; } else { for (const provider of providers) { if (!playground.providers.has(provider)) { - throw userError(`Provider "${provider}" not found for playground "${playground.path}"`, 21); + throw userError( + `Provider "${provider}" not found for playground "${playground.path}"`, + 21 + ); } } } - this.debug("Playground:", playground); - this.debug("Providers:", providers); - this.debug("Skip:", skip); + this.debug('Playground:', playground); + this.debug('Providers:', providers); + this.debug('Skip:', skip); await this.executePlayground(playground, providers, skip); } @@ -265,57 +305,83 @@ clean: the \`node_modules\` folder and compilation artifacts are cleaned.` providers: string[], skip: Record<'npm' | 'ast' | 'tsc', SkipFileType> ): Promise { - const profilePath = nodePath.join(playground.path, `${playground.name}.supr`); - const mapPaths = providers.map( - provider => nodePath.join(playground.path, `${playground.name}.${provider}.suma`) + const profilePath = nodePath.join( + playground.path, + `${playground.name}.supr` + ); + const mapPaths = providers.map(provider => + nodePath.join(playground.path, `${playground.name}.${provider}.suma`) ); - const gluePaths = providers.map( - provider => nodePath.join(playground.path, `${playground.name}.${provider}.ts`) + const gluePaths = providers.map(provider => + nodePath.join(playground.path, `${playground.name}.${provider}.ts`) ); const compiledGluePaths = providers.map( provider => `${playground.name}.${provider}.js` ); - const skipNpm = await resolveSkipFile(skip.npm, [nodePath.join(playground.path, 'node_modules')]); + const skipNpm = await resolveSkipFile(skip.npm, [ + nodePath.join(playground.path, 'node_modules'), + ]); if (!skipNpm) { this.logCli('$ npm install'); try { await execFilePromise('npm', ['install'], { - cwd: playground.path + cwd: playground.path, }); } catch (err) { + if (!('stdout' in err)) { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + throw developerError(`unexpected error: ${err}`, 21); + } + + // we check the 'stdout` member + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/restrict-template-expressions throw userError(`npm install failed: ${err.stdout}`, 22); } } - const skipAst = await resolveSkipFile(skip.ast, mapPaths.map(m => `${m}.ast.json`)); + const skipAst = await resolveSkipFile( + skip.ast, + mapPaths.map(m => `${m}.ast.json`) + ); if (!skipAst) { - this.logCli(`$ superface compile '${profilePath}' ${mapPaths.map(p => `'${p}'`).join(' ')}`); + this.logCli( + `$ superface compile '${profilePath}' ${mapPaths + .map(p => `'${p}'`) + .join(' ')}` + ); try { await Compile.run([profilePath, ...mapPaths]); } catch (err) { + if (!('message' in err)) { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + throw developerError(`unexpected error: ${err}`, 22); + } + + // we check the 'message` member + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/restrict-template-expressions throw userError(`superface compilation failed: ${err.message}`, 23); } } - const skipTsc = await resolveSkipFile(skip.tsc, compiledGluePaths.map(g => nodePath.join(playground.path, g))); + const skipTsc = await resolveSkipFile( + skip.tsc, + compiledGluePaths.map(g => nodePath.join(playground.path, g)) + ); if (!skipTsc) { this.logCli(`$ tsc ${gluePaths.map(p => `'${p}'`).join(' ')}`); - const program = ts.createProgram( - gluePaths, - { - sourceMap: false, - outDir: playground.path, - declaration: false, - target: ts.ScriptTarget.ES2015, - module: ts.ModuleKind.CommonJS, - moduleResolution: ts.ModuleResolutionKind.NodeJs, - strict: true, - noEmitOnError: true, - typeRoots: ["node_modules/@types"] - } - ); + const program = ts.createProgram(gluePaths, { + sourceMap: false, + outDir: playground.path, + declaration: false, + target: ts.ScriptTarget.ES2015, + module: ts.ModuleKind.CommonJS, + moduleResolution: ts.ModuleResolutionKind.NodeJs, + strict: true, + noEmitOnError: true, + typeRoots: ['node_modules/@types'], + }); const result = program.emit(); if (result.emitSkipped) { @@ -323,16 +389,30 @@ clean: the \`node_modules\` folder and compilation artifacts are cleaned.` for (const diagnostic of result.diagnostics) { if (!diagnostic.file || !diagnostic.start) { - throw developerError('Invalid typescript compiler diagnostic output', 21); + throw developerError( + 'Invalid typescript compiler diagnostic output', + 23 + ); } - let { line, character } = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start); - let message = ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"); + const { + line, + character, + } = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start); + const message = ts.flattenDiagnosticMessageText( + diagnostic.messageText, + '\n' + ); - diagnosticMessage += `\t${diagnostic.file.fileName}:${line + 1}:${character + 1} ${message}`; + diagnosticMessage += `\t${diagnostic.file.fileName}:${line + 1}:${ + character + 1 + } ${message}`; } - throw userError(`Typescript compilation failed: ${diagnosticMessage}`, 24); + throw userError( + `Typescript compilation failed: ${diagnosticMessage}`, + 24 + ); } } @@ -346,14 +426,14 @@ clean: the \`node_modules\` folder and compilation artifacts are cleaned.` cwd: playground.path, env: { ...process.env, - 'DEBUG': '*' - } + DEBUG: '*', + }, }, { forwardStdout: true, - forwardStderr: true + forwardStderr: true, } - ) + ); } } @@ -365,43 +445,36 @@ clean: the \`node_modules\` folder and compilation artifacts are cleaned.` } const playground = await Play.detectPlayground(playgroundPath); - this.debug("Playground:", playground); + this.debug('Playground:', playground); const files = [ `${playground.name}.supr.ast.json`, 'node_modules', - 'package-lock.json' + 'package-lock.json', ]; for (const provider of playground.providers.values()) { - files.push( - `${playground.name}.${provider}.suma.ast.json` - ); - files.push( - `${playground.name}.${provider}.js`, - ); + files.push(`${playground.name}.${provider}.suma.ast.json`); + files.push(`${playground.name}.${provider}.js`); } this.logCli(`$ rimraf ${files.map(f => `'${f}'`).join(' ')}`); await Promise.all( - files.map( - file => rimrafPromise( - nodePath.join(playground.path, file) - ) - ) + files.map(file => rimrafPromise(nodePath.join(playground.path, file))) ); } // UTILITY // private static parseProviderNames(input: string): string[] { - return input.split(' ').filter( - i => i.trim() !== '' - ).filter( - p => validateDocumentName(p) - ); + return input + .split(' ') + .filter(i => i.trim() !== '') + .filter(p => validateDocumentName(p)); } private static async promptExistingPlayground(): Promise { + // HOW COME I SEE IT THEN ESLINT? + // eslint-disable-next-line import/namespace const response: { playground: string } = await inquirer.prompt({ name: 'playground', message: `Path to playground to execute (navigate to a valid playground, use space to expand folders)`, @@ -409,7 +482,7 @@ clean: the \`node_modules\` folder and compilation artifacts are cleaned.` onlyShowValid: false, onlyShowDir: true, hideChildrenOfValid: true, - validate: async (input: any): Promise => { + validate: async (input: unknown): Promise => { if (typeof input !== 'string') { throw developerError('unexpected argument type', 21); } @@ -421,7 +494,8 @@ clean: the \`node_modules\` folder and compilation artifacts are cleaned.` } return true; - } + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); // have to cast to any because the registration of the new prompt is not known to typescript return response.playground; @@ -436,17 +510,19 @@ clean: the \`node_modules\` folder and compilation artifacts are cleaned.` * - `.*.suma` (at least one pair with `.ts` below) * - `.*.ts` */ - private static async detectPlayground(path: string): Promise { + private static async detectPlayground( + path: string + ): Promise { let stat; try { stat = await statPromise(path); } catch (e) { throw userError('The playground path must exist and be accessible', 31); - }; + } if (!stat.isDirectory()) { throw userError('The playground path must be a directory', 32); - }; + } const baseName = nodePath.basename(path); const startName = baseName + '.'; @@ -454,8 +530,8 @@ clean: the \`node_modules\` folder and compilation artifacts are cleaned.` let foundPackageJson = false; let foundProfile = false; - let foundMaps: Set = new Set(); - let foundGlues: Set = new Set(); + const foundMaps: Set = new Set(); + const foundGlues: Set = new Set(); for (const entry of entries) { if (entry === 'package.json') { @@ -488,18 +564,18 @@ clean: the \`node_modules\` folder and compilation artifacts are cleaned.` } } - let providers: Set = new Set(); + const providers: Set = new Set(); for (const x of foundMaps) { if (foundGlues.has(x)) { providers.add(x); } - }; + } if (foundPackageJson && foundProfile && providers.size > 0) { return { name: baseName, path, - providers + providers, }; } @@ -509,4 +585,4 @@ clean: the \`node_modules\` folder and compilation artifacts are cleaned.` private logCli(message: string) { this.log(chalk.grey(message)); } -} \ No newline at end of file +} diff --git a/src/common/document.ts b/src/common/document.ts index 7c79f8f0..b974746c 100644 --- a/src/common/document.ts +++ b/src/common/document.ts @@ -52,4 +52,4 @@ export const DOCUMENT_PARSE_FUNCTION = { export function validateDocumentName(name: string): boolean { return /^[_a-zA-Z][_a-zA-Z0-9]*$/.test(name); -} \ No newline at end of file +} diff --git a/src/common/error.ts b/src/common/error.ts index 3e514b2e..8d34faa1 100644 --- a/src/common/error.ts +++ b/src/common/error.ts @@ -2,24 +2,24 @@ import { CLIError } from '@oclif/errors'; /** * User error. - * + * * It is a normal occurence to return an user error. - * + * * Has a positive exit code. */ export function userError(message: string, index: number): CLIError { if (index <= 0) { throw developerError('expected positive error index', 1); } - - return new CLIError(message, { exit: index }) + + return new CLIError(message, { exit: index }); } /** * Developer error. - * + * * It should only be returned from unexpected states and unreachable code. - * + * * Has a negative exit code. */ export function developerError(message: string, index: number): CLIError { @@ -27,5 +27,5 @@ export function developerError(message: string, index: number): CLIError { throw developerError('expected positive error index', 1); } - return new CLIError(`Internal error: ${message}`, { exit: -index }) -} \ No newline at end of file + return new CLIError(`Internal error: ${message}`, { exit: -index }); +} diff --git a/src/common/flags.ts b/src/common/flags.ts index bee94c43..f5474a22 100644 --- a/src/common/flags.ts +++ b/src/common/flags.ts @@ -1,5 +1,6 @@ import { flags } from '@oclif/command'; import { Definition, IOptionFlag } from '@oclif/command/lib/flags'; + import { developerError } from './error'; export type DocumentTypeFlag = 'auto' | 'map' | 'profile'; @@ -29,4 +30,4 @@ export const skipFileFlag: Definition = flags.build({ return input; }, -}) \ No newline at end of file +}); diff --git a/src/common/io.ts b/src/common/io.ts index 2d85c18d..10720823 100644 --- a/src/common/io.ts +++ b/src/common/io.ts @@ -1,11 +1,13 @@ +import * as childProcess from 'child_process'; +// eslint is having a bad day +// eslint-disable-next-line import/named +import { debug as createDebug } from 'debug'; import * as fs from 'fs'; +import rimraf from 'rimraf'; import { Writable } from 'stream'; import { promisify } from 'util'; -import rimraf from 'rimraf'; -import * as childProcess from 'child_process'; -import { SkipFileType } from './flags'; -import { debug as createDebug } from 'debug'; +import { SkipFileType } from './flags'; export const readFilePromise = promisify(fs.readFile); export const accessPromise = promisify(fs.access); @@ -43,47 +45,44 @@ export function execFilePromise( args?: string[], execOptions?: fs.BaseEncodingOptions & childProcess.ExecFileOptions, options?: { - forwardStdout?: boolean, - forwardStderr?: boolean + forwardStdout?: boolean; + forwardStderr?: boolean; } ): Promise { - return new Promise( - (resolve, reject) => { - const child = childProcess.execFile( - path, - args, - execOptions, - (err, _stdout, _stderr) => { - if (err) { - reject(err) - } else { - resolve() - } + return new Promise((resolve, reject) => { + const child = childProcess.execFile( + path, + args, + execOptions, + (err, _stdout, _stderr) => { + if (err) { + reject(err); + } else { + resolve(); } - ); - - if (options?.forwardStdout === true) { - child.stdout?.on('data', chunk => process.stdout.write(chunk)); - } - if (options?.forwardStderr === true) { - child.stderr?.on('data', chunk => process.stderr.write(chunk)); } + ); + + if (options?.forwardStdout === true) { + child.stdout?.on('data', chunk => process.stdout.write(chunk)); } - ) + if (options?.forwardStderr === true) { + child.stderr?.on('data', chunk => process.stderr.write(chunk)); + } + }); } -export async function resolveSkipFile(flag: SkipFileType, files: string[]): Promise { +export async function resolveSkipFile( + flag: SkipFileType, + files: string[] +): Promise { if (flag === 'never') { return false; } else if (flag === 'always') { return true; } else { try { - await Promise.all( - files.map( - file => accessPromise(file) - ) - ); + await Promise.all(files.map(file => accessPromise(file))); } catch (e) { // If at least one file cannot be accessed return false return false; @@ -115,7 +114,7 @@ export class OutputStream { constructor(path: string, append?: boolean) { switch (path) { case '-': - outputStreamDebug("Opening stdout"); + outputStreamDebug('Opening stdout'); this.name = 'stdout'; this.stream = process.stdout; this.isStdStream = true; @@ -123,7 +122,7 @@ export class OutputStream { break; case '-2': - outputStreamDebug("Opening stderr"); + outputStreamDebug('Opening stderr'); this.name = 'stderr'; this.stream = process.stderr; this.isStdStream = true; @@ -131,7 +130,9 @@ export class OutputStream { break; default: - outputStreamDebug(`Opening/creating "${path}" in ${append ? 'append' : 'write'} mode`); + outputStreamDebug( + `Opening/creating "${path}" in ${append ? 'append' : 'write'} mode` + ); this.name = path; this.stream = fs.createWriteStream(path, { flags: append ? 'a' : 'w', @@ -146,6 +147,7 @@ export class OutputStream { write(data: string): Promise { outputStreamDebug(`Wiritng ${data.length} characters to "${this.name}"`); + return streamWritePromise(this.stream, data); } @@ -160,10 +162,15 @@ export class OutputStream { return Promise.resolve(); } - static async writeOnce(path: string, data: string, append?: boolean): Promise { + static async writeOnce( + path: string, + data: string, + append?: boolean + ): Promise { const stream = new OutputStream(path, append); await stream.write(data); + return stream.cleanup(); } } diff --git a/src/declarations/inquirer-file-tree-selection-prompt.d.ts b/src/declarations/inquirer-file-tree-selection-prompt.d.ts index c3235ee2..f63f31e9 100644 --- a/src/declarations/inquirer-file-tree-selection-prompt.d.ts +++ b/src/declarations/inquirer-file-tree-selection-prompt.d.ts @@ -1,4 +1,6 @@ declare module 'inquirer-file-tree-selection-prompt' { - const FileTreeSelectionPrompt: any; - export default FileTreeSelectionPrompt; -} \ No newline at end of file + // Allow because the type is not really public + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const FileTreeSelectionPrompt: any; + export default FileTreeSelectionPrompt; +} diff --git a/src/templates/map.ts b/src/templates/map.ts index 88f2850d..93ff3705 100644 --- a/src/templates/map.ts +++ b/src/templates/map.ts @@ -10,7 +10,7 @@ provider = "https://example.com/${provider}/${name}" export type MapTemplateType = 'empty' | 'pubs'; /** * Returns a map of given template `type` with given `name`. -*/ + */ export function map(type: MapTemplateType, name: string): string { switch (type) { case 'empty': @@ -65,4 +65,4 @@ operation BuildQueryFor${name} { return query; } `; -} \ No newline at end of file +} diff --git a/src/templates/playground.ts b/src/templates/playground.ts index 03a42a9f..04772388 100644 --- a/src/templates/playground.ts +++ b/src/templates/playground.ts @@ -12,14 +12,18 @@ export function packageJson(name: string): string { } export function npmRc(): string { - return '@superfaceai:registry=https://npm.pkg.github.com\n' + return '@superfaceai:registry=https://npm.pkg.github.com\n'; } export type GlueTemplateType = 'empty' | 'pubs'; /** * Returns a glue script of given template `type` with given `name` and `provider`. -*/ -export function glueScript(type: GlueTemplateType, name: string, provider: string): string { + */ +export function glueScript( + type: GlueTemplateType, + name: string, + provider: string +): string { switch (type) { case 'empty': return empty(name, provider); @@ -77,7 +81,7 @@ async function main() { } main() -` +`; } export function pubs(name: string, provider: string): string { @@ -138,5 +142,5 @@ async function main() { } main() -` -} \ No newline at end of file +`; +} diff --git a/src/templates/profile.ts b/src/templates/profile.ts index 96b12ecb..32ba2962 100644 --- a/src/templates/profile.ts +++ b/src/templates/profile.ts @@ -1,15 +1,15 @@ /** * Returns a usecase header with filled in `name` and `provider`. */ -export function header(name: string) { - return `profile = "https://example.com/profile/${name}" +export function header(name: string): string { + return `profile = "https://example.com/profile/${name}" `; } export type UsecaseTemplateType = 'empty' | 'pubs'; /** * Returns a usecase of given template `type` with given `name`. -*/ + */ export function usecase(type: UsecaseTemplateType, name: string): string { switch (type) { case 'empty': @@ -19,7 +19,7 @@ export function usecase(type: UsecaseTemplateType, name: string): string { } } -export function empty(name: string) { +export function empty(name: string): string { return ` """ ${name} usecase @@ -28,7 +28,7 @@ usecase ${name} {} `; } -export function pubs(name: string) { +export function pubs(name: string): string { return ` """ List pub opening hours @@ -51,4 +51,4 @@ model ${name}Error enum { TIMEOUT } `; -} \ No newline at end of file +}