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 config/tsconfig.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,6 @@
}
]
},
"include": ["../static/app", "../static/gsApp", "../tests/js"],
"include": ["../static/app", "../static/gsApp", "../tests/js", "../static/eslint"],
"exclude": ["../node_modules"]
}
28 changes: 28 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
import invariant from 'invariant';
import typescript from 'typescript-eslint';

import * as sentryScrapsPlugin from './static/eslint/eslintPluginScraps/index.mjs';

Check warning on line 36 in eslint.config.mjs

View workflow job for this annotation

GitHub Actions / eslint

configs is not allowed to import eslint
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can’t to .ts because that would require a build-step for the eslint config 😭


invariant(react.configs.flat, 'For typescript');
invariant(react.configs.flat.recommended, 'For typescript');
invariant(react.configs.flat['jsx-runtime'], 'For typescript');
Expand Down Expand Up @@ -417,6 +419,13 @@
'import/no-unresolved': 'off', // Disabled in favor of typescript-eslint
},
},
{
name: 'plugin/@sentry/scraps',
plugins: {'@sentry/scraps': sentryScrapsPlugin},
rules: {
'@sentry/scraps/no-token-import': 'error',
},
},
{
name: 'plugin/no-relative-import-paths',
// https://github.com/MelvinVermeer/eslint-plugin-no-relative-import-paths?tab=readme-ov-file#rule-options
Expand Down Expand Up @@ -767,6 +776,20 @@
'import/no-nodejs-modules': 'off',
},
},
{
name: 'eslint',
files: ['static/eslint/**/*.mjs'],
languageOptions: {
globals: {
...globals.node,
},
},

rules: {
'no-console': 'off',
'import/no-nodejs-modules': 'off',
},
},
{
name: 'files/scripts',
files: ['scripts/**/*.{js,ts}', 'tests/js/test-balancer/index.js'],
Expand Down Expand Up @@ -1065,6 +1088,11 @@
type: 'scripts',
pattern: 'scripts',
},
// --- eslint ---
{
type: 'eslint',
pattern: 'static/eslint',
},
],
},
rules: {
Expand Down
1 change: 1 addition & 0 deletions jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@ const config: Config.InitialOptions = {
transform: {
'^.+\\.jsx?$': ['babel-jest', babelConfig as any],
'^.+\\.tsx?$': ['babel-jest', babelConfig as any],
'^.+\\.mjs?$': ['babel-jest', babelConfig as any],
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jest can’t read mjs so we now transform it with babel like all other ts files

'^.+\\.pegjs?$': '<rootDir>/tests/js/jest-pegjs-transform.js',
},
transformIgnorePatterns: [
Expand Down
3 changes: 3 additions & 0 deletions knip.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ const config: KnipConfig = {
...productionEntryPoints.map(entry => `${entry}!`),
...testingEntryPoints,
...storyBookEntryPoints,
'static/eslint/**/index.mjs',
],
project: [
'static/**/*.{js,mjs,ts,tsx}!',
Expand All @@ -61,6 +62,8 @@ const config: KnipConfig = {
// helper files for stories - it's fine that they are only used in tests
'!static/app/**/__stories__/*.{js,mjs,ts,tsx}!',
'!static/app/stories/**/*.{js,mjs,ts,tsx}!',
// ignore eslint plugins in production
'!static/eslint/**/*.mjs!',
// TEMPORARY! Abdullah Khan: WILL BE REMOVING IN STACKED PRs. Trying to merge PRs in smaller batches.
'!static/app/views/performance/newTraceDetails/traceModels/traceTreeNode/**/*.{js,mjs,ts,tsx}!',
],
Expand Down
5 changes: 5 additions & 0 deletions static/eslint/eslintPluginScraps/index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import noTokenImport from './no-token-import.mjs';

export const rules = {
'no-token-import': noTokenImport,
};
52 changes: 52 additions & 0 deletions static/eslint/eslintPluginScraps/no-token-import.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
const TOKEN_PATH = 'utils/theme/scraps';
const EXCEPT_DIR_NAME = 'static/app/utils/theme';

/**
*
* @param {unknown} importPath
* @returns {boolean}
*/
function isForbiddenImportPath(importPath) {
if (typeof importPath !== 'string') return false;

return importPath.includes(TOKEN_PATH);
}

/**
* @type {import('eslint').Rule.RuleModule}
*/
const noTokenImport = {
meta: {
type: 'problem',
docs: {
description: `Disallow imports from "${TOKEN_PATH}" except within a directory named "${EXCEPT_DIR_NAME}".`,
recommended: false,
},
schema: [],
messages: {
forbidden: `Do not import scraps tokens directly - prefer using theme tokens.`,
},
},

create(context) {
const importerIsInAllowedDir = context.filename?.includes(EXCEPT_DIR_NAME);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Path matching allows unintended directories

The includes() check for EXCEPT_DIR_NAME matches any path containing the substring 'static/app/utils/theme', which incorrectly allows imports from unintended directories like static/app/utils/theme-old/ or static/app/utils/themeConfig/. This violates the rule's intent to only allow imports from within the static/app/utils/theme/ directory. A proper directory boundary check is needed.

Fix in Cursor Fix in Web


return {
ImportDeclaration(node) {
if (!node || node.source.type !== 'Literal') return;
if (importerIsInAllowedDir) return;

const value = node.source.value;

if (isForbiddenImportPath(value)) {
context.report({
node,
messageId: 'forbidden',
});
}
},
};
},
};

export default noTokenImport;
31 changes: 31 additions & 0 deletions static/eslint/eslintPluginScraps/no-token-import.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {RuleTester} from 'eslint';

import noTokenImport from './no-token-import.mjs';

const ruleTester = new RuleTester();

ruleTester.run('no-token-import', noTokenImport, {
valid: [
{
code: 'import x from "other-package";',
filename: '/project/src/foo/file.ts',
},
{
code: 'const x = require("other-package");',
filename: '/project/src/foo/file.js',
},

{
code: 'import {colors} from "sentry/utils/theme/scraps/colors";',
filename: '/static/app/utils/theme/theme.tsx',
},
],

invalid: [
{
code: 'import {colors} from "sentry/utils/theme/scraps/colors";',
filename: '/static/app/index.tsx',
errors: [{messageId: 'forbidden'}],
},
],
});
11 changes: 10 additions & 1 deletion tests/js/setup.ts
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jest is just so weird. structuredClone exists in the node version we use but I think the jsdom env doesn’t see it? it exists in node:util to “polyfill” it but ts doesn’t know that. What a mess!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you could change the test env for this test, doesn't need to be jsdom https://jestjs.io/docs/configuration#testenvironment-string but some of the beforeEach etc might all break i guess

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I tried that and was hit with window is not defined or something similar :/

Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
import '@testing-library/jest-dom';

import {webcrypto} from 'node:crypto';
import {TextDecoder, TextEncoder} from 'node:util';
import {
// @ts-expect-error structuredClone is available in Node 17+ but types don't like it
structuredClone as nodeStructuredClone,
TextDecoder,
TextEncoder,
} from 'node:util';

import {type ReactElement} from 'react';
import {configure as configureRtl} from '@testing-library/react'; // eslint-disable-line no-restricted-imports
Expand Down Expand Up @@ -335,3 +340,7 @@ Object.defineProperty(global.self, 'crypto', {
subtle: webcrypto.subtle,
},
});

if (typeof globalThis.structuredClone === 'undefined') {
globalThis.structuredClone = nodeStructuredClone;
}
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"static/app",
"static/gsApp",
"static/gsAdmin",
"static/eslint",
"tests/js",
"config",
"scripts"
Expand Down
Loading