Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/classes/config-schema-handler/resolve-ajv-validate.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const os = require('os');
const standaloneCode = require('ajv/dist/standalone').default;
const { log } = require('../../utils/serverless-utils/log');
const fsp = require('fs').promises;
const resolveTmpdir = require('process-utils/tmpdir');
const resolveTmpdir = require('../../utils/resolve-process-tmp-dir');
const safeMoveFile = require('../../utils/fs/safe-move-file');
const requireFromString = require('require-from-string');
const deepSortObjectByKey = require('../../utils/deep-sort-object-by-key');
Expand Down
4 changes: 2 additions & 2 deletions lib/cli/run-compose.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,8 @@ module.exports = async () => {

// Add progress bar
if (shouldInstallCompose) {
const getCliProgressFooter = require('cli-progress-footer');
const cliProgressFooter = getCliProgressFooter();
const createProgressFooter = require('../utils/progress-footer');
const cliProgressFooter = createProgressFooter();
cliProgressFooter.shouldAddProgressAnimationPrefix = true;
cliProgressFooter.progressAnimationPrefixFrames =
cliProgressFooter.progressAnimationPrefixFrames.map((frame) => `\x1b[91m${frame}\x1b[39m`);
Expand Down
2 changes: 1 addition & 1 deletion lib/utils/log-deprecation.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const path = require('path');
const fse = require('fs-extra');
const fsp = require('fs').promises;
const weakMemoizee = require('memoizee/weak');
const resolveTmpdir = require('process-utils/tmpdir');
const resolveTmpdir = require('./resolve-process-tmp-dir');
const ServerlessError = require('../serverless-error');
const safeMoveFile = require('./fs/safe-move-file');
const { style, log } = require('./serverless-utils/log');
Expand Down
272 changes: 272 additions & 0 deletions lib/utils/progress-footer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
'use strict';

const { stripVTControlCharacters } = require('node:util');

const moveUp = '\x1b[1A';
const clearLine = '\x1b[2K';
const emptyLinesPattern = /^[\n\r\u2028\u2029]{2}$/u;

const defaultFrames =
process.platform === 'win32'
? ['┤', '┘', '┴', '└', '├', '┌', '┬', '┐']
: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
const defaultAnimationIntervalMs = process.platform === 'win32' ? 100 : 80;

const splitRows = (rows) => {
if (Array.isArray(rows)) return rows.map(String);
if (!rows) return [];
return String(rows).split(/[\n\r]/u);
Comment thread
GrahamCampbell marked this conversation as resolved.
};

const stringifyChunk = (chunk) => {
if (typeof chunk === 'string') return chunk;
if (chunk instanceof Uint8Array) return Buffer.from(chunk).toString();
return String(chunk);
};

const getVisibleLength = (value) => [...stripVTControlCharacters(value)].length;

const areEmptyLines = (value) => emptyLinesPattern.test(value);

class ProgressFooter {
constructor(options = {}) {
if (!options || typeof options !== 'object') options = {};
const { stdout = process.stdout, stderr = process.stderr } = options;

this.stdout = stdout;
this.stderr = stderr;
this.rawRows = [];
this.renderedLineCount = 0;
this.hasLeadingProgressSpacer = false;
this.hasTerminatedPartialLine = false;
this.lastOutLineLength = 0;
this.lastOutCharacters = '';
this.frameIndex = 0;
this.animationIntervalId = null;
this.isActive = false;
this.originalStdoutWrite = this.stdout.write;
this.originalStderrWrite = this.stderr.write;
this._shouldAddProgressAnimationPrefix = false;
this._progressAnimationPrefixFrames = defaultFrames;
this.installPassiveObserver();
}

get progressAnimationPrefixFrames() {
return this._progressAnimationPrefixFrames;
}

set progressAnimationPrefixFrames(frames) {
const normalizedFrames = Array.from(frames, String);
if (normalizedFrames.length < 2) {
throw new TypeError('Expected at least two animation frames');
}
this._progressAnimationPrefixFrames = normalizedFrames;
this.frameIndex = 0;
this.repaint();
}

get shouldAddProgressAnimationPrefix() {
return this._shouldAddProgressAnimationPrefix;
}

set shouldAddProgressAnimationPrefix(value) {
this._shouldAddProgressAnimationPrefix = Boolean(value);
if (this.isActive) {
if (this._shouldAddProgressAnimationPrefix) {
this.startAnimation();
} else {
this.stopAnimation();
}
}
this.repaint();
}

updateProgress(rows) {
this.rawRows = splitRows(rows);

if (!this.rawRows.length) {
this.clearRenderedProgress();
this.deactivate();
return;
}

this.activate();
this.repaint();
}

startAnimation() {
if (this.animationIntervalId) return;

this.animationIntervalId = setInterval(() => {
if (!this.rawRows.length) return;
this.frameIndex = (this.frameIndex + 1) % this.progressAnimationPrefixFrames.length;
this.repaint();
}, defaultAnimationIntervalMs);

if (this.animationIntervalId.unref) {
this.animationIntervalId.unref();
}
}

stopAnimation() {
clearInterval(this.animationIntervalId);
this.animationIntervalId = null;
}

activate() {
if (this.isActive) return;

this.restoreOriginalWrites();

this.stdout.write = (chunk, encoding, callback) =>
this.writeAroundProgress(this.writeOriginalStdout.bind(this), chunk, encoding, callback);

this.stderr.write = (chunk, encoding, callback) =>
this.writeAroundProgress(this.writeOriginalStdout.bind(this), chunk, encoding, callback);

this.isActive = true;
if (this.shouldAddProgressAnimationPrefix) this.startAnimation();
}

deactivate() {
if (!this.isActive) return;

this.restoreOriginalWrites();
this.stopAnimation();
this.isActive = false;
this.installPassiveObserver();
}

installPassiveObserver() {
this.stdout.write = (chunk, ...args) => {
const result = this.originalStdoutWrite.call(this.stdout, chunk, ...args);
this.updateLastOutLineLength(stringifyChunk(chunk));
return result;
};

this.stderr.write = (chunk, ...args) => {
const result = this.originalStderrWrite.call(this.stderr, chunk, ...args);
this.updateLastOutLineLength(stringifyChunk(chunk));
return result;
};
}

restoreOriginalWrites() {
this.stdout.write = this.originalStdoutWrite;
this.stderr.write = this.originalStderrWrite;
}

writeOriginalStdout(chunk, ...args) {
return this.originalStdoutWrite.call(this.stdout, chunk, ...args);
}

writeAroundProgress(write, chunk, encoding, callback) {
if (typeof encoding === 'function') {
callback = encoding;
encoding = undefined;
}

this.clearRenderedProgress();

let result;
if (encoding === undefined) {
result = callback ? write(chunk, callback) : write(chunk);
} else {
result = write(chunk, encoding, callback);
}

this.updateLastOutLineLength(stringifyChunk(chunk));
this.writeProgress();
return result;
}

updateLastOutLineLength(content) {
const strippedContent = stripVTControlCharacters(content);
const trailingContent = strippedContent.slice(-50);

if (trailingContent.length === 1) {
this.lastOutCharacters = `${this.lastOutCharacters.slice(-1)}${trailingContent}`;
} else if (trailingContent.length > 1) {
this.lastOutCharacters = trailingContent.slice(-2);
}

const lines = strippedContent.split(/[\n\r]/u);
const lastLine = lines[lines.length - 1];

if (lines.length === 1) {
this.lastOutLineLength += [...lastLine].length;
} else {
this.lastOutLineLength = [...lastLine].length;
}
}

repaint() {
if (!this.rawRows.length || !this.isActive) return;
this.clearRenderedProgress();
this.writeProgress();
}

getRows() {
if (!this.shouldAddProgressAnimationPrefix) return this.rawRows;

const prefix = `${this.progressAnimationPrefixFrames[this.frameIndex]} `;
const padding = ' '.repeat(getVisibleLength(prefix));

return this.rawRows.map((row) => {
if (!row) return row;
return `${prefix}${row.split('\n').join(`\n${padding}`)}`;
});
}

writeProgress() {
const rows = this.getRows();
if (!rows.length) return;

this.hasTerminatedPartialLine = Boolean(this.lastOutLineLength);
this.hasLeadingProgressSpacer = !areEmptyLines(this.lastOutCharacters);

if (this.hasTerminatedPartialLine) {
this.writeOriginalStdout('\n');
}

if (this.hasLeadingProgressSpacer) {
this.writeOriginalStdout('\n');
}

for (const row of rows) {
this.writeOriginalStdout(`${row}\n`);
}

this.renderedLineCount = rows.reduce((count, row) => count + this.getLineCount(row), 0);
}

clearRenderedProgress() {
if (!this.renderedLineCount) return;

const lineCountToClear = this.renderedLineCount + (this.hasLeadingProgressSpacer ? 1 : 0);

for (let index = 0; index < lineCountToClear; index += 1) {
this.writeOriginalStdout(`${moveUp}${clearLine}`);
}

this.renderedLineCount = 0;
this.hasLeadingProgressSpacer = false;

if (this.hasTerminatedPartialLine) {
const columns = this.stdout.columns || 80;
const column = this.lastOutLineLength % columns || columns;
this.writeOriginalStdout(`${moveUp}\x1b[${column}C`);
}

this.hasTerminatedPartialLine = false;
}

getLineCount(row) {
const columns = this.stdout.columns || 80;
return String(row)
.split(/[\n\r]/u)
.reduce((count, line) => count + (Math.ceil(getVisibleLength(line) / columns) || 1), 0);
}
}

module.exports = (options) => new ProgressFooter(options);
Comment thread
GrahamCampbell marked this conversation as resolved.
34 changes: 34 additions & 0 deletions lib/utils/resolve-process-tmp-dir.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
'use strict';

const crypto = require('crypto');
const fs = require('fs');
const fsp = fs.promises;
const os = require('os');
const path = require('path');

const tmpDirPrefix = 'node-process-';

let processTmpDirPromise;

module.exports = async () => {
if (!processTmpDirPromise) {
processTmpDirPromise = fsp
.mkdtemp(path.join(os.tmpdir(), `${tmpDirPrefix}${crypto.randomBytes(2).toString('hex')}-`))
.then((tmpDir) => {
process.once('exit', () => {
try {
fs.rmSync(tmpDir, { recursive: true, force: true });
} catch {
// Best-effort cleanup during process exit.
}
});
return tmpDir;
Comment thread
GrahamCampbell marked this conversation as resolved.
})
.catch((error) => {
processTmpDirPromise = undefined;
throw error;
});
}

return processTmpDirPromise;
};
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
'use strict';

const getCliProgressFooter = require('cli-progress-footer');
const createProgressFooter = require('../../../../progress-footer');
const { emitter } = require('../../log/get-progress-reporter');
const { progress, log } = require('../../../log');
const joinTextTokens = require('../../log/join-text-tokens');
const style = require('./style');

module.exports = ({ logLevelIndex }) => {
const cliProgressFooter = getCliProgressFooter();
const cliProgressFooter = createProgressFooter();
cliProgressFooter.shouldAddProgressAnimationPrefix = true;
cliProgressFooter.progressAnimationPrefixFrames =
cliProgressFooter.progressAnimationPrefixFrames.map((frame) => style.noticeSymbol(frame));
Expand Down
3 changes: 0 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@
"ajv-formats": "^2.1.1",
"aws-sdk": "^2.1693.0",
"cachedir": "^2.3.0",
"cli-progress-footer": "^2.3.2",
"content-disposition": "^0.5.4",
"cross-spawn": "^7.0.6",
"d": "^1.0.1",
Expand All @@ -53,7 +52,6 @@
"filesize": "^10.0.7",
"fs-extra": "^11.3.4",
"get-stdin": "^8.0.0",
"graceful-fs": "^4.2.11",
"https-proxy-agent": "^9.0.0",
"js-yaml": "^4.1.0",
"json-cycle": "^1.5.0",
Expand All @@ -65,7 +63,6 @@
"module-alias": "^2.2.3",
"object-hash": "^3.0.0",
"open": "^8.4.2",
"process-utils": "^4.0.0",
"punycode": "^2.3.1",
"require-from-string": "^2.0.2",
"semver": "^7.5.3",
Expand Down
4 changes: 0 additions & 4 deletions scripts/serverless.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,6 @@

Error.stackTraceLimit = Infinity;

// global graceful-fs patch
// https://github.com/isaacs/node-graceful-fs#global-patching
require('graceful-fs').gracefulify(require('fs'));

// Setup log writing
require('../lib/utils/serverless-utils/log-reporters/node');
const { log, progress } = require('../lib/utils/serverless-utils/log');
Expand Down
2 changes: 1 addition & 1 deletion test/lib/resolve-env.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use strict';

const createEnv = require('process-utils/create-env');
const { createEnv } = require('../utils/process');

module.exports = (options = {}) => {
if (!options) options = {};
Expand Down
Loading