@sap/cds-dk
Version:
Command line client and development toolkit for the SAP Cloud Application Programming Model
199 lines (168 loc) • 7.55 kB
JavaScript
const cds = require ('../cds')
const { local, tar } = cds.utils;
const fs = require('fs');
const { join, dirname } = require('path');
const axios = require('axios');
const { login } = require('./auth_manager');
require('../util/axios').pruneErrors();
const { getMessage } = require('./util/logging');
const { handleHttpError } = require('./util/errors');
const { grepQR } = require('./util/fs');
const { escapeRegex } = require('./util/strings');
function tabSize(jsonString, defaultSize) {
return /^ +/m.exec(jsonString)?.[0].length ?? defaultSize;
}
function getPackageContentsAndTabSize(pkgPath, defaultContents = undefined, defaultTabSize = 0) {
try {
const contents = fs.readFileSync(pkgPath, { encoding: 'utf-8' });
const tab = tabSize(contents, defaultTabSize);
return [JSON.parse(contents), tab];
} catch (error) {
if (defaultContents) {
return [defaultContents, defaultTabSize];
}
throw getMessage('package.json missing or unreadable', { error });
}
}
function writeObj(obj, objPath, tabSize) {
if (!fs.existsSync(dirname(objPath))) {
fs.mkdirSync(dirname(objPath), { recursive: true });
}
fs.writeFileSync(objPath, JSON.stringify(obj, undefined, tabSize), { encoding: 'utf-8' });
}
const INVALID_BASE_MODEL_NAME = /^[._]/; // See https://docs.npmjs.com/cli/v10/configuring-npm/package-json#name
module.exports = class Pull {
static async run(paramValues) {
const params = await login(paramValues);
return new Pull(params).run();
}
params;
projectFolder;
env;
baseModelName;
packagePath;
baseModelFolder;
#amendPackage = {};
#baseModelWorkspace;
#packageContentsAndTabSize;
static get DEFAULTS() {
return {
tabSize: 2,
baseModelFolder: '.base',
baseModelName: 'base-model'
};
}
constructor(params) {
this.params = params;
this.projectFolder = params.get('projectFolder');
this.env = cds.env.for('cds', this.projectFolder);
this.baseModelName = this.env.extends;
this.packagePath = join(this.projectFolder, 'package.json');
this.baseModelFolder = join(this.projectFolder, this.baseModelWorkspace);
}
async run() {
console.log(`Pulling app base model`, {
from: this.params.get('appUrl'),
to: local(this.baseModelFolder),
...(this.params.has('subdomain') && { subdomain: this.params.get('subdomain') })
});
this.validateBaseModelName();
fs.rmSync(this.baseModelFolder, { force: true, recursive: true });
fs.mkdirSync(this.baseModelFolder, { recursive: true });
const baseModelTgz = await this.pullTgz();
await tar.xz(baseModelTgz).to(this.baseModelFolder);
Pull.transformBasePackage(this.baseModelFolder, this.baseModelName);
this.amendExtPackage();
console.log(`Finished. Refer to the base model like so: using from '${this.baseModelName}'`);
const baseModelSymlink = join(this.projectFolder, 'node_modules', this.baseModelName);
if (!fs.statSync(baseModelSymlink, { throwIfNoEntry: false })?.isSymbolicLink()) {
console.log(`Note: the base model has been pulled to an NPM workspace in folder '${local(this.baseModelFolder)}'. Run 'npm install' to install it.`);
}
}
get baseModelWorkspace() {
const defaultFolder = Pull.DEFAULTS.baseModelFolder;
if (!this.#baseModelWorkspace) {
if (this.packageContents.workspaces) {
if (this.packageContents.workspaces.includes(defaultFolder)) {
this.#baseModelWorkspace = defaultFolder;
} else {
this.#baseModelWorkspace = this.packageContents.workspaces
.map(ws => String(ws))
.find(ws => {
try {
return require(join(this.projectFolder, ws, 'package.json')).name === this.baseModelName;
} catch (e) { /* ignore */ }
}) ?? defaultFolder;
}
} else {
this.#amendPackage.workspaces = [defaultFolder];
this.#baseModelWorkspace = defaultFolder;
}
}
return this.#baseModelWorkspace;
}
validateBaseModelName() {
if (!this.baseModelName) {
throw getMessage(`Missing configuration 'cds.extends' in package.json`);
}
if (this.baseModelName.match(INVALID_BASE_MODEL_NAME)) {
console.warn(`Invalid base-model name '${this.baseModelName}': must not start with '_' or '.'. ` +
`Using '${Pull.DEFAULTS.baseModelName}' instead.`);
const invalidNameInUse = grepQR(this.projectFolder, ['.cds', '.csn'], new RegExp(`\\b${escapeRegex(this.baseModelName)}\\b`));
if (invalidNameInUse) {
console.error(`Some model files seem to reference the invalid name. Please update them manually.`);
}
this.baseModelName = Pull.DEFAULTS.baseModelName;
this.#amendPackage.extends = this.baseModelName;
}
}
getExtensionName() {
try {
return require(join(this.projectFolder, 'package.json')).name;
} catch (error) {
throw getMessage(`Extension project at ${this.projectFolder} is missing package.json file`, { error, command: 'push' });
}
}
async pullTgz() {
const pullUrl = `${(this.params.get('appUrl'))}/-/cds/extensibility/pull`;
const options = { ...this.params.get('reqAuth'), responseType: 'arraybuffer' };
return axios.post(pullUrl, { tag: this.getExtensionName() }, options)
.then(response => Buffer.from(response.data))
.catch(error => handleHttpError(error, this.params, { url: pullUrl }));
}
static transformBasePackage(baseModelFolder, baseModelName) {
const baseRcPath = join(baseModelFolder, '.cdsrc.json');
const baseRc = getPackageContentsAndTabSize(baseRcPath, {}, this.DEFAULTS.tabSize)[0];
fs.rmSync(baseRcPath, { force: true });
const basePkgPath = join(baseModelFolder, 'package.json');
const basePkg = {
...getPackageContentsAndTabSize(basePkgPath, {}, this.DEFAULTS.tabSize)[0],
name: baseModelName,
cds: baseRc
};
writeObj(basePkg, basePkgPath, this.DEFAULTS.tabSize);
}
amendExtPackage() {
if (Object.keys(this.#amendPackage).length === 0) {
return;
}
console.log(`Amending extension package.json with project configuration`);
fs.writeFileSync(this.packagePath, JSON.stringify({
...this.packageContents,
...this.#amendPackage
}, undefined, this.tabSize));
this.#packageContentsAndTabSize = undefined;
}
get packageContents() {
if (!this.#packageContentsAndTabSize) {
this.#packageContentsAndTabSize = getPackageContentsAndTabSize(this.packagePath, undefined, Pull.DEFAULTS.tabSize);
}
return this.#packageContentsAndTabSize[0];
}
get tabSize() {
if (!this.#packageContentsAndTabSize) {
this.#packageContentsAndTabSize = getPackageContentsAndTabSize(this.packagePath, undefined, Pull.DEFAULTS.tabSize);
}
return this.#packageContentsAndTabSize[1];
}
}