Skip to content

Commit 396ce6c

Browse files
authoredApr 11, 2024··
feat: add strictOptions to config to toggle --strict checks (#724)
1 parent 1c5f7d3 commit 396ce6c

15 files changed

+330
-21
lines changed
 

‎package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,8 @@
115115
"rxjs": "^7.8.1",
116116
"treeify": "^1.1.0",
117117
"uuid": "^9.0.1",
118-
"zod": "^3.22.4"
118+
"zod": "3.22.4",
119+
"zod-validation-error": "3.1.0"
119120
},
120121
"devDependencies": {
121122
"@commitlint/cli": "^19.2.1",

‎playground/ts/package.config.ts

+4
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,8 @@ import {defineConfig} from '@sanity/pkg-utils'
22

33
export default defineConfig({
44
tsconfig: 'tsconfig.dist.json',
5+
strictOptions: {
6+
noImplicitSideEffects: 'error',
7+
noImplicitBrowsersList: 'off',
8+
},
59
})

‎playground/ts/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
"version": "0.0.0-development",
44
"private": true,
55
"license": "MIT",
6-
"sideEffects": false,
76
"type": "module",
87
"exports": {
98
".": {
@@ -16,6 +15,7 @@
1615
},
1716
"main": "./dist/index.cjs",
1817
"types": "./dist/index.d.ts",
18+
"sideEffects": false,
1919
"scripts": {
2020
"build": "pkg build --strict --check --clean",
2121
"check:types": "tsc",

‎pnpm-lock.yaml

+13-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
+145
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2+
3+
exports[`{'noPackageJsonTypings': 'error'}} 1`] = `
4+
{
5+
"noImplicitBrowsersList": "warn",
6+
"noImplicitSideEffects": "warn",
7+
"noPackageJsonTypings": "error",
8+
}
9+
`;
10+
11+
exports[`{'noPackageJsonTypings': 'off'}} 1`] = `
12+
{
13+
"noImplicitBrowsersList": "warn",
14+
"noImplicitSideEffects": "warn",
15+
"noPackageJsonTypings": "off",
16+
}
17+
`;
18+
19+
exports[`{'noPackageJsonTypings': 'warn'}} 1`] = `
20+
{
21+
"noImplicitBrowsersList": "warn",
22+
"noImplicitSideEffects": "warn",
23+
"noPackageJsonTypings": "warn",
24+
}
25+
`;
26+
27+
exports[`{'noPackageJsonTypings': false}} 1`] = `
28+
[ZodError: [
29+
{
30+
"code": "invalid_union",
31+
"unionErrors": [
32+
{
33+
"issues": [
34+
{
35+
"received": false,
36+
"code": "invalid_literal",
37+
"expected": "error",
38+
"path": [
39+
"strictOptions",
40+
"noPackageJsonTypings"
41+
],
42+
"message": "Validation error: Invalid literal value, expected \\"error\\" at \\"strictOptions.noPackageJsonTypings\\""
43+
}
44+
],
45+
"name": "ZodError"
46+
},
47+
{
48+
"issues": [
49+
{
50+
"received": false,
51+
"code": "invalid_literal",
52+
"expected": "warn",
53+
"path": [
54+
"strictOptions",
55+
"noPackageJsonTypings"
56+
],
57+
"message": "Validation error: Invalid literal value, expected \\"warn\\" at \\"strictOptions.noPackageJsonTypings\\""
58+
}
59+
],
60+
"name": "ZodError"
61+
},
62+
{
63+
"issues": [
64+
{
65+
"received": false,
66+
"code": "invalid_literal",
67+
"expected": "off",
68+
"path": [
69+
"strictOptions",
70+
"noPackageJsonTypings"
71+
],
72+
"message": "Validation error: Invalid literal value, expected \\"off\\" at \\"strictOptions.noPackageJsonTypings\\""
73+
}
74+
],
75+
"name": "ZodError"
76+
}
77+
],
78+
"path": [
79+
"strictOptions",
80+
"noPackageJsonTypings"
81+
],
82+
"message": "Validation error: Validation error: Invalid literal value, expected \\"error\\" at \\"strictOptions.noPackageJsonTypings\\" at \\"strictOptions.noPackageJsonTypings\\", or Validation error: Invalid literal value, expected \\"warn\\" at \\"strictOptions.noPackageJsonTypings\\" at \\"strictOptions.noPackageJsonTypings\\", or Validation error: Invalid literal value, expected \\"off\\" at \\"strictOptions.noPackageJsonTypings\\" at \\"strictOptions.noPackageJsonTypings\\""
83+
}
84+
]]
85+
`;
86+
87+
exports[`{'noPackageJsonTypings': true}} 1`] = `
88+
[ZodError: [
89+
{
90+
"code": "invalid_union",
91+
"unionErrors": [
92+
{
93+
"issues": [
94+
{
95+
"received": true,
96+
"code": "invalid_literal",
97+
"expected": "error",
98+
"path": [
99+
"strictOptions",
100+
"noPackageJsonTypings"
101+
],
102+
"message": "Validation error: Invalid literal value, expected \\"error\\" at \\"strictOptions.noPackageJsonTypings\\""
103+
}
104+
],
105+
"name": "ZodError"
106+
},
107+
{
108+
"issues": [
109+
{
110+
"received": true,
111+
"code": "invalid_literal",
112+
"expected": "warn",
113+
"path": [
114+
"strictOptions",
115+
"noPackageJsonTypings"
116+
],
117+
"message": "Validation error: Invalid literal value, expected \\"warn\\" at \\"strictOptions.noPackageJsonTypings\\""
118+
}
119+
],
120+
"name": "ZodError"
121+
},
122+
{
123+
"issues": [
124+
{
125+
"received": true,
126+
"code": "invalid_literal",
127+
"expected": "off",
128+
"path": [
129+
"strictOptions",
130+
"noPackageJsonTypings"
131+
],
132+
"message": "Validation error: Invalid literal value, expected \\"off\\" at \\"strictOptions.noPackageJsonTypings\\""
133+
}
134+
],
135+
"name": "ZodError"
136+
}
137+
],
138+
"path": [
139+
"strictOptions",
140+
"noPackageJsonTypings"
141+
],
142+
"message": "Validation error: Validation error: Invalid literal value, expected \\"error\\" at \\"strictOptions.noPackageJsonTypings\\" at \\"strictOptions.noPackageJsonTypings\\", or Validation error: Invalid literal value, expected \\"warn\\" at \\"strictOptions.noPackageJsonTypings\\" at \\"strictOptions.noPackageJsonTypings\\", or Validation error: Invalid literal value, expected \\"off\\" at \\"strictOptions.noPackageJsonTypings\\" at \\"strictOptions.noPackageJsonTypings\\""
143+
}
144+
]]
145+
`;

‎src/node/core/config/types.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ import type {PluginItem as BabelPluginItem} from '@babel/core'
22
import type {OptimizeLodashOptions} from '@optimize-lodash/rollup-plugin'
33
import type {NormalizedOutputOptions, Plugin as RollupPlugin, TreeshakingOptions} from 'rollup'
44

5+
import type {StrictOptions} from '../../strict'
6+
57
// re-export
6-
export type {RollupPlugin}
8+
export type {RollupPlugin, StrictOptions}
79

810
/** @public */
911
export type PkgFormat = 'commonjs' | 'esm'
@@ -157,4 +159,8 @@ export interface PkgConfigOptions {
157159
*/
158160
src?: string
159161
tsconfig?: string
162+
/**
163+
* Configure what checks are made when running `--strict` builds and checks
164+
*/
165+
strictOptions?: StrictOptions
160166
}

‎src/node/core/pkg/parseExports.ts

+15-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type {Logger} from '../../logger'
2+
import type {InferredStrictOptions} from '../../strict'
13
import {defaultEnding, fileEnding, legacyEnding} from '../../tasks/dts/getTargetPaths'
24
import type {PkgExport} from '../config'
35
import {isRecord} from '../isRecord'
@@ -9,12 +11,22 @@ import {validateExports} from './validateExports'
911
export function parseExports(options: {
1012
pkg: PackageJSON
1113
strict: boolean
14+
strictOptions: InferredStrictOptions
1215
legacyExports: boolean
16+
logger: Logger
1317
}): (PkgExport & {_path: string})[] {
14-
const {pkg, strict, legacyExports} = options
18+
const {pkg, strict, strictOptions, legacyExports, logger} = options
1519
const type = pkg.type || 'commonjs'
1620
const errors: string[] = []
1721

22+
const report = (kind: 'warn' | 'error', message: string) => {
23+
if (kind === 'warn') {
24+
logger.warn(message)
25+
} else {
26+
errors.push(message)
27+
}
28+
}
29+
1830
if (pkg.source) {
1931
if (
2032
strict &&
@@ -145,8 +157,8 @@ export function parseExports(options: {
145157

146158
// @TODO validate typesVersions when legacyExports is true
147159

148-
if (strict && 'typings' in pkg) {
149-
errors.push('package.json: `typings` should be `types`')
160+
if (strict && strictOptions.noPackageJsonTypings !== 'off' && 'typings' in pkg) {
161+
report(strictOptions.noPackageJsonTypings, 'package.json: `typings` should be `types`')
150162
}
151163

152164
if (strict && !pkg.types && pkg.source?.endsWith('.ts')) {

‎src/node/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ export * from './core'
44
export * from './init'
55
export * from './logger'
66
export * from './resolveBuildTasks'
7+
export {type InferredStrictOptions, strictOptions, toggle, type ToggleType} from './strict'
78
export * from './tasks'
89
export * from './watch'

‎src/node/resolveBuildContext.ts

+28-8
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {findCommonDirPath, pathContains} from './core/findCommonPath'
1717
import type {Logger} from './logger'
1818
import {resolveBrowserTarget} from './resolveBrowserTarget'
1919
import {resolveNodeTarget} from './resolveNodeTarget'
20+
import {parseStrictOptions} from './strict'
2021

2122
export async function resolveBuildContext(options: {
2223
config?: PkgConfigOptions
@@ -37,24 +38,41 @@ export async function resolveBuildContext(options: {
3738
tsconfig: tsconfigPath,
3839
} = options
3940
const tsconfig = await loadTSConfig({cwd, tsconfigPath})
41+
const strictOptions = parseStrictOptions(config?.strictOptions ?? {})
4042

4143
/* eslint-disable padding-line-between-statements */
4244
let browserslist = pkg.browserslist
4345
if (!browserslist) {
44-
if (strict) {
45-
logger.warn(
46-
'Could not detect a `browserslist` property in `package.json`, using default configuration. Add `"browserslist": "extends @sanity/browserslist-config"` to silence this warning.',
47-
)
46+
if (strict && strictOptions.noImplicitBrowsersList !== 'off') {
47+
if (strictOptions.noImplicitBrowsersList === 'error') {
48+
throw new Error(
49+
'\n- ' +
50+
`package.json: "browserslist" is missing, set it to \`"browserslist": "extends @sanity/browserslist-config"\``,
51+
)
52+
} else {
53+
logger.warn(
54+
'Could not detect a `browserslist` property in `package.json`, using default configuration. Add `"browserslist": "extends @sanity/browserslist-config"` to silence this warning.',
55+
)
56+
}
4857
}
4958
browserslist = DEFAULT_BROWSERSLIST_QUERY
5059
}
5160
const targetVersions = browserslistToEsbuild(browserslist)
5261
/* eslint-enable padding-line-between-statements */
5362

54-
if (strict && typeof pkg.sideEffects === 'undefined') {
55-
logger.error(
56-
'No `sideEffects` field in `package.json`, assuming all files are side-effectful. Add `"sideEffects": true` to silence this warning. See https://webpack.js.org/guides/tree-shaking/#clarifying-tree-shaking-and-sideeffects for how to define `sideEffects`.',
57-
)
63+
if (
64+
strict &&
65+
strictOptions.noImplicitSideEffects !== 'off' &&
66+
typeof pkg.sideEffects === 'undefined'
67+
) {
68+
const msg =
69+
'package.json: `sideEffects` is missing, see https://webpack.js.org/guides/tree-shaking/#clarifying-tree-shaking-and-sideeffects for how to define `sideEffects`'
70+
71+
if (strictOptions.noImplicitSideEffects === 'error') {
72+
throw new Error(msg)
73+
} else {
74+
logger.warn(msg)
75+
}
5876
}
5977

6078
const nodeTarget = resolveNodeTarget(targetVersions)
@@ -78,6 +96,8 @@ export async function resolveBuildContext(options: {
7896
pkg,
7997
strict,
8098
legacyExports: config?.legacyExports ?? false,
99+
strictOptions,
100+
logger,
81101
}).reduce<PkgExports>((acc, x) => {
82102
const {_path: exportPath, ...exportEntry} = x
83103

‎src/node/strict.test-d.ts

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import {expectTypeOf, test} from 'vitest'
2+
3+
import {InferredStrictOptions, StrictOptions} from './strict'
4+
5+
test('the zod schema types matches the manual types', () => {
6+
expectTypeOf<StrictOptions>().toEqualTypeOf<Partial<InferredStrictOptions>>()
7+
})

‎src/node/strict.test.ts

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import {expect, test} from 'vitest'
2+
3+
import {parseStrictOptions} from './strict'
4+
5+
test.each([
6+
{key: 'noPackageJsonTypings', value: 'error', fails: false},
7+
{key: 'noPackageJsonTypings', value: 'warn', fails: false},
8+
{key: 'noPackageJsonTypings', value: 'off', fails: false},
9+
{key: 'noPackageJsonTypings', value: true, fails: true},
10+
{key: 'noPackageJsonTypings', value: false, fails: true},
11+
])('{$key: $value}}', ({key, value, fails}) => {
12+
if (fails) {
13+
expect(() => parseStrictOptions({[key]: value})).toThrowErrorMatchingSnapshot()
14+
} else {
15+
expect(parseStrictOptions({[key]: value})).toMatchSnapshot()
16+
}
17+
})

‎src/node/strict.ts

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import {z} from 'zod'
2+
import {errorMap} from 'zod-validation-error'
3+
4+
/**
5+
* @public
6+
*/
7+
export const toggle = z.union([z.literal('error'), z.literal('warn'), z.literal('off')])
8+
9+
/**
10+
* @public
11+
*/
12+
export type ToggleType = z.infer<typeof toggle>
13+
14+
/**
15+
* @public
16+
*/
17+
export const strictOptions = z
18+
.object({
19+
noPackageJsonTypings: toggle.default('error'),
20+
noImplicitSideEffects: toggle.default('warn'),
21+
noImplicitBrowsersList: toggle.default('warn'),
22+
})
23+
.strict()
24+
25+
/**
26+
* To make error message paths line up with the paths in package.config.ts the schema is hoisted into a root schema
27+
* This way errors will say `Expected boolean, received string at "strict.noPackageJsonTypings"` instead of `Expected boolean, received string at "noPackageJsonTypings"`.
28+
*/
29+
const validationSchema = z.object({
30+
strictOptions: strictOptions.default({}),
31+
})
32+
33+
/**
34+
* @public
35+
*/
36+
export type InferredStrictOptions = z.infer<typeof strictOptions>
37+
38+
/**
39+
* @public
40+
*/
41+
export interface StrictOptions {
42+
/**
43+
* Disallows a top level `typings` field in `package.json` if it is equal to `exports['.'].source`.
44+
* @defaultValue 'error'
45+
*/
46+
noPackageJsonTypings?: ToggleType
47+
/**
48+
* Requires specifying `sideEffects` in `package.json`.
49+
* @defaultValue 'warn'
50+
*/
51+
noImplicitSideEffects?: ToggleType
52+
/**
53+
* Requires specifying `browserslist` in `package.json`, instead of relying on it implicitly being:
54+
* @example
55+
* ```
56+
* "browserslist": "extends @sanity/browserslist-config"
57+
* ```
58+
* @defaultValue 'warn'
59+
*/
60+
noImplicitBrowsersList?: ToggleType
61+
}
62+
63+
/** @internal */
64+
export function parseStrictOptions(input: unknown): InferredStrictOptions {
65+
return validationSchema.parse({strictOptions: input}, {errorMap}).strictOptions
66+
}

‎test/parseExports.test.ts

+9-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {describe, expect, test} from 'vitest'
22

3-
import {type PackageJSON, parseExports} from '../src/node'
3+
import {createLogger, type PackageJSON, parseExports} from '../src/node'
4+
import {parseStrictOptions} from '../src/node/strict'
45

56
const name = 'test'
67
const version = '0.0.0-test'
@@ -12,6 +13,8 @@ const defaults = {
1213
default: './dist/index.js',
1314
},
1415
} as const
16+
const strictOptions = parseStrictOptions({})
17+
const logger = createLogger()
1518

1619
describe.each([
1720
{type: 'commonjs' as const, legacyExports: false},
@@ -22,8 +25,11 @@ describe.each([
2225
{type: undefined, legacyExports: true},
2326
])('parseExports({type: $type, legacyExports: $legacyExports})', ({type, legacyExports}) => {
2427
const testParseExports = (
25-
options: Omit<Parameters<typeof parseExports>[0], 'strict' | 'legacyExports'>,
26-
) => parseExports({strict: true, legacyExports, ...options})
28+
options: Omit<
29+
Parameters<typeof parseExports>[0],
30+
'strict' | 'strictOptions' | 'legacyExports' | 'logger'
31+
>,
32+
) => parseExports({strict: true, legacyExports, logger, strictOptions, ...options})
2733
const reference = {
2834
'.': {
2935
source: defaults['.'].source,

‎test/parseTasks.test.ts

+13-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
import {expect, test, vi} from 'vitest'
22

3-
import {type BuildContext, type PackageJSON, parseExports, resolveBuildTasks} from '../src/node'
3+
import {
4+
type BuildContext,
5+
createLogger,
6+
type PackageJSON,
7+
parseExports,
8+
resolveBuildTasks,
9+
} from '../src/node'
10+
import {parseStrictOptions} from '../src/node/strict'
11+
12+
const strictOptions = parseStrictOptions({})
13+
const logger = createLogger()
414

515
test('should parse tasks (type: module)', () => {
616
const pkg: PackageJSON = {
@@ -30,7 +40,7 @@ test('should parse tasks (type: module)', () => {
3040
},
3141
}
3242

33-
const exports = parseExports({pkg, strict: true, legacyExports: false})
43+
const exports = parseExports({pkg, strict: true, strictOptions, logger, legacyExports: false})
3444

3545
const ctx: BuildContext = {
3646
cwd: '/test',
@@ -165,7 +175,7 @@ test('should parse tasks (type: commonjs, legacyExports: true)', () => {
165175
},
166176
}
167177

168-
const exports = parseExports({pkg, strict: true, legacyExports: true})
178+
const exports = parseExports({pkg, strict: true, logger, strictOptions, legacyExports: true})
169179

170180
const ctx: BuildContext = {
171181
config: {legacyExports: true},

‎tsconfig.settings.json

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
// "esModuleInterop": true,
1414
// "resolveJsonModule": true,
1515
"moduleDetection": "force",
16+
"isolatedModules": true,
17+
"forceConsistentCasingInFileNames": true,
1618

1719
// Type checking
1820
"allowSyntheticDefaultImports": true,

0 commit comments

Comments
 (0)
Please sign in to comment.