├── .all-contributorsrc ├── .eslintignore ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md ├── dependabot.yml ├── stale.yml └── workflows │ └── nodejs.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── jest.config.js ├── package.json ├── src ├── babelPluginTsdx.ts ├── constants.ts ├── createBuildConfigs.ts ├── createEslintConfig.ts ├── createJestConfig.ts ├── createProgressEstimator.ts ├── createRollupConfig.ts ├── deprecated.ts ├── env.d.ts ├── errors │ ├── evalToString.ts │ ├── extractErrors.ts │ ├── invertObject.ts │ └── transformErrorMessages.ts ├── getInstallArgs.ts ├── getInstallCmd.ts ├── index.ts ├── logError.ts ├── messages.ts ├── output.ts ├── templates │ ├── basic.ts │ ├── index.ts │ ├── react-with-storybook.ts │ ├── react.ts │ ├── template.d.ts │ └── utils │ │ └── index.ts ├── types.ts └── utils.ts ├── templates ├── basic │ ├── .github │ │ └── workflows │ │ │ ├── main.yml │ │ │ └── size.yml │ ├── LICENSE │ ├── README.md │ ├── gitignore │ ├── src │ │ └── index.ts │ ├── test │ │ ├── import.mjs │ │ ├── index.test.ts │ │ └── require.cjs │ └── tsconfig.json ├── react-with-storybook │ ├── .github │ │ └── workflows │ │ │ ├── main.yml │ │ │ └── size.yml │ ├── .storybook │ │ ├── main.js │ │ └── preview.js │ ├── LICENSE │ ├── README.md │ ├── example │ │ ├── .gitignore │ │ ├── index.html │ │ ├── index.tsx │ │ ├── package.json │ │ └── tsconfig.json │ ├── gitignore │ ├── src │ │ └── index.tsx │ ├── stories │ │ └── Thing.stories.tsx │ ├── test │ │ ├── import.mjs │ │ ├── index.test.tsx │ │ └── require.cjs │ └── tsconfig.json └── react │ ├── .github │ └── workflows │ │ ├── main.yml │ │ └── size.yml │ ├── LICENSE │ ├── README.md │ ├── example │ ├── .gitignore │ ├── index.html │ ├── index.tsx │ ├── package.json │ └── tsconfig.json │ ├── gitignore │ ├── src │ └── index.tsx │ ├── test │ ├── import.mjs │ ├── index.test.tsx │ └── require.cjs │ └── tsconfig.json ├── test ├── README.md ├── e2e │ ├── fixtures │ │ ├── README.md │ │ ├── build-default │ │ │ ├── package.json │ │ │ ├── package2.json │ │ │ ├── src │ │ │ │ ├── index.ts │ │ │ │ ├── returnsTrue.ts │ │ │ │ └── syntax │ │ │ │ │ ├── async.ts │ │ │ │ │ ├── generator.ts │ │ │ │ │ ├── jsx-import │ │ │ │ │ ├── JSX-A.jsx │ │ │ │ │ ├── JSX-B.jsx │ │ │ │ │ └── JSX-import-JSX.jsx │ │ │ │ │ ├── nullish-coalescing.ts │ │ │ │ │ └── optional-chaining.ts │ │ │ ├── test │ │ │ │ ├── some-test.test.ts │ │ │ │ └── testUtil.ts │ │ │ ├── tsconfig.json │ │ │ └── types │ │ │ │ └── blar.d.ts │ │ ├── build-invalid │ │ │ ├── package.json │ │ │ ├── src │ │ │ │ └── index.ts │ │ │ └── tsconfig.json │ │ ├── build-withTsconfig │ │ │ ├── package.json │ │ │ ├── src │ │ │ │ ├── index.ts │ │ │ │ └── tsconfig.json │ │ │ ├── tsconfig.base.json │ │ │ └── tsconfig.json │ │ └── lint │ │ │ ├── file-with-lint-errors.ts │ │ │ ├── file-with-lint-warnings.ts │ │ │ ├── file-with-prettier-lint-errors.ts │ │ │ ├── file-without-lint-error.ts │ │ │ ├── react-file-with-lint-errors.tsx │ │ │ └── react-file-without-lint-error.tsx │ ├── tsdx-build-default.test.ts │ ├── tsdx-build-invalid.test.ts │ ├── tsdx-build-options.test.ts │ ├── tsdx-build-withTsconfig.test.ts │ └── tsdx-lint.test.ts ├── integration │ ├── fixtures │ │ ├── README.md │ │ ├── build-options │ │ │ ├── package.json │ │ │ ├── src │ │ │ │ └── index.ts │ │ │ └── tsconfig.json │ │ ├── build-withBabel │ │ │ ├── .babelrc.js │ │ │ ├── package.json │ │ │ ├── src │ │ │ │ ├── index.ts │ │ │ │ └── styled.tsx │ │ │ ├── test-babel-preset.js │ │ │ └── tsconfig.json │ │ └── build-withConfig │ │ │ ├── package.json │ │ │ ├── src │ │ │ ├── index.css │ │ │ └── index.ts │ │ │ ├── tsconfig.json │ │ │ └── tsdx.config.js │ ├── tsdx-build-options.test.ts │ ├── tsdx-build-withBabel.test.ts │ └── tsdx-build-withConfig.test.ts ├── unit │ └── utils-safePackageName.test.ts └── utils │ ├── fixture.ts │ └── shell.ts ├── tsconfig.json ├── website ├── .babelrc ├── .gitignore ├── .nextra │ ├── arrow-right.js │ ├── babel-plugin-nextjs-mdx-patch.js │ ├── config.js │ ├── directories.js │ ├── docsearch.js │ ├── github-icon.js │ ├── layout.js │ ├── nextra-loader.js │ ├── nextra.js │ ├── search.js │ ├── ssg.js │ ├── styles.css │ └── theme.js ├── components │ ├── features.js │ ├── features.module.css │ ├── logo.js │ └── video.js ├── jsconfig.json ├── next.config.js ├── nextra.config.js ├── package.json ├── pages │ ├── _app.js │ ├── _document.js │ ├── api-reference.mdx │ ├── change-log.mdx │ ├── customization.md │ ├── index.mdx │ ├── meta.json │ └── optimization.mdx ├── postcss.config.js ├── public │ ├── bg.svg │ ├── favicon.png │ ├── favicon │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon.ico │ │ └── site.webmanifest │ ├── logo.svg │ └── og_image.jpg ├── tailwind.config.js └── yarn.lock └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | dist/ 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'react-app', 4 | 'prettier/@typescript-eslint', 5 | 'plugin:prettier/recommended', 6 | ], 7 | }; 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Current Behavior 11 | 12 | 13 | 14 | ### Expected behavior 15 | 16 | 17 | 18 | ### Suggested solution(s) 19 | 20 | 21 | 22 | ### Additional context 23 | 24 | 25 | 26 | ### Your environment 27 | 28 | 35 | 36 | ```text 37 | 38 | ``` 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Current Behavior 11 | 12 | 13 | 14 | ### Desired Behavior 15 | 16 | 17 | 18 | ### Suggested Solution 19 | 20 | 21 | 22 | 23 | 24 | ### Who does this impact? Who is this for? 25 | 26 | 27 | 28 | ### Describe alternatives you've considered 29 | 30 | 31 | 32 | ### Additional context 33 | 34 | 35 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | # configuration for / 5 | - package-ecosystem: npm 6 | directory: '/' 7 | schedule: 8 | interval: weekly # don't spam daily 9 | commit-message: 10 | prefix: 'deps:' # prefix commit with deps: for consistency 11 | # only increase version when required, don't bump every patch or minor 12 | versioning-strategy: increase-if-necessary 13 | allow: 14 | # only upgrade prod deps (not devDeps) 15 | - dependency-name: '*' 16 | dependency-type: production 17 | ignore: 18 | # ignore eslint-config-react-app's peerDeps 19 | - dependency-name: '@typescript-eslint/eslint-plugin' 20 | - dependency-name: '@typescript-eslint/parser' 21 | - dependency-name: 'babel-eslint' 22 | - dependency-name: 'eslint-plugin-flowtype' 23 | - dependency-name: 'eslint-plugin-import' 24 | - dependency-name: 'eslint-plugin-jsx-a11y' 25 | - dependency-name: 'eslint-plugin-react' 26 | - dependency-name: 'eslint-plugin-react-hooks' 27 | # ignore Jest's "peers" that should be upgraded simultaneously with Jest 28 | - dependency-name: '@types/jest' 29 | - dependency-name: 'jest-watch-typeahead' 30 | - dependency-name: 'ts-jest' 31 | # temporarily disable dep upgrade PRs for / as they're being updated 32 | open-pull-requests-limit: 0 33 | 34 | # configuration for /website 35 | - package-ecosystem: npm 36 | directory: /website 37 | schedule: 38 | interval: weekly # don't spam daily 39 | commit-message: 40 | prefix: 'deps:' # prefix commit with deps: for consistency 41 | # only increase version when required, don't bump every patch or minor 42 | versioning-strategy: increase-if-necessary 43 | allow: 44 | # only upgrade prod deps (not devDeps) 45 | - dependency-name: '*' 46 | dependency-type: production 47 | # /website is not a published package and doesn't really have an attack 48 | # surface area, should only be updated as needed, not as soon as deps change 49 | ignore: 50 | # no security PRs for /website 51 | - dependency-name: '*' 52 | # disable dep upgrade PRs for /website 53 | open-pull-requests-limit: 0 54 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 3600 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - enhancement 8 | - pinned 9 | - RFC 10 | - bug 11 | - in progress 12 | - 2.0 13 | # Label to use when marking an issue as stale 14 | staleLabel: stale 15 | # Comment to post when marking an issue as stale. Set to `false` to disable 16 | markComment: false 17 | # Comment to post when closing a stale issue. Set to `false` to disable 18 | closeComment: false 19 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize, reopened] 6 | 7 | jobs: 8 | lint-and-dedupe: 9 | runs-on: ubuntu-latest 10 | 11 | name: Lint & Deduplicate deps on node 10.x and ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout repo 15 | uses: actions/checkout@v2 16 | - name: Use Node 10.x 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: 10.x 20 | 21 | - name: Install deps and build (with cache) 22 | uses: bahmutov/npm-install@v1 23 | 24 | - name: Lint codebase 25 | run: yarn lint:post-build 26 | 27 | - name: Deduplicate dependencies 28 | run: yarn deduplicate:check 29 | 30 | test: 31 | name: Test on Node ${{ matrix.node }} and ${{ matrix.os }} 32 | 33 | runs-on: ${{ matrix.os }} 34 | strategy: 35 | matrix: 36 | node: ['10.x', '12.x', '14.x'] 37 | os: [ubuntu-latest, windows-latest, macOS-latest] 38 | 39 | steps: 40 | - name: Checkout repo 41 | uses: actions/checkout@v2 42 | - name: Use Node ${{ matrix.node }} 43 | uses: actions/setup-node@v1 44 | with: 45 | node-version: ${{ matrix.node }} 46 | 47 | - name: Install deps and build (with cache) 48 | uses: bahmutov/npm-install@v1 49 | 50 | - name: Test package 51 | run: yarn test:post-build 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | dist 5 | tester 6 | tester-react 7 | package-lock.json 8 | # Local Netlify folder 9 | .netlify 10 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at hello@formium.io. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to TSDX 2 | 3 | Thanks for your interest in TSDX! You are very welcome to contribute. If you are proposing a new feature, make sure to [open an issue](https://github.com/palmerhq/tsdx/issues/new/choose) to make sure it is inline with the project goals. 4 | 5 | ## Setup 6 | 7 | 0. First, remove any existing `tsdx` global installations that may conflict. 8 | 9 | ``` 10 | yarn global remove tsdx # or npm uninstall -g tsdx 11 | ``` 12 | 13 | 1. Fork this repository to your own GitHub account and clone it to your local device: 14 | 15 | ``` 16 | git clone https://github.com/your-name/tsdx.git 17 | cd tsdx 18 | ``` 19 | 20 | 1. Install the dependencies and build the TypeScript files to JavaScript: 21 | 22 | ``` 23 | yarn && yarn build 24 | ``` 25 | 26 | > **Note:** you'll need to run `yarn build` any time you want to see your changes, or run `yarn watch` to leave it in watch mode. 27 | 28 | 1. Make it so running `tsdx` anywhere will run your local dev version: 29 | 30 | ``` 31 | yarn link 32 | ``` 33 | 34 | 4) To use your local version when running `yarn build`/`yarn start`/`yarn test` in a TSDX project, run this in the project: 35 | 36 | ``` 37 | yarn link tsdx 38 | ``` 39 | 40 | You should see a success message: `success Using linked package for "tsdx".` The project will now use the locally linked version instead of a copy from `node_modules`. 41 | 42 | ## Submitting a PR 43 | 44 | Be sure to run `yarn test` before you make your PR to make sure you haven't broken anything. 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Jared Palmer https://jaredpalmer.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | testMatch: ['/**/*(*.)@(test).[tj]s?(x)'], 4 | testPathIgnorePatterns: [ 5 | '/node_modules/', // default 6 | '/templates/', // don't run tests in the templates 7 | '/test/.*/fixtures/', // don't run tests in fixtures 8 | '/stage-.*/', // don't run tests in auto-generated (and auto-removed) test dirs 9 | ], 10 | }; 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tsdx", 3 | "version": "0.14.1", 4 | "author": "Jared Palmer ", 5 | "description": "Zero-config TypeScript package development", 6 | "license": "MIT", 7 | "homepage": "https://github.com/formium/tsdx", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/formium/tsdx.git" 11 | }, 12 | "keywords": [ 13 | "react", 14 | "typescript", 15 | "bundle", 16 | "rollup" 17 | ], 18 | "bugs": { 19 | "url": "https://github.com/formium/tsdx/issues" 20 | }, 21 | "bin": { 22 | "tsdx": "./dist/index.js" 23 | }, 24 | "scripts": { 25 | "prepare": "tsc -p tsconfig.json", 26 | "build": "tsc -p tsconfig.json", 27 | "lint": "yarn build && yarn lint:post-build", 28 | "lint:post-build": "node dist/index.js lint ./ --ignore-pattern 'test/e2e/fixtures/lint'", 29 | "test": "yarn build && yarn test:post-build", 30 | "test:post-build": "node dist/index.js test", 31 | "start": "tsc -p tsconfig.json --watch", 32 | "release": "np", 33 | "deduplicate": "yarn-deduplicate -s fewer yarn.lock", 34 | "deduplicate:check": "yarn-deduplicate -s fewer yarn.lock --list --fail" 35 | }, 36 | "files": [ 37 | "dist", 38 | "templates" 39 | ], 40 | "engines": { 41 | "node": ">=14" 42 | }, 43 | "dependencies": { 44 | "@babel/core": "^7.4.4", 45 | "@babel/helper-module-imports": "^7.0.0", 46 | "@babel/parser": "^7.11.5", 47 | "@babel/plugin-proposal-class-properties": "^7.4.4", 48 | "@babel/preset-env": "^7.11.0", 49 | "@babel/traverse": "^7.11.5", 50 | "@rollup/plugin-babel": "^5.1.0", 51 | "@rollup/plugin-commonjs": "^19.0.0", 52 | "@rollup/plugin-json": "^4.0.0", 53 | "@rollup/plugin-node-resolve": "^13.0.0", 54 | "@rollup/plugin-replace": "^2.2.1", 55 | "@types/jest": "^26.0.24", 56 | "@typescript-eslint/eslint-plugin": "^4.28.2", 57 | "@typescript-eslint/parser": "^4.28.2", 58 | "ansi-escapes": "^4.2.1", 59 | "asyncro": "^3.0.0", 60 | "babel-eslint": "^10.0.3", 61 | "babel-plugin-annotate-pure-calls": "^0.4.0", 62 | "babel-plugin-dev-expression": "^0.2.1", 63 | "babel-plugin-macros": "^3.1.0", 64 | "babel-plugin-polyfill-regenerator": "^0.2.2", 65 | "babel-plugin-transform-rename-import": "^2.3.0", 66 | "camelcase": "^6.0.0", 67 | "chalk": "^4.0.0", 68 | "enquirer": "^2.3.4", 69 | "eslint": "^7.30.0", 70 | "eslint-config-prettier": "^6.0.0", 71 | "eslint-config-react-app": "^6.0.0", 72 | "eslint-plugin-flowtype": "^5.8.0", 73 | "eslint-plugin-import": "^2.18.2", 74 | "eslint-plugin-jsx-a11y": "^6.2.3", 75 | "eslint-plugin-prettier": "^3.1.0", 76 | "eslint-plugin-react": "^7.14.3", 77 | "eslint-plugin-react-hooks": "^4.2.0", 78 | "execa": "^5.1.1", 79 | "fs-extra": "^10.0.0", 80 | "jest": "^27.0.6", 81 | "jest-watch-typeahead": "^0.6.4", 82 | "jpjs": "^1.2.1", 83 | "lodash.merge": "^4.6.2", 84 | "ora": "^5.4.1", 85 | "pascal-case": "^3.1.1", 86 | "prettier": "^2.3.2", 87 | "progress-estimator": "^0.3.0", 88 | "regenerator-runtime": "^0.13.7", 89 | "rollup": "^2.52.8", 90 | "rollup-plugin-sourcemaps": "^0.6.2", 91 | "rollup-plugin-terser": "^7.0.2", 92 | "rollup-plugin-typescript2": "^0.30.0", 93 | "sade": "^1.4.2", 94 | "semver": "^7.1.1", 95 | "shelljs": "^0.8.3", 96 | "tiny-glob": "^0.2.6", 97 | "ts-jest": "^27.0.3", 98 | "tslib": "^2.3.0", 99 | "typescript": "^4.3.5" 100 | }, 101 | "devDependencies": { 102 | "@types/eslint": "^7.2.14", 103 | "@types/fs-extra": "^9.0.1", 104 | "@types/lodash": "^4.14.161", 105 | "@types/node": "^16.0.1", 106 | "@types/react": "^17.0.14", 107 | "@types/rollup-plugin-json": "^3.0.2", 108 | "@types/sade": "^1.6.0", 109 | "@types/semver": "^7.1.0", 110 | "@types/shelljs": "^0.8.5", 111 | "@types/styled-components": "^5.0.1", 112 | "autoprefixer": "^9.7.4", 113 | "babel-plugin-replace-identifiers": "^0.1.1", 114 | "cssnano": "^4.1.10", 115 | "doctoc": "^2.0.1", 116 | "husky": "^7.0.1", 117 | "np": "^7.5.0", 118 | "pretty-quick": "^3.1.1", 119 | "react": "^17.0.2", 120 | "react-dom": "^17.0.2", 121 | "react-is": "^17.0.2", 122 | "rollup-plugin-postcss": "^2.5.0", 123 | "styled-components": "^5.0.1", 124 | "tiny-invariant": "^1.1.0", 125 | "tiny-warning": "^1.0.3", 126 | "yarn-deduplicate": "^3.1.0" 127 | }, 128 | "husky": { 129 | "hooks": { 130 | "pre-commit": "pretty-quick --staged --pattern '!test/tests/lint/**' && yarn lint && yarn deduplicate:check && doctoc README.md" 131 | } 132 | }, 133 | "prettier": { 134 | "printWidth": 80, 135 | "semi": true, 136 | "singleQuote": true, 137 | "trailingComma": "es5" 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/babelPluginTsdx.ts: -------------------------------------------------------------------------------- 1 | import { createConfigItem } from '@babel/core'; 2 | import { createBabelInputPluginFactory } from '@rollup/plugin-babel'; 3 | import merge from 'lodash.merge'; 4 | 5 | export const isTruthy = (obj?: any) => { 6 | if (!obj) { 7 | return false; 8 | } 9 | 10 | return obj.constructor !== Object || Object.keys(obj).length > 0; 11 | }; 12 | 13 | // replace lodash with lodash-es, but not lodash/fp 14 | const replacements = [{ original: 'lodash(?!/fp)', replacement: 'lodash-es' }]; 15 | 16 | export const mergeConfigItems = (type: any, ...configItemsToMerge: any[]) => { 17 | const mergedItems: any[] = []; 18 | 19 | configItemsToMerge.forEach(configItemToMerge => { 20 | configItemToMerge.forEach((item: any) => { 21 | const itemToMergeWithIndex = mergedItems.findIndex( 22 | mergedItem => mergedItem.file.resolved === item.file.resolved 23 | ); 24 | 25 | if (itemToMergeWithIndex === -1) { 26 | mergedItems.push(item); 27 | return; 28 | } 29 | 30 | mergedItems[itemToMergeWithIndex] = createConfigItem( 31 | [ 32 | mergedItems[itemToMergeWithIndex].file.resolved, 33 | merge(mergedItems[itemToMergeWithIndex].options, item.options), 34 | ], 35 | { 36 | type, 37 | } 38 | ); 39 | }); 40 | }); 41 | 42 | return mergedItems; 43 | }; 44 | 45 | export const createConfigItems = (type: any, items: any[]) => { 46 | return items.map(({ name, ...options }) => { 47 | return createConfigItem([require.resolve(name), options], { type }); 48 | }); 49 | }; 50 | 51 | export const babelPluginTsdx = createBabelInputPluginFactory(() => ({ 52 | // Passed the plugin options. 53 | options({ custom: customOptions, ...pluginOptions }: any) { 54 | return { 55 | // Pull out any custom options that the plugin might have. 56 | customOptions, 57 | 58 | // Pass the options back with the two custom options removed. 59 | pluginOptions, 60 | }; 61 | }, 62 | config(config: any, { customOptions }: any) { 63 | const defaultPlugins = createConfigItems( 64 | 'plugin', 65 | [ 66 | // { 67 | // name: '@babel/plugin-transform-react-jsx', 68 | // pragma: customOptions.jsx || 'h', 69 | // pragmaFrag: customOptions.jsxFragment || 'Fragment', 70 | // }, 71 | { name: 'babel-plugin-macros' }, 72 | { name: 'babel-plugin-annotate-pure-calls' }, 73 | { name: 'babel-plugin-dev-expression' }, 74 | customOptions.format !== 'cjs' && { 75 | name: 'babel-plugin-transform-rename-import', 76 | replacements, 77 | }, 78 | { 79 | name: 'babel-plugin-polyfill-regenerator', 80 | // don't pollute global env as this is being used in a library 81 | method: 'usage-pure', 82 | }, 83 | { 84 | name: '@babel/plugin-proposal-class-properties', 85 | loose: true, 86 | }, 87 | isTruthy(customOptions.extractErrors) && { 88 | name: './errors/transformErrorMessages', 89 | }, 90 | ].filter(Boolean) 91 | ); 92 | 93 | const babelOptions = config.options || {}; 94 | babelOptions.presets = babelOptions.presets || []; 95 | 96 | const presetEnvIdx = babelOptions.presets.findIndex((preset: any) => 97 | preset.file.request.includes('@babel/preset-env') 98 | ); 99 | 100 | // if they use preset-env, merge their options with ours 101 | if (presetEnvIdx !== -1) { 102 | const presetEnv = babelOptions.presets[presetEnvIdx]; 103 | babelOptions.presets[presetEnvIdx] = createConfigItem( 104 | [ 105 | presetEnv.file.resolved, 106 | merge( 107 | { 108 | loose: true, 109 | targets: customOptions.targets, 110 | }, 111 | presetEnv.options, 112 | { 113 | modules: false, 114 | } 115 | ), 116 | ], 117 | { 118 | type: `preset`, 119 | } 120 | ); 121 | } else { 122 | // if no preset-env, add it & merge with their presets 123 | const defaultPresets = createConfigItems('preset', [ 124 | { 125 | name: '@babel/preset-env', 126 | targets: customOptions.targets, 127 | modules: false, 128 | loose: true, 129 | }, 130 | ]); 131 | 132 | babelOptions.presets = mergeConfigItems( 133 | 'preset', 134 | defaultPresets, 135 | babelOptions.presets 136 | ); 137 | } 138 | 139 | // Merge babelrc & our plugins together 140 | babelOptions.plugins = mergeConfigItems( 141 | 'plugin', 142 | defaultPlugins, 143 | babelOptions.plugins || [] 144 | ); 145 | 146 | return babelOptions; 147 | }, 148 | })); 149 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | import { resolveApp } from './utils'; 2 | 3 | export const paths = { 4 | appPackageJson: resolveApp('package.json'), 5 | tsconfigJson: resolveApp('tsconfig.json'), 6 | testsSetup: resolveApp('test/setupTests.ts'), 7 | appRoot: resolveApp('.'), 8 | appSrc: resolveApp('src'), 9 | appErrorsJson: resolveApp('errors/codes.json'), 10 | appErrors: resolveApp('errors'), 11 | appDist: resolveApp('dist'), 12 | appConfig: resolveApp('tsdx.config.js'), 13 | jestConfig: resolveApp('jest.config.js'), 14 | progressEstimatorCache: resolveApp('node_modules/.cache/.progress-estimator'), 15 | }; 16 | -------------------------------------------------------------------------------- /src/createBuildConfigs.ts: -------------------------------------------------------------------------------- 1 | import { RollupOptions, OutputOptions } from 'rollup'; 2 | import * as fs from 'fs-extra'; 3 | import { concatAllArray } from 'jpjs'; 4 | 5 | import { paths } from './constants'; 6 | import { TsdxOptions, NormalizedOpts } from './types'; 7 | 8 | import { createRollupConfig } from './createRollupConfig'; 9 | 10 | // check for custom tsdx.config.js 11 | let tsdxConfig = { 12 | rollup(config: RollupOptions, _options: TsdxOptions): RollupOptions { 13 | return config; 14 | }, 15 | }; 16 | 17 | if (fs.existsSync(paths.appConfig)) { 18 | tsdxConfig = require(paths.appConfig); 19 | } 20 | 21 | export async function createBuildConfigs( 22 | opts: NormalizedOpts 23 | ): Promise> { 24 | const allInputs = concatAllArray( 25 | opts.input.map((input: string) => 26 | createAllFormats(opts, input).map( 27 | (options: TsdxOptions, index: number) => ({ 28 | ...options, 29 | // We want to know if this is the first run for each entryfile 30 | // for certain plugins (e.g. css) 31 | writeMeta: index === 0, 32 | }) 33 | ) 34 | ) 35 | ); 36 | 37 | return await Promise.all( 38 | allInputs.map(async (options: TsdxOptions, index: number) => { 39 | // pass the full rollup config to tsdx.config.js override 40 | const config = await createRollupConfig(options, index); 41 | return tsdxConfig.rollup(config, options); 42 | }) 43 | ); 44 | } 45 | 46 | function createAllFormats( 47 | opts: NormalizedOpts, 48 | input: string 49 | ): [TsdxOptions, ...TsdxOptions[]] { 50 | return [ 51 | opts.format.includes('cjs') && { 52 | ...opts, 53 | format: 'cjs', 54 | env: 'development', 55 | input, 56 | }, 57 | opts.format.includes('cjs') && { 58 | ...opts, 59 | format: 'cjs', 60 | env: 'production', 61 | input, 62 | }, 63 | opts.format.includes('esm') && { ...opts, format: 'esm', input }, 64 | opts.format.includes('umd') && { 65 | ...opts, 66 | format: 'umd', 67 | env: 'development', 68 | input, 69 | }, 70 | opts.format.includes('umd') && { 71 | ...opts, 72 | format: 'umd', 73 | env: 'production', 74 | input, 75 | }, 76 | opts.format.includes('system') && { 77 | ...opts, 78 | format: 'system', 79 | env: 'development', 80 | input, 81 | }, 82 | opts.format.includes('system') && { 83 | ...opts, 84 | format: 'system', 85 | env: 'production', 86 | input, 87 | }, 88 | ].filter(Boolean) as [TsdxOptions, ...TsdxOptions[]]; 89 | } 90 | -------------------------------------------------------------------------------- /src/createEslintConfig.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra'; 2 | import path from 'path'; 3 | import { CLIEngine } from 'eslint'; 4 | import { PackageJson } from './types'; 5 | import { getReactVersion } from './utils'; 6 | 7 | interface CreateEslintConfigArgs { 8 | pkg: PackageJson; 9 | rootDir: string; 10 | writeFile: boolean; 11 | } 12 | export async function createEslintConfig({ 13 | pkg, 14 | rootDir, 15 | writeFile, 16 | }: CreateEslintConfigArgs): Promise { 17 | const isReactLibrary = Boolean(getReactVersion(pkg)); 18 | 19 | const config = { 20 | extends: [ 21 | 'react-app', 22 | 'prettier/@typescript-eslint', 23 | 'plugin:prettier/recommended', 24 | ], 25 | settings: { 26 | react: { 27 | // Fix for https://github.com/jaredpalmer/tsdx/issues/279 28 | version: isReactLibrary ? 'detect' : '999.999.999', 29 | }, 30 | }, 31 | }; 32 | 33 | if (!writeFile) { 34 | return config; 35 | } 36 | 37 | const file = path.join(rootDir, '.eslintrc.js'); 38 | try { 39 | await fs.writeFile( 40 | file, 41 | `module.exports = ${JSON.stringify(config, null, 2)}`, 42 | { flag: 'wx' } 43 | ); 44 | } catch (e) { 45 | if (e.code === 'EEXIST') { 46 | console.error( 47 | 'Error trying to save the Eslint configuration file:', 48 | `${file} already exists.` 49 | ); 50 | } else { 51 | console.error(e); 52 | } 53 | 54 | return config; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/createJestConfig.ts: -------------------------------------------------------------------------------- 1 | import { Config } from '@jest/types'; 2 | 3 | export type JestConfigOptions = Partial; 4 | 5 | export function createJestConfig( 6 | _: (relativePath: string) => void, 7 | rootDir: string 8 | ): JestConfigOptions { 9 | const config: JestConfigOptions = { 10 | transform: { 11 | '.(ts|tsx)$': require.resolve('ts-jest/dist'), 12 | '.(js|jsx)$': require.resolve('babel-jest'), // jest's default 13 | }, 14 | transformIgnorePatterns: ['[/\\\\]node_modules[/\\\\].+\\.(js|jsx)$'], 15 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], 16 | collectCoverageFrom: ['src/**/*.{ts,tsx,js,jsx}'], 17 | testMatch: ['/**/*.(spec|test).{ts,tsx,js,jsx}'], 18 | testURL: 'http://localhost', 19 | rootDir, 20 | watchPlugins: [ 21 | require.resolve('jest-watch-typeahead/filename'), 22 | require.resolve('jest-watch-typeahead/testname'), 23 | ], 24 | }; 25 | 26 | return config; 27 | } 28 | -------------------------------------------------------------------------------- /src/createProgressEstimator.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra'; 2 | 3 | import { paths } from './constants'; 4 | 5 | const progressEstimator = require('progress-estimator'); 6 | 7 | export async function createProgressEstimator() { 8 | await fs.ensureDir(paths.progressEstimatorCache); 9 | return progressEstimator({ 10 | // All configuration keys are optional, but it's recommended to specify a storage location. 11 | storagePath: paths.progressEstimatorCache, 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /src/createRollupConfig.ts: -------------------------------------------------------------------------------- 1 | import { safeVariableName, safePackageName, external } from './utils'; 2 | import { paths } from './constants'; 3 | import { RollupOptions } from 'rollup'; 4 | import { terser } from 'rollup-plugin-terser'; 5 | import { DEFAULT_EXTENSIONS as DEFAULT_BABEL_EXTENSIONS } from '@babel/core'; 6 | import commonjs from '@rollup/plugin-commonjs'; 7 | import json from '@rollup/plugin-json'; 8 | import replace from '@rollup/plugin-replace'; 9 | import resolve, { 10 | DEFAULTS as RESOLVE_DEFAULTS, 11 | } from '@rollup/plugin-node-resolve'; 12 | import sourceMaps from 'rollup-plugin-sourcemaps'; 13 | import typescript from 'rollup-plugin-typescript2'; 14 | import ts from 'typescript'; 15 | 16 | import { extractErrors } from './errors/extractErrors'; 17 | import { babelPluginTsdx } from './babelPluginTsdx'; 18 | import { TsdxOptions } from './types'; 19 | 20 | const errorCodeOpts = { 21 | errorMapFilePath: paths.appErrorsJson, 22 | }; 23 | 24 | // shebang cache map thing because the transform only gets run once 25 | let shebang: any = {}; 26 | 27 | export async function createRollupConfig( 28 | opts: TsdxOptions, 29 | outputNum: number 30 | ): Promise { 31 | const findAndRecordErrorCodes = await extractErrors({ 32 | ...errorCodeOpts, 33 | ...opts, 34 | }); 35 | 36 | const isEsm = opts.format.includes('es') || opts.format.includes('esm'); 37 | 38 | const shouldMinify = 39 | opts.minify !== undefined ? opts.minify : opts.env === 'production' || isEsm; 40 | 41 | let formatString = ['esm', 'cjs'].includes(opts.format) ? '' : opts.format; 42 | let fileExtension = opts.format === 'esm' ? 'mjs' : 'cjs'; 43 | 44 | const outputName = [ 45 | `${paths.appDist}/${safePackageName(opts.name)}`, 46 | formatString, 47 | opts.env, 48 | shouldMinify ? 'min' : '', 49 | fileExtension, 50 | ] 51 | .filter(Boolean) 52 | .join('.'); 53 | 54 | const tsconfigPath = opts.tsconfig || paths.tsconfigJson; 55 | // borrowed from https://github.com/facebook/create-react-app/pull/7248 56 | const tsconfigJSON = ts.readConfigFile(tsconfigPath, ts.sys.readFile).config; 57 | // borrowed from https://github.com/ezolenko/rollup-plugin-typescript2/blob/42173460541b0c444326bf14f2c8c27269c4cb11/src/parse-tsconfig.ts#L48 58 | const tsCompilerOptions = ts.parseJsonConfigFileContent( 59 | tsconfigJSON, 60 | ts.sys, 61 | './' 62 | ).options; 63 | 64 | return { 65 | // Tell Rollup the entry point to the package 66 | input: opts.input, 67 | // Tell Rollup which packages to ignore 68 | external: (id: string) => { 69 | // bundle in polyfills as TSDX can't (yet) ensure they're installed as deps 70 | if (id.startsWith('regenerator-runtime')) { 71 | return false; 72 | } 73 | 74 | return external(id); 75 | }, 76 | // Rollup has treeshaking by default, but we can optimize it further... 77 | treeshake: { 78 | // We assume reading a property of an object never has side-effects. 79 | // This means tsdx WILL remove getters and setters defined directly on objects. 80 | // Any getters or setters defined on classes will not be effected. 81 | // 82 | // @example 83 | // 84 | // const foo = { 85 | // get bar() { 86 | // console.log('effect'); 87 | // return 'bar'; 88 | // } 89 | // } 90 | // 91 | // const result = foo.bar; 92 | // const illegalAccess = foo.quux.tooDeep; 93 | // 94 | // Punchline....Don't use getters and setters 95 | propertyReadSideEffects: false, 96 | }, 97 | // Establish Rollup output 98 | output: { 99 | // Set filenames of the consumer's package 100 | file: outputName, 101 | // Pass through the file format 102 | format: opts.format, 103 | // Do not let Rollup call Object.freeze() on namespace import objects 104 | // (i.e. import * as namespaceImportObject from...) that are accessed dynamically. 105 | freeze: false, 106 | // Respect tsconfig esModuleInterop when setting __esModule. 107 | esModule: Boolean(tsCompilerOptions?.esModuleInterop), 108 | name: opts.name || safeVariableName(opts.name), 109 | sourcemap: true, 110 | globals: { react: 'React', 'react-native': 'ReactNative', 'lodash-es': 'lodashEs', 'lodash/fp': 'lodashFp' }, 111 | exports: 'named', 112 | }, 113 | plugins: [ 114 | !!opts.extractErrors && { 115 | async transform(code: string) { 116 | try { 117 | await findAndRecordErrorCodes(code); 118 | } catch (e) { 119 | return null; 120 | } 121 | return { code, map: null }; 122 | }, 123 | }, 124 | resolve({ 125 | mainFields: [ 126 | 'module', 127 | 'main', 128 | opts.target !== 'node' ? 'browser' : undefined, 129 | ].filter(Boolean) as string[], 130 | extensions: [...RESOLVE_DEFAULTS.extensions, '.cjs', '.mjs', '.jsx'], 131 | }), 132 | // all bundled external modules need to be converted from CJS to ESM 133 | commonjs({ 134 | // use a regex to make sure to include eventual hoisted packages 135 | include: 136 | opts.format === 'umd' 137 | ? /\/node_modules\// 138 | : /\/regenerator-runtime\//, 139 | }), 140 | json(), 141 | { 142 | // Custom plugin that removes shebang from code because newer 143 | // versions of bublé bundle their own private version of `acorn` 144 | // and I don't know a way to patch in the option `allowHashBang` 145 | // to acorn. Taken from microbundle. 146 | // See: https://github.com/Rich-Harris/buble/pull/165 147 | transform(code: string) { 148 | let reg = /^#!(.*)/; 149 | let match = code.match(reg); 150 | 151 | shebang[opts.name] = match ? '#!' + match[1] : ''; 152 | 153 | code = code.replace(reg, ''); 154 | 155 | return { 156 | code, 157 | map: null, 158 | }; 159 | }, 160 | }, 161 | typescript({ 162 | typescript: ts, 163 | tsconfig: opts.tsconfig, 164 | tsconfigDefaults: { 165 | exclude: [ 166 | // all TS test files, regardless whether co-located or in test/ etc 167 | '**/*.spec.ts', 168 | '**/*.test.ts', 169 | '**/*.spec.tsx', 170 | '**/*.test.tsx', 171 | // TS defaults below 172 | 'node_modules', 173 | 'bower_components', 174 | 'jspm_packages', 175 | paths.appDist, 176 | ], 177 | compilerOptions: { 178 | sourceMap: true, 179 | declaration: true, 180 | jsx: 'react', 181 | }, 182 | }, 183 | tsconfigOverride: { 184 | compilerOptions: { 185 | // TS -> esnext, then leave the rest to babel-preset-env 186 | target: 'esnext', 187 | // don't output declarations more than once 188 | ...(outputNum > 0 189 | ? { declaration: false, declarationMap: false } 190 | : {}), 191 | }, 192 | }, 193 | check: !opts.transpileOnly && outputNum === 0, 194 | useTsconfigDeclarationDir: Boolean(tsCompilerOptions?.declarationDir), 195 | }), 196 | babelPluginTsdx({ 197 | exclude: 'node_modules/**', 198 | extensions: [...DEFAULT_BABEL_EXTENSIONS, 'ts', 'tsx'], 199 | passPerPreset: true, 200 | custom: { 201 | targets: opts.target === 'node' ? { node: '14' } : undefined, 202 | extractErrors: opts.extractErrors, 203 | format: opts.format, 204 | }, 205 | babelHelpers: 'bundled', 206 | }), 207 | opts.env !== undefined && 208 | replace({ 209 | preventAssignment: true, 210 | 'process.env.NODE_ENV': JSON.stringify(opts.env), 211 | }), 212 | sourceMaps(), 213 | shouldMinify && 214 | terser({ 215 | output: { comments: false }, 216 | compress: { 217 | keep_infinity: true, 218 | pure_getters: true, 219 | passes: 10, 220 | }, 221 | ecma: opts.legacy ? 5 : 2020, 222 | module: isEsm, 223 | toplevel: opts.format === 'cjs' || isEsm, 224 | warnings: true, 225 | }), 226 | /** 227 | * Ensure there's an empty default export to prevent runtime errors. 228 | * 229 | * @see https://www.npmjs.com/package/rollup-plugin-export-default 230 | */ 231 | { 232 | renderChunk: async (code: string, chunk: any) => { 233 | if (chunk.exports.includes('default') || !isEsm) { 234 | return null; 235 | } 236 | 237 | return { 238 | code: `${code}\nexport default {};`, 239 | map: null, 240 | }; 241 | }, 242 | }, 243 | ], 244 | }; 245 | } 246 | -------------------------------------------------------------------------------- /src/deprecated.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs-extra'; 2 | 3 | import { paths } from './constants'; 4 | 5 | /* 6 | This was originally needed because the default 7 | tsconfig.compilerOptions.rootDir was set to './' instead of './src'. 8 | Now that it's set to './src', this is now deprecated. 9 | To ensure a stable upgrade path for users, leave the warning in for 10 | 6 months - 1 year, then change it to an error in a breaking bump and leave 11 | that in for some time too. 12 | */ 13 | export async function moveTypes() { 14 | const appDistSrc = paths.appDist + '/src'; 15 | 16 | const pathExists = await fs.pathExists(appDistSrc); 17 | if (!pathExists) return; 18 | 19 | // see note above about deprecation window 20 | console.warn( 21 | '[tsdx]: Your rootDir is currently set to "./". Please change your ' + 22 | 'rootDir to "./src".\n' + 23 | 'TSDX has deprecated setting tsconfig.compilerOptions.rootDir to ' + 24 | '"./" as it caused buggy output for declarationMaps and more.\n' + 25 | 'You may also need to change your include to remove "test", which also ' + 26 | 'caused declarations to be unnecessarily created for test files.' 27 | ); 28 | 29 | // Move the type declarations to the base of the ./dist folder 30 | await fs.copy(appDistSrc, paths.appDist, { 31 | overwrite: true, 32 | }); 33 | await fs.remove(appDistSrc); 34 | } 35 | -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'asyncro'; // doesn't have types (unmerged 2+ year old PR: https://github.com/developit/asyncro/pull/10) 2 | declare module 'enquirer'; // doesn't have types for Input or Select 3 | declare module 'jpjs'; // doesn't ship types (written in TS though) 4 | declare module 'tiny-glob/sync'; // /sync isn't typed (but maybe we can use async?) 5 | 6 | // Patch Babel 7 | // @see line 226 of https://unpkg.com/@babel/core@7.4.4/lib/index.js 8 | declare module '@babel/core' { 9 | export const DEFAULT_EXTENSIONS: string[]; 10 | export function createConfigItem(boop: any[], options: any): any[]; 11 | } 12 | 13 | // Rollup plugins 14 | declare module 'rollup-plugin-terser'; 15 | declare module '@babel/traverse'; 16 | declare module '@babel/helper-module-imports'; 17 | 18 | declare module 'lodash.merge'; 19 | -------------------------------------------------------------------------------- /src/errors/evalToString.ts: -------------------------------------------------------------------------------- 1 | // largely borrowed from https://github.com/facebook/react/blob/8b2d3783e58d1acea53428a10d2035a8399060fe/scripts/shared/evalToString.js 2 | /** 3 | * Copyright (c) Facebook, Inc. and its affiliates. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | export function evalToString(ast: any): string { 10 | switch (ast.type) { 11 | case 'StringLiteral': 12 | case 'Literal': // ESLint 13 | return ast.value; 14 | case 'BinaryExpression': // `+` 15 | if (ast.operator !== '+') { 16 | throw new Error('Unsupported binary operator ' + ast.operator); 17 | } 18 | return evalToString(ast.left) + evalToString(ast.right); 19 | default: 20 | throw new Error('Unsupported type ' + ast.type); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/errors/extractErrors.ts: -------------------------------------------------------------------------------- 1 | // largely borrowed from https://github.com/facebook/react/blob/8b2d3783e58d1acea53428a10d2035a8399060fe/scripts/error-codes/extract-errors.js 2 | /** 3 | * Copyright (c) Facebook, Inc. and its affiliates. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | import fs from 'fs-extra'; 9 | import { parse, ParserOptions } from '@babel/parser'; 10 | import traverse from '@babel/traverse'; 11 | import { invertObject } from './invertObject'; 12 | import { evalToString } from './evalToString'; 13 | import { paths } from '../constants'; 14 | import { safeVariableName } from '../utils'; 15 | import { pascalCase } from 'pascal-case'; 16 | 17 | const babelParserOptions: ParserOptions = { 18 | sourceType: 'module', 19 | // As a parser, @babel/parser has its own options and we can't directly 20 | // import/require a babel preset. It should be kept **the same** as 21 | // the `babel-plugin-syntax-*` ones specified in 22 | // https://github.com/facebook/fbjs/blob/master/packages/babel-preset-fbjs/configure.js 23 | plugins: [ 24 | 'classProperties', 25 | 'flow', 26 | 'jsx', 27 | 'trailingFunctionCommas', 28 | 'objectRestSpread', 29 | ], 30 | } as ParserOptions; // workaround for trailingFunctionCommas syntax 31 | 32 | export async function extractErrors(opts: any) { 33 | if (!opts || !opts.errorMapFilePath) { 34 | throw new Error( 35 | 'Missing options. Ensure you pass an object with `errorMapFilePath`.' 36 | ); 37 | } 38 | 39 | if (!opts.name || !opts.name) { 40 | throw new Error('Missing options. Ensure you pass --name flag to tsdx'); 41 | } 42 | 43 | const errorMapFilePath = opts.errorMapFilePath; 44 | let existingErrorMap: any; 45 | try { 46 | /** 47 | * Using `fs.readFile` instead of `require` here, because `require()` calls 48 | * are cached, and the cache map is not properly invalidated after file 49 | * changes. 50 | */ 51 | const fileContents = await fs.readFile(errorMapFilePath, 'utf-8'); 52 | existingErrorMap = JSON.parse(fileContents); 53 | } catch (e) { 54 | existingErrorMap = {}; 55 | } 56 | 57 | const allErrorIDs = Object.keys(existingErrorMap); 58 | let currentID: any; 59 | 60 | if (allErrorIDs.length === 0) { 61 | // Map is empty 62 | currentID = 0; 63 | } else { 64 | currentID = Math.max.apply(null, allErrorIDs as any) + 1; 65 | } 66 | 67 | // Here we invert the map object in memory for faster error code lookup 68 | existingErrorMap = invertObject(existingErrorMap); 69 | 70 | function transform(source: string) { 71 | const ast = parse(source, babelParserOptions); 72 | 73 | traverse(ast, { 74 | CallExpression: { 75 | exit(astPath: any) { 76 | if (astPath.get('callee').isIdentifier({ name: 'invariant' })) { 77 | const node = astPath.node; 78 | 79 | // error messages can be concatenated (`+`) at runtime, so here's a 80 | // trivial partial evaluator that interprets the literal value 81 | const errorMsgLiteral = evalToString(node.arguments[1]); 82 | addToErrorMap(errorMsgLiteral); 83 | } 84 | }, 85 | }, 86 | }); 87 | } 88 | 89 | function addToErrorMap(errorMsgLiteral: any) { 90 | if (existingErrorMap.hasOwnProperty(errorMsgLiteral)) { 91 | return; 92 | } 93 | existingErrorMap[errorMsgLiteral] = '' + currentID++; 94 | } 95 | 96 | async function flush() { 97 | const prettyName = pascalCase(safeVariableName(opts.name)); 98 | // Ensure that the ./src/errors directory exists or create it 99 | await fs.ensureDir(paths.appErrors); 100 | 101 | // Output messages to ./errors/codes.json 102 | await fs.writeFile( 103 | errorMapFilePath, 104 | JSON.stringify(invertObject(existingErrorMap), null, 2) + '\n', 105 | 'utf-8' 106 | ); 107 | 108 | // Write the error files, unless they already exist 109 | await fs.writeFile( 110 | paths.appErrors + '/ErrorDev.js', 111 | ` 112 | function ErrorDev(message) { 113 | const error = new Error(message); 114 | error.name = 'Invariant Violation'; 115 | return error; 116 | } 117 | 118 | export default ErrorDev; 119 | `, 120 | 'utf-8' 121 | ); 122 | 123 | await fs.writeFile( 124 | paths.appErrors + '/ErrorProd.js', 125 | ` 126 | function ErrorProd(code) { 127 | // TODO: replace this URL with yours 128 | let url = 'https://reactjs.org/docs/error-decoder.html?invariant=' + code; 129 | for (let i = 1; i < arguments.length; i++) { 130 | url += '&args[]=' + encodeURIComponent(arguments[i]); 131 | } 132 | return new Error( 133 | \`Minified ${prettyName} error #$\{code}; visit $\{url} for the full message or \` + 134 | 'use the non-minified dev environment for full errors and additional ' + 135 | 'helpful warnings. ' 136 | ); 137 | } 138 | 139 | export default ErrorProd; 140 | `, 141 | 'utf-8' 142 | ); 143 | } 144 | 145 | return async function extractErrors(source: any) { 146 | transform(source); 147 | await flush(); 148 | }; 149 | } 150 | -------------------------------------------------------------------------------- /src/errors/invertObject.ts: -------------------------------------------------------------------------------- 1 | // largely borrowed from https://github.com/facebook/react/blob/8b2d3783e58d1acea53428a10d2035a8399060fe/scripts/error-codes/invertObject.js 2 | 3 | /** 4 | * Copyright (c) Facebook, Inc. and its affiliates. 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | */ 9 | 10 | /** 11 | * turns 12 | * { 'MUCH ERROR': '0', 'SUCH WRONG': '1' } 13 | * into 14 | * { 0: 'MUCH ERROR', 1: 'SUCH WRONG' } 15 | */ 16 | 17 | type Dict = { [key: string]: any }; 18 | 19 | export function invertObject(targetObj: Dict) { 20 | const result: Dict = {}; 21 | const mapKeys = Object.keys(targetObj); 22 | 23 | for (const originalKey of mapKeys) { 24 | const originalVal = targetObj[originalKey]; 25 | 26 | result[originalVal] = originalKey; 27 | } 28 | 29 | return result; 30 | } 31 | -------------------------------------------------------------------------------- /src/errors/transformErrorMessages.ts: -------------------------------------------------------------------------------- 1 | // largely borrowed from https://github.com/facebook/react/blob/2c8832075b05009bd261df02171bf9888ac76350/scripts/error-codes/transform-error-messages.js 2 | /** 3 | * Copyright (c) Facebook, Inc. and its affiliates. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | import fs from 'fs'; 10 | import { invertObject } from './invertObject'; 11 | import { evalToString } from './evalToString'; 12 | import { addDefault } from '@babel/helper-module-imports'; 13 | import { paths } from '../constants'; 14 | 15 | export default function transformErrorMessages(babel: any) { 16 | const t = babel.types; 17 | 18 | const DEV_EXPRESSION = t.identifier('__DEV__'); 19 | 20 | return { 21 | visitor: { 22 | CallExpression(path: any, file: any) { 23 | const node = path.node; 24 | const noMinify = file.opts.noMinify; 25 | if (path.get('callee').isIdentifier({ name: 'invariant' })) { 26 | // Turns this code: 27 | // 28 | // invariant(condition, 'A %s message that contains %s', adj, noun); 29 | // 30 | // into this: 31 | // 32 | // if (!condition) { 33 | // if (__DEV__) { 34 | // throw ReactError(`A ${adj} message that contains ${noun}`); 35 | // } else { 36 | // throw ReactErrorProd(ERR_CODE, adj, noun); 37 | // } 38 | // } 39 | // 40 | // where ERR_CODE is an error code: a unique identifier (a number 41 | // string) that references a verbose error message. The mapping is 42 | // stored in `paths.appErrorsJson`. 43 | const condition = node.arguments[0]; 44 | const errorMsgLiteral = evalToString(node.arguments[1]); 45 | const errorMsgExpressions = Array.from(node.arguments.slice(2)); 46 | const errorMsgQuasis = errorMsgLiteral 47 | .split('%s') 48 | .map((raw: any) => 49 | t.templateElement({ raw, cooked: String.raw({ raw } as any) }) 50 | ); 51 | 52 | // Import ReactError 53 | const reactErrorIdentfier = addDefault( 54 | path, 55 | paths.appRoot + '/errors/ErrorDev.js', 56 | { 57 | nameHint: 'InvariantError', 58 | } 59 | ); 60 | 61 | // Outputs: 62 | // throw ReactError(`A ${adj} message that contains ${noun}`); 63 | const devThrow = t.throwStatement( 64 | t.callExpression(reactErrorIdentfier, [ 65 | t.templateLiteral(errorMsgQuasis, errorMsgExpressions), 66 | ]) 67 | ); 68 | 69 | if (noMinify) { 70 | // Error minification is disabled for this build. 71 | // 72 | // Outputs: 73 | // if (!condition) { 74 | // throw ReactError(`A ${adj} message that contains ${noun}`); 75 | // } 76 | path.replaceWith( 77 | t.ifStatement( 78 | t.unaryExpression('!', condition), 79 | t.blockStatement([devThrow]) 80 | ) 81 | ); 82 | return; 83 | } 84 | 85 | // Avoid caching because we write it as we go. 86 | const existingErrorMap = JSON.parse( 87 | fs.readFileSync(paths.appErrorsJson, 'utf-8') 88 | ); 89 | const errorMap = invertObject(existingErrorMap); 90 | 91 | let prodErrorId = errorMap[errorMsgLiteral]; 92 | 93 | if (prodErrorId === undefined) { 94 | // There is no error code for this message. Add an inline comment 95 | // that flags this as an unminified error. This allows the build 96 | // to proceed, while also allowing a post-build linter to detect it. 97 | // 98 | // Outputs: 99 | // /* FIXME (minify-errors-in-prod): Unminified error message in production build! */ 100 | // if (!condition) { 101 | // throw ReactError(`A ${adj} message that contains ${noun}`); 102 | // } 103 | path.replaceWith( 104 | t.ifStatement( 105 | t.unaryExpression('!', condition), 106 | t.blockStatement([devThrow]) 107 | ) 108 | ); 109 | path.addComment( 110 | 'leading', 111 | 'FIXME (minify-errors-in-prod): Unminified error message in production build!' 112 | ); 113 | return; 114 | } 115 | prodErrorId = parseInt(prodErrorId, 10); 116 | 117 | // Import ReactErrorProd 118 | const reactErrorProdIdentfier = addDefault( 119 | path, 120 | paths.appRoot + '/errors/ErrorProd.js', 121 | { 122 | nameHint: 'InvariantErrorProd', 123 | } 124 | ); 125 | 126 | // Outputs: 127 | // throw ReactErrorProd(ERR_CODE, adj, noun); 128 | const prodThrow = t.throwStatement( 129 | t.callExpression(reactErrorProdIdentfier, [ 130 | t.numericLiteral(prodErrorId), 131 | ...errorMsgExpressions, 132 | ]) 133 | ); 134 | 135 | // Outputs: 136 | // if (!condition) { 137 | // if (__DEV__) { 138 | // throw ReactError(`A ${adj} message that contains ${noun}`); 139 | // } else { 140 | // throw ReactErrorProd(ERR_CODE, adj, noun); 141 | // } 142 | // } 143 | path.replaceWith( 144 | t.ifStatement( 145 | t.unaryExpression('!', condition), 146 | t.blockStatement([ 147 | t.ifStatement( 148 | DEV_EXPRESSION, 149 | t.blockStatement([devThrow]), 150 | t.blockStatement([prodThrow]) 151 | ), 152 | ]) 153 | ) 154 | ); 155 | } 156 | }, 157 | }, 158 | }; 159 | } 160 | -------------------------------------------------------------------------------- /src/getInstallArgs.ts: -------------------------------------------------------------------------------- 1 | import { InstallCommand } from './getInstallCmd'; 2 | 3 | export default function getInstallArgs( 4 | cmd: InstallCommand, 5 | packages: string[] 6 | ) { 7 | switch (cmd) { 8 | case 'npm': 9 | return ['install', ...packages, '--save-dev']; 10 | case 'yarn': 11 | return ['add', ...packages, '--dev']; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/getInstallCmd.ts: -------------------------------------------------------------------------------- 1 | import execa from 'execa'; 2 | 3 | let cmd: InstallCommand; 4 | 5 | export type InstallCommand = 'yarn' | 'npm'; 6 | 7 | export default async function getInstallCmd(): Promise { 8 | if (cmd) { 9 | return cmd; 10 | } 11 | 12 | try { 13 | await execa('yarnpkg', ['--version']); 14 | cmd = 'yarn'; 15 | } catch (e) { 16 | cmd = 'npm'; 17 | } 18 | 19 | return cmd; 20 | } 21 | -------------------------------------------------------------------------------- /src/logError.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | 3 | const stderr = console.error.bind(console); 4 | 5 | export default function logError(err: any) { 6 | const error = err.error || err; 7 | const description = `${error.name ? error.name + ': ' : ''}${error.message || 8 | error}`; 9 | const message = error.plugin 10 | ? error.plugin === 'rpt2' 11 | ? `(typescript) ${description}` 12 | : `(${error.plugin} plugin) ${description}` 13 | : description; 14 | 15 | stderr(chalk.bold.red(message)); 16 | 17 | if (error.loc) { 18 | stderr(); 19 | stderr(`at ${error.loc.file}:${error.loc.line}:${error.loc.column}`); 20 | } 21 | 22 | if (error.frame) { 23 | stderr(); 24 | stderr(chalk.dim(error.frame)); 25 | } else if (err.stack) { 26 | const headlessStack = error.stack.replace(message, ''); 27 | stderr(chalk.dim(headlessStack)); 28 | } 29 | 30 | stderr(); 31 | } 32 | -------------------------------------------------------------------------------- /src/messages.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import getInstallCmd from './getInstallCmd'; 3 | import * as Output from './output'; 4 | 5 | // This was copied from Razzle. Lots of unused stuff. 6 | const program = { 7 | name: 'tsdx', 8 | }; 9 | 10 | export const help = function() { 11 | return ` 12 | Only ${chalk.green('')} is required. 13 | If you have any problems, do not hesitate to file an issue: 14 | ${chalk.cyan('https://github.com/formium/tsdx/issues/new')} 15 | `; 16 | }; 17 | 18 | export const missingProjectName = function() { 19 | return ` 20 | Please specify the project directory: 21 | ${chalk.cyan(program.name)} ${chalk.green('')} 22 | For example: 23 | ${chalk.cyan(program.name)} ${chalk.green('my-tsdx-lib')} 24 | Run ${chalk.cyan(`${program.name} --help`)} to see all options. 25 | `; 26 | }; 27 | 28 | export const alreadyExists = function(projectName: string) { 29 | return ` 30 | Uh oh! Looks like there's already a directory called ${chalk.red( 31 | projectName 32 | )}. Please try a different name or delete that folder.`; 33 | }; 34 | 35 | export const installing = function(packages: string[]) { 36 | const pkgText = packages 37 | .map(function(pkg) { 38 | return ` ${chalk.cyan(chalk.bold(pkg))}`; 39 | }) 40 | .join('\n'); 41 | 42 | return `Installing npm modules: 43 | ${pkgText} 44 | `; 45 | }; 46 | 47 | export const installError = function(packages: string[]) { 48 | const pkgText = packages 49 | .map(function(pkg) { 50 | return `${chalk.cyan(chalk.bold(pkg))}`; 51 | }) 52 | .join(', '); 53 | 54 | Output.error(`Failed to install ${pkgText}, try again.`); 55 | }; 56 | 57 | export const copying = function(projectName: string) { 58 | return ` 59 | Creating ${chalk.bold(chalk.green(projectName))}... 60 | `; 61 | }; 62 | 63 | export const start = async function(projectName: string) { 64 | const cmd = await getInstallCmd(); 65 | 66 | const commands = { 67 | install: cmd === 'npm' ? 'npm install' : 'yarn install', 68 | build: cmd === 'npm' ? 'npm run build' : 'yarn build', 69 | start: cmd === 'npm' ? 'npm run start' : 'yarn start', 70 | test: cmd === 'npm' ? 'npm test' : 'yarn test', 71 | }; 72 | 73 | return ` 74 | ${chalk.green('Awesome!')} You're now ready to start coding. 75 | 76 | I already ran ${Output.cmd(commands.install)} for you, so your next steps are: 77 | ${Output.cmd(`cd ${projectName}`)} 78 | 79 | To start developing (rebuilds on changes): 80 | ${Output.cmd(commands.start)} 81 | 82 | To build for production: 83 | ${Output.cmd(commands.build)} 84 | 85 | To test your library with Jest: 86 | ${Output.cmd(commands.test)} 87 | 88 | Questions? Feedback? Please let me know! 89 | ${chalk.green('https://github.com/formium/tsdx/issues')} 90 | `; 91 | }; 92 | 93 | export const incorrectNodeVersion = function(requiredVersion: string) { 94 | return `Unsupported Node version! Your current Node version (${chalk.red( 95 | process.version 96 | )}) does not satisfy the requirement of Node ${chalk.cyan(requiredVersion)}.`; 97 | }; 98 | -------------------------------------------------------------------------------- /src/output.ts: -------------------------------------------------------------------------------- 1 | import { eraseLine } from 'ansi-escapes'; 2 | import chalk from 'chalk'; 3 | import ora from 'ora'; 4 | 5 | // This was copied from Razzle. Lots of unused stuff. 6 | export const info = (msg: string) => { 7 | console.log(`${chalk.gray('>')} ${msg}`); 8 | }; 9 | 10 | export const error = (msg: string | Error) => { 11 | if (msg instanceof Error) { 12 | msg = msg.message; 13 | } 14 | 15 | console.error(`${chalk.red('> Error!')} ${msg}`); 16 | }; 17 | 18 | export const success = (msg: string) => { 19 | console.log(`${chalk.green('> Success!')} ${msg}`); 20 | }; 21 | 22 | export const wait = (msg: string) => { 23 | const spinner = ora(chalk.green(msg)); 24 | spinner.color = 'blue'; 25 | spinner.start(); 26 | 27 | return () => { 28 | spinner.stop(); 29 | process.stdout.write(eraseLine); 30 | }; 31 | }; 32 | 33 | export const cmd = (cmd: string) => { 34 | return chalk.bold(chalk.cyan(cmd)); 35 | }; 36 | 37 | export const code = (cmd: string) => { 38 | return `${chalk.gray('`')}${chalk.bold(cmd)}${chalk.gray('`')}`; 39 | }; 40 | 41 | export const param = (param: string) => { 42 | return chalk.bold(`${chalk.gray('{')}${chalk.bold(param)}${chalk.gray('}')}`); 43 | }; 44 | -------------------------------------------------------------------------------- /src/templates/basic.ts: -------------------------------------------------------------------------------- 1 | import { Template } from './template'; 2 | 3 | const basicTemplate: Template = { 4 | name: 'basic', 5 | dependencies: [ 6 | 'husky', 7 | 'tsdx', 8 | 'tslib', 9 | 'typescript', 10 | 'size-limit', 11 | '@size-limit/preset-small-lib', 12 | ], 13 | packageJson: { 14 | // name: safeName, 15 | version: '0.1.0', 16 | license: 'MIT', 17 | // author: author, 18 | main: './dist/index.cjs', 19 | module: './dist/index.mjs', 20 | exports: { 21 | './package.json': './package.json', 22 | '.': { 23 | import: './dist/index.mjs', 24 | require: './dist/index.cjs', 25 | }, 26 | }, 27 | // module: `dist/${safeName}.mjs`, 28 | typings: `dist/index.d.ts`, 29 | files: ['dist', 'src'], 30 | engines: { 31 | node: '>=14', 32 | }, 33 | scripts: { 34 | start: 'tsdx watch', 35 | build: 'tsdx build', 36 | test: 'tsdx test', 37 | posttest: 'node test/import.mjs && node test/require.cjs', 38 | lint: 'tsdx lint', 39 | prepare: 'tsdx build', 40 | size: 'size-limit', 41 | analyze: 'size-limit --why', 42 | }, 43 | peerDependencies: {}, 44 | husky: { 45 | hooks: { 46 | 'pre-commit': 'tsdx lint', 47 | }, 48 | }, 49 | prettier: { 50 | printWidth: 80, 51 | semi: true, 52 | singleQuote: true, 53 | trailingComma: 'es5', 54 | }, 55 | }, 56 | }; 57 | 58 | export default basicTemplate; 59 | -------------------------------------------------------------------------------- /src/templates/index.ts: -------------------------------------------------------------------------------- 1 | import reactTemplate from './react'; 2 | import basicTemplate from './basic'; 3 | import storybookTemplate from './react-with-storybook'; 4 | 5 | export const templates = { 6 | basic: basicTemplate, 7 | react: reactTemplate, 8 | 'react-with-storybook': storybookTemplate, 9 | }; 10 | -------------------------------------------------------------------------------- /src/templates/react-with-storybook.ts: -------------------------------------------------------------------------------- 1 | import { Template } from './template'; 2 | import reactTemplate from './react'; 3 | import { PackageJson } from 'type-fest'; 4 | 5 | const storybookTemplate: Template = { 6 | dependencies: [ 7 | ...reactTemplate.dependencies, 8 | '@babel/core', 9 | '@storybook/addon-essentials', 10 | '@storybook/addon-links', 11 | '@storybook/addon-info', 12 | '@storybook/addons', 13 | '@storybook/react', 14 | 'react-is', 15 | 'babel-loader', 16 | ], 17 | name: 'react-with-storybook', 18 | packageJson: { 19 | ...reactTemplate.packageJson, 20 | scripts: { 21 | ...reactTemplate.packageJson.scripts, 22 | storybook: 'start-storybook -p 6006', 23 | 'build-storybook': 'build-storybook', 24 | } as PackageJson['scripts'], 25 | }, 26 | }; 27 | 28 | export default storybookTemplate; 29 | -------------------------------------------------------------------------------- /src/templates/react.ts: -------------------------------------------------------------------------------- 1 | import { Template } from './template'; 2 | 3 | import basicTemplate from './basic'; 4 | import { PackageJson } from 'type-fest'; 5 | 6 | const reactTemplate: Template = { 7 | name: 'react', 8 | dependencies: [ 9 | ...basicTemplate.dependencies, 10 | '@types/react', 11 | '@types/react-dom', 12 | 'react', 13 | 'react-dom', 14 | ], 15 | packageJson: { 16 | ...basicTemplate.packageJson, 17 | peerDependencies: { 18 | react: '>=16', 19 | }, 20 | scripts: { 21 | ...basicTemplate.packageJson.scripts, 22 | test: 'tsdx test', 23 | } as PackageJson['scripts'], 24 | }, 25 | }; 26 | 27 | export default reactTemplate; 28 | -------------------------------------------------------------------------------- /src/templates/template.d.ts: -------------------------------------------------------------------------------- 1 | import { PackageJson } from 'type-fest'; 2 | 3 | interface Template { 4 | dependencies: string[]; 5 | name: string; 6 | packageJson: PackageJson & { husky: any; prettier: any; }; 7 | } 8 | -------------------------------------------------------------------------------- /src/templates/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { Template } from '../template'; 2 | 3 | interface ProjectArgs { 4 | name: string; 5 | author: string; 6 | } 7 | export const composePackageJson = (template: Template) => ({ 8 | name, 9 | author, 10 | }: ProjectArgs) => { 11 | return { 12 | ...template.packageJson, 13 | name, 14 | author, 15 | 'size-limit': [ 16 | { 17 | path: `dist/${name}.production.min.cjs`, 18 | limit: '10 KB', 19 | }, 20 | { 21 | path: `dist/${name}.min.mjs`, 22 | limit: '10 KB', 23 | }, 24 | ], 25 | }; 26 | }; 27 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | interface SharedOpts { 2 | // JS target 3 | target: 'node' | 'browser'; 4 | // Path to tsconfig file 5 | tsconfig?: string; 6 | // Is error extraction running? 7 | extractErrors?: boolean; 8 | } 9 | 10 | export type ModuleFormat = 'cjs' | 'umd' | 'esm' | 'system'; 11 | 12 | export interface BuildOpts extends SharedOpts { 13 | name?: string; 14 | entry?: string | string[]; 15 | format: 'cjs,esm'; 16 | target: 'browser'; 17 | } 18 | 19 | export interface WatchOpts extends BuildOpts { 20 | verbose?: boolean; 21 | noClean?: boolean; 22 | // callback hooks 23 | onFirstSuccess?: string; 24 | onSuccess?: string; 25 | onFailure?: string; 26 | } 27 | 28 | export interface NormalizedOpts 29 | extends Omit { 30 | name: string; 31 | input: string[]; 32 | format: [ModuleFormat, ...ModuleFormat[]]; 33 | } 34 | 35 | export interface TsdxOptions extends SharedOpts { 36 | // Name of package 37 | name: string; 38 | // path to file 39 | input: string; 40 | // Environment 41 | env: 'development' | 'production'; 42 | // Module format 43 | format: ModuleFormat; 44 | /** If `true`, Babel transpile and emit ES5. */ 45 | legacy: boolean; 46 | // Is minifying? 47 | minify?: boolean; 48 | // Is this the very first rollup config (and thus should one-off metadata be extracted)? 49 | writeMeta?: boolean; 50 | // Only transpile, do not type check (makes compilation faster) 51 | transpileOnly?: boolean; 52 | } 53 | 54 | export interface PackageJson { 55 | name: string; 56 | source?: string; 57 | jest?: any; 58 | eslint?: any; 59 | dependencies?: { [packageName: string]: string }; 60 | devDependencies?: { [packageName: string]: string }; 61 | engines?: { 62 | node?: string; 63 | }; 64 | } 65 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra'; 2 | import path from 'path'; 3 | import camelCase from 'camelcase'; 4 | 5 | import { PackageJson } from './types'; 6 | 7 | // Remove the package name scope if it exists 8 | export const removeScope = (name: string) => name.replace(/^@.*\//, ''); 9 | 10 | // UMD-safe package name 11 | export const safeVariableName = (name: string) => 12 | camelCase( 13 | removeScope(name) 14 | .toLowerCase() 15 | .replace(/((^[^a-zA-Z]+)|[^\w.-])|([^a-zA-Z0-9]+$)/g, '') 16 | ); 17 | 18 | export const safePackageName = (name: string) => 19 | name 20 | .toLowerCase() 21 | .replace(/(^@.*\/)|((^[^a-zA-Z]+)|[^\w.-])|([^a-zA-Z0-9]+$)/g, ''); 22 | 23 | export const external = (id: string) => 24 | !id.startsWith('.') && !path.isAbsolute(id); 25 | 26 | // Make sure any symlinks in the project folder are resolved: 27 | // https://github.com/facebookincubator/create-react-app/issues/637 28 | export const appDirectory = fs.realpathSync(process.cwd()); 29 | export const resolveApp = function(relativePath: string) { 30 | return path.resolve(appDirectory, relativePath); 31 | }; 32 | 33 | // Taken from Create React App, react-dev-utils/clearConsole 34 | // @see https://github.com/facebook/create-react-app/blob/master/packages/react-dev-utils/clearConsole.js 35 | export function clearConsole() { 36 | process.stdout.write( 37 | process.platform === 'win32' ? '\x1B[2J\x1B[0f' : '\x1B[2J\x1B[3J\x1B[H' 38 | ); 39 | } 40 | 41 | export function getReactVersion({ 42 | dependencies, 43 | devDependencies, 44 | }: PackageJson) { 45 | return ( 46 | (dependencies && dependencies.react) || 47 | (devDependencies && devDependencies.react) 48 | ); 49 | } 50 | 51 | export function getNodeEngineRequirement({ engines }: PackageJson) { 52 | return engines && engines.node; 53 | } 54 | -------------------------------------------------------------------------------- /templates/basic/.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push] 3 | jobs: 4 | build: 5 | name: Build, lint, and test on Node ${{ matrix.node }} and ${{ matrix.os }} 6 | 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | node: ['10.x', '12.x', '14.x'] 11 | os: [ubuntu-latest, windows-latest, macOS-latest] 12 | 13 | steps: 14 | - name: Checkout repo 15 | uses: actions/checkout@v2 16 | 17 | - name: Use Node ${{ matrix.node }} 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: ${{ matrix.node }} 21 | 22 | - name: Install deps and build (with cache) 23 | uses: bahmutov/npm-install@v1 24 | 25 | - name: Lint 26 | run: yarn lint 27 | 28 | - name: Test 29 | run: yarn test --ci --coverage --maxWorkers=2 30 | 31 | - name: Build 32 | run: yarn build 33 | -------------------------------------------------------------------------------- /templates/basic/.github/workflows/size.yml: -------------------------------------------------------------------------------- 1 | name: size 2 | on: [pull_request] 3 | jobs: 4 | size: 5 | runs-on: ubuntu-latest 6 | env: 7 | CI_JOB_NUMBER: 1 8 | steps: 9 | - uses: actions/checkout@v1 10 | - uses: andresz1/size-limit-action@v1 11 | with: 12 | github_token: ${{ secrets.GITHUB_TOKEN }} 13 | -------------------------------------------------------------------------------- /templates/basic/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /templates/basic/README.md: -------------------------------------------------------------------------------- 1 | # TSDX User Guide 2 | 3 | Congrats! You just saved yourself hours of work by bootstrapping this project with TSDX. Let’s get you oriented with what’s here and how to use it. 4 | 5 | > This TSDX setup is meant for developing libraries (not apps!) that can be published to NPM. If you’re looking to build a Node app, you could use `ts-node-dev`, plain `ts-node`, or simple `tsc`. 6 | 7 | > If you’re new to TypeScript, checkout [this handy cheatsheet](https://devhints.io/typescript) 8 | 9 | ## Commands 10 | 11 | TSDX scaffolds your new library inside `/src`. 12 | 13 | To run TSDX, use: 14 | 15 | ```bash 16 | npm start # or yarn start 17 | ``` 18 | 19 | This builds to `/dist` and runs the project in watch mode so any edits you save inside `src` causes a rebuild to `/dist`. 20 | 21 | To do a one-off build, use `npm run build` or `yarn build`. 22 | 23 | To run tests, use `npm test` or `yarn test`. 24 | 25 | ## Configuration 26 | 27 | Code quality is set up for you with `prettier`, `husky`, and `lint-staged`. Adjust the respective fields in `package.json` accordingly. 28 | 29 | ### Jest 30 | 31 | Jest tests are set up to run with `npm test` or `yarn test`. 32 | 33 | ### Bundle Analysis 34 | 35 | [`size-limit`](https://github.com/ai/size-limit) is set up to calculate the real cost of your library with `npm run size` and visualize the bundle with `npm run analyze`. 36 | 37 | #### Setup Files 38 | 39 | This is the folder structure we set up for you: 40 | 41 | ```txt 42 | /src 43 | index.ts # EDIT THIS 44 | /test 45 | index.test.ts # EDIT THIS 46 | .gitignore 47 | package.json 48 | README.md # EDIT THIS 49 | tsconfig.json 50 | ``` 51 | 52 | ### Rollup 53 | 54 | TSDX uses [Rollup](https://rollupjs.org) as a bundler and generates multiple rollup configs for various module formats and build settings. See [Optimizations](#optimizations) for details. 55 | 56 | ### TypeScript 57 | 58 | `tsconfig.json` is set up to interpret `dom` and `esnext` types, as well as `react` for `jsx`. Adjust according to your needs. 59 | 60 | ## Continuous Integration 61 | 62 | ### GitHub Actions 63 | 64 | Two actions are added by default: 65 | 66 | - `main` which installs deps w/ cache, lints, tests, and builds on all pushes against a Node and OS matrix 67 | - `size` which comments cost comparison of your library on every pull request using [`size-limit`](https://github.com/ai/size-limit) 68 | 69 | ## Optimizations 70 | 71 | Please see the main `tsdx` [optimizations docs](https://github.com/palmerhq/tsdx#optimizations). In particular, know that you can take advantage of development-only optimizations: 72 | 73 | ```js 74 | // ./types/index.d.ts 75 | declare var __DEV__: boolean; 76 | 77 | // inside your code... 78 | if (__DEV__) { 79 | console.log('foo'); 80 | } 81 | ``` 82 | 83 | You can also choose to install and use [invariant](https://github.com/palmerhq/tsdx#invariant) and [warning](https://github.com/palmerhq/tsdx#warning) functions. 84 | 85 | ## Module Formats 86 | 87 | CJS, ESModules, and UMD module formats are supported. 88 | 89 | The appropriate paths are configured in `package.json` and `dist/index.js` accordingly. Please report if any issues are found. 90 | 91 | ## Named Exports 92 | 93 | Per Palmer Group guidelines, [always use named exports.](https://github.com/palmerhq/typescript#exports) Code split inside your React app instead of your React library. 94 | 95 | ## Including Styles 96 | 97 | There are many ways to ship styles, including with CSS-in-JS. TSDX has no opinion on this, configure how you like. 98 | 99 | For vanilla CSS, you can include it at the root directory and add it to the `files` section in your `package.json`, so that it can be imported separately by your users and run through their bundler's loader. 100 | 101 | ## Publishing to NPM 102 | 103 | We recommend using [np](https://github.com/sindresorhus/np). 104 | -------------------------------------------------------------------------------- /templates/basic/gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | dist 5 | -------------------------------------------------------------------------------- /templates/basic/src/index.ts: -------------------------------------------------------------------------------- 1 | export const sum = (a: number, b: number) => { 2 | if ('development' === process.env.NODE_ENV) { 3 | console.log('dev only output'); 4 | } 5 | return a + b; 6 | }; 7 | -------------------------------------------------------------------------------- /templates/basic/test/import.mjs: -------------------------------------------------------------------------------- 1 | // import * as module from 'your-package-name'; 2 | import * as module from '../dist/index.mjs'; 3 | console.log(module); 4 | -------------------------------------------------------------------------------- /templates/basic/test/index.test.ts: -------------------------------------------------------------------------------- 1 | const sum = (a: number, b: number) => a + b; 2 | 3 | describe('sum', () => { 4 | it('adds two numbers together', () => { 5 | expect(sum(1, 1)).toEqual(2); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /templates/basic/test/require.cjs: -------------------------------------------------------------------------------- 1 | console.log(require('..')) -------------------------------------------------------------------------------- /templates/basic/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // see https://www.typescriptlang.org/tsconfig to better understand tsconfigs 3 | "include": ["src", "types"], 4 | "compilerOptions": { 5 | "module": "esnext", 6 | "lib": ["dom", "esnext"], 7 | "importHelpers": true, 8 | // output .d.ts declaration files for consumers 9 | "declaration": true, 10 | // output .js.map sourcemap files for consumers 11 | "sourceMap": true, 12 | // match output dir to input dir. e.g. dist/index instead of dist/src/index 13 | "rootDir": "./src", 14 | // stricter type-checking for stronger correctness. Recommended by TS 15 | "strict": true, 16 | // linter checks for common issues 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": true, 19 | // noUnused* overlap with @typescript-eslint/no-unused-vars, can disable if duplicative 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | // use Node's module resolution algorithm, instead of the legacy TS one 23 | "moduleResolution": "node", 24 | // transpile JSX to React.createElement 25 | "jsx": "react", 26 | // interop between ESM and CJS modules. Recommended by TS 27 | "esModuleInterop": true, 28 | // significant perf increase by skipping checking .d.ts files, particularly those in node_modules. Recommended by TS 29 | "skipLibCheck": true, 30 | // error out if import and file system have a casing mismatch. Recommended by TS 31 | "forceConsistentCasingInFileNames": true, 32 | // `tsdx build` ignores this option, but it is commonly used when type-checking separately with `tsc` 33 | "noEmit": true, 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /templates/react-with-storybook/.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push] 3 | jobs: 4 | build: 5 | name: Build, lint, and test on Node ${{ matrix.node }} and ${{ matrix.os }} 6 | 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | node: ['10.x', '12.x', '14.x'] 11 | os: [ubuntu-latest, windows-latest, macOS-latest] 12 | 13 | steps: 14 | - name: Checkout repo 15 | uses: actions/checkout@v2 16 | 17 | - name: Use Node ${{ matrix.node }} 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: ${{ matrix.node }} 21 | 22 | - name: Install deps and build (with cache) 23 | uses: bahmutov/npm-install@v1 24 | 25 | - name: Lint 26 | run: yarn lint 27 | 28 | - name: Test 29 | run: yarn test --ci --coverage --maxWorkers=2 30 | 31 | - name: Build 32 | run: yarn build 33 | -------------------------------------------------------------------------------- /templates/react-with-storybook/.github/workflows/size.yml: -------------------------------------------------------------------------------- 1 | name: size 2 | on: [pull_request] 3 | jobs: 4 | size: 5 | runs-on: ubuntu-latest 6 | env: 7 | CI_JOB_NUMBER: 1 8 | steps: 9 | - uses: actions/checkout@v1 10 | - uses: andresz1/size-limit-action@v1 11 | with: 12 | github_token: ${{ secrets.GITHUB_TOKEN }} 13 | -------------------------------------------------------------------------------- /templates/react-with-storybook/.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | stories: ['../stories/**/*.stories.@(ts|tsx|js|jsx)'], 3 | addons: ['@storybook/addon-links', '@storybook/addon-essentials'], 4 | // https://storybook.js.org/docs/react/configure/typescript#mainjs-configuration 5 | typescript: { 6 | check: true, // type-check stories during Storybook build 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /templates/react-with-storybook/.storybook/preview.js: -------------------------------------------------------------------------------- 1 | // https://storybook.js.org/docs/react/writing-stories/parameters#global-parameters 2 | export const parameters = { 3 | // https://storybook.js.org/docs/react/essentials/actions#automatically-matching-args 4 | actions: { argTypesRegex: '^on.*' }, 5 | }; 6 | -------------------------------------------------------------------------------- /templates/react-with-storybook/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /templates/react-with-storybook/README.md: -------------------------------------------------------------------------------- 1 | # TSDX React w/ Storybook User Guide 2 | 3 | Congrats! You just saved yourself hours of work by bootstrapping this project with TSDX. Let’s get you oriented with what’s here and how to use it. 4 | 5 | > This TSDX setup is meant for developing React component libraries (not apps!) that can be published to NPM. If you’re looking to build a React-based app, you should use `create-react-app`, `razzle`, `nextjs`, `gatsby`, or `react-static`. 6 | 7 | > If you’re new to TypeScript and React, checkout [this handy cheatsheet](https://github.com/sw-yx/react-typescript-cheatsheet/) 8 | 9 | ## Commands 10 | 11 | TSDX scaffolds your new library inside `/src`, and also sets up a [Parcel-based](https://parceljs.org) playground for it inside `/example`. 12 | 13 | The recommended workflow is to run TSDX in one terminal: 14 | 15 | ```bash 16 | npm start # or yarn start 17 | ``` 18 | 19 | This builds to `/dist` and runs the project in watch mode so any edits you save inside `src` causes a rebuild to `/dist`. 20 | 21 | Then run either Storybook or the example playground: 22 | 23 | ### Storybook 24 | 25 | Run inside another terminal: 26 | 27 | ```bash 28 | yarn storybook 29 | ``` 30 | 31 | This loads the stories from `./stories`. 32 | 33 | > NOTE: Stories should reference the components as if using the library, similar to the example playground. This means importing from the root project directory. This has been aliased in the tsconfig and the storybook webpack config as a helper. 34 | 35 | ### Example 36 | 37 | Then run the example inside another: 38 | 39 | ```bash 40 | cd example 41 | npm i # or yarn to install dependencies 42 | npm start # or yarn start 43 | ``` 44 | 45 | The default example imports and live reloads whatever is in `/dist`, so if you are seeing an out of date component, make sure TSDX is running in watch mode like we recommend above. **No symlinking required**, we use [Parcel's aliasing](https://parceljs.org/module_resolution.html#aliases). 46 | 47 | To do a one-off build, use `npm run build` or `yarn build`. 48 | 49 | To run tests, use `npm test` or `yarn test`. 50 | 51 | ## Configuration 52 | 53 | Code quality is set up for you with `prettier`, `husky`, and `lint-staged`. Adjust the respective fields in `package.json` accordingly. 54 | 55 | ### Jest 56 | 57 | Jest tests are set up to run with `npm test` or `yarn test`. 58 | 59 | ### Bundle analysis 60 | 61 | Calculates the real cost of your library using [size-limit](https://github.com/ai/size-limit) with `npm run size` and visulize it with `npm run analyze`. 62 | 63 | #### Setup Files 64 | 65 | This is the folder structure we set up for you: 66 | 67 | ```txt 68 | /example 69 | index.html 70 | index.tsx # test your component here in a demo app 71 | package.json 72 | tsconfig.json 73 | /src 74 | index.tsx # EDIT THIS 75 | /test 76 | index.test.tsx # EDIT THIS 77 | /stories 78 | Thing.stories.tsx # EDIT THIS 79 | /.storybook 80 | main.js 81 | preview.js 82 | .gitignore 83 | package.json 84 | README.md # EDIT THIS 85 | tsconfig.json 86 | ``` 87 | 88 | #### React Testing Library 89 | 90 | We do not set up `react-testing-library` for you yet, we welcome contributions and documentation on this. 91 | 92 | ### Rollup 93 | 94 | TSDX uses [Rollup](https://rollupjs.org) as a bundler and generates multiple rollup configs for various module formats and build settings. See [Optimizations](#optimizations) for details. 95 | 96 | ### TypeScript 97 | 98 | `tsconfig.json` is set up to interpret `dom` and `esnext` types, as well as `react` for `jsx`. Adjust according to your needs. 99 | 100 | ## Continuous Integration 101 | 102 | ### GitHub Actions 103 | 104 | Two actions are added by default: 105 | 106 | - `main` which installs deps w/ cache, lints, tests, and builds on all pushes against a Node and OS matrix 107 | - `size` which comments cost comparison of your library on every pull request using [size-limit](https://github.com/ai/size-limit) 108 | 109 | ## Optimizations 110 | 111 | Please see the main `tsdx` [optimizations docs](https://github.com/palmerhq/tsdx#optimizations). In particular, know that you can take advantage of development-only optimizations: 112 | 113 | ```js 114 | // ./types/index.d.ts 115 | declare var __DEV__: boolean; 116 | 117 | // inside your code... 118 | if (__DEV__) { 119 | console.log('foo'); 120 | } 121 | ``` 122 | 123 | You can also choose to install and use [invariant](https://github.com/palmerhq/tsdx#invariant) and [warning](https://github.com/palmerhq/tsdx#warning) functions. 124 | 125 | ## Module Formats 126 | 127 | CJS, ESModules, and UMD module formats are supported. 128 | 129 | The appropriate paths are configured in `package.json` and `dist/index.js` accordingly. Please report if any issues are found. 130 | 131 | ## Deploying the Example Playground 132 | 133 | The Playground is just a simple [Parcel](https://parceljs.org) app, you can deploy it anywhere you would normally deploy that. Here are some guidelines for **manually** deploying with the Netlify CLI (`npm i -g netlify-cli`): 134 | 135 | ```bash 136 | cd example # if not already in the example folder 137 | npm run build # builds to dist 138 | netlify deploy # deploy the dist folder 139 | ``` 140 | 141 | Alternatively, if you already have a git repo connected, you can set up continuous deployment with Netlify: 142 | 143 | ```bash 144 | netlify init 145 | # build command: yarn build && cd example && yarn && yarn build 146 | # directory to deploy: example/dist 147 | # pick yes for netlify.toml 148 | ``` 149 | 150 | ## Named Exports 151 | 152 | Per Palmer Group guidelines, [always use named exports.](https://github.com/palmerhq/typescript#exports) Code split inside your React app instead of your React library. 153 | 154 | ## Including Styles 155 | 156 | There are many ways to ship styles, including with CSS-in-JS. TSDX has no opinion on this, configure how you like. 157 | 158 | For vanilla CSS, you can include it at the root directory and add it to the `files` section in your `package.json`, so that it can be imported separately by your users and run through their bundler's loader. 159 | 160 | ## Publishing to NPM 161 | 162 | We recommend using [np](https://github.com/sindresorhus/np). 163 | 164 | ## Usage with Lerna 165 | 166 | When creating a new package with TSDX within a project set up with Lerna, you might encounter a `Cannot resolve dependency` error when trying to run the `example` project. To fix that you will need to make changes to the `package.json` file _inside the `example` directory_. 167 | 168 | The problem is that due to the nature of how dependencies are installed in Lerna projects, the aliases in the example project's `package.json` might not point to the right place, as those dependencies might have been installed in the root of your Lerna project. 169 | 170 | Change the `alias` to point to where those packages are actually installed. This depends on the directory structure of your Lerna project, so the actual path might be different from the diff below. 171 | 172 | ```diff 173 | "alias": { 174 | - "react": "../node_modules/react", 175 | - "react-dom": "../node_modules/react-dom" 176 | + "react": "../../../node_modules/react", 177 | + "react-dom": "../../../node_modules/react-dom" 178 | }, 179 | ``` 180 | 181 | An alternative to fixing this problem would be to remove aliases altogether and define the dependencies referenced as aliases as dev dependencies instead. [However, that might cause other problems.](https://github.com/palmerhq/tsdx/issues/64) 182 | -------------------------------------------------------------------------------- /templates/react-with-storybook/example/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache 3 | dist -------------------------------------------------------------------------------- /templates/react-with-storybook/example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Playground 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /templates/react-with-storybook/example/index.tsx: -------------------------------------------------------------------------------- 1 | import 'react-app-polyfill/ie11'; 2 | import * as React from 'react'; 3 | import * as ReactDOM from 'react-dom'; 4 | import { Thing } from '../.'; 5 | 6 | const App = () => { 7 | return ( 8 |
9 | 10 |
11 | ); 12 | }; 13 | 14 | ReactDOM.render(, document.getElementById('root')); 15 | -------------------------------------------------------------------------------- /templates/react-with-storybook/example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "start": "parcel index.html", 8 | "build": "parcel build index.html" 9 | }, 10 | "dependencies": { 11 | "react-app-polyfill": "^1.0.0" 12 | }, 13 | "alias": { 14 | "react": "../node_modules/react", 15 | "react-dom": "../node_modules/react-dom/profiling", 16 | "scheduler/tracing": "../node_modules/scheduler/tracing-profiling" 17 | }, 18 | "devDependencies": { 19 | "@types/react": "^16.9.11", 20 | "@types/react-dom": "^16.8.4", 21 | "parcel": "^1.12.3", 22 | "typescript": "^3.4.5" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /templates/react-with-storybook/example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": false, 4 | "target": "es5", 5 | "module": "commonjs", 6 | "jsx": "react", 7 | "moduleResolution": "node", 8 | "noImplicitAny": false, 9 | "noUnusedLocals": false, 10 | "noUnusedParameters": false, 11 | "removeComments": true, 12 | "strictNullChecks": true, 13 | "preserveConstEnums": true, 14 | "sourceMap": true, 15 | "lib": ["es2015", "es2016", "dom"], 16 | "types": ["node"] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /templates/react-with-storybook/gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | .cache 5 | dist 6 | -------------------------------------------------------------------------------- /templates/react-with-storybook/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, HTMLAttributes, ReactChild } from 'react'; 2 | 3 | export interface Props extends HTMLAttributes { 4 | /** custom content, defaults to 'the snozzberries taste like snozzberries' */ 5 | children?: ReactChild; 6 | } 7 | 8 | // Please do not use types off of a default export module or else Storybook Docs will suffer. 9 | // see: https://github.com/storybookjs/storybook/issues/9556 10 | /** 11 | * A custom Thing component. Neat! 12 | */ 13 | export const Thing: FC = ({ children }) => { 14 | return
{children || `the snozzberries taste like snozzberries`}
; 15 | }; 16 | -------------------------------------------------------------------------------- /templates/react-with-storybook/stories/Thing.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Meta, Story } from '@storybook/react'; 3 | import { Thing, Props } from '../src'; 4 | 5 | const meta: Meta = { 6 | title: 'Welcome', 7 | component: Thing, 8 | argTypes: { 9 | children: { 10 | control: { 11 | type: 'text', 12 | }, 13 | }, 14 | }, 15 | parameters: { 16 | controls: { expanded: true }, 17 | }, 18 | }; 19 | 20 | export default meta; 21 | 22 | const Template: Story = args => ; 23 | 24 | // By passing using the Args format for exported stories, you can control the props for a component for reuse in a test 25 | // https://storybook.js.org/docs/react/workflows/unit-testing 26 | export const Default = Template.bind({}); 27 | 28 | Default.args = {}; 29 | -------------------------------------------------------------------------------- /templates/react-with-storybook/test/import.mjs: -------------------------------------------------------------------------------- 1 | // import * as module from 'your-package-name'; 2 | import * as module from '../dist/index.mjs'; 3 | console.log(module); 4 | -------------------------------------------------------------------------------- /templates/react-with-storybook/test/index.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import { Default as Thing } from '../stories/Thing.stories'; 4 | 5 | describe('Thing', () => { 6 | it('renders without crashing', () => { 7 | const div = document.createElement('div'); 8 | ReactDOM.render(, div); 9 | ReactDOM.unmountComponentAtNode(div); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /templates/react-with-storybook/test/require.cjs: -------------------------------------------------------------------------------- 1 | console.log(require('..')) -------------------------------------------------------------------------------- /templates/react-with-storybook/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // see https://www.typescriptlang.org/tsconfig to better understand tsconfigs 3 | "include": ["src", "types"], 4 | "compilerOptions": { 5 | "module": "esnext", 6 | "lib": ["dom", "esnext"], 7 | "importHelpers": true, 8 | // output .d.ts declaration files for consumers 9 | "declaration": true, 10 | // output .js.map sourcemap files for consumers 11 | "sourceMap": true, 12 | // match output dir to input dir. e.g. dist/index instead of dist/src/index 13 | "rootDir": "./src", 14 | // stricter type-checking for stronger correctness. Recommended by TS 15 | "strict": true, 16 | // linter checks for common issues 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": true, 19 | // noUnused* overlap with @typescript-eslint/no-unused-vars, can disable if duplicative 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | // use Node's module resolution algorithm, instead of the legacy TS one 23 | "moduleResolution": "node", 24 | // transpile JSX to React.createElement 25 | "jsx": "react", 26 | // interop between ESM and CJS modules. Recommended by TS 27 | "esModuleInterop": true, 28 | // significant perf increase by skipping checking .d.ts files, particularly those in node_modules. Recommended by TS 29 | "skipLibCheck": true, 30 | // error out if import and file system have a casing mismatch. Recommended by TS 31 | "forceConsistentCasingInFileNames": true, 32 | // `tsdx build` ignores this option, but it is commonly used when type-checking separately with `tsc` 33 | "noEmit": true, 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /templates/react/.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push] 3 | jobs: 4 | build: 5 | name: Build, lint, and test on Node ${{ matrix.node }} and ${{ matrix.os }} 6 | 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | node: ['10.x', '12.x', '14.x'] 11 | os: [ubuntu-latest, windows-latest, macOS-latest] 12 | 13 | steps: 14 | - name: Checkout repo 15 | uses: actions/checkout@v2 16 | 17 | - name: Use Node ${{ matrix.node }} 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: ${{ matrix.node }} 21 | 22 | - name: Install deps and build (with cache) 23 | uses: bahmutov/npm-install@v1 24 | 25 | - name: Lint 26 | run: yarn lint 27 | 28 | - name: Test 29 | run: yarn test --ci --coverage --maxWorkers=2 30 | 31 | - name: Build 32 | run: yarn build 33 | -------------------------------------------------------------------------------- /templates/react/.github/workflows/size.yml: -------------------------------------------------------------------------------- 1 | name: size 2 | on: [pull_request] 3 | jobs: 4 | size: 5 | runs-on: ubuntu-latest 6 | env: 7 | CI_JOB_NUMBER: 1 8 | steps: 9 | - uses: actions/checkout@v1 10 | - uses: andresz1/size-limit-action@v1 11 | with: 12 | github_token: ${{ secrets.GITHUB_TOKEN }} 13 | -------------------------------------------------------------------------------- /templates/react/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /templates/react/README.md: -------------------------------------------------------------------------------- 1 | # TSDX React User Guide 2 | 3 | Congrats! You just saved yourself hours of work by bootstrapping this project with TSDX. Let’s get you oriented with what’s here and how to use it. 4 | 5 | > This TSDX setup is meant for developing React component libraries (not apps!) that can be published to NPM. If you’re looking to build a React-based app, you should use `create-react-app`, `razzle`, `nextjs`, `gatsby`, or `react-static`. 6 | 7 | > If you’re new to TypeScript and React, checkout [this handy cheatsheet](https://github.com/sw-yx/react-typescript-cheatsheet/) 8 | 9 | ## Commands 10 | 11 | TSDX scaffolds your new library inside `/src`, and also sets up a [Parcel-based](https://parceljs.org) playground for it inside `/example`. 12 | 13 | The recommended workflow is to run TSDX in one terminal: 14 | 15 | ```bash 16 | npm start # or yarn start 17 | ``` 18 | 19 | This builds to `/dist` and runs the project in watch mode so any edits you save inside `src` causes a rebuild to `/dist`. 20 | 21 | Then run the example inside another: 22 | 23 | ```bash 24 | cd example 25 | npm i # or yarn to install dependencies 26 | npm start # or yarn start 27 | ``` 28 | 29 | The default example imports and live reloads whatever is in `/dist`, so if you are seeing an out of date component, make sure TSDX is running in watch mode like we recommend above. **No symlinking required**, we use [Parcel's aliasing](https://parceljs.org/module_resolution.html#aliases). 30 | 31 | To do a one-off build, use `npm run build` or `yarn build`. 32 | 33 | To run tests, use `npm test` or `yarn test`. 34 | 35 | ## Configuration 36 | 37 | Code quality is set up for you with `prettier`, `husky`, and `lint-staged`. Adjust the respective fields in `package.json` accordingly. 38 | 39 | ### Jest 40 | 41 | Jest tests are set up to run with `npm test` or `yarn test`. 42 | 43 | ### Bundle analysis 44 | 45 | Calculates the real cost of your library using [size-limit](https://github.com/ai/size-limit) with `npm run size` and visulize it with `npm run analyze`. 46 | 47 | #### Setup Files 48 | 49 | This is the folder structure we set up for you: 50 | 51 | ```txt 52 | /example 53 | index.html 54 | index.tsx # test your component here in a demo app 55 | package.json 56 | tsconfig.json 57 | /src 58 | index.tsx # EDIT THIS 59 | /test 60 | index.test.tsx # EDIT THIS 61 | .gitignore 62 | package.json 63 | README.md # EDIT THIS 64 | tsconfig.json 65 | ``` 66 | 67 | #### React Testing Library 68 | 69 | We do not set up `react-testing-library` for you yet, we welcome contributions and documentation on this. 70 | 71 | ### Rollup 72 | 73 | TSDX uses [Rollup](https://rollupjs.org) as a bundler and generates multiple rollup configs for various module formats and build settings. See [Optimizations](#optimizations) for details. 74 | 75 | ### TypeScript 76 | 77 | `tsconfig.json` is set up to interpret `dom` and `esnext` types, as well as `react` for `jsx`. Adjust according to your needs. 78 | 79 | ## Continuous Integration 80 | 81 | ### GitHub Actions 82 | 83 | Two actions are added by default: 84 | 85 | - `main` which installs deps w/ cache, lints, tests, and builds on all pushes against a Node and OS matrix 86 | - `size` which comments cost comparison of your library on every pull request using [`size-limit`](https://github.com/ai/size-limit) 87 | 88 | ## Optimizations 89 | 90 | Please see the main `tsdx` [optimizations docs](https://github.com/palmerhq/tsdx#optimizations). In particular, know that you can take advantage of development-only optimizations: 91 | 92 | ```js 93 | // ./types/index.d.ts 94 | declare var __DEV__: boolean; 95 | 96 | // inside your code... 97 | if (__DEV__) { 98 | console.log('foo'); 99 | } 100 | ``` 101 | 102 | You can also choose to install and use [invariant](https://github.com/palmerhq/tsdx#invariant) and [warning](https://github.com/palmerhq/tsdx#warning) functions. 103 | 104 | ## Module Formats 105 | 106 | CJS, ESModules, and UMD module formats are supported. 107 | 108 | The appropriate paths are configured in `package.json` and `dist/index.js` accordingly. Please report if any issues are found. 109 | 110 | ## Deploying the Example Playground 111 | 112 | The Playground is just a simple [Parcel](https://parceljs.org) app, you can deploy it anywhere you would normally deploy that. Here are some guidelines for **manually** deploying with the Netlify CLI (`npm i -g netlify-cli`): 113 | 114 | ```bash 115 | cd example # if not already in the example folder 116 | npm run build # builds to dist 117 | netlify deploy # deploy the dist folder 118 | ``` 119 | 120 | Alternatively, if you already have a git repo connected, you can set up continuous deployment with Netlify: 121 | 122 | ```bash 123 | netlify init 124 | # build command: yarn build && cd example && yarn && yarn build 125 | # directory to deploy: example/dist 126 | # pick yes for netlify.toml 127 | ``` 128 | 129 | ## Named Exports 130 | 131 | Per Palmer Group guidelines, [always use named exports.](https://github.com/palmerhq/typescript#exports) Code split inside your React app instead of your React library. 132 | 133 | ## Including Styles 134 | 135 | There are many ways to ship styles, including with CSS-in-JS. TSDX has no opinion on this, configure how you like. 136 | 137 | For vanilla CSS, you can include it at the root directory and add it to the `files` section in your `package.json`, so that it can be imported separately by your users and run through their bundler's loader. 138 | 139 | ## Publishing to NPM 140 | 141 | We recommend using [np](https://github.com/sindresorhus/np). 142 | 143 | ## Usage with Lerna 144 | 145 | When creating a new package with TSDX within a project set up with Lerna, you might encounter a `Cannot resolve dependency` error when trying to run the `example` project. To fix that you will need to make changes to the `package.json` file _inside the `example` directory_. 146 | 147 | The problem is that due to the nature of how dependencies are installed in Lerna projects, the aliases in the example project's `package.json` might not point to the right place, as those dependencies might have been installed in the root of your Lerna project. 148 | 149 | Change the `alias` to point to where those packages are actually installed. This depends on the directory structure of your Lerna project, so the actual path might be different from the diff below. 150 | 151 | ```diff 152 | "alias": { 153 | - "react": "../node_modules/react", 154 | - "react-dom": "../node_modules/react-dom" 155 | + "react": "../../../node_modules/react", 156 | + "react-dom": "../../../node_modules/react-dom" 157 | }, 158 | ``` 159 | 160 | An alternative to fixing this problem would be to remove aliases altogether and define the dependencies referenced as aliases as dev dependencies instead. [However, that might cause other problems.](https://github.com/palmerhq/tsdx/issues/64) 161 | -------------------------------------------------------------------------------- /templates/react/example/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache 3 | dist -------------------------------------------------------------------------------- /templates/react/example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Playground 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /templates/react/example/index.tsx: -------------------------------------------------------------------------------- 1 | import 'react-app-polyfill/ie11'; 2 | import * as React from 'react'; 3 | import * as ReactDOM from 'react-dom'; 4 | import { Thing } from '../.'; 5 | 6 | const App = () => { 7 | return ( 8 |
9 | 10 |
11 | ); 12 | }; 13 | 14 | ReactDOM.render(, document.getElementById('root')); 15 | -------------------------------------------------------------------------------- /templates/react/example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "start": "parcel index.html", 8 | "build": "parcel build index.html" 9 | }, 10 | "dependencies": { 11 | "react-app-polyfill": "^1.0.0" 12 | }, 13 | "alias": { 14 | "react": "../node_modules/react", 15 | "react-dom": "../node_modules/react-dom/profiling", 16 | "scheduler/tracing": "../node_modules/scheduler/tracing-profiling" 17 | }, 18 | "devDependencies": { 19 | "@types/react": "^16.9.11", 20 | "@types/react-dom": "^16.8.4", 21 | "parcel": "^1.12.3", 22 | "typescript": "^3.4.5" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /templates/react/example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": false, 4 | "target": "es5", 5 | "module": "commonjs", 6 | "jsx": "react", 7 | "moduleResolution": "node", 8 | "noImplicitAny": false, 9 | "noUnusedLocals": false, 10 | "noUnusedParameters": false, 11 | "removeComments": true, 12 | "strictNullChecks": true, 13 | "preserveConstEnums": true, 14 | "sourceMap": true, 15 | "lib": ["es2015", "es2016", "dom"], 16 | "types": ["node"] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /templates/react/gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | .cache 5 | dist 6 | -------------------------------------------------------------------------------- /templates/react/src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | // Delete me 4 | export const Thing = () => { 5 | return
the snozzberries taste like snozzberries
; 6 | }; 7 | -------------------------------------------------------------------------------- /templates/react/test/import.mjs: -------------------------------------------------------------------------------- 1 | // import * as module from 'your-package-name'; 2 | import * as module from '../dist/index.mjs'; 3 | console.log(module); 4 | -------------------------------------------------------------------------------- /templates/react/test/index.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import { Thing } from '../src'; 4 | 5 | describe('Thing', () => { 6 | it('renders without crashing', () => { 7 | const div = document.createElement('div'); 8 | ReactDOM.render(, div); 9 | ReactDOM.unmountComponentAtNode(div); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /templates/react/test/require.cjs: -------------------------------------------------------------------------------- 1 | console.log(require('..')) -------------------------------------------------------------------------------- /templates/react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // see https://www.typescriptlang.org/tsconfig to better understand tsconfigs 3 | "include": ["src", "types"], 4 | "compilerOptions": { 5 | "module": "esnext", 6 | "lib": ["dom", "esnext"], 7 | "importHelpers": true, 8 | // output .d.ts declaration files for consumers 9 | "declaration": true, 10 | // output .js.map sourcemap files for consumers 11 | "sourceMap": true, 12 | // match output dir to input dir. e.g. dist/index instead of dist/src/index 13 | "rootDir": "./src", 14 | // stricter type-checking for stronger correctness. Recommended by TS 15 | "strict": true, 16 | // linter checks for common issues 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": true, 19 | // noUnused* overlap with @typescript-eslint/no-unused-vars, can disable if duplicative 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | // use Node's module resolution algorithm, instead of the legacy TS one 23 | "moduleResolution": "node", 24 | // transpile JSX to React.createElement 25 | "jsx": "react", 26 | // interop between ESM and CJS modules. Recommended by TS 27 | "esModuleInterop": true, 28 | // significant perf increase by skipping checking .d.ts files, particularly those in node_modules. Recommended by TS 29 | "skipLibCheck": true, 30 | // error out if import and file system have a casing mismatch. Recommended by TS 31 | "forceConsistentCasingInFileNames": true, 32 | // `tsdx build` ignores this option, but it is commonly used when type-checking separately with `tsc` 33 | "noEmit": true, 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | # Tests 2 | 3 | - `unit` contains unit tests of internals 4 | - `e2e` contains end-to-end (E2E) tests of the CLI 5 | - `integration` contains tests ensuring that common or recommended plugins work properly together with TSDX 6 | -------------------------------------------------------------------------------- /test/e2e/fixtures/README.md: -------------------------------------------------------------------------------- 1 | # E2E Test Fixtures Directory 2 | 3 | - `build-default` focuses on our zero config defaults 4 | - `build-invalid` lets us check what happens when we have invalid builds due to type errors 5 | - `build-withTsconfig` lets us check that `tsconfig.json` options are correctly used 6 | - `lint` lets us check that lint errors are correctly detected 7 | -------------------------------------------------------------------------------- /test/e2e/fixtures/build-default/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "build": "tsdx build" 4 | }, 5 | "name": "build-default", 6 | "license": "MIT" 7 | } 8 | -------------------------------------------------------------------------------- /test/e2e/fixtures/build-default/package2.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "build": "tsdx build" 4 | }, 5 | "name": "build-default-2", 6 | "license": "MIT" 7 | } 8 | -------------------------------------------------------------------------------- /test/e2e/fixtures/build-default/src/index.ts: -------------------------------------------------------------------------------- 1 | import './syntax/jsx-import/JSX-import-JSX'; 2 | 3 | export { testNullishCoalescing } from './syntax/nullish-coalescing'; 4 | export { testOptionalChaining } from './syntax/optional-chaining'; 5 | 6 | export { testGenerator } from './syntax/generator'; 7 | export { testAsync } from './syntax/async'; 8 | 9 | export { kebabCase } from 'lodash'; 10 | export { merge, mergeAll } from 'lodash/fp'; 11 | 12 | export { returnsTrue } from './returnsTrue'; 13 | 14 | export const sum = (a: number, b: number) => { 15 | if ('development' === process.env.NODE_ENV) { 16 | console.log('dev only output'); 17 | } 18 | return a + b; 19 | }; 20 | -------------------------------------------------------------------------------- /test/e2e/fixtures/build-default/src/returnsTrue.ts: -------------------------------------------------------------------------------- 1 | // this just ensure a simple import works 2 | export const returnsTrue = () => true; 3 | -------------------------------------------------------------------------------- /test/e2e/fixtures/build-default/src/syntax/async.ts: -------------------------------------------------------------------------------- 1 | // regression test for async/await 2 | // code inspired by https://github.com/formium/tsdx/issues/869 3 | let shouldBeTrue = false; 4 | (async () => { 5 | shouldBeTrue = true; // a side effect to make sure this is output 6 | await Promise.resolve(); 7 | })(); 8 | 9 | export async function testAsync() { 10 | return await Promise.resolve(shouldBeTrue); 11 | } 12 | -------------------------------------------------------------------------------- /test/e2e/fixtures/build-default/src/syntax/generator.ts: -------------------------------------------------------------------------------- 1 | // regression test for generators 2 | export function* testGenerator() { 3 | yield true; 4 | } 5 | -------------------------------------------------------------------------------- /test/e2e/fixtures/build-default/src/syntax/jsx-import/JSX-A.jsx: -------------------------------------------------------------------------------- 1 | // DO NOT IMPORT THIS FILE DIRECTLY FROM index.ts 2 | // THIS FILE IS INTENTIONALLY TO TEST JSX CHAINING IMPORT 3 | // SEE https://github.com/jaredpalmer/tsdx/issues/523 4 | 5 | import JSXB from './JSX-B'; 6 | 7 | export default JSXB; 8 | -------------------------------------------------------------------------------- /test/e2e/fixtures/build-default/src/syntax/jsx-import/JSX-B.jsx: -------------------------------------------------------------------------------- 1 | // DO NOT IMPORT THIS FILE DIRECTLY FROM index.ts 2 | // THIS FILE IS INTENTIONALLY TO TEST JSX CHAINING IMPORT 3 | // SEE https://github.com/jaredpalmer/tsdx/issues/523 4 | 5 | export default function JSXComponent() { 6 | return 'JSXC'; 7 | } 8 | -------------------------------------------------------------------------------- /test/e2e/fixtures/build-default/src/syntax/jsx-import/JSX-import-JSX.jsx: -------------------------------------------------------------------------------- 1 | // Testing for jsx chaining import 2 | // https://github.com/jaredpalmer/tsdx/issues/523 3 | 4 | export * from './JSX-A'; 5 | -------------------------------------------------------------------------------- /test/e2e/fixtures/build-default/src/syntax/nullish-coalescing.ts: -------------------------------------------------------------------------------- 1 | // regression test for nullish coalescing syntax 2 | // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#nullish-coalescing 3 | 4 | export function testNullishCoalescing() { 5 | const someFunc = () => 'some string'; 6 | const someFalse = false; 7 | const shouldBeTrue = !(someFalse ?? someFunc()); 8 | return shouldBeTrue; 9 | } 10 | -------------------------------------------------------------------------------- /test/e2e/fixtures/build-default/src/syntax/optional-chaining.ts: -------------------------------------------------------------------------------- 1 | // regression test for optional chaining syntax 2 | // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#optional-chaining 3 | 4 | export function testOptionalChaining() { 5 | const someObj: { someOptionalString?: string } = {}; 6 | const shouldBeTrue = someObj?.someOptionalString || true; 7 | return shouldBeTrue; 8 | } 9 | -------------------------------------------------------------------------------- /test/e2e/fixtures/build-default/test/some-test.test.ts: -------------------------------------------------------------------------------- 1 | // this is to test that .test/.spec files in the test/ dir are excluded 2 | 3 | // and that rootDir: './src' doesn't error with test/ files 4 | -------------------------------------------------------------------------------- /test/e2e/fixtures/build-default/test/testUtil.ts: -------------------------------------------------------------------------------- 1 | // this is to test that test helper files in the test/ dir are excluded 2 | // i.e. files in test/ that don't have a .spec/.test suffix 3 | 4 | // and that rootDir: './src' doesn't error with test/ files 5 | -------------------------------------------------------------------------------- /test/e2e/fixtures/build-default/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ESNext", 4 | "lib": ["dom", "esnext"], 5 | "declaration": true, 6 | "sourceMap": true, 7 | "rootDir": "./src", 8 | "strict": true, 9 | "noUnusedLocals": true, 10 | "noUnusedParameters": true, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "moduleResolution": "node", 14 | "jsx": "react", 15 | "esModuleInterop": true, 16 | "skipLibCheck": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "noEmit": true 19 | }, 20 | "include": ["src", "types"] 21 | } 22 | -------------------------------------------------------------------------------- /test/e2e/fixtures/build-default/types/blar.d.ts: -------------------------------------------------------------------------------- 1 | // this is to test that rootDir: './src' doesn't error with types/ files 2 | 3 | // and that declaration files aren't re-output in dist/ 4 | -------------------------------------------------------------------------------- /test/e2e/fixtures/build-invalid/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "build": "tsdx build" 4 | }, 5 | "name": "build-invalid", 6 | "license": "MIT" 7 | } 8 | -------------------------------------------------------------------------------- /test/e2e/fixtures/build-invalid/src/index.ts: -------------------------------------------------------------------------------- 1 | export const inconsistentType: number = '123'; 2 | -------------------------------------------------------------------------------- /test/e2e/fixtures/build-invalid/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ESNext", 4 | "lib": ["dom", "esnext"], 5 | "declaration": true, 6 | "sourceMap": true, 7 | "rootDir": "./src", 8 | "strict": true, 9 | "noUnusedLocals": true, 10 | "noUnusedParameters": true, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "moduleResolution": "node", 14 | "jsx": "react", 15 | "esModuleInterop": true, 16 | "skipLibCheck": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "noEmit": true 19 | }, 20 | "include": ["src", "types"] 21 | } 22 | -------------------------------------------------------------------------------- /test/e2e/fixtures/build-withTsconfig/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "build": "tsdx build" 4 | }, 5 | "name": "build-withtsconfig", 6 | "license": "MIT" 7 | } 8 | -------------------------------------------------------------------------------- /test/e2e/fixtures/build-withTsconfig/src/index.ts: -------------------------------------------------------------------------------- 1 | export const sum = (a: number, b: number) => { 2 | if ('development' === process.env.NODE_ENV) { 3 | console.log('dev only output'); 4 | } 5 | return a + b; 6 | }; 7 | -------------------------------------------------------------------------------- /test/e2e/fixtures/build-withTsconfig/src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // ensure that extends works (trailing comma & comment too) 3 | "extends": "../tsconfig.base.json", 4 | "compilerOptions": { 5 | "declarationDir": "../typingsCustom/" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/e2e/fixtures/build-withTsconfig/tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ESNext", 4 | "lib": ["dom", "esnext"], 5 | "declaration": true, 6 | "declarationDir": "typings", 7 | "declarationMap": true, 8 | "sourceMap": true, 9 | "rootDir": "./src", 10 | "strict": true, 11 | "noUnusedLocals": true, 12 | "noUnusedParameters": true, 13 | "noImplicitReturns": true, 14 | "noFallthroughCasesInSwitch": true, 15 | "moduleResolution": "node", 16 | "jsx": "react", 17 | "esModuleInterop": false, 18 | "skipLibCheck": true, 19 | "forceConsistentCasingInFileNames": true, 20 | "noEmit": true 21 | }, 22 | "include": ["src", "types"], // test parsing of trailing comma & comment 23 | } 24 | -------------------------------------------------------------------------------- /test/e2e/fixtures/build-withTsconfig/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // ensure that extends works (trailing comma & comment too) 3 | "extends": "./tsconfig.base.json", 4 | } 5 | -------------------------------------------------------------------------------- /test/e2e/fixtures/lint/file-with-lint-errors.ts: -------------------------------------------------------------------------------- 1 | export const foo () => !!'bar'; 2 | -------------------------------------------------------------------------------- /test/e2e/fixtures/lint/file-with-lint-warnings.ts: -------------------------------------------------------------------------------- 1 | // this file should have 3 "unused var" lint warnings 2 | const unusedVar1 = () => { 3 | const unusedVar2 = 'baz'; 4 | const unusedVar3 = ''; 5 | }; 6 | -------------------------------------------------------------------------------- /test/e2e/fixtures/lint/file-with-prettier-lint-errors.ts: -------------------------------------------------------------------------------- 1 | export const foo = ( ) => 2 | !! ('bar') 3 | ; 4 | 5 | -------------------------------------------------------------------------------- /test/e2e/fixtures/lint/file-without-lint-error.ts: -------------------------------------------------------------------------------- 1 | export const foo = () => !!'bar'; 2 | -------------------------------------------------------------------------------- /test/e2e/fixtures/lint/react-file-with-lint-errors.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Foobar = (props: any) => { 4 | return <
foobar
; 5 | }; 6 | -------------------------------------------------------------------------------- /test/e2e/fixtures/lint/react-file-without-lint-error.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Foobar = (props: any) => { 4 | return
foobar
; 5 | }; 6 | -------------------------------------------------------------------------------- /test/e2e/tsdx-build-default.test.ts: -------------------------------------------------------------------------------- 1 | import * as shell from 'shelljs'; 2 | 3 | import * as util from '../utils/fixture'; 4 | import { execWithCache, grep } from '../utils/shell'; 5 | 6 | shell.config.silent = false; 7 | 8 | const testDir = 'e2e'; 9 | const fixtureName = 'build-default'; 10 | const stageName = `stage-${fixtureName}`; 11 | 12 | describe('tsdx build :: zero-config defaults', () => { 13 | beforeAll(() => { 14 | util.teardownStage(stageName); 15 | util.setupStageWithFixture(testDir, stageName, fixtureName); 16 | }); 17 | 18 | it('should compile files into a dist directory', () => { 19 | const output = execWithCache('node ../dist/index.js build --legacy'); 20 | 21 | expect(shell.test('-f', 'dist/index.cjs')).toBeTruthy(); 22 | expect(shell.test('-f', 'dist/build-default.development.cjs')).toBeTruthy(); 23 | expect( 24 | shell.test('-f', 'dist/build-default.production.min.cjs') 25 | ).toBeTruthy(); 26 | expect(shell.test('-f', 'dist/build-default.min.mjs')).toBeTruthy(); 27 | 28 | expect(shell.test('-f', 'dist/index.d.ts')).toBeTruthy(); 29 | 30 | expect(output.code).toBe(0); 31 | }); 32 | 33 | it("shouldn't compile files in test/ or types/", () => { 34 | const output = execWithCache('node ../dist/index.js build --legacy'); 35 | 36 | expect(shell.test('-d', 'dist/test/')).toBeFalsy(); 37 | expect(shell.test('-d', 'dist/types/')).toBeFalsy(); 38 | 39 | expect(output.code).toBe(0); 40 | }); 41 | 42 | it('should create the library correctly', async () => { 43 | const output = execWithCache('node ../dist/index.js build --legacy'); 44 | 45 | /** 46 | * Cannot test ESM import here since it is emitted as CJS. Getting tsdx to 47 | * compile itself will be a future goal. 48 | * 49 | * @todo Compile TSDX with itself so `import` statements can be tested 50 | */ 51 | // const libs = [ 52 | // require(`../../${stageName}/dist/index.cjs`), 53 | // await import(`../../${stageName}/dist/index.mjs`) 54 | // ]; 55 | /** 56 | * So just test CJS import for now. The package.jsons aren't simulated using 57 | * the CLI, so we're resolving it ourselves here to dist/index.cjs. 58 | */ 59 | const libs = [require(`../../${stageName}/dist/index.cjs`)]; 60 | 61 | for (const lib of libs) { 62 | expect(lib.returnsTrue()).toBe(true); 63 | expect(lib.__esModule).toBe(true); // test that ESM -> CJS interop was output 64 | 65 | // syntax tests 66 | expect(lib.testNullishCoalescing()).toBe(true); 67 | expect(lib.testOptionalChaining()).toBe(true); 68 | // can't use an async generator in Jest yet, so use next().value instead of yield 69 | expect(lib.testGenerator().next().value).toBe(true); 70 | expect(await lib.testAsync()).toBe(true); 71 | 72 | expect(output.code).toBe(0); 73 | } 74 | }); 75 | 76 | it('should bundle regeneratorRuntime', () => { 77 | const output = execWithCache('node ../dist/index.js build --legacy'); 78 | expect(output.code).toBe(0); 79 | 80 | const matched = grep(/regeneratorRuntime = r/, [ 81 | 'dist/build-default.*.cjs', 82 | ]); 83 | expect(matched).toBeTruthy(); 84 | }); 85 | 86 | it('should use lodash for the CJS build', () => { 87 | const output = execWithCache('node ../dist/index.js build --legacy'); 88 | expect(output.code).toBe(0); 89 | 90 | const matched = grep(/lodash/, ['dist/build-default.*.cjs']); 91 | expect(matched).toBeTruthy(); 92 | }); 93 | 94 | it('should use lodash-es for the ESM build', () => { 95 | const output = execWithCache('node ../dist/index.js build --legacy'); 96 | expect(output.code).toBe(0); 97 | 98 | const matched = grep(/lodash-es/, ['dist/build-default.min.mjs']); 99 | expect(matched).toBeTruthy(); 100 | }); 101 | 102 | it("shouldn't replace lodash/fp", () => { 103 | const output = execWithCache('node ../dist/index.js build --legacy'); 104 | expect(output.code).toBe(0); 105 | 106 | const matched = grep(/lodash\/fp/, ['dist/build-default.*.cjs']); 107 | expect(matched).toBeTruthy(); 108 | }); 109 | 110 | it('should clean the dist directory before rebuilding', () => { 111 | let output = execWithCache('node ../dist/index.js build --legacy'); 112 | expect(output.code).toBe(0); 113 | 114 | shell.mv('package.json', 'package-og.json'); 115 | shell.mv('package2.json', 'package.json'); 116 | 117 | // cache bust because we want to re-run this command with new package.json 118 | output = execWithCache('node ../dist/index.js build --legacy', { noCache: true }); 119 | expect(shell.test('-f', 'dist/index.cjs')).toBeTruthy(); 120 | 121 | // build-default files have been cleaned out 122 | expect(shell.test('-f', 'dist/build-default.development.cjs')).toBeFalsy(); 123 | expect( 124 | shell.test('-f', 'dist/build-default.production.min.cjs') 125 | ).toBeFalsy(); 126 | expect(shell.test('-f', 'dist/build-default.min.mjs')).toBeFalsy(); 127 | 128 | // build-default-2 files have been added 129 | expect( 130 | shell.test('-f', 'dist/build-default-2.development.cjs') 131 | ).toBeTruthy(); 132 | expect( 133 | shell.test('-f', 'dist/build-default-2.production.min.cjs') 134 | ).toBeTruthy(); 135 | expect(shell.test('-f', 'dist/build-default-2.min.mjs')).toBeTruthy(); 136 | 137 | expect(shell.test('-f', 'dist/index.d.ts')).toBeTruthy(); 138 | 139 | expect(output.code).toBe(0); 140 | 141 | // reset package.json files 142 | shell.mv('package.json', 'package2.json'); 143 | shell.mv('package-og.json', 'package.json'); 144 | }); 145 | 146 | afterAll(() => { 147 | util.teardownStage(stageName); 148 | }); 149 | }); 150 | -------------------------------------------------------------------------------- /test/e2e/tsdx-build-invalid.test.ts: -------------------------------------------------------------------------------- 1 | import * as shell from 'shelljs'; 2 | 3 | import * as util from '../utils/fixture'; 4 | import { execWithCache } from '../utils/shell'; 5 | 6 | shell.config.silent = false; 7 | 8 | const testDir = 'e2e'; 9 | const fixtureName = 'build-invalid'; 10 | const stageName = `stage-${fixtureName}`; 11 | 12 | describe('tsdx build :: invalid build', () => { 13 | beforeAll(() => { 14 | util.teardownStage(stageName); 15 | util.setupStageWithFixture(testDir, stageName, fixtureName); 16 | }); 17 | 18 | it('should fail gracefully with exit code 1 when build failed', () => { 19 | const output = execWithCache('node ../dist/index.js build --legacy'); 20 | expect(output.code).toBe(1); 21 | }); 22 | 23 | it('should only transpile and not type check', () => { 24 | const output = execWithCache('node ../dist/index.js build --legacy --transpileOnly'); 25 | 26 | expect(shell.test('-f', 'dist/index.cjs')).toBeTruthy(); 27 | expect( 28 | shell.test('-f', 'dist/build-invalid.development.cjs') 29 | ).toBeTruthy(); 30 | expect( 31 | shell.test('-f', 'dist/build-invalid.production.min.cjs') 32 | ).toBeTruthy(); 33 | expect(shell.test('-f', 'dist/build-invalid.min.mjs')).toBeTruthy(); 34 | 35 | expect(shell.test('-f', 'dist/index.d.ts')).toBeTruthy(); 36 | 37 | expect(output.code).toBe(0); 38 | }); 39 | 40 | afterAll(() => { 41 | util.teardownStage(stageName); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /test/e2e/tsdx-build-options.test.ts: -------------------------------------------------------------------------------- 1 | import * as shell from 'shelljs'; 2 | 3 | import * as util from '../utils/fixture'; 4 | import { execWithCache, grep } from '../utils/shell'; 5 | 6 | shell.config.silent = false; 7 | 8 | const testDir = 'e2e'; 9 | const fixtureName = 'build-default'; 10 | // create a second version of build-default's stage for concurrent testing 11 | const stageName = 'stage-build-options'; 12 | 13 | describe('tsdx build :: options', () => { 14 | beforeAll(() => { 15 | util.teardownStage(stageName); 16 | util.setupStageWithFixture(testDir, stageName, fixtureName); 17 | }); 18 | 19 | it('should compile all formats', () => { 20 | const output = execWithCache( 21 | 'node ../dist/index.js build --legacy --format cjs,esm,umd,system' 22 | ); 23 | 24 | expect(shell.test('-f', 'dist/index.cjs')).toBeTruthy(); 25 | expect(shell.test('-f', 'dist/build-default.development.cjs')).toBeTruthy(); 26 | expect( 27 | shell.test('-f', 'dist/build-default.production.min.cjs') 28 | ).toBeTruthy(); 29 | expect(shell.test('-f', 'dist/build-default.min.mjs')).toBeTruthy(); 30 | expect( 31 | shell.test('-f', 'dist/build-default.umd.development.cjs') 32 | ).toBeTruthy(); 33 | expect( 34 | shell.test('-f', 'dist/build-default.umd.production.min.cjs') 35 | ).toBeTruthy(); 36 | expect( 37 | shell.test('-f', 'dist/build-default.system.development.cjs') 38 | ).toBeTruthy(); 39 | expect( 40 | shell.test('-f', 'dist/build-default.system.production.min.cjs') 41 | ).toBeTruthy(); 42 | 43 | expect(shell.test('-f', 'dist/index.d.ts')).toBeTruthy(); 44 | 45 | expect(output.code).toBe(0); 46 | }); 47 | 48 | it('should not bundle regeneratorRuntime when targeting Node', () => { 49 | const output = execWithCache('node ../dist/index.js build --legacy --target node'); 50 | expect(output.code).toBe(0); 51 | 52 | const matched = grep(/regeneratorRuntime = r/, [ 53 | 'dist/build-default.*.cjs', 54 | ]); 55 | expect(matched).toBeFalsy(); 56 | }); 57 | 58 | afterAll(() => { 59 | util.teardownStage(stageName); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /test/e2e/tsdx-build-withTsconfig.test.ts: -------------------------------------------------------------------------------- 1 | import * as shell from 'shelljs'; 2 | 3 | import * as util from '../utils/fixture'; 4 | import { execWithCache } from '../utils/shell'; 5 | 6 | shell.config.silent = false; 7 | 8 | const testDir = 'e2e'; 9 | const fixtureName = 'build-withTsconfig'; 10 | const stageName = `stage-${fixtureName}`; 11 | 12 | describe('tsdx build :: build with custom tsconfig.json options', () => { 13 | beforeAll(() => { 14 | util.teardownStage(stageName); 15 | util.setupStageWithFixture(testDir, stageName, fixtureName); 16 | }); 17 | 18 | it('should use the declarationDir when set', () => { 19 | const output = execWithCache('node ../dist/index.js build --legacy'); 20 | 21 | expect(shell.test('-f', 'dist/index.cjs')).toBeTruthy(); 22 | expect( 23 | shell.test('-f', 'dist/build-withtsconfig.development.cjs') 24 | ).toBeTruthy(); 25 | expect( 26 | shell.test('-f', 'dist/build-withtsconfig.production.min.cjs') 27 | ).toBeTruthy(); 28 | expect(shell.test('-f', 'dist/build-withtsconfig.min.mjs')).toBeTruthy(); 29 | 30 | expect(shell.test('-f', 'dist/index.d.ts')).toBeFalsy(); 31 | expect(shell.test('-f', 'typings/index.d.ts')).toBeTruthy(); 32 | expect(shell.test('-f', 'typings/index.d.ts.map')).toBeTruthy(); 33 | 34 | expect(output.code).toBe(0); 35 | }); 36 | 37 | it('should set __esModule according to esModuleInterop', () => { 38 | const output = execWithCache('node ../dist/index.js build --legacy'); 39 | 40 | const lib = require(`../../${stageName}/dist/build-withtsconfig.production.min.cjs`); 41 | // if esModuleInterop: false, no __esModule is added, therefore undefined 42 | expect(lib.__esModule).toBe(undefined); 43 | 44 | expect(output.code).toBe(0); 45 | }); 46 | 47 | it('should read custom --tsconfig path', () => { 48 | const output = execWithCache( 49 | 'node ../dist/index.js build --legacy --format cjs --tsconfig ./src/tsconfig.json' 50 | ); 51 | 52 | expect(shell.test('-f', 'dist/index.cjs')).toBeTruthy(); 53 | expect( 54 | shell.test('-f', 'dist/build-withtsconfig.development.cjs') 55 | ).toBeTruthy(); 56 | expect( 57 | shell.test('-f', 'dist/build-withtsconfig.production.min.cjs') 58 | ).toBeTruthy(); 59 | 60 | expect(shell.test('-f', 'dist/index.d.ts')).toBeFalsy(); 61 | expect(shell.test('-f', 'typingsCustom/index.d.ts')).toBeTruthy(); 62 | expect(shell.test('-f', 'typingsCustom/index.d.ts.map')).toBeTruthy(); 63 | 64 | expect(output.code).toBe(0); 65 | }); 66 | 67 | afterAll(() => { 68 | util.teardownStage(stageName); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /test/e2e/tsdx-lint.test.ts: -------------------------------------------------------------------------------- 1 | import * as shell from 'shelljs'; 2 | 3 | import * as util from '../utils/fixture'; 4 | 5 | shell.config.silent = true; 6 | 7 | const testDir = 'e2e'; 8 | const stageName = 'stage-lint'; 9 | 10 | const lintDir = `test/${testDir}/fixtures/lint`; 11 | 12 | describe('tsdx lint', () => { 13 | it('should fail to lint a ts file with errors', () => { 14 | const testFile = `${lintDir}/file-with-lint-errors.ts`; 15 | const output = shell.exec(`node dist/index.js lint ${testFile}`); 16 | expect(output.code).toBe(1); 17 | expect(output.stdout.includes('Parsing error:')).toBe(true); 18 | }); 19 | 20 | it('should succeed linting a ts file without errors', () => { 21 | const testFile = `${lintDir}/file-without-lint-error.ts`; 22 | const output = shell.exec(`node dist/index.js lint ${testFile}`); 23 | expect(output.code).toBe(0); 24 | }); 25 | 26 | it('should fail to lint a ts file with prettier errors', () => { 27 | const testFile = `${lintDir}/file-with-prettier-lint-errors.ts`; 28 | const output = shell.exec(`node dist/index.js lint ${testFile}`); 29 | expect(output.code).toBe(1); 30 | expect(output.stdout.includes('prettier/prettier')).toBe(true); 31 | }); 32 | 33 | it('should fail to lint a tsx file with errors', () => { 34 | const testFile = `${lintDir}/react-file-with-lint-errors.tsx`; 35 | const output = shell.exec(`node dist/index.js lint ${testFile}`); 36 | expect(output.code).toBe(1); 37 | expect(output.stdout.includes('Parsing error:')).toBe(true); 38 | }); 39 | 40 | it('should succeed linting a tsx file without errors', () => { 41 | const testFile = `${lintDir}/react-file-without-lint-error.tsx`; 42 | const output = shell.exec(`node dist/index.js lint ${testFile}`); 43 | expect(output.code).toBe(0); 44 | }); 45 | 46 | it('should succeed linting a ts file with warnings when --max-warnings is not used', () => { 47 | const testFile = `${lintDir}/file-with-lint-warnings.ts`; 48 | const output = shell.exec(`node dist/index.js lint ${testFile}`); 49 | expect(output.code).toBe(0); 50 | expect(output.stdout.includes('@typescript-eslint/no-unused-vars')).toBe( 51 | true 52 | ); 53 | }); 54 | 55 | it('should succeed linting a ts file with fewer warnings than --max-warnings', () => { 56 | const testFile = `${lintDir}/file-with-lint-warnings.ts`; 57 | const output = shell.exec( 58 | `node dist/index.js lint ${testFile} --max-warnings 4` 59 | ); 60 | expect(output.code).toBe(0); 61 | expect(output.stdout.includes('@typescript-eslint/no-unused-vars')).toBe( 62 | true 63 | ); 64 | }); 65 | 66 | it('should succeed linting a ts file with same number of warnings as --max-warnings', () => { 67 | const testFile = `${lintDir}/file-with-lint-warnings.ts`; 68 | const output = shell.exec( 69 | `node dist/index.js lint ${testFile} --max-warnings 3` 70 | ); 71 | expect(output.code).toBe(0); 72 | expect(output.stdout.includes('@typescript-eslint/no-unused-vars')).toBe( 73 | true 74 | ); 75 | }); 76 | 77 | it('should fail to lint a ts file with more warnings than --max-warnings', () => { 78 | const testFile = `${lintDir}/file-with-lint-warnings.ts`; 79 | const output = shell.exec( 80 | `node dist/index.js lint ${testFile} --max-warnings 2` 81 | ); 82 | expect(output.code).toBe(1); 83 | expect(output.stdout.includes('@typescript-eslint/no-unused-vars')).toBe( 84 | true 85 | ); 86 | }); 87 | 88 | it('should not lint', () => { 89 | const output = shell.exec(`node dist/index.js lint`); 90 | expect(output.code).toBe(1); 91 | expect(output.toString()).toContain('Defaulting to "tsdx lint src test"'); 92 | expect(output.toString()).toContain( 93 | 'You can override this in the package.json scripts, like "lint": "tsdx lint src otherDir"' 94 | ); 95 | }); 96 | 97 | describe('when --write-file is used', () => { 98 | beforeEach(() => { 99 | util.teardownStage(stageName); 100 | util.setupStageWithFixture(testDir, stageName, 'build-default'); 101 | }); 102 | 103 | it('should create the file', () => { 104 | const output = shell.exec(`node ../dist/index.js lint --write-file`); 105 | expect(shell.test('-f', '.eslintrc.js')).toBeTruthy(); 106 | expect(output.code).toBe(0); 107 | }); 108 | 109 | afterAll(() => { 110 | util.teardownStage(stageName); 111 | }); 112 | }); 113 | }); 114 | -------------------------------------------------------------------------------- /test/integration/fixtures/README.md: -------------------------------------------------------------------------------- 1 | # Integration Test Fixtures Directory 2 | 3 | - `build-options` lets us check that TSDX's flags work as expected 4 | - `build-withConfig` lets us check that `tsdx.config.js` works as expected 5 | - `build-withBabel` lets us check that `.babelrc` works as expected 6 | -------------------------------------------------------------------------------- /test/integration/fixtures/build-options/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "build": "tsdx build --extractErrors" 4 | }, 5 | "name": "build-options", 6 | "license": "MIT" 7 | } 8 | -------------------------------------------------------------------------------- /test/integration/fixtures/build-options/src/index.ts: -------------------------------------------------------------------------------- 1 | import invariant from 'tiny-invariant'; 2 | import warning from 'tiny-warning'; 3 | 4 | invariant(true, 'error occurred! o no'); 5 | warning(true, 'warning - water is wet'); 6 | 7 | export const sum = (a: number, b: number) => { 8 | if ('development' === process.env.NODE_ENV) { 9 | console.log('dev only output'); 10 | } 11 | return a + b; 12 | }; 13 | -------------------------------------------------------------------------------- /test/integration/fixtures/build-options/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ESNext", 4 | "lib": ["dom", "esnext"], 5 | "declaration": true, 6 | "sourceMap": true, 7 | "rootDir": "./src", 8 | "strict": true, 9 | "noUnusedLocals": true, 10 | "noUnusedParameters": true, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "moduleResolution": "node", 14 | "jsx": "react", 15 | "esModuleInterop": true, 16 | "skipLibCheck": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "noEmit": true 19 | }, 20 | "include": ["src", "types"], 21 | } 22 | -------------------------------------------------------------------------------- /test/integration/fixtures/build-withBabel/.babelrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | // ensure Babel presets are merged and applied 4 | './test-babel-preset' 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /test/integration/fixtures/build-withBabel/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "build": "tsdx build" 4 | }, 5 | "name": "build-withbabel", 6 | "license": "MIT" 7 | } 8 | -------------------------------------------------------------------------------- /test/integration/fixtures/build-withBabel/src/index.ts: -------------------------------------------------------------------------------- 1 | export { Title } from './styled'; 2 | 3 | export const sum = (a: number, b: number) => { 4 | if ('development' === process.env.NODE_ENV) { 5 | console.log('dev only output'); 6 | } 7 | return a + b; 8 | }; 9 | -------------------------------------------------------------------------------- /test/integration/fixtures/build-withBabel/src/styled.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/macro'; 2 | 3 | export const Title = styled.h1` 4 | /* this comment should be removed */ 5 | font-size: 1.5em; 6 | text-align: center; 7 | color: palevioletred; 8 | `; 9 | -------------------------------------------------------------------------------- /test/integration/fixtures/build-withBabel/test-babel-preset.js: -------------------------------------------------------------------------------- 1 | // a simple babel preset to ensure presets are merged and applied 2 | module.exports = () => ({ 3 | plugins: [['replace-identifiers', { sum: 'replacedSum' }]], 4 | }); 5 | -------------------------------------------------------------------------------- /test/integration/fixtures/build-withBabel/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ESNext", 4 | "lib": ["dom", "esnext"], 5 | "declaration": true, 6 | "sourceMap": true, 7 | "rootDir": "./src", 8 | "strict": true, 9 | "noUnusedLocals": true, 10 | "noUnusedParameters": true, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "moduleResolution": "node", 14 | "jsx": "react", 15 | "esModuleInterop": true, 16 | "skipLibCheck": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "noEmit": false 19 | }, 20 | "include": ["src", "types"], 21 | } 22 | -------------------------------------------------------------------------------- /test/integration/fixtures/build-withConfig/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "build": "tsdx build" 4 | }, 5 | "name": "build-withconfig", 6 | "license": "MIT" 7 | } 8 | -------------------------------------------------------------------------------- /test/integration/fixtures/build-withConfig/src/index.css: -------------------------------------------------------------------------------- 1 | /* ::placeholder should be autoprefixed, and everything minified */ 2 | .test::placeholder { 3 | color: 'blue'; 4 | } 5 | -------------------------------------------------------------------------------- /test/integration/fixtures/build-withConfig/src/index.ts: -------------------------------------------------------------------------------- 1 | import './index.css'; 2 | 3 | export const sum = (a: number, b: number) => { 4 | if ('development' === process.env.NODE_ENV) { 5 | console.log('dev only output'); 6 | } 7 | return a + b; 8 | }; 9 | -------------------------------------------------------------------------------- /test/integration/fixtures/build-withConfig/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ESNext", 4 | "lib": ["dom", "esnext"], 5 | "declaration": true, 6 | "sourceMap": true, 7 | "rootDir": "./src", 8 | "strict": true, 9 | "noUnusedLocals": true, 10 | "noUnusedParameters": true, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "moduleResolution": "node", 14 | "jsx": "react", 15 | "esModuleInterop": true, 16 | "skipLibCheck": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "noEmit": true 19 | }, 20 | "include": ["src", "types"], 21 | } 22 | -------------------------------------------------------------------------------- /test/integration/fixtures/build-withConfig/tsdx.config.js: -------------------------------------------------------------------------------- 1 | const postcss = require('rollup-plugin-postcss'); 2 | const autoprefixer = require('autoprefixer'); 3 | const cssnano = require('cssnano'); 4 | 5 | module.exports = { 6 | rollup(config, options) { 7 | config.plugins.push( 8 | postcss({ 9 | plugins: [ 10 | autoprefixer(), 11 | cssnano({ 12 | preset: 'default', 13 | }), 14 | ], 15 | inject: false, 16 | // only write out CSS for the first bundle (avoids pointless extra files): 17 | extract: !!options.writeMeta, 18 | }) 19 | ); 20 | return config; 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /test/integration/tsdx-build-options.test.ts: -------------------------------------------------------------------------------- 1 | import * as shell from 'shelljs'; 2 | 3 | import * as util from '../utils/fixture'; 4 | import { execWithCache } from '../utils/shell'; 5 | 6 | shell.config.silent = false; 7 | 8 | const testDir = 'integration'; 9 | const fixtureName = 'build-options'; 10 | const stageName = `stage-integration-${fixtureName}`; 11 | 12 | describe('integration :: tsdx build :: options', () => { 13 | beforeAll(() => { 14 | util.teardownStage(stageName); 15 | util.setupStageWithFixture(testDir, stageName, fixtureName); 16 | }); 17 | 18 | it('should create errors/ dir with --extractErrors', () => { 19 | const output = execWithCache('node ../dist/index.js build --legacy --extractErrors'); 20 | 21 | expect(shell.test('-f', 'errors/ErrorDev.js')).toBeTruthy(); 22 | expect(shell.test('-f', 'errors/ErrorProd.js')).toBeTruthy(); 23 | expect(shell.test('-f', 'errors/codes.json')).toBeTruthy(); 24 | 25 | expect(output.code).toBe(0); 26 | }); 27 | 28 | it('should have correct errors/codes.json', () => { 29 | const output = execWithCache('node ../dist/index.js build --legacy --extractErrors'); 30 | 31 | const errors = require(`../../${stageName}/errors/codes.json`); 32 | expect(errors['0']).toBe('error occurred! o no'); 33 | // TODO: warning is actually not extracted, only invariant 34 | // expect(errors['1']).toBe('warning - water is wet'); 35 | 36 | expect(output.code).toBe(0); 37 | }); 38 | 39 | it('should compile files into a dist directory', () => { 40 | const output = execWithCache('node ../dist/index.js build --legacy --extractErrors'); 41 | 42 | expect(shell.test('-f', 'dist/index.cjs')).toBeTruthy(); 43 | expect(shell.test('-f', 'dist/build-options.development.cjs')).toBeTruthy(); 44 | expect( 45 | shell.test('-f', 'dist/build-options.production.min.cjs') 46 | ).toBeTruthy(); 47 | expect(shell.test('-f', 'dist/build-options.min.mjs')).toBeTruthy(); 48 | 49 | expect(shell.test('-f', 'dist/index.d.ts')).toBeTruthy(); 50 | 51 | expect(output.code).toBe(0); 52 | }); 53 | 54 | afterAll(() => { 55 | util.teardownStage(stageName); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /test/integration/tsdx-build-withBabel.test.ts: -------------------------------------------------------------------------------- 1 | import * as shell from 'shelljs'; 2 | 3 | import * as util from '../utils/fixture'; 4 | import { execWithCache, grep } from '../utils/shell'; 5 | 6 | shell.config.silent = false; 7 | 8 | const testDir = 'integration'; 9 | const fixtureName = 'build-withBabel'; 10 | const stageName = `stage-integration-${fixtureName}`; 11 | 12 | describe('integration :: tsdx build :: .babelrc.js', () => { 13 | beforeAll(() => { 14 | util.teardownStage(stageName); 15 | util.setupStageWithFixture(testDir, stageName, fixtureName); 16 | }); 17 | 18 | it('should convert styled-components template tags', () => { 19 | const output = execWithCache('node ../dist/index.js build --legacy'); 20 | expect(output.code).toBe(0); 21 | 22 | // from styled.h1` to styled.h1.withConfig( 23 | const matched = grep(/default.h1.withConfig\(/, [ 24 | 'dist/build-withbabel.production.min.cjs', 25 | ]); 26 | expect(matched).toBeTruthy(); 27 | }); 28 | 29 | // TODO: make styled-components work with its Babel plugin and not just its 30 | // macro by allowing customization of plugin order 31 | it('should remove comments in the CSS', () => { 32 | const output = execWithCache('node ../dist/index.js build --legacy'); 33 | expect(output.code).toBe(0); 34 | 35 | // the comment "should be removed" should no longer be there 36 | const matched = grep(/should be removed/, [ 37 | 'dist/build-withbabel.production.min.cjs', 38 | ]); 39 | expect(matched).toBeFalsy(); 40 | }); 41 | 42 | it('should merge and apply presets', () => { 43 | const output = execWithCache('node ../dist/index.js build --legacy'); 44 | expect(output.code).toBe(0); 45 | 46 | // ensures replace-identifiers was used 47 | const matched = grep(/replacedSum/, [ 48 | 'dist/build-withbabel.production.min.cjs', 49 | ]); 50 | expect(matched).toBeTruthy(); 51 | }); 52 | 53 | it('should compile files into a dist directory', () => { 54 | const output = execWithCache('node ../dist/index.js build --legacy'); 55 | 56 | expect(shell.test('-f', 'dist/index.cjs')).toBeTruthy(); 57 | expect( 58 | shell.test('-f', 'dist/build-withbabel.development.cjs') 59 | ).toBeTruthy(); 60 | expect( 61 | shell.test('-f', 'dist/build-withbabel.production.min.cjs') 62 | ).toBeTruthy(); 63 | expect(shell.test('-f', 'dist/build-withbabel.min.mjs')).toBeTruthy(); 64 | 65 | expect(shell.test('-f', 'dist/index.d.ts')).toBeTruthy(); 66 | 67 | expect(output.code).toBe(0); 68 | }); 69 | 70 | afterAll(() => { 71 | util.teardownStage(stageName); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /test/integration/tsdx-build-withConfig.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs-extra'; 2 | import * as shell from 'shelljs'; 3 | 4 | import * as util from '../utils/fixture'; 5 | import { execWithCache } from '../utils/shell'; 6 | 7 | shell.config.silent = false; 8 | 9 | const testDir = 'integration'; 10 | const fixtureName = 'build-withConfig'; 11 | const stageName = `stage-integration-${fixtureName}`; 12 | 13 | describe('integration :: tsdx build :: tsdx.config.js', () => { 14 | beforeAll(() => { 15 | util.teardownStage(stageName); 16 | util.setupStageWithFixture(testDir, stageName, fixtureName); 17 | }); 18 | 19 | it('should create a CSS file in the dist/ directory', () => { 20 | const output = execWithCache('node ../dist/index.js build --legacy'); 21 | 22 | // TODO: this is kind of subpar naming, rollup-plugin-postcss just names it 23 | // the same as the output file, but with the .css extension 24 | expect(shell.test('-f', 'dist/build-withconfig.development.css')); 25 | 26 | expect(output.code).toBe(0); 27 | }); 28 | 29 | it('should autoprefix and minify the CSS file', async () => { 30 | const output = execWithCache('node ../dist/index.js build --legacy'); 31 | 32 | const cssText = await fs.readFile( 33 | './dist/build-withconfig.development.css' 34 | ); 35 | 36 | // autoprefixed and minifed output 37 | expect( 38 | cssText.includes('.test::-moz-placeholder{color:"blue"}') 39 | ).toBeTruthy(); 40 | 41 | expect(output.code).toBe(0); 42 | }); 43 | 44 | it('should compile files into a dist directory', () => { 45 | const output = execWithCache('node ../dist/index.js build --legacy'); 46 | 47 | expect(shell.test('-f', 'dist/index.cjs')).toBeTruthy(); 48 | expect( 49 | shell.test('-f', 'dist/build-withconfig.development.cjs') 50 | ).toBeTruthy(); 51 | expect( 52 | shell.test('-f', 'dist/build-withconfig.production.min.cjs') 53 | ).toBeTruthy(); 54 | expect(shell.test('-f', 'dist/build-withconfig.min.mjs')).toBeTruthy(); 55 | 56 | expect(shell.test('-f', 'dist/index.d.ts')).toBeTruthy(); 57 | 58 | expect(output.code).toBe(0); 59 | }); 60 | 61 | afterAll(() => { 62 | util.teardownStage(stageName); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /test/unit/utils-safePackageName.test.ts: -------------------------------------------------------------------------------- 1 | const { safePackageName } = require('../../src/utils'); 2 | 3 | describe('utils | safePackageName', () => { 4 | it('should generate safe package name', () => { 5 | expect(safePackageName('@babel/core')).toBe('core'); 6 | expect(safePackageName('react')).toBe('react'); 7 | expect(safePackageName('react-dom')).toBe('react-dom'); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /test/utils/fixture.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as shell from 'shelljs'; 3 | 4 | export const rootDir = process.cwd(); 5 | 6 | shell.config.silent = true; 7 | 8 | export function setupStageWithFixture( 9 | testDir: string, 10 | stageName: string, 11 | fixtureName: string 12 | ): void { 13 | const stagePath = path.join(rootDir, stageName); 14 | shell.mkdir(stagePath); 15 | shell.exec( 16 | `cp -a ${rootDir}/test/${testDir}/fixtures/${fixtureName}/. ${stagePath}/` 17 | ); 18 | shell.ln( 19 | '-s', 20 | path.join(rootDir, 'node_modules'), 21 | path.join(stagePath, 'node_modules') 22 | ); 23 | shell.cd(stagePath); 24 | } 25 | 26 | export function teardownStage(stageName: string): void { 27 | shell.cd(rootDir); 28 | shell.rm('-rf', path.join(rootDir, stageName)); 29 | } 30 | -------------------------------------------------------------------------------- /test/utils/shell.ts: -------------------------------------------------------------------------------- 1 | // this file contains helper utils for working with shell.js functions 2 | import * as shell from 'shelljs'; 3 | 4 | shell.config.silent = true; 5 | 6 | // simple shell.exec "cache" that doesn't re-run the same command twice in a row 7 | let prevCommand = ''; 8 | let prevCommandOutput = {} as shell.ShellReturnValue; 9 | export function execWithCache( 10 | command: string, 11 | { noCache = false } = {} 12 | ): shell.ShellReturnValue { 13 | // return the old output 14 | if (!noCache && prevCommand === command) return prevCommandOutput; 15 | 16 | const output = shell.exec(command); 17 | 18 | // reset if command is not to be cached 19 | if (noCache) { 20 | prevCommand = ''; 21 | prevCommandOutput = {} as shell.ShellReturnValue; 22 | } else { 23 | prevCommand = command; 24 | prevCommandOutput = output; 25 | } 26 | 27 | return output; 28 | } 29 | 30 | // shell.js grep wrapper returns true if pattern has matches in file 31 | export function grep(pattern: RegExp, fileName: string[]): boolean { 32 | const output = shell.grep(pattern, fileName); 33 | // output.code is always 0 regardless of matched/unmatched patterns 34 | // so need to test output.stdout 35 | // https://github.com/jaredpalmer/tsdx/pull/525#discussion_r395571779 36 | return Boolean(output.stdout.match(pattern)); 37 | } 38 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src/**/*"], 3 | "exclude": ["src/template/**/*"], 4 | "compilerOptions": { 5 | "allowJs": true, 6 | "jsx": "react", 7 | "importHelpers": true, 8 | "esModuleInterop": true, 9 | "outDir": "dist", 10 | "declaration": false, 11 | "module": "commonjs", 12 | "rootDir": "src", 13 | "strict": true, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "skipLibCheck": true, 19 | "target": "es2017" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /website/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["next/babel"], 3 | "plugins": ["babel-plugin-macros", "./.nextra/babel-plugin-nextjs-mdx-patch"] 4 | } -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .next 3 | .DS_Store 4 | yarn-error.log -------------------------------------------------------------------------------- /website/.nextra/arrow-right.js: -------------------------------------------------------------------------------- 1 | export default ({ width = 24, height = 24, ...props }) => { 2 | return ( 3 | 10 | 17 | 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /website/.nextra/babel-plugin-nextjs-mdx-patch.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Currently it's not possible to export data fetching functions from MDX pages 3 | * because MDX includes them in `layoutProps`, and Next.js removes them at some 4 | * point, causing a `ReferenceError`. 5 | * 6 | * https://github.com/mdx-js/mdx/issues/742#issuecomment-612652071 7 | * 8 | * This plugin can be removed once MDX removes `layoutProps`, at least that 9 | * seems to be the current plan. 10 | */ 11 | 12 | // https://nextjs.org/docs/basic-features/data-fetching 13 | const DATA_FETCH_FNS = ['getStaticPaths', 'getStaticProps', 'getServerProps'] 14 | 15 | module.exports = () => { 16 | return { 17 | visitor: { 18 | ObjectProperty(path) { 19 | if ( 20 | DATA_FETCH_FNS.includes(path.node.value.name) && 21 | path.findParent( 22 | (path) => 23 | path.isVariableDeclarator() && 24 | path.node.id.name === 'layoutProps', 25 | ) 26 | ) { 27 | path.remove() 28 | } 29 | }, 30 | }, 31 | } 32 | } 33 | 34 | // https://github.com/vercel/next.js/issues/12053#issuecomment-622939046 35 | -------------------------------------------------------------------------------- /website/.nextra/config.js: -------------------------------------------------------------------------------- 1 | import userConfig from '../nextra.config'; 2 | 3 | const defaultConfig = { 4 | nextLinks: true, 5 | prevLinks: true, 6 | search: true, 7 | }; 8 | 9 | export default () => { 10 | return { ...defaultConfig, ...userConfig }; 11 | }; 12 | -------------------------------------------------------------------------------- /website/.nextra/directories.js: -------------------------------------------------------------------------------- 1 | import preval from 'preval.macro' 2 | import title from 'title' 3 | 4 | const excludes = ['/_app.js', '/_document.js', '/_error.js'] 5 | 6 | // watch all meta files 7 | const meta = {} 8 | function importAll(r) { 9 | return r.keys().forEach(key => { 10 | meta[key.slice(1)] = r(key) 11 | }) 12 | } 13 | importAll(require.context('../pages/', true, /meta\.json$/)) 14 | 15 | // use macro to load the file list 16 | const items = preval` 17 | const { readdirSync, readFileSync } = require('fs') 18 | const { resolve, join } = require('path') 19 | const extension = /\.(mdx?|jsx?)$/ 20 | 21 | function getFiles(dir, route) { 22 | const files = readdirSync(dir, { withFileTypes: true }) 23 | 24 | // go through the directory 25 | const items = files 26 | .map(f => { 27 | const filePath = resolve(dir, f.name) 28 | const fileRoute = join(route, f.name.replace(extension, '').replace(/^index$/, '')) 29 | 30 | if (f.isDirectory()) { 31 | const children = getFiles(filePath, fileRoute) 32 | if (!children.length) return null 33 | return { name: f.name, children, route: fileRoute } 34 | } else if (f.name === 'meta.json') { 35 | return null 36 | } else if (extension.test(f.name)) { 37 | return { name: f.name.replace(extension, ''), route: fileRoute } 38 | } 39 | }) 40 | .map(item => { 41 | if (!item) return 42 | return { ...item, metaPath: join(route, 'meta.json') } 43 | }) 44 | .filter(Boolean) 45 | 46 | return items 47 | } 48 | module.exports = getFiles(join(process.cwd(), 'pages'), '/') 49 | ` 50 | 51 | const attachPageConfig = function (items) { 52 | let folderMeta = null 53 | let fnames = null 54 | 55 | return items 56 | .filter(item => !excludes.includes(item.name)) 57 | .map(item => { 58 | const { metaPath, ...rest } = item 59 | folderMeta = meta[metaPath] 60 | if (folderMeta) { 61 | fnames = Object.keys(folderMeta) 62 | } 63 | 64 | const pageConfig = folderMeta?.[item.name] 65 | 66 | if (rest.children) rest.children = attachPageConfig(rest.children) 67 | 68 | if (pageConfig) { 69 | if (typeof pageConfig === 'string') { 70 | return { ...rest, title: pageConfig } 71 | } 72 | return { ...rest, ...pageConfig } 73 | } else { 74 | if (folderMeta) { 75 | return null 76 | } 77 | return { ...rest, title: title(item.name) } 78 | } 79 | }) 80 | .filter(Boolean) 81 | .sort((a, b) => { 82 | if (folderMeta) { 83 | return fnames.indexOf(a.name) - fnames.indexOf(b.name) 84 | } 85 | // by default, we put directories first 86 | if (!!a.children !== !!b.children) { 87 | return !!a.children ? -1 : 1 88 | } 89 | // sort by file name 90 | return a.name < b.name ? -1 : 1 91 | }) 92 | } 93 | 94 | export default () => { 95 | return attachPageConfig(items) 96 | } 97 | -------------------------------------------------------------------------------- /website/.nextra/docsearch.js: -------------------------------------------------------------------------------- 1 | import { useRef, useEffect } from 'react' 2 | 3 | export default function () { 4 | const input = useRef(null) 5 | 6 | useEffect(() => { 7 | const inputs = ['input', 'select', 'button', 'textarea'] 8 | 9 | const down = (e) => { 10 | if ( 11 | document.activeElement && 12 | inputs.indexOf(document.activeElement.tagName.toLowerCase() !== -1) 13 | ) { 14 | if (e.key === '/') { 15 | e.preventDefault() 16 | input.current?.focus() 17 | } 18 | } 19 | } 20 | 21 | window.addEventListener('keydown', down) 22 | return () => window.removeEventListener('keydown', down) 23 | }, []) 24 | 25 | useEffect(() => { 26 | if (window.docsearch) { 27 | window.docsearch({ 28 | apiKey: '247dd86c8ddbbbe6d7a2d4adf4f3a68a', 29 | indexName: 'vercel_swr', 30 | inputSelector: 'input#algolia-doc-search' 31 | }) 32 | } 33 | }, []) 34 | 35 | return
36 | 43 |
44 | } 45 | -------------------------------------------------------------------------------- /website/.nextra/github-icon.js: -------------------------------------------------------------------------------- 1 | export default ({ height = 40 }) => { 2 | return 3 | 4 | 5 | } -------------------------------------------------------------------------------- /website/.nextra/nextra-loader.js: -------------------------------------------------------------------------------- 1 | module.exports = function(source, map) { 2 | this.cacheable() 3 | const fileName = this.resourcePath.slice(this.resourcePath.lastIndexOf('/') + 1) 4 | const prefix = `import withNextraLayout from '.nextra/layout'\n\n` 5 | const suffix = `\n\nexport default withNextraLayout("${fileName}")` 6 | source = prefix + source + suffix 7 | this.callback(null, source, map) 8 | } 9 | -------------------------------------------------------------------------------- /website/.nextra/nextra.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = (pluginOptions = { 4 | extension: /\.mdx?$/ 5 | }) => (nextConfig = { 6 | pageExtensions: ['js', 'jsx', 'md', 'mdx'] 7 | }) => { 8 | const extension = pluginOptions.extension || /\.mdx$/ 9 | 10 | return Object.assign({}, nextConfig, { 11 | webpack(config, options) { 12 | config.module.rules.push({ 13 | test: extension, 14 | use: [ 15 | options.defaultLoaders.babel, 16 | { 17 | loader: '@mdx-js/loader', 18 | options: pluginOptions.options 19 | }, 20 | { 21 | loader: path.resolve('.nextra', 'nextra-loader.js') 22 | } 23 | ] 24 | }) 25 | 26 | if (typeof nextConfig.webpack === 'function') { 27 | return nextConfig.webpack(config, options) 28 | } 29 | 30 | return config 31 | } 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /website/.nextra/search.js: -------------------------------------------------------------------------------- 1 | import { useMemo, useCallback, useRef, useState, useEffect } from 'react'; 2 | import matchSorter from 'match-sorter'; 3 | import cn from 'classnames'; 4 | import { useRouter } from 'next/router'; 5 | import Link from 'next/link'; 6 | 7 | const Item = ({ title, active, href, onMouseOver, search }) => { 8 | const highlight = title.toLowerCase().indexOf(search.toLowerCase()); 9 | 10 | return ( 11 | 12 | 13 |
  • 18 | {title.substring(0, highlight)} 19 | 20 | {title.substring(highlight, highlight + search.length)} 21 | 22 | {title.substring(highlight + search.length)} 23 |
  • 24 |
    25 | 26 | ); 27 | }; 28 | 29 | const Search = ({ directories }) => { 30 | const router = useRouter(); 31 | const [show, setShow] = useState(false); 32 | const [search, setSearch] = useState(''); 33 | const [active, setActive] = useState(0); 34 | const input = useRef(null); 35 | 36 | const results = useMemo(() => { 37 | if (!search) return []; 38 | 39 | // Will need to scrape all the headers from each page and search through them here 40 | // (similar to what we already do to render the hash links in sidebar) 41 | // We could also try to search the entire string text from each page 42 | return matchSorter(directories, search, { keys: ['title'] }); 43 | }, [search]); 44 | 45 | const handleKeyDown = useCallback( 46 | (e) => { 47 | switch (e.key) { 48 | case 'ArrowDown': { 49 | e.preventDefault(); 50 | if (active + 1 < results.length) { 51 | setActive(active + 1); 52 | } 53 | break; 54 | } 55 | case 'ArrowUp': { 56 | e.preventDefault(); 57 | if (active - 1 >= 0) { 58 | setActive(active - 1); 59 | } 60 | break; 61 | } 62 | case 'Enter': { 63 | router.push(results[active].route); 64 | break; 65 | } 66 | } 67 | }, 68 | [active, results, router] 69 | ); 70 | 71 | useEffect(() => { 72 | setActive(0); 73 | }, [search]); 74 | 75 | useEffect(() => { 76 | const inputs = ['input', 'select', 'button', 'textarea']; 77 | 78 | const down = (e) => { 79 | if ( 80 | document.activeElement && 81 | inputs.indexOf(document.activeElement.tagName.toLowerCase() !== -1) 82 | ) { 83 | if (e.key === '/') { 84 | e.preventDefault(); 85 | input.current.focus(); 86 | } else if (e.key === 'Escape') { 87 | setShow(false); 88 | } 89 | } 90 | }; 91 | 92 | window.addEventListener('keydown', down); 93 | return () => window.removeEventListener('keydown', down); 94 | }, []); 95 | 96 | const renderList = show && results.length > 0; 97 | 98 | return ( 99 |
    100 | {renderList && ( 101 |
    setShow(false)} /> 102 | )} 103 |
    104 | 113 | 114 | 115 |
    116 | { 118 | setSearch(e.target.value); 119 | setShow(true); 120 | }} 121 | className="appearance-none pl-8 border rounded-md py-2 pr-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline w-full" 122 | type="search" 123 | placeholder='Search ("/" to focus)' 124 | onKeyDown={handleKeyDown} 125 | onFocus={() => setShow(true)} 126 | ref={input} 127 | aria-label="Search documentation" 128 | /> 129 | {renderList && ( 130 |
      131 | {results.map((res, i) => { 132 | return ( 133 | setActive(i)} 140 | /> 141 | ); 142 | })} 143 |
    144 | )} 145 |
    146 | ); 147 | }; 148 | 149 | export default Search; 150 | -------------------------------------------------------------------------------- /website/.nextra/ssg.js: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from 'react' 2 | 3 | const SSGContext = createContext({}) 4 | 5 | export default SSGContext 6 | export const useSSG = () => useContext(SSGContext) 7 | -------------------------------------------------------------------------------- /website/.nextra/styles.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | 3 | html { 4 | font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 5 | 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 6 | sans-serif; 7 | } 8 | @supports (font-variation-settings: normal) { 9 | html { 10 | font-family: 'Inter var', -apple-system, BlinkMacSystemFont, 'Segoe UI', 11 | 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 12 | 'Helvetica Neue', sans-serif; 13 | } 14 | } 15 | 16 | html { 17 | @apply subpixel-antialiased; 18 | font-size: 16px; 19 | font-feature-settings: 'rlig' 1, 'calt' 1, 'ss01' 1; 20 | } 21 | body { 22 | @apply bg-white; 23 | } 24 | 25 | h1 { 26 | @apply text-4xl font-bold tracking-tight; 27 | } 28 | h2 { 29 | @apply text-3xl font-semibold tracking-tight mt-10; 30 | @apply border-b; 31 | } 32 | h3 { 33 | @apply text-2xl font-semibold tracking-tight mt-8; 34 | } 35 | h4 { 36 | @apply text-xl font-semibold tracking-tight mt-8; 37 | } 38 | h5 { 39 | @apply text-lg font-semibold tracking-tight mt-8; 40 | } 41 | h6 { 42 | @apply text-base font-semibold tracking-tight mt-8; 43 | } 44 | a { 45 | @apply text-blue-600 underline; 46 | } 47 | p { 48 | @apply mt-6 leading-7; 49 | } 50 | hr { 51 | @apply my-8; 52 | } 53 | code { 54 | @apply p-1 text-sm text-gray-800 bg-gray-500 bg-opacity-25 rounded; 55 | } 56 | pre { 57 | @apply p-4 bg-gray-200 rounded-lg mt-6 mb-4 overflow-x-auto scrolling-touch; 58 | } 59 | pre code { 60 | @apply p-0 text-black bg-transparent rounded-none; 61 | } 62 | a code { 63 | @apply text-current no-underline; 64 | } 65 | figure { 66 | @apply mb-8 relative; 67 | } 68 | video { 69 | @apply absolute top-0 left-0; 70 | cursor: pointer; 71 | } 72 | figure figcaption { 73 | @apply text-sm text-gray-600; 74 | } 75 | @media (min-width: 768px) { 76 | figure { 77 | /* allow figures to overflow, but not exceeding the viewport width */ 78 | @apply -mr-56; 79 | max-width: calc(100vw - 20rem); 80 | } 81 | } 82 | 83 | table { 84 | @apply my-8 w-full text-gray-700 text-sm; 85 | } 86 | 87 | table > thead > tr { 88 | @apply border-b border-t rounded-t-lg; 89 | } 90 | 91 | table > thead > tr > th { 92 | @apply px-3 py-2 text-left text-sm font-bold bg-gray-200 text-gray-700; 93 | } 94 | 95 | table > tbody > tr { 96 | @apply border-b; 97 | } 98 | 99 | table > tbody > tr > td { 100 | @apply p-3; 101 | } 102 | 103 | table > tbody > tr > td:not(:first-child) > code { 104 | @apply text-xs; 105 | } 106 | 107 | table > tbody > tr > td > a > code, 108 | table > tbody > tr > td > code { 109 | @apply text-sm; 110 | } 111 | table > tbody > tr > td > a { 112 | @apply text-blue-600 font-semibold transition-colors duration-150 ease-out; 113 | } 114 | table > tbody > tr > td > a:hover { 115 | @apply text-blue-800 transition-colors duration-150 ease-out; 116 | } 117 | @tailwind components; 118 | @tailwind utilities; 119 | 120 | .main-container { 121 | min-height: 100vh; 122 | } 123 | 124 | .sidebar { 125 | @apply select-none; 126 | } 127 | .sidebar ul ul { 128 | @apply ml-5; 129 | } 130 | .sidebar a:focus-visible, 131 | .sidebar button:focus-visible { 132 | @apply shadow-outline; 133 | } 134 | .sidebar li.active > a { 135 | @apply font-semibold text-black bg-gray-200; 136 | } 137 | .sidebar button, 138 | .sidebar a { 139 | @apply block w-full text-left text-base text-black no-underline text-gray-600 mt-1 p-2 rounded select-none outline-none; 140 | -webkit-tap-highlight-color: transparent; 141 | -webkit-touch-callout: none; 142 | } 143 | .sidebar a:hover, 144 | .sidebar button:hover { 145 | @apply text-gray-900 bg-gray-100; 146 | } 147 | 148 | .sidebar .active-route > button { 149 | @apply text-black font-medium; 150 | } 151 | 152 | .sidebar .active-route > button:hover { 153 | @apply text-current bg-transparent cursor-default; 154 | } 155 | 156 | content ul { 157 | @apply list-disc ml-6 mt-6; 158 | } 159 | content li { 160 | @apply mt-2; 161 | } 162 | .subheading-anchor { 163 | margin-top: -84px; 164 | display: inline-block; 165 | position: absolute; 166 | width: 1px; 167 | } 168 | 169 | .subheading-anchor + a:hover .anchor-icon, 170 | .subheading-anchor + a:focus .anchor-icon { 171 | opacity: 1; 172 | } 173 | .anchor-icon { 174 | opacity: 0; 175 | @apply ml-2 text-gray-500; 176 | } 177 | 178 | h2 a { 179 | @apply no-underline; 180 | } 181 | input[type='search']::-webkit-search-decoration, 182 | input[type='search']::-webkit-search-cancel-button, 183 | input[type='search']::-webkit-search-results-button, 184 | input[type='search']::-webkit-search-results-decoration { 185 | -webkit-appearance: none; 186 | } 187 | 188 | .search-overlay { 189 | position: fixed; 190 | top: 0; 191 | bottom: 0; 192 | left: 0; 193 | right: 0; 194 | } 195 | 196 | .algolia-autocomplete .algolia-docsearch-suggestion--category-header span { 197 | display: inline-block; 198 | } 199 | .algolia-autocomplete .ds-dropdown-menu { 200 | width: 500px; 201 | min-width: 300px; 202 | max-width: calc(100vw - 50px); 203 | } 204 | 205 | [data-reach-skip-link] { 206 | @apply sr-only; 207 | } 208 | 209 | [data-reach-skip-link]:focus { 210 | @apply not-sr-only fixed ml-6 top-0 bg-white text-lg px-6 py-2 mt-2 outline-none shadow-outline z-50; 211 | } 212 | -------------------------------------------------------------------------------- /website/.nextra/theme.js: -------------------------------------------------------------------------------- 1 | import { MDXProvider } from '@mdx-js/react'; 2 | import * as ReactDOM from 'react-dom/server'; 3 | import Link from 'next/link'; 4 | import Highlight, { defaultProps } from 'prism-react-renderer'; 5 | import stripHtml from 'string-strip-html'; 6 | import slugify from '@sindresorhus/slugify'; 7 | 8 | const THEME = { 9 | plain: { 10 | color: '#000', 11 | backgroundColor: 'transparent', 12 | }, 13 | styles: [ 14 | { 15 | types: ['keyword'], 16 | style: { 17 | color: '#ff0078', 18 | fontWeight: 'bold', 19 | }, 20 | }, 21 | { 22 | types: ['comment'], 23 | style: { 24 | color: '#999', 25 | fontStyle: 'italic', 26 | }, 27 | }, 28 | { 29 | types: ['string', 'url', 'attr-value'], 30 | style: { 31 | color: '#028265', 32 | }, 33 | }, 34 | { 35 | types: ['builtin', 'char', 'constant', 'language-javascript'], 36 | style: { 37 | color: '#000', 38 | }, 39 | }, 40 | { 41 | types: ['attr-name'], 42 | style: { 43 | color: '#d9931e', 44 | fontStyle: 'normal', 45 | }, 46 | }, 47 | { 48 | types: ['punctuation', 'operator'], 49 | style: { 50 | color: '#333', 51 | }, 52 | }, 53 | { 54 | types: ['number', 'function', 'tag'], 55 | style: { 56 | color: '#0076ff', 57 | }, 58 | }, 59 | { 60 | types: ['boolean', 'regex'], 61 | style: { 62 | color: '#d9931e', 63 | }, 64 | }, 65 | ], 66 | }; 67 | 68 | // Anchor links 69 | 70 | const HeaderLink = ({ tag: Tag, children, ...props }) => { 71 | const slug = slugify(ReactDOM.renderToString(children) || ''); 72 | return ( 73 | 74 | 75 | 76 | {children} 77 | 78 | # 79 | 80 | 81 | 82 | ); 83 | }; 84 | 85 | const H2 = ({ children, ...props }) => { 86 | return ( 87 | 88 | {children} 89 | 90 | ); 91 | }; 92 | 93 | const H3 = ({ children, ...props }) => { 94 | return ( 95 | 96 | {children} 97 | 98 | ); 99 | }; 100 | 101 | const H4 = ({ children, ...props }) => { 102 | return ( 103 | 104 | {children} 105 | 106 | ); 107 | }; 108 | 109 | const H5 = ({ children, ...props }) => { 110 | return ( 111 | 112 | {children} 113 | 114 | ); 115 | }; 116 | 117 | const H6 = ({ children, ...props }) => { 118 | return ( 119 | 120 | {children} 121 | 122 | ); 123 | }; 124 | 125 | const A = ({ children, ...props }) => { 126 | const isExternal = props.href?.startsWith('https://'); 127 | if (isExternal) { 128 | return ( 129 | 130 | {children} 131 | 132 | ); 133 | } 134 | return ( 135 | 136 | {children} 137 | 138 | ); 139 | }; 140 | 141 | const Code = ({ children, className, highlight, ...props }) => { 142 | if (!className) return {children}; 143 | 144 | const highlightedLines = highlight ? highlight.split(',').map(Number) : []; 145 | 146 | // https://mdxjs.com/guides/syntax-highlighting#all-together 147 | const language = className.replace(/language-/, ''); 148 | return ( 149 | 155 | {({ className, style, tokens, getLineProps, getTokenProps }) => ( 156 | 157 | {tokens.map((line, i) => ( 158 |
    171 | {line.map((token, key) => ( 172 | 173 | ))} 174 |
    175 | ))} 176 |
    177 | )} 178 |
    179 | ); 180 | }; 181 | 182 | const components = { 183 | h2: H2, 184 | h3: H3, 185 | h4: H4, 186 | h5: H5, 187 | h6: H6, 188 | a: A, 189 | code: Code, 190 | }; 191 | 192 | export default ({ children }) => { 193 | return {children}; 194 | }; 195 | -------------------------------------------------------------------------------- /website/components/features.module.css: -------------------------------------------------------------------------------- 1 | .features { 2 | display: flex; 3 | flex-wrap: wrap; 4 | margin: 2.5rem -0.5rem 2rem; 5 | } 6 | 7 | .feature { 8 | flex: 0 0 33%; 9 | align-items: center; 10 | display: inline-flex; 11 | padding: 0 0.5rem 1.5rem; 12 | margin: 0 auto; 13 | } 14 | .feature h4 { 15 | margin: 0 0 0 0.5rem; 16 | font-weight: 700; 17 | font-size: 0.95rem; 18 | white-space: nowrap; 19 | } 20 | @media (max-width: 860px) { 21 | .feature { 22 | padding-left: 0; 23 | } 24 | .feature h4 { 25 | font-size: 0.75rem; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /website/components/logo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Logo = ({ height }) => ( 4 | 10 | 11 | 15 | 19 | 23 | 27 | 28 | ); 29 | -------------------------------------------------------------------------------- /website/components/video.js: -------------------------------------------------------------------------------- 1 | import React, { useRef, useCallback, useEffect } from 'react'; 2 | import { useInView } from 'react-intersection-observer'; 3 | import 'intersection-observer'; 4 | 5 | export default ({ src, caption, ratio }) => { 6 | const [inViewRef, inView] = useInView({ 7 | threshold: 1, 8 | }); 9 | const videoRef = useRef(); 10 | 11 | const setRefs = useCallback( 12 | node => { 13 | // Ref's from useRef needs to have the node assigned to `current` 14 | videoRef.current = node; 15 | // Callback refs, like the one from `useInView`, is a function that takes the node as an argument 16 | inViewRef(node); 17 | 18 | if (node) { 19 | node.addEventListener('click', function() { 20 | if (this.paused) { 21 | this.play(); 22 | } else { 23 | this.pause(); 24 | } 25 | }); 26 | } 27 | }, 28 | [inViewRef] 29 | ); 30 | 31 | useEffect(() => { 32 | if (!videoRef || !videoRef.current) { 33 | return; 34 | } 35 | 36 | if (inView) { 37 | videoRef.current.play(); 38 | } else { 39 | videoRef.current.pause(); 40 | } 41 | }, [inView]); 42 | 43 | return ( 44 |
    45 |
    46 | 49 | {caption &&
    {caption}
    } 50 |
    51 | ); 52 | }; 53 | -------------------------------------------------------------------------------- /website/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "." 4 | } 5 | } -------------------------------------------------------------------------------- /website/next.config.js: -------------------------------------------------------------------------------- 1 | const withNextra = require('./.nextra/nextra')(); 2 | module.exports = withNextra(); 3 | -------------------------------------------------------------------------------- /website/nextra.config.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Logo } from 'components/logo'; 3 | 4 | export default { 5 | github: 'https://github.com/formium/tsdx', 6 | titleSuffix: ' – TSDX', 7 | logo: ( 8 | <> 9 | 10 | TSDX 11 | 12 | ), 13 | head: () => ( 14 | <> 15 | {/* Favicons, meta */} 16 | 21 | 27 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 43 | 47 | 48 | 49 | 50 | 54 | 55 | 56 | 57 | {/* */} 63 | 64 | ), 65 | footer: ({ filepath }) => ( 66 | <> 67 |
    68 | 74 | A Jared Palmer Project 75 | 76 | 89 | 90 | ), 91 | }; 92 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tsdx-site", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "next", 8 | "start": "next", 9 | "build": "next build" 10 | }, 11 | "author": "Jared Palmer", 12 | "license": "MIT", 13 | "dependencies": { 14 | "@reach/skip-nav": "^0.10.5", 15 | "@sindresorhus/slugify": "^1.0.0", 16 | "classnames": "^2.2.6", 17 | "focus-visible": "^5.1.0", 18 | "intersection-observer": "^0.10.0", 19 | "markdown-to-jsx": "^6.11.4", 20 | "match-sorter": "^4.1.0", 21 | "next": "^9.4.4", 22 | "next-google-fonts": "^1.1.0", 23 | "prism-react-renderer": "^1.1.1", 24 | "react": "^16.13.1", 25 | "react-dom": "^16.13.1", 26 | "react-intersection-observer": "^8.26.2", 27 | "string-strip-html": "^4.5.0" 28 | }, 29 | "devDependencies": { 30 | "@mdx-js/loader": "^1.6.5", 31 | "babel-plugin-macros": "^2.8.0", 32 | "postcss-preset-env": "^6.7.0", 33 | "preval.macro": "^5.0.0", 34 | "tailwindcss": "^1.4.6", 35 | "title": "^3.4.2" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /website/pages/_app.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import '.nextra/styles.css'; 3 | import GoogleFonts from 'next-google-fonts'; 4 | 5 | export default function Nextra({ Component, pageProps }) { 6 | return ( 7 | <> 8 | 12 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /website/pages/_document.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Document, { Html, Head, Main, NextScript } from 'next/document'; 3 | import { SkipNavLink } from '@reach/skip-nav'; 4 | 5 | class MyDocument extends Document { 6 | render() { 7 | return ( 8 | 9 | 10 | 11 | 12 |
    13 | 14 |