@google/clasp
Version:
Develop Apps Script Projects locally
227 lines (226 loc) • 7.92 kB
JavaScript
import path from 'path';
import Debug from 'debug';
import { findUpSync } from 'find-up';
import fs from 'fs/promises';
import splitLines from 'split-lines';
import stripBom from 'strip-bom';
import { getUserInfo } from '../auth/auth.js';
import { Files } from './files.js';
import { Functions } from './functions.js';
import { Logs } from './logs.js';
import { Project } from './project.js';
import { Services } from './services.js';
import { ensureStringArray } from './utils.js';
const debug = Debug('clasp:core');
const DEFAULT_CLASP_IGNORE = [
'**/**',
'!**/appsscript.json',
'!**/*.gs',
'!**/*.js',
'!**/*.ts',
'!**/*.html',
'.git/**',
'node_modules/**',
];
export class Clasp {
constructor(options) {
debug('Creating clasp instance with options: %O', options);
this.options = options;
this.services = new Services(options);
this.files = new Files(options);
this.project = new Project(options);
this.logs = new Logs(options);
this.functions = new Functions(options);
}
async authorizedUser() {
if (!this.options.credentials) {
return undefined;
}
try {
const user = await getUserInfo(this.options.credentials);
return user === null || user === void 0 ? void 0 : user.id;
}
catch (err) {
debug('Unable to fetch user info, %O', err);
}
return undefined;
}
withScriptId(scriptId) {
if (this.options.project) {
throw new Error('Science project already set, create new instance instead');
}
this.options.project = {
scriptId,
};
return this;
}
withContentDir(contentDir) {
if (!path.isAbsolute(contentDir)) {
contentDir = path.resolve(this.options.files.projectRootDir, contentDir);
}
this.options.files.contentDir = contentDir;
return this;
}
}
export async function initClaspInstance(options) {
var _a;
debug('Initializing clasp instance');
const projectRoot = await findProjectRootdDir(options.configFile);
if (!projectRoot) {
const dir = (_a = options.rootDir) !== null && _a !== void 0 ? _a : process.cwd();
debug(`No project found, defaulting to ${dir}`);
const rootDir = path.resolve(dir);
const configFilePath = path.resolve(rootDir, '.clasp.json');
const ignoreFile = await findIgnoreFile(rootDir, options.ignoreFile);
const ignoreRules = await loadIgnoreFileOrDefaults(ignoreFile);
return new Clasp({
credentials: options.credentials,
configFilePath,
files: {
projectRootDir: rootDir,
contentDir: rootDir,
ignoreFilePath: ignoreFile,
ignorePatterns: ignoreRules,
filePushOrder: [],
skipSubdirectories: false,
fileExtensions: readFileExtensions({}),
},
});
}
debug('Project config found at %s', projectRoot.configPath);
const ignoreFile = await findIgnoreFile(projectRoot.rootDir, options.ignoreFile);
const ignoreRules = await loadIgnoreFileOrDefaults(ignoreFile);
const content = await fs.readFile(projectRoot.configPath, { encoding: 'utf8' });
const config = JSON.parse(content);
const fileExtensions = readFileExtensions(config);
const filePushOrder = config.filePushOrder || [];
const contentDir = path.resolve(projectRoot.rootDir, config.srcDir || config.rootDir || '.');
return new Clasp({
credentials: options.credentials,
configFilePath: projectRoot.configPath,
files: {
projectRootDir: projectRoot.rootDir,
contentDir: contentDir,
ignoreFilePath: ignoreFile,
ignorePatterns: ignoreRules,
filePushOrder: filePushOrder,
fileExtensions: fileExtensions,
skipSubdirectories: config.ignoreSubdirectories,
},
project: {
scriptId: config.scriptId,
projectId: config.projectId,
parentId: firstValue(config.parentId),
},
});
}
function readFileExtensions(config) {
let scriptExtensions = ['js', 'gs'];
let htmlExtensions = ['html'];
let jsonExtensions = ['json'];
if (config === null || config === void 0 ? void 0 : config.fileExtension) {
// legacy fileExtension setting
scriptExtensions = [config.fileExtension];
}
if (config === null || config === void 0 ? void 0 : config.scriptExtensions) {
scriptExtensions = ensureStringArray(config.scriptExtensions);
}
if (config === null || config === void 0 ? void 0 : config.htmlExtensions) {
htmlExtensions = ensureStringArray(config.htmlExtensions);
}
if (config === null || config === void 0 ? void 0 : config.jsonExtensions) {
jsonExtensions = ensureStringArray(config.jsonExtensions);
}
const fixupExtension = (ext) => {
ext = ext.toLowerCase().trim();
if (!ext.startsWith('.')) {
ext = `.${ext}`;
}
return ext;
};
return {
SERVER_JS: scriptExtensions.map(fixupExtension),
HTML: htmlExtensions.map(fixupExtension),
JSON: jsonExtensions.map(fixupExtension),
};
}
async function findProjectRootdDir(configFilePath) {
debug('Searching for project root');
if (configFilePath) {
debug('Checking for config file at %s', configFilePath);
const info = await fs.stat(configFilePath);
if (info.isDirectory()) {
debug('Is directory, trying file');
configFilePath = path.join(configFilePath, '.clasp.json');
}
}
else {
debug('Searching parent paths for .clasp.json');
configFilePath = findUpSync('.clasp.json');
}
if (!configFilePath) {
debug('No project found');
return undefined;
}
const configFileExists = await hasReadAccess(configFilePath);
if (!configFileExists) {
debug('Project file %s does not exist', configFilePath);
return undefined;
}
debug('Project found at %s', configFilePath);
const rootDir = path.dirname(configFilePath);
return {
rootDir,
configPath: configFilePath,
};
}
async function findIgnoreFile(projectDir, configFilePath) {
debug('Searching for ignore file');
if (configFilePath) {
debug('Checking for ignore file at %s', configFilePath);
const info = await fs.stat(configFilePath);
if (info.isDirectory()) {
debug('Is directory, trying file');
configFilePath = path.join(configFilePath, '.claspignore');
}
}
else {
debug('Checking default location');
configFilePath = path.join(projectDir, '.claspignore');
}
if (!configFilePath) {
debug('No ignore file found');
return undefined;
}
const configFileExists = await hasReadAccess(configFilePath);
if (!configFileExists) {
debug('ignore file %s does not exist', configFilePath);
return undefined;
}
debug('Ignore file found at %s', configFilePath);
return configFilePath;
}
async function loadIgnoreFileOrDefaults(configPath) {
if (!configPath) {
debug('Using default file ignore rules');
return DEFAULT_CLASP_IGNORE;
}
let content = await fs.readFile(configPath, { encoding: 'utf8' });
content = stripBom(content);
return splitLines(content).filter((name) => name.length > 0);
}
async function hasReadAccess(path) {
try {
await fs.access(path, fs.constants.R_OK);
}
catch {
return false;
}
return true;
}
function firstValue(values) {
if (Array.isArray(values) && values.length > 0) {
return values[0];
}
return values;
}