Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
58 changes: 58 additions & 0 deletions build/commands/PRESUBMIT.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Copyright (c) 2026 The Brave Authors. All rights reserved.
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at https://mozilla.org/MPL/2.0/.

import os
import re

PRESUBMIT_VERSION = '2.0.0'


def CheckImportSpecifierMatchesFile(input_api, output_api):
"""Checks that relative import specifier matches the actual file on disk.

When running TypeScript natively via Node.js (no transpilation), imports
must use the real file extension. For example, if a file is named config.ts,
importing it as './config.js' is an error.
"""

IMPORT_RE = re.compile(
r'''
(?:
\bfrom \s+ # static: import/export ... from
| \bimport \s* \( # dynamic: import(
| \bimport \s+ # side-effect: import './foo.js'
)
\s* ['"]
( \.\.?/ # ./ or ../
[^'"]* # path
\. [cm]?[jt]s # file extension
)
['"]
''', re.VERBOSE)

files_to_check = (r'.+\.[cm]?[jt]s$', )
file_filter = lambda f: input_api.FilterSourceFile(
f, files_to_check=files_to_check)

items = []
for f in input_api.AffectedSourceFiles(file_filter):
file_dir = os.path.dirname(f.AbsoluteLocalPath())
for lineno, line in enumerate(f.NewContents(), 1):
for match in IMPORT_RE.finditer(line):
specifier = match.group(1)
resolved = os.path.normpath(os.path.join(file_dir, specifier))

if not os.path.isfile(resolved):
items.append(f'{f.LocalPath()}:{lineno}: '
f'file not found: {specifier}')

if not items:
return []

return [
output_api.PresubmitError(
'Import path references a file that doesn\'t exist. '
'Check the file extension.', items)
]
211 changes: 211 additions & 0 deletions build/commands/PRESUBMIT_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
# Copyright (c) 2026 The Brave Authors. All rights reserved.
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at https://mozilla.org/MPL/2.0/.

import os
import tempfile
import unittest

import brave_chromium_utils

# pylint: disable=import-error,no-member
import PRESUBMIT

with brave_chromium_utils.sys_path("//"):
from PRESUBMIT_test_mocks import MockAffectedFile
from PRESUBMIT_test_mocks import MockInputApi, MockOutputApi


class ImportSpecifierTest(unittest.TestCase):

def setUp(self):
self._tmpdir = tempfile.mkdtemp()

def _run_check(self, files):
"""Run the check with a files map.

Args:
files: dict mapping path -> lines list (affected file) or None
(exists on disk only, not in the changelist).
"""
input_api = MockInputApi()
input_api.files = []
for path, contents in files.items():
full = os.path.join(self._tmpdir, path)
os.makedirs(os.path.dirname(full), exist_ok=True)
if contents is not None:
affected_file = MockAffectedFile(path, contents)
affected_file.AbsoluteLocalPath = lambda p=full: p
input_api.files.append(affected_file)
else:
open(full, 'w').close()
return PRESUBMIT.CheckImportSpecifierMatchesFile(
input_api, MockOutputApi())

def testImportSpecifiers(self):
# files: path -> lines (affected source) or None (on-disk only).
cases = [
{
'name': 'correct .js extension',
'files': {
'lib/build.js': ["import config from './config.js'"],
'lib/config.js': None,
},
'expected_errors': 0,
},
{
'name': 'wrong extension: .js import but .ts file',
'files': {
'lib/build.js': ["import config from './config.js'"],
'lib/config.ts': None,
},
'expected_errors': 1,
},
{
'name': 'multiline import',
'files': {
'lib/build.js': [
"import {",
" getTestBinary,",
" getTestsToRun,",
"} from './utils.js'",
],
'lib/utils.ts': None,
},
'expected_errors': 1,
},
{
'name': 'dynamic import()',
'files': {
'lib/build.js': [
"const config = await import('./config.js')",
],
'lib/config.ts': None,
},
'expected_errors': 1,
},
{
'name': 'side-effect import, wrong extension',
'files': {
'lib/build.js': ["import './setup.js'"],
'lib/setup.ts': None,
},
'expected_errors': 1,
},
{
'name': 'side-effect import, correct extension',
'files': {
'lib/build.js': ["import './setup.js'"],
'lib/setup.js': None,
},
'expected_errors': 0,
},
{
'name': 'export { } from',
'files': {
'lib/index.js': ["export { foo } from './utils.js'"],
'lib/utils.ts': None,
},
'expected_errors': 1,
},
{
'name': 'export * from, correct extension',
'files': {
'lib/index.js': ["export * from './utils.js'"],
'lib/utils.js': None,
},
'expected_errors': 0,
},
{
'name': 'correct .ts extension',
'files': {
'lib/build.ts': ["import config from './config.ts'"],
'lib/config.ts': None,
},
'expected_errors': 0,
},
{
'name': 'parent directory import',
'files': {
'lib/build.js': ["import config from '../config.js'"],
'config.ts': None,
},
'expected_errors': 1,
},
{
'name': 'non-relative imports are ignored',
'files': {
'lib/build.js': [
"import fs from 'fs-extra'",
"import path from 'path'",
],
},
'expected_errors': 0,
},
{
'name': 'multiple errors in one file',
'files': {
'lib/build.js': [
"import config from './config.js'",
"import util from './util.js'",
],
'lib/config.ts': None,
'lib/util.ts': None,
},
'expected_errors': 2,
},
{
'name': 'wrong .mjs extension',
'files': {
'lib/build.mjs': ["import config from './config.mjs'"],
'lib/config.mts': None,
},
'expected_errors': 1,
},
{
'name': 'require() is not matched',
'files': {
'lib/build.cjs': [
"const config = require('./config.cjs')",
],
'lib/config.cts': None,
},
'expected_errors': 0,
},
{
'name': 'dotted filename, wrong extension',
'files': {
'lib/build.js': [
"import config from './config.base.js'",
],
'lib/config.base.ts': None,
},
'expected_errors': 1,
},
{
'name': 'dotted filename, correct extension',
'files': {
'lib/build.js': [
"import config from './config.base.js'",
],
'lib/config.base.js': None,
},
'expected_errors': 0,
},
]

for case in cases:
with self.subTest(name=case['name']):
self._tmpdir = tempfile.mkdtemp()
results = self._run_check(case['files'])

items = [i for r in results for i in r.items]
self.assertEqual(
case['expected_errors'], len(items),
f"Expected {case['expected_errors']} errors, "
f"got {len(items)}: {items}")


if __name__ == '__main__':
unittest.main()
10 changes: 4 additions & 6 deletions build/commands/lib/actionGuard.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.

const assert = require('assert')
const fs = require('fs-extra')
const path = require('path')
import assert from 'assert'
import fs from 'fs-extra'
import path from 'path'

// This function is used to get the call stack of the guarded operation. It is
// stored in the guard file.
Expand All @@ -21,7 +21,7 @@ function getGuardCallStack() {

// This class is used to ensure that a given action is successfully completed,
// otherwise a rerun might be required.
class ActionGuard {
export default class ActionGuard {
// Path to the guard file.
#guardFilePath
// Cleanup closure to perform a cleanup (optional) before running the action.
Expand Down Expand Up @@ -86,5 +86,3 @@ class ActionGuard {
}
}
}

module.exports = ActionGuard
6 changes: 3 additions & 3 deletions build/commands/lib/actionGuard.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.

const fs = require('fs-extra')
const path = require('path')
const ActionGuard = require('./actionGuard')
import fs from 'fs-extra'
import path from 'path'
import ActionGuard from './actionGuard.js'

describe('ActionGuard', () => {
const guardFilePath = '/path/to/guard/file'
Expand Down
29 changes: 14 additions & 15 deletions build/commands/lib/affectedTests.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,21 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.

const { promisify } = require('util')
const { readFile, writeFile } = require('fs/promises')
const exec = promisify(require('child_process').execFile)
const path = require('path')
const config = require('./config')
const { unlink } = require('fs-extra')
const { randomUUID } = require('crypto')
const {
import { promisify } from 'node:util'
import { readFile, writeFile } from 'fs/promises'
import child_process from 'node:child_process'
import path from 'path'
import config from './config.js'
import fs from 'fs-extra'
import { randomUUID } from 'crypto'
import { tmpdir } from 'os'
import {
getApplicableFilters,
getTestsToRun,
gnTargetToExecutableName,
} = require('./testUtils')
const { tmpdir } = require('os')
} from './testUtils.js'

const exec = promisify(child_process.execFile)

const getTestTargets = (outDir, filters = ['//*']) => {
const { env, shell } = config.defaultOptions
Expand Down Expand Up @@ -113,7 +115,7 @@ async function analyzeAffectedTests(

const output = await readFile(analyzeOutJson, 'utf-8').then(JSON.parse)

await Promise.all([unlink(analyzeJson), unlink(analyzeOutJson)])
await Promise.all([fs.unlink(analyzeJson), fs.unlink(analyzeOutJson)])

return {
outDir,
Expand Down Expand Up @@ -167,7 +169,4 @@ async function getAffectedTests(args = {}) {
return [...new Set([...affectedTests, ...testAffectedDueModifiedFilterFiles])]
}

module.exports = {
analyzeAffectedTests,
getAffectedTests,
}
export { analyzeAffectedTests, getAffectedTests }
6 changes: 3 additions & 3 deletions build/commands/lib/applyPatches.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.

const config = require('../lib/config')
const util = require('../lib/util')
import config from './config.js'
import util from './util.js'

const applyPatches = (
buildConfig = config.defaultBuildConfig,
Expand All @@ -22,4 +22,4 @@ const applyPatches = (
})
}

module.exports = applyPatches
export default applyPatches
Loading
Loading