@google/dscc-gen
Version:
Create component & connector projects with sane defaults.
293 lines (275 loc) • 9.75 kB
text/typescript
/**
* @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;
}
};