@google/aside
Version:
Apps Script IDE framework
302 lines (301 loc) • 9.87 kB
JavaScript
/**
* Copyright 2025 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
*
* http://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 chalk from 'chalk';
import fs from 'fs-extra';
import path from 'path';
import prompts from 'prompts';
import { fileURLToPath } from 'url';
import writeFileAtomic from 'write-file-atomic';
import { ClaspHelper } from './clasp-helper.js';
import { config, configForUi } from './config.js';
import { PackageHelper } from './package-helper.js';
/**
* This is required to avoid treeshaking this file.
* As long as anything from a file is being used, the entire file
* is being kept.
*/
export const app = null;
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
let CONFIG;
/**
* Handle package.json creation and update.
*
* @param {Options} options
*/
export async function handlePackageJson(options) {
let needsSave = false;
// Load or initialize a package.json
let packageJson = PackageHelper.load();
if (!packageJson) {
const init = await query('', `Generate ${chalk.bold('package.json')}?`, true, options);
if (init) {
packageJson = PackageHelper.init(options.title);
}
else {
packageJson = new PackageHelper();
}
needsSave = true;
}
// Synchronize scripts
console.log(`${chalk.green('\u2714')}`, 'Adding scripts...');
const existingScripts = packageJson.getScripts();
for (const [name, script] of Object.entries(CONFIG.scripts)) {
if (name in existingScripts && existingScripts[name] !== script) {
const replace = await query(`package.json already has a script for ${chalk.bold(name)}:\n` +
`-${chalk.red(existingScripts[name])}\n+${chalk.green(script)}`, 'Replace', false, options);
if (replace) {
packageJson.updateScript(name, script);
needsSave = true;
}
}
else {
packageJson.updateScript(name, script);
needsSave = true;
}
}
// Write if changed
if (needsSave) {
console.log(`${chalk.green('\u2714')}`, 'Saving package.json...');
await packageJson.save();
}
// Install dev dependencies
console.log(`${chalk.green('\u2714')}`, 'Installing dependencies...');
packageJson.installPackages(CONFIG.dependencies);
}
/**
* Prompt user for text input.
*
* @param {string} message
* @param {string} defaultVal
* @param {Options} options
* @returns {Promise<string>}
*/
async function queryText(message, defaultVal, options) {
if (options.yes) {
return defaultVal;
}
const response = await prompts({
type: 'text',
name: 'answer',
message: `${message}:`,
initial: defaultVal,
});
return response.answer;
}
/**
* Prompt user for toggle input.
*
* @param {string} message
* @param {string} question
* @param {string} defaultVal
* @param {Options} options
* @returns {Promise<boolean>}
*/
async function query(message, question, defaultVal, options) {
if (options.yes) {
return true;
}
else if (options.no) {
return false;
}
if (message) {
console.log(message);
}
const answer = await prompts({
type: 'toggle',
name: 'result',
message: question,
initial: defaultVal,
active: 'Yes',
inactive: 'No',
});
return answer.result;
}
/**
* Read file.
*
* @param {string} path
* @returns {Promise<string>}
*/
async function readFile(path) {
try {
return await fs.readFile(path, 'utf8');
}
catch (e) {
const err = e;
if (err.code !== 'ENOENT') {
throw new Error(`Unknown error reading ${path}: ${err.message}`);
}
}
return undefined;
}
/**
* Handle config merge.
* Compares source and target config files and merges if required.
*
* @param {Options} options
*/
async function handleConfigMerge(options) {
for (const filename of Object.keys(CONFIG.filesMerge)) {
const sourcePath = path.join(__dirname, '../../', filename);
let sourceLines = (await readFile(sourcePath))?.split('\n');
const targetFile = await readFile(CONFIG.filesMerge[filename]);
const targetLines = targetFile?.split('\n') ?? [];
const missingLines = sourceLines?.filter(item => targetLines.indexOf(item) === -1) ?? [];
if (missingLines.length === 0)
continue;
if (targetFile !== undefined) {
const message = `${chalk.bold(CONFIG.filesMerge[filename])} already exists but is missing content\n` +
missingLines.map(line => `+${chalk.green(line)}`).join('\n');
const writeFile = await query(message, 'Merge', false, options);
if (!writeFile)
continue;
}
sourceLines = targetLines.concat(missingLines);
await writeFileAtomic(CONFIG.filesMerge[filename], `${sourceLines.filter(item => item).join('\n')}\n`);
}
}
/**
* Handle config copy.
*
* @param {Options} options
*/
async function handleConfigCopy(options) {
for (const filename of Object.keys(CONFIG.filesCopy)) {
try {
const sourcePath = path.join(__dirname, '../../', filename);
const source = await readFile(sourcePath);
const target = await readFile(CONFIG.filesCopy[filename]);
if (source === target || typeof source === 'undefined')
continue;
const writeFile = target
? await query(`${chalk.bold(CONFIG.filesCopy[filename])} already exists`, 'Overwrite', false, options)
: true;
if (writeFile) {
await writeFileAtomic(CONFIG.filesCopy[filename], source);
}
}
catch (e) {
const err = e;
if (err.code !== 'ENOENT') {
throw new Error(`Unknown error reading ${path}: ${err.message}`);
}
}
}
}
/**
* Handle putting template files in place.
*
* @param {Options} options
*/
async function handleTemplate(options) {
const cwd = process.cwd();
let templates;
if (options.ui) {
templates = path.join(__dirname, '../../template-ui');
}
else {
templates = path.join(__dirname, '../../template');
}
const items = await fs.readdir(templates);
for (const item of items) {
const targetDirName = path.join(cwd, item);
// Create folder
fs.mkdirSync(targetDirName, { recursive: true });
// Only install the template if no ts files exist in target directory.
const files = fs.readdirSync(targetDirName);
const tsFiles = files.filter((file) => file.toLowerCase().endsWith('.ts'));
// Copy files
if (tsFiles.length === 0) {
console.log(`${chalk.green('\u2714')}`, `Installing ${item} template...`);
await fs.copy(path.join(templates, item), targetDirName, {
overwrite: false,
});
}
}
}
/**
* Set up clasp.
*
* @param {Options} options
*/
async function handleClasp(options) {
const claspHelper = new ClaspHelper();
await claspHelper.login();
const claspConfigExists = await claspHelper.isConfigured();
const overrideClasp = claspConfigExists
? await query('', 'Override existing clasp config?', false, options)
: false;
if (claspConfigExists && !overrideClasp) {
return;
}
const scriptIdDev = await queryText('Script ID (optional)', '', options);
const scriptIdProd = await queryText('Script ID for production environment (optional)', scriptIdDev, options);
// Prepare clasp project environment
if (scriptIdDev) {
console.log(`${chalk.green('\u2714')}`, `Cloning ${scriptIdDev}...`);
await claspHelper.cloneAndPull(scriptIdDev, scriptIdProd, 'dist');
}
else {
console.log(`${chalk.green('\u2714')}`, `Creating ${options.title}...`);
const res = await claspHelper.create(options.title, scriptIdProd, './dist');
// Output URLs
console.log();
console.log('-> Google Sheets Link:', res.sheetLink);
console.log('-> Apps Script Link:', res.scriptLink);
console.log();
}
}
/**
* Handle environment initialization.
*/
export async function init(flags) {
const projectTitle = flags.title ??
(await queryText('Project Title', 'Untitled', {
yes: flags.yes,
no: flags.no,
}));
const options = {
yes: flags.yes || false,
no: flags.no || false,
title: projectTitle,
ui: flags.no || false,
};
options.ui = await query('', 'Create an Angular UI?', false, options);
if (options.ui) {
CONFIG = configForUi;
}
else {
CONFIG = config;
}
// Handle package.json
await handlePackageJson(options);
// Handle config copy
await handleConfigCopy(options);
// Handle config merge
await handleConfigMerge(options);
// Handle template
await handleTemplate(options);
// Handle clasp
await handleClasp(options);
if (options.ui) {
console.log();
console.log('Make sure to run npm install to install all the Angular UI dependencies');
console.log();
}
}