Skip to content

poc: preserve comments in esbuild #1565

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Mar 17, 2025
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
4 changes: 3 additions & 1 deletion injected/scripts/utils/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { readFileSync } from 'fs';
import { cwd } from '../../../scripts/script-utils.js';
import { join } from 'path';
import * as esbuild from 'esbuild';
import { commentPlugin } from './comment-plugin.js';
const ROOT = join(cwd(import.meta.url), '..', '..');
const DEBUG = false;

Expand Down Expand Up @@ -38,6 +39,7 @@ export async function bundle(params) {
format: 'iife',
bundle: true,
metafile: true,
legalComments: 'inline',
globalName: name,
loader: {
'.css': 'text',
Expand All @@ -48,7 +50,7 @@ export async function bundle(params) {
'import.meta.injectName': JSON.stringify(platform),
'import.meta.trackerLookup': trackerLookup,
},
plugins: [loadFeaturesPlugin],
plugins: [loadFeaturesPlugin, commentPlugin()],
banner: {
js: prefixMessage,
},
Expand Down
80 changes: 80 additions & 0 deletions injected/scripts/utils/comment-plugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { promises } from 'node:fs';

/**
* @returns {import("esbuild").Plugin}
*/
export function commentPlugin() {
const PLUGIN_ID = 'comment-override';

/** @type {import("esbuild").Plugin} */
const plugin = {
name: PLUGIN_ID,
setup(build) {
build.onLoad({ filter: /.*/ }, async (args) => {
if (!args.path.includes('node_modules')) return undefined;
const text = await promises.readFile(args.path, 'utf8');
return {
contents: convertToLegalComments(text.toString()),
loader: 'js',
};
});
},
};
return plugin;
}

/**
* Detect the start of a particular comment and change the
* lines to have the prefix `//!` - this allows esbuild to keep it
*
* When a line is matched, continue to match further lines until a non-comment is seen.
*
* @param {string} source
*/
export function convertToLegalComments(source) {
// Process block comments - find all block comments
const blockComments = source.match(/\/\*[\s\S]*?\*\//g) || [];

// Selectively replace only block comments that contain "copyright"
let modifiedSource = source;
for (const comment of blockComments) {
if (/copyright/i.test(comment)) {
// Replace only the block comments with copyright
modifiedSource = modifiedSource.replace(comment, comment.replace(/\/\*/, '/*!'));
}
}

// Process line comments
const lines = modifiedSource.split('\n');
const result = [];
let inCommentBlock = false;

for (let i = 0; i < lines.length; i++) {
const line = lines[i];

// Check if the line contains a block comment - this breaks any line comment sequence
if (line.includes('/*') || line.includes('*/')) {
inCommentBlock = false;
result.push(line);
}
// Check if this line starts a line comment block with "copyright"
else if (!inCommentBlock && /^\s*\/\/(?=.*copyright.*$)/i.test(line)) {
// Start of a copyright comment - mark it and convert
inCommentBlock = true;
result.push(line.replace(/^\s*\/\//, (match) => match.replace('//', '//!')));
}
// Check if we're continuing a line comment block
else if (inCommentBlock && /^\s*\/\//.test(line)) {
// Continue the comment block - convert the prefix
result.push(line.replace(/^\s*\/\//, (match) => match.replace('//', '//!')));
}
// Check if we're exiting a comment block
else {
// Not a comment line or doesn't match our criteria, end the block
inCommentBlock = false;
result.push(line);
}
}

return result.join('\n');
}
199 changes: 199 additions & 0 deletions injected/unit-test/comment-plugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import { convertToLegalComments } from '../scripts/utils/comment-plugin.js';

describe('convertToLegalComments', () => {
it('should convert single line comments with copyright', () => {
const input = `// This is a copyright notice
const foo = 'bar';`;

const expected = `//! This is a copyright notice
const foo = 'bar';`;

expect(convertToLegalComments(input)).toEqual(expected);
});

it('should convert multiple consecutive line comments following a copyright line', () => {
const input = `// This is a copyright notice
// This is a second line
// This is a third line
const foo = 'bar';`;

const expected = `//! This is a copyright notice
//! This is a second line
//! This is a third line
const foo = 'bar';`;

expect(convertToLegalComments(input)).toEqual(expected);
});

it('should stop converting after a non-comment line is encountered', () => {
const input = `// This is a copyright notice
// This is a second line
const foo = 'bar';
// This is a regular comment that should not be converted`;

const expected = `//! This is a copyright notice
//! This is a second line
const foo = 'bar';
// This is a regular comment that should not be converted`;

expect(convertToLegalComments(input)).toEqual(expected);
});

it('should handle multiple separate comment blocks', () => {
const input = `// This is a regular comment
const a = 1;

// This has copyright info
// And continues here
const b = 2;

// Another copyright notice
// With more details
// And even more info
const c = 3;`;

const expected = `// This is a regular comment
const a = 1;

//! This has copyright info
//! And continues here
const b = 2;

//! Another copyright notice
//! With more details
//! And even more info
const c = 3;`;

expect(convertToLegalComments(input)).toEqual(expected);
});

it('should handle indented comments', () => {
const input = `function test() {
// This has copyright info
// This is indented
return true;
}`;

const expected = `function test() {
//! This has copyright info
//! This is indented
return true;
}`;

expect(convertToLegalComments(input)).toEqual(expected);
});

it('should handle block comments with copyright', () => {
const input = `/* This is a copyright block comment */
const foo = 'bar';

/* This is a regular
multiline comment */`;

const expected = `/*! This is a copyright block comment */
const foo = 'bar';

/* This is a regular
multiline comment */`;

expect(convertToLegalComments(input)).toEqual(expected);
});

it('should handle mixed comment types', () => {
const input = `// This has copyright info
// This continues
/* This is a regular block comment */
const foo = 'bar';

/* This is a copyright block comment */
// This is a regular comment after a block`;

const expected = `//! This has copyright info
//! This continues
/* This is a regular block comment */
const foo = 'bar';

/*! This is a copyright block comment */
// This is a regular comment after a block`;

expect(convertToLegalComments(input)).toEqual(expected);
});

it('should handle block comments breaking line comment sequences', () => {
const input = `// This has copyright info
// This line should be converted
/* This block comment breaks the sequence */
// This line should NOT be converted
// Even though it follows another comment`;

const expected = `//! This has copyright info
//! This line should be converted
/* This block comment breaks the sequence */
// This line should NOT be converted
// Even though it follows another comment`;

expect(convertToLegalComments(input)).toEqual(expected);
});

it('should handle case insensitivity for "copyright"', () => {
const input = `// This has COPYRIGHT info
// This continues
const foo = 'bar';

// This has Copyright mixed case
// More comments
const baz = 'qux';`;

const expected = `//! This has COPYRIGHT info
//! This continues
const foo = 'bar';

//! This has Copyright mixed case
//! More comments
const baz = 'qux';`;

expect(convertToLegalComments(input)).toEqual(expected);
});

it('should not convert comments without copyright', () => {
const input = `// This is a regular comment
// Another regular comment
const foo = 'bar';`;

const expected = `// This is a regular comment
// Another regular comment
const foo = 'bar';`;

expect(convertToLegalComments(input)).toEqual(expected);
});

it('should handle code with no comments', () => {
const input = `const foo = 'bar';
function test() {
return true;
}
const obj = { key: 'value' };`;

const expected = input; // Should remain unchanged

expect(convertToLegalComments(input)).toEqual(expected);
});

it('should treat empty lines as non-comment lines that break the sequence', () => {
const input = `// This has copyright info
// This continues

// These comments should NOT be converted
// Because empty line breaks the sequence
const foo = 'bar';`;

const expected = `//! This has copyright info
//! This continues

// These comments should NOT be converted
// Because empty line breaks the sequence
const foo = 'bar';`;

expect(convertToLegalComments(input)).toEqual(expected);
});
});
3 changes: 2 additions & 1 deletion injected/unit-test/verify-artifacts.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const checks = {
tests: [
{ kind: 'maxFileSize', value: CSS_OUTPUT_SIZE },
{ kind: 'containsString', text: '$TRACKER_LOOKUP$', includes: true },
{ kind: 'containsString', text: 'Copyright (C) 2010 by Johannes Baagøe <[email protected]>', includes: true },
],
},
firefox: {
Expand All @@ -34,7 +35,7 @@ const checks = {
},
integration: {
file: join(BUILD, 'integration/contentScope.js'),
tests: [{ kind: 'containsString', text: 'trackerLookup: ', includes: true }],
tests: [{ kind: 'containsString', text: 'init_define_import_meta_trackerLookup = ', includes: true }],
},
windows: {
file: join(BUILD, 'windows/contentScope.js'),
Expand Down
Loading