UNPKG

@google/dscc-gen

Version:

Create component & connector projects with sane defaults.

293 lines (275 loc) 9.75 kB
/** * @license * Copyright 2018 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import * as execa from 'execa'; import {Options} from 'execa'; import * as fs from 'mz/fs'; import * as path from 'path'; import terminalLink from 'terminal-link'; import {PWD} from '../constants'; import * as files from '../files'; import {AuthType, ConnectorConfig, Template} from '../types'; import * as util from '../util'; import {format} from '../util'; import * as appsscript from './appsscript'; import * as validation from './validation'; const OAUTH2_LIBRARY = { userSymbol: 'OAuth2', libraryId: '1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF', version: '33', }; const getTemplates = (config: ConnectorConfig): Template[] => { return [ {match: /{{MANIFEST_NAME}}/, replace: config.projectName}, {match: /{{MANIFEST_LOGO_URL}}/, replace: config.manifestLogoUrl!}, {match: /{{MANIFEST_COMPANY}}/, replace: config.manifestCompany!}, {match: /{{MANIFEST_COMPANY_URL}}/, replace: config.manifestCompanyUrl!}, {match: /{{MANIFEST_ADDON_URL}}/, replace: config.manifestAddonUrl!}, {match: /{{MANIFEST_SUPPORT_URL}}/, replace: config.manifestSupportUrl!}, {match: /{{MANIFEST_DESCRIPTION}}/, replace: config.manifestDescription!}, { match: /{{MANIFEST_SOURCES}}/, replace: `[${config .manifestSources!.split(',') .map((a: string) => `"${a}"`) .join(',')}]`, }, ]; }; const ensureAuthenticated = async (execOptions: Options): Promise<void> => { const authenticated = await util.spinnify( 'Ensuring clasp is authenticated...', async () => { return validation.claspAuthenticated(); } ); if (authenticated === false) { const infoText = format.yellow( 'Clasp must be globally authenticated for dscc-gen.' ); const claspLogin = format.green('npx @google/clasp login'); console.log(`${infoText}\nrunning ${claspLogin} ...\n`); await execa( 'npx', ['@google/clasp', 'login'], Object.assign({}, execOptions, {stdio: 'inherit'}) ); } }; const installDependencies = async ( projectPath: string, config: ConnectorConfig ) => { return util.spinnify('Installing project dependencies...', async () => util.npmInstall(projectPath, config) ); }; const createAppsScriptProject = async ( projectPath: string, projectName: string, execOptions: Options, config: ConnectorConfig ): Promise<void> => { return util.spinnify('Creating Apps Script project...', async () => { await appsscript.create(projectPath, projectName); // Since clasp creating a new project overwrites the manifest, we want to // copy the template manifest over the one generated by clasp. files.mv( [execOptions.cwd!, 'temp', 'appsscript.json'], [execOptions.cwd!, 'src'] ); if (config.authType === AuthType.OAUTH2) { const fileOptions = {encoding: 'utf8'}; const manifestPath = path.resolve(projectPath, 'src', 'appsscript.json'); const manifestString = await fs.readFile(manifestPath, fileOptions); const manifest = JSON.parse(manifestString); // Add the OAUTH2_LIBRARY dependency. manifest.dependencies.libraries.push(OAUTH2_LIBRARY); await fs.writeFile( manifestPath, JSON.stringify(manifest, undefined, ' '), fileOptions ); } await appsscript.push(projectPath); }); }; const cloneAppsScriptProject = async ( projectPath: string, config: ConnectorConfig ): Promise<void> => { const scriptId = config.scriptId!; if (config.ts === true) { // The user is trying to migrate an existing project to be an appsscript // one. return util.spinnify('Cloning existing project...', async () => { await appsscript.clone(projectPath, scriptId, 'old_js'); files.cp( [projectPath, 'old_js', 'appsscript.json'], [projectPath, 'src', 'appsscript.json'] ); }); } else { // We don't need the template source files since we want the Apps Scripts project's return util.spinnify('Cloning existing project...', async () => { files.remove(projectPath, 'src'); await appsscript.clone(projectPath, scriptId, 'src'); }); } }; const manageDeployments = async ( projectPath: string, config: ConnectorConfig ) => { let productionDeploymentId: string | undefined; if (config.scriptId !== undefined) { // See if there is already a 'Production' deployment. productionDeploymentId = await util.spinnify( 'Checking for a production deployment', async () => { return appsscript.getDeploymentIdByName(projectPath, 'Production'); } ); } if (productionDeploymentId === undefined) { productionDeploymentId = await util.spinnify( 'Creating a production deployment', async () => { return await appsscript.deploy(projectPath, 'Production'); } ); } const latestDeploymentId = await util.spinnify( 'Getting the latest deploymentId', async () => { return await appsscript.getDeploymentIdByName(projectPath, '@HEAD'); } ); if (latestDeploymentId === undefined) { throw new Error( `Wasn't able to get the latest deploymentId. This is probably a bug with dscc-gen.` ); } return util.spinnify('Updating templates with your values...', async () => { return files.fixTemplates(projectPath, [ {match: /{{PRODUCTION_DEPLOYMENT_ID}}/, replace: productionDeploymentId!}, {match: /{{LATEST_DEPLOYMENT_ID}}/, replace: latestDeploymentId!}, ]); }); }; type AuthTypeFileMap = {[TKey in AuthType]: string}; const authTypeToFile: AuthTypeFileMap = { [AuthType.NONE]: 'NONE_auth', [AuthType.USER_PASS]: 'USER_PASS_auth', [AuthType.USER_TOKEN]: 'USER_TOKEN_auth', [AuthType.OAUTH2]: 'OAUTH2_auth', [AuthType.KEY]: 'KEY_auth', [AuthType.PATH_USER_PASS]: 'PATH_USER_PASS_auth', }; const removeExcessAuthFiles = async ( projectPath: string, config: ConnectorConfig ) => { const extension = config.ts ? '.ts' : '.js'; const chosenAuthType = config.authType; return Promise.all( Object.values(AuthType).map(async (authType: AuthType) => { const authFile = authTypeToFile[authType] + extension; if (authType !== chosenAuthType) { return files.remove(projectPath, 'src', authFile); } else { const projectAuthFile = 'auth' + extension; return files.rename( [projectPath, 'src', authFile], [projectPath, 'src', projectAuthFile] ); } }) ); }; export const createFromTemplate = async ( config: ConnectorConfig ): Promise<number> => { const {projectName, basePath} = config; const templatePath = path.join( basePath, 'templates', config.projectChoice.toString() + (config.ts ? '-ts' : '') ); const projectPath = path.join(PWD, projectName); await files.createAndCopyFiles(projectPath, templatePath, projectName); try { await util.spinnify('Updating templates with your values...', async () => { await files.fixTemplates(projectPath, getTemplates(config)); await removeExcessAuthFiles(projectPath, config); }); const execOptions: Options = {cwd: projectPath}; await installDependencies(projectPath, config); await ensureAuthenticated(execOptions); if (config.scriptId !== undefined) { await cloneAppsScriptProject(projectPath, config); } else { await createAppsScriptProject( projectPath, projectName, execOptions, config ); } await manageDeployments(projectPath, config); // Remove temp directory. files.remove(projectPath, 'temp'); const connectorOverview = format.blue( terminalLink( 'connector overview', 'https://developers.google.com/datastudio/connector/' ) ); const styledProjectName = format.green(projectName); const cdDirection = format.yellow(`cd ${projectName}`); const runCmd = config.yarn ? 'yarn' : 'npm run'; const open = format.red(`${runCmd} open`); const push = format.blue(`${runCmd} push`); const watch = format.green(`${runCmd} watch`); const prettier = format.yellow(`${runCmd} prettier`); const tryLatest = format.red(`${runCmd} try_latest`); const tryProduction = format.blue(`${runCmd} try_production`); const updateProduction = format.green(`${runCmd} update_production`); console.log( `\ Created a new community connector: ${styledProjectName}\n\ \n\ If this is your first connector, see ${connectorOverview}\n\ \n\ ${cdDirection} to start working on your connector\n\ \n\ Scripts are provided to simplify development:\n\ \n\ ${open} - open your project in Apps Script.\n\ ${push} - push your local changes to Apps Script.\n\ ${watch} - watches for local changes & pushes them to Apps Script.\n\ ${prettier} - formats your code using community standards.\n\ ${tryLatest} - opens the deployment with your latest code.\n\ ${tryProduction} - opens your production deployment.\n\ ${updateProduction} - updates your production deployment to use the latest code.\n\ ` ); return 0; } catch (e) { files.remove(projectPath); throw e; } };