Skip to content
Open
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
3 changes: 2 additions & 1 deletion apps/docs/src/content/getting-started/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ The file holds the same options as `createPangular()`, as JSON. Save this as `pa
{
"inspectors": {"analog": false},
"agent": {"readOnly": true},
"redaction": {"secretNames": ["passport"]}
"redaction": {"secretNames": ["passport"]},
"lint": {"rules": {"img-missing-alt": false}}
}
```

Expand Down
21 changes: 21 additions & 0 deletions apps/docs/src/content/getting-started/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ interface PangularConfig {
};
popup?: {hotkey?: string | false};
plugins?: Record<string, boolean | {enabled?: boolean; tools?: boolean; actions?: boolean}>;
lint?: {rules?: Record<string, boolean>};
}
```

Expand Down Expand Up @@ -264,6 +265,25 @@ The server plugins themselves go in `serverPlugins`, an option of the Express hu

It is off by default because it starts processes on your machine. A value other than `true` or `false` prints a warning and keeps it off.

### Lint rules

| Option | Default | What it does |
| ------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------- |
| `lint.rules.<rule>` | `true` | `false` turns off an individual lint rule across linters (template lint, pipe usage, routes, forms and Analog diagnostics). |

```json
{
"lint": {
"rules": {
"img-missing-alt": false,
"track-by-index": false
}
}
}
```

Rules set to `false` are omitted from panel findings, agent tools (`scan-templates`, `scan-pipe-usage`, `lint-routes`, `lint-forms`, `analog-lint`), `pangular audit` and the CI exporter (`pangular lint`).

## Check the active configuration

The server checks the options when it starts. An unknown key, a value of the wrong type or a limit outside its bounds prints one `[pangular]` warning in the terminal that lists each problem and the value used instead. An unknown key names the closest known key:
Expand All @@ -290,6 +310,7 @@ The [Dashboard](../inspectors/dashboard.md#configuration-block) has a **Configur
| **Popup shortcut** | `popup.hotkey`, or **Off** when it is `false`. |
| **Plugins off** | Plugins set to `false` in `plugins`. |
| **Plugin tools or actions off** | Plugins with `tools` or `actions` set to `false`. |
| **Lint rules off** | Rules set to `false` in `lint.rules`. |

## Where to next

Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion extension/ui/index.html

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 19 additions & 0 deletions packages/devtools/src/__tests__/audit-tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -412,4 +412,23 @@ describe('the audit RPC functions for the panel', () => {
ok: false,
});
});

it('excludes findings for rules turned off in config.lint.rules', async () => {
const { call } = await boot({
lint: { rules: { 'wildcard-not-last': false } },
inspectors: { pipes: false },
});
await call('push-component-tree', tree('tab1'));
await call('push-router', {
pageId: 'tab1',
reportedAt: Date.now(),
config: [
{ path: '**', fullPath: '**' },
{ path: 'home', fullPath: 'home' },
],
setup: { features: {}, guards: {} },
});
const result = await call('get-audit', { pageId: 'tab1' });
expect(result.report.findings.filter((f: { area: string }) => f.area === 'router')).toEqual([]);
});
});
39 changes: 39 additions & 0 deletions packages/devtools/src/__tests__/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
agentAllowed,
agentSafety,
connectionConfig,
isLintRuleEnabled,
pangularConfigProblems,
pickPangularConfig,
resolvePangularConfig,
Expand Down Expand Up @@ -76,6 +77,27 @@ describe('resolvePangularConfig', () => {
tanstackQuery: true,
plugins: true,
});
expect(config.lint).toEqual({ rules: {} });
});

it('resolves lint rules and tests whether a rule is enabled', () => {
const config = resolvePangularConfig({
lint: {
rules: {
'img-missing-alt': false,
'track-by-index': true,
invalid: 'no' as unknown as boolean,
},
},
});
expect(config.lint.rules).toEqual({
'img-missing-alt': false,
'track-by-index': true,
});
expect(isLintRuleEnabled('img-missing-alt', config)).toBe(false);
expect(isLintRuleEnabled('track-by-index', config)).toBe(true);
expect(isLintRuleEnabled('other-rule', config)).toBe(true);
expect(isLintRuleEnabled('img-missing-alt', undefined)).toBe(true);
});

it('carries a disabled inspector into its agent tools and panel actions', () => {
Expand Down Expand Up @@ -226,6 +248,7 @@ describe('summarizePangularConfig', () => {
actions: { forms: false, http: false, ngrx: false },
redaction: { secretNames: ['passport'], unmask: ['pin'] },
limits: { refreshMs: 1000, httpCalls: 50 },
lint: { rules: { 'img-missing-alt': false, 'track-by-index': true } },
});
expect(summarizePangularConfig(config)).toEqual([
{ label: 'Inspectors off', value: 'NgRx, Pipes' },
Expand All @@ -235,6 +258,7 @@ describe('summarizePangularConfig', () => {
{ label: 'Extra secret names', value: 'passport' },
{ label: 'Unmasked names', value: 'pin' },
{ label: 'Limits', value: 'poll every 1000 ms, 50 HTTP calls' },
{ label: 'Lint rules off', value: 'img-missing-alt' },
]);
});
});
Expand Down Expand Up @@ -631,6 +655,7 @@ describe('pangularConfigProblems', () => {
actions: { ngrx: false },
redaction: { secretNames: ['passport'], unmask: ['pin'] },
limits: { refreshMs: 1000 },
lint: { rules: { 'img-missing-alt': false } },
};
expect(pangularConfigProblems(valid)).toEqual([]);
expect(pangularConfigProblems(resolvePangularConfig(valid))).toEqual([]);
Expand Down Expand Up @@ -661,6 +686,20 @@ describe('pangularConfigProblems', () => {
expect(pangularConfigProblems({ agent: { readOnly: 'true' } })).toEqual([
'`agent.readOnly` should be true or false, got the string "true". It was ignored, so it is off.',
]);
expect(pangularConfigProblems({ lint: 'false' as never })).toEqual([
'`lint` should be an object, got the string "false". It was ignored.',
]);
expect(pangularConfigProblems({ lint: { rules: 'none' as never } })).toEqual([
'`lint.rules` should be an object, got the string "none". It was ignored.',
]);
expect(
pangularConfigProblems({ lint: { rules: { 'img-missing-alt': 'no' as never } } }),
).toEqual([
'`lint.rules.img-missing-alt` should be true or false, got the string "no". It was ignored, so it is on.',
]);
expect(pangularConfigProblems({ lint: { ruls: {} } as never })).toEqual([
'Unknown option `lint.ruls` was ignored. Did you mean `rules`?',
]);
});

it('prints one warning from createPangular and none for a clean config', () => {
Expand Down
26 changes: 26 additions & 0 deletions packages/devtools/src/__tests__/lint-export.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,21 @@ describe('runSourceLints', () => {
expect(failsAt(result, 'warning')).toBe(true);
expect(failsAt(result, 'info')).toBe(true);
});

it('omits findings for rules disabled via options.rules or options.config', async () => {
const dir = project();
const withRules = await runSourceLints(dir, {
compiler,
rules: { 'img-missing-alt': false },
});
expect(withRules.findings.map((f) => f.rule)).toEqual(['impure-pipe-in-for']);

const withConfig = await runSourceLints(dir, {
compiler,
config: { lint: { rules: { 'impure-pipe-in-for': false } } },
});
expect(withConfig.findings.map((f) => f.rule)).toEqual(['img-missing-alt']);
});
});

describe('toSarif', () => {
Expand Down Expand Up @@ -186,4 +201,15 @@ describe('pangular lint', () => {
const unknown = await run(['lint', '--root', dir, '--only', 'forms']);
expect(unknown.notes.join('\n')).toContain('Unknown lint "forms"');
});

it('respects rules turned off in pangular.config.json', async () => {
const dir = project();
writeFileSync(
join(dir, 'pangular.config.json'),
JSON.stringify({ lint: { rules: { 'impure-pipe-in-for': false } } }),
);
const { out } = await run(['lint', '--root', dir, '--only', 'pipes']);
const json = JSON.parse(out);
expect(json.findings).toEqual([]);
});
});
6 changes: 5 additions & 1 deletion packages/devtools/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,11 @@ export function createPangularCli(options: PangularCliOptions = {}) {
cli.option('--config <file>', `Devtools config as JSON (default: ${PANGULAR_CONFIG_FILE})`);
cli.option('--read-only', 'Drop the agent tools that act on the page or the server');
registerSkillsCommand(cli, { cwd: (flags) => resolveCliRoot(flags, env), log });
registerLintCommand(cli, { cwd: (flags) => resolveCliRoot(flags, env), log });
registerLintCommand(cli, {
cwd: (flags) => resolveCliRoot(flags, env),
config: (flags, root) => loadCliConfig(flags, root, env),
log,
});
const defaultCommand = cli.commands.find((command) => command.isDefaultCommand);
if (defaultCommand) {
const run = async (
Expand Down
62 changes: 60 additions & 2 deletions packages/devtools/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,11 @@ export interface PangularConfig {
* `inspectors.plugins: false` turns every plugin off.
*/
plugins?: Record<string, boolean | PluginSwitchInput>;
/** Turn individual lint rules off: `{ 'img-missing-alt': false }`. */
lint?: {
/** Rule status by rule name. `false` turns the rule off; `true` or omitted keeps it on. */
rules?: Record<string, boolean>;
};
}

/** What a `plugins` entry can turn off; each is on when left out. */
Expand Down Expand Up @@ -263,6 +268,9 @@ export interface ResolvedPangularConfig {
popup: { hotkey: string | false };
/** The `plugins` entries that were given, by plugin id. */
plugins: Record<string, PluginSwitches>;
lint: {
rules: Record<string, boolean>;
};
}

/** The switches of one plugin under `config`, whether or not it has a `plugins` entry. */
Expand Down Expand Up @@ -648,9 +656,37 @@ export function resolvePangularConfig(config?: unknown): ResolvedPangularConfig
) as Record<PangularLimit, number>,
popup: { hotkey: popupHotkey(record(input['popup'])['hotkey']) },
plugins: pluginEntries(input['plugins']),
lint: { rules: lintRules(input['lint']) },
};
}

const MAX_LINT_RULE_ENTRIES = 200;

function lintRules(value: unknown): Record<string, boolean> {
const rules = record(record(value)['rules']);
const resolved: Record<string, boolean> = {};
for (const [key, val] of Object.entries(rules).slice(0, MAX_LINT_RULE_ENTRIES)) {
if (typeof val === 'boolean') {
resolved[key] = val;
}
}
return resolved;
}

/**
* Whether a lint rule is enabled by the devtools config: on unless the config
* explicitly sets `lint.rules[rule]` to `false`.
*/
export function isLintRuleEnabled(
rule: string,
config?: ResolvedPangularConfig | PangularConfig,
): boolean {
if (!config) return true;
const rules = 'lint' in config && config.lint ? config.lint.rules : undefined;
if (!rules || typeof rules !== 'object') return true;
return rules[rule] !== false;
}

const MAX_PLUGIN_ENTRIES = 100;

function pluginEntries(value: unknown): Record<string, PluginSwitches> {
Expand Down Expand Up @@ -713,6 +749,7 @@ const CONFIG_KEYS = [
'limits',
'popup',
'plugins',
'lint',
] as const;

/**
Expand Down Expand Up @@ -834,6 +871,21 @@ export function pangularConfigProblems(config: unknown): string[] {
booleans(`plugins.${id}`, entry, ['enabled', 'tools', 'actions'], 'on');
}
}
const lint = object('lint', input['lint'], ['rules']);
const rules = lint['rules'];
if (rules !== undefined) {
if (!isRecord(rules)) {
problems.push(`\`lint.rules\` should be an object, got ${describe(rules)}. It was ignored.`);
} else {
for (const [key, value] of Object.entries(rules)) {
if (typeof value !== 'boolean') {
problems.push(
`\`lint.rules.${key}\` should be true or false, got ${describe(value)}. It was ignored, so it is on.`,
);
}
}
}
}
return problems;
}

Expand Down Expand Up @@ -868,8 +920,8 @@ export function configFromConnection(
export function pickPangularConfig<T extends PangularConfig>(
options: T,
): { config: PangularConfig; rest: Omit<T, keyof PangularConfig> } {
const { inspectors, agent, actions, redaction, limits, popup, plugins, ...rest } = options;
return { config: { inspectors, agent, actions, redaction, limits, popup, plugins }, rest };
const { inspectors, agent, actions, redaction, limits, popup, plugins, lint, ...rest } = options;
return { config: { inspectors, agent, actions, redaction, limits, popup, plugins, lint }, rest };
}

/** Form actions that work without a `formId`: they act on the page, not on one form. */
Expand Down Expand Up @@ -1780,6 +1832,12 @@ export function summarizePangularConfig(
`${id} (${[!entry.tools && 'tools', !entry.actions && 'actions'].filter(Boolean).join(', ')})`,
),
);
add(
'Lint rules off',
Object.entries(config.lint?.rules ?? {})
.filter(([, enabled]) => !enabled)
.map(([rule]) => rule),
);
return items;
}

Expand Down
Loading
Loading