Skip to content

Commit

Permalink
Strip less-important logging from modular variant
Browse files Browse the repository at this point in the history
In order to reduce bundle size further, we’ve decided to strip all
logging from the modular variant of the SDK, except for errors and
certain network events. (We also considered providing a separate
tree-shakable module with all of this logging code so that a user of the
modular variant of the library can opt in to it, but decided against it
for now; we might add it in later. This does mean that there is
currently no version of the SDK that allows you to use both deltas and
verbose logging on web.)

I couldn’t find any out-of-the-box esbuild functionality that let us do
this. The only stuff I could find related to stripping code was:

- the `pure` option, but that code only gets stripped if you minify the
  code (and even in that case I couldn’t actually get it to be stripped,
  perhaps would have been able to with further trying though), but
  minifying our generated modules bundle causes the bundle size of those
  who use it (as tested by our modulereport script) to increase
  considerably (for reasons I’m not sure of)

- the `drop` option, but that only lets you remove calls to `console` or
  `debugger`

So instead I’ve implemented it as an esbuild plugin.

Resolves #1526
  • Loading branch information
lawrence-forooghian committed Dec 6, 2023
1 parent 8cf7433 commit 740c29b
Show file tree
Hide file tree
Showing 12 changed files with 214 additions and 78 deletions.
92 changes: 91 additions & 1 deletion Gruntfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ var esbuild = require('esbuild');
var umdWrapper = require('esbuild-plugin-umd-wrapper');
var banner = require('./src/fragments/license');
var process = require('process');
var babel = {
types: require('@babel/types'),
parser: require('@babel/parser'),
traverse: require('@babel/traverse'),
generator: require('@babel/generator'),
};

module.exports = function (grunt) {
grunt.loadNpmTasks('grunt-contrib-concat');
Expand Down Expand Up @@ -123,6 +129,90 @@ module.exports = function (grunt) {
};
}

// This esbuild plugin strips all log messages from the modular variant of
// the library, except for error-level logs and other logging statements
// explicitly marked as not to be stripped.
var stripLogsPlugin = {
name: 'stripLogs',
setup(build) {
let foundErrorLog = false;
let foundNoStripLog = false;

const filter = new RegExp(`^${__dirname}/src/.*\.[tj]s$`);
build.onLoad({ filter }, async (args) => {
const contents = (await fs.promises.readFile(args.path)).toString();
const lines = contents.split('\n');
const ast = babel.parser.parse(contents, { sourceType: 'module', plugins: ['typescript'] });
const errors = [];

babel.traverse.default(ast, {
enter(path) {
if (
path.isCallExpression() &&
babel.types.isMemberExpression(path.node.callee) &&
babel.types.isIdentifier(path.node.callee.object, { name: 'Logger' })
) {
if (babel.types.isIdentifier(path.node.callee.property, { name: 'logAction' })) {
const firstArgument = path.node.arguments[0];

if (
babel.types.isMemberExpression(firstArgument) &&
babel.types.isIdentifier(firstArgument.object, { name: 'Logger' }) &&
firstArgument.property.name.startsWith('LOG_')
) {
if (firstArgument.property.name === 'LOG_ERROR') {
// `path` is a call to `Logger.logAction(Logger.LOG_ERROR, ...)`; preserve it.
foundErrorLog = true;
} else {
// `path` is a call to `Logger.logAction(Logger.LOG_*, ...) for some other log level; strip it.
path.remove();
}
} else {
// `path` is a call to `Logger.logAction(...)` with some argument other than a `Logger.LOG_*` expression; raise an error because we can’t determine whether to strip it.
errors.push({
location: {
file: args.path,
column: firstArgument.loc.start.column,
line: firstArgument.loc.start.line,
lineText: lines[firstArgument.loc.start.line - 1],
},
text: `First argument passed to Logger.logAction() must be Logger.LOG_*, got \`${
babel.generator.default(firstArgument).code
}\``,
});
}
} else if (babel.types.isIdentifier(path.node.callee.property, { name: 'logActionNoStrip' })) {
// `path` is a call to `Logger.logActionNoStrip()`; preserve it.
foundNoStripLog = true;
}
}
},
});

return { contents: babel.generator.default(ast).code, loader: 'ts', errors };
});

build.onEnd(() => {
const errorMessages = [];

// Perform a sense check to make sure that we found some logging
// calls to preserve (to protect us against accidentally changing the
// internal logging API in such a way that would cause us to
// accidentally strip all logging calls).

if (!foundErrorLog) {
errorMessages.push('Did not find any Logger.logAction(Logger.LOG_ERROR, ...) calls to preserve');
}

if (!foundNoStripLog) {
errorMessages.push('Did not find any Logger.logActionNoStrip(...) calls to preserve');
}

return { errors: errorMessages.map((text) => ({ text })) };
});
},
};

function createModulesConfig() {
return {
// We need to create a new copy of the base config, because calling
Expand All @@ -133,7 +223,7 @@ module.exports = function (grunt) {
entryPoints: ['src/platform/web/modules.ts'],
outfile: 'build/modules/index.js',
format: 'esm',
plugins: [],
plugins: [stripLogsPlugin],
};
}

Expand Down
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,14 @@ You must provide:

`BaseRealtime` offers the same API as the `Realtime` class described in the rest of this `README`. This means that you can develop an application using the default variant of the SDK and switch to the modular version when you wish to optimize your bundle size.

For more information, see the [generated documentation](https://sdk.ably.com/builds/ably/ably-js/main/typedoc/modules/index.html) (this link points to the documentation for the `main` branch).
In order to further reduce bundle size, the modular variant of the SDK performs less logging than the default variant. It only logs:

- messages that have a `logLevel` of 1 (that is, errors)
- a small number of other network events

If you need more verbose logging, use the default variant of the SDK.

For more information about the modular variant of the SDK, see the [generated documentation](https://sdk.ably.com/builds/ably/ably-js/main/typedoc/modules/index.html) (this link points to the documentation for the `main` branch).

### TypeScript

Expand Down
18 changes: 18 additions & 0 deletions modules.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,15 @@ export interface ModulesMap {
* A client that offers a simple stateless API to interact directly with Ably's REST API.
*
* `BaseRest` is the equivalent, in the modular variant of the Ably Client Library SDK, of the [`Rest`](../../default/classes/Rest.html) class in the default variant of the SDK. The difference is that its constructor allows you to decide exactly which functionality the client should include. This allows unused functionality to be tree-shaken, reducing bundle size.
*
* > **Note**
* >
* > In order to further reduce bundle size, `BaseRest` performs less logging than the `Rest` class exported by the default variant of the SDK. It only logs:
* >
* > - messages that have a {@link Types.ClientOptions.logLevel | `logLevel`} of 1 (that is, errors)
* > - a small number of other network events
* >
* > If you need more verbose logging, use the default variant of the SDK.
*/
export declare class BaseRest extends Types.Rest {
/**
Expand All @@ -260,6 +269,15 @@ export declare class BaseRest extends Types.Rest {
* A client that extends the functionality of {@link BaseRest} and provides additional realtime-specific features.
*
* `BaseRealtime` is the equivalent, in the modular variant of the Ably Client Library SDK, of the [`Realtime`](../../default/classes/Realtime.html) class in the default variant of the SDK. The difference is that its constructor allows you to decide exactly which functionality the client should include. This allows unused functionality to be tree-shaken, reducing bundle size.
*
* > **Note**
* >
* > In order to further reduce bundle size, `BaseRealtime` performs less logging than the `Realtime` class exported by the default variant of the SDK. It only logs:
* >
* > - messages that have a {@link Types.ClientOptions.logLevel | `logLevel`} of 1 (that is, errors)
* > - a small number of other network events
* >
* > If you need more verbose logging, use the default variant of the SDK.
*/
export declare class BaseRealtime extends Types.Realtime {
/**
Expand Down
Loading

0 comments on commit 740c29b

Please sign in to comment.