Skip to content

Commit

Permalink
Add tsdx replacement
Browse files Browse the repository at this point in the history
* build.mjs for consistent builds + package.json validation
* Common TypeScript config
* Common ESLint config for TypeScript and React
* Common Prettier config
* Common Jest config
* Fix pre-commit setup to actually run eslint

Change-Id: If14ce54ffa6e7ec64413096611a8a033ab60a033
  • Loading branch information
jaslong committed Apr 24, 2023
1 parent 5893e0f commit 73ec801
Show file tree
Hide file tree
Showing 10 changed files with 2,525 additions and 67 deletions.
11 changes: 11 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/** ESLint config for packages NOT using frameworks like React. */
module.exports = {
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
root: true,
};
9 changes: 9 additions & 0 deletions .eslintrc.react.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/** ESLint config for packages using React. */
module.exports = {
extends: ['./.eslintrc.js', 'react-app'],
settings: {
react: {
version: 'detect',
},
},
};
42 changes: 19 additions & 23 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,33 +1,29 @@
repos:
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v2.3.1
hooks:
- id: prettier
files: ^.*\.(js|jsx|ts|tsx|json|css|scss|md|toml|xml)$
exclude: (^.*/plasmic/.*$|.md$)
additional_dependencies:
- [email protected]
- prettier-plugin-organize-imports
- typescript
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v2.2.0
hooks:
- id: prettier
files: ^.*/plasmic/.*\.(js|jsx|ts|tsx|json|css|scss|md|toml|xml)$
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v2.3.0
rev: v4.4.0
hooks:
- id: check-merge-conflict
- id: trailing-whitespace
exclude: ^.*/__snapshots__/.*$
- repo: https://github.com/pre-commit/mirrors-eslint
rev: v7.20.0
rev: v8.38.0
hooks:
- id: eslint
files: ^wab/src/.*\.(ts|tsx)$
types: [file]
files: ^.*\.(ts|tsx)$
exclude: ^.*/plasmic/.*$
additional_dependencies:
- "@typescript-eslint/[email protected]"
- "@typescript-eslint/[email protected]"
- "[email protected]"
- "[email protected]"
- "[email protected]"
- "[email protected]"
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v2.7.1
hooks:
- id: prettier
types: [file]
files: ^.*\.(js|jsx|ts|tsx|json|css|scss|toml|xml)$
exclude: ^.*/plasmic/.*$
additional_dependencies:
- eslint
- "@typescript-eslint/eslint-plugin"
- "@typescript-eslint/parser"
- [email protected]
- [email protected]
6 changes: 4 additions & 2 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
{
"trailingComma": "es5",
"semi": true
"printWidth": 80,
"semi": true,
"singleQuote": true,
"trailingComma": "es5"
}
169 changes: 169 additions & 0 deletions build.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
/**
* Validates package.json and builds bundles in a consistently across our packages.
*/

import fs from 'fs/promises';
import console from 'console';
import path from 'path';
import process from 'process';
import esbuild from 'esbuild';

async function main() {
if (process.argv.length < 3) {
throw new Error('missing entry point');
}

const entryPoint = process.argv[2];
const options = process.argv.slice(3);

const useClient = findAndRemoveOption(options, '--use-client');
// By default, we only ship cjs bundles. Use this option to also ship esm bundles.
// However, there are many issues that need to be resolved before turning this option on.
// https://app.shortcut.com/plasmic/story/33688/es-module-support
const esm = findAndRemoveOption(options, '--esm-do-not-use');
const watch = findAndRemoveOption(options, '--watch');
if (options.length > 0) {
throw new Error(`unknown or duplicate options: ${options.join(' ')}`);
}

const name = path.parse(entryPoint).name;

const expectedPackageJson = generateExpectedPackageJson(name, esm);
const actualPackageJson = JSON.parse(
await fs.readFile(path.resolve('package.json'))
);
validatePackageJson(actualPackageJson, expectedPackageJson);

const ctx = {
entryPoint,
name,
formats: esm ? ['cjs', 'esm'] : ['cjs'],
useClient,
watch,
};

await buildBundle(ctx);
}

function findAndRemoveOption(options, option) {
const index = options.indexOf(option);
const found = index >= 0;
if (found) {
options.splice(index, 1);
}
return found;
}

function generateExpectedPackageJson(name, esm) {
const subpath = name === 'index' ? '.' : `./${name}`;
const packageJson = {
exports: {
[subpath]: generateExpectedPackageJsonSubpath(name, esm),
},
};

if (name === 'index') {
packageJson.types = './dist/index.d.ts';
packageJson.main = './dist/index.js';
if (esm) {
// "index.esm.js" should be set as the "module" field for webpack 4 and other tools,
// since they don't support the "exports" field.
// We change the extension from ".mjs" to ".js" because ".mjs" doesn't work properly in webpack 4.
// https://github.com/adobe/react-spectrum/pull/4038
packageJson.module = './dist/index.esm.js';
}
} else if (name === 'react-server') {
packageJson.exports['./react-server-conditional'] = {
'react-server': generateExpectedPackageJsonSubpath('react-server', esm),
default: generateExpectedPackageJsonSubpath('index', esm),
};
}

return packageJson;
}

function generateExpectedPackageJsonSubpath(name, esm) {
if (esm) {
return {
types: `./dist/${name}.d.ts`,
import: `./dist/${name}.mjs`,
require: `./dist/${name}.js`,
};
} else {
return {
types: `./dist/${name}.d.ts`,
default: `./dist/${name}.js`,
};
}
}

/** Validates that `expected` is a subset of `actual`. Throws if not. */
function validatePackageJson(actual, expected, path = '') {
for (const [key, expectedValue] of Object.entries(expected)) {
const nestedPath = path ? `${path} > "${key}"` : `"${key}"`;
if (!(key in actual)) {
throw new Error(`package.json ${nestedPath} field missing`);
}

if (typeof expectedValue === 'string') {
if (expectedValue !== actual[key]) {
throw new Error(
`package.json ${nestedPath} field should be "${expectedValue}"`
);
}
} else {
validatePackageJson(actual[key], expectedValue, nestedPath);
}
}
}

async function buildBundle({ entryPoint, name, formats, useClient, watch }) {
return Promise.all(
formats.map(async (format) => {
const outfile = `dist/${name}.${format === 'cjs' ? 'js' : 'mjs'}`;
const esbuildOptions = {
bundle: true,
packages: 'external', // don't bundle node_modules

entryPoints: [entryPoint],
format,
target: 'es6',
outfile,

banner: {
js: useClient ? `"use client";` : ``,
},

sourcemap: true,
};
if (watch) {
await (await esbuild.context(esbuildOptions)).watch();
console.info(
`watching and rebuilding ${format.toUpperCase()} bundle at "${outfile}"...`
);
} else {
await esbuild.build(esbuildOptions);
console.info(`built ${format.toUpperCase()} bundle at "${outfile}"`);

// Copy "index.mjs" to "index.esm.js".
// "index.esm.js" should be set as the "module" field for webpack 4 and other tools,
// since they don't support the "exports" field.
// We change the extension from ".mjs" to ".js" because ".mjs" doesn't work properly in webpack 4.
// https://github.com/adobe/react-spectrum/pull/4038
if (outfile === 'dist/index.mjs') {
await fs.copyFile('dist/index.mjs', 'dist/index.esm.js');
console.info(
`built ${format.toUpperCase()} bundle at "dist/index.esm.js"`
);
}
}
})
);
}

try {
await main();
} catch (error) {
console.error(error);
process.exit(1);
}
17 changes: 17 additions & 0 deletions jest-transform-esbuild.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const esbuild = require('esbuild');

// This transforms TypeScript to JavaScript for Jest.
// We use esbuild here for speed and consistency with our actual builds.
// https://jestjs.io/docs/code-transformation
module.exports = {
process: (sourceText, sourcePath, _options) => {
const { code, map } = esbuild.transformSync(sourceText, {
format: 'cjs',
loader: 'ts',
sourcefile: sourcePath,
sourcemap: 'both',
target: `node${process.versions.node}`,
});
return { code, map };
},
};
6 changes: 6 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = {
testRegex: '.(spec|test).(js|jsx|ts|tsx)$',
transform: {
'\\.tsx?$': '<rootDir>/jest-transform-esbuild.js',
},
};
11 changes: 10 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,18 @@
"name": "root",
"private": true,
"devDependencies": {
"@typescript-eslint/eslint-plugin": "5.59.0",
"@typescript-eslint/parser": "5.59.0",
"esbuild": "0.17.17",
"eslint": "8.38.0",
"eslint-config-prettier": "8.8.0",
"eslint-config-react-app": "7.0.1",
"eslint-plugin-prettier": "4.2.1",
"if-env": "^1.0.4",
"jest": "29.5.0",
"lerna": "^6.3.0",
"prettier": "2.3.0"
"prettier": "2.7.1",
"typescript": "5.0.4"
},
"scripts": {
"bootstrap": "yarn install --ignore-scripts && nx run-many --target=build",
Expand Down
40 changes: 40 additions & 0 deletions tsconfig.types.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
// Common tsconfig used for type checking and generating declaration files.
// Docs: https://www.typescriptlang.org/tsconfig
"exclude": [
"**/__specs__/**",
"**/__tests__/**",
"**/*.spec.ts",
"**/*.spec.tsx",
"**/*.test.ts",
"**/*.test.tsx"
],
"compilerOptions": {
"module": "ESNext",
"lib": ["DOM", "ESNext"],
// output .d.ts declaration files for consumers
"declaration": true,
// output .d.ts declaration files only (esbuild will transpile to .js for us)
"emitDeclarationOnly": true,
// stricter type-checking for stronger correctness. Recommended by TS
"strict": true,
// linter checks for common issues
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
// noUnused* overlap with @typescript-eslint/no-unused-vars, can disable if duplicative
"noUnusedLocals": true,
"noUnusedParameters": true,
// use modern Node's module resolution algorithm, instead of the legacy TS one
"moduleResolution": "NodeNext",
// transpile JSX to React.createElement
"jsx": "react",
// interop between ESM and CJS modules. Recommended by TS
"esModuleInterop": true,
// significant perf increase by skipping checking .d.ts files, particularly those in node_modules. Recommended by TS
"skipLibCheck": true,
// error out if import and file system have a casing mismatch. Recommended by TS
"forceConsistentCasingInFileNames": true,
// esbuild transpiles files individually, so ensure each file is valid when isolated
"isolatedModules": true
}
}
Loading

0 comments on commit 73ec801

Please sign in to comment.