From bfa70a3b39ddfee9506679b58abce3960c147f0c Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Sat, 25 Jan 2020 16:06:05 +0100 Subject: [PATCH] Add rudimentary implementation --- .editorconfig | 12 +++ .github/CODE_OF_CONDUCT.md | 76 +++++++++++++++++++ .github/CONTRIBUTING.md | 7 ++ .github/ISSUE_TEMPLATE/bug.md | 15 ++++ .github/ISSUE_TEMPLATE/config.yml | 8 ++ .github/ISSUE_TEMPLATE/feature.md | 11 +++ .github/workflows/ci.yml | 29 ++++++++ .gitignore | 3 + .npmrc | 1 + README.md | 61 ++++++++++++++++ index.js | 93 ++++++++++++++++++++++++ package.json | 38 ++++++++++ test/base.js | 7 ++ test/fixtures/file.js | 1 + test/fixtures/install-and-load.js | 19 +++++ test/protocol-ava-3.js | 101 ++++++++++++++++++++++++++ test/snapshots/protocol-ava-3.js.md | 91 +++++++++++++++++++++++ test/snapshots/protocol-ava-3.js.snap | Bin 0 -> 494 bytes 18 files changed, 573 insertions(+) create mode 100644 .editorconfig create mode 100644 .github/CODE_OF_CONDUCT.md create mode 100644 .github/CONTRIBUTING.md create mode 100644 .github/ISSUE_TEMPLATE/bug.md create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature.md create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 .npmrc create mode 100644 README.md create mode 100644 index.js create mode 100644 package.json create mode 100644 test/base.js create mode 100644 test/fixtures/file.js create mode 100644 test/fixtures/install-and-load.js create mode 100644 test/protocol-ava-3.js create mode 100644 test/snapshots/protocol-ava-3.js.md create mode 100644 test/snapshots/protocol-ava-3.js.snap diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..1c6314a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +indent_style = tab +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.yml] +indent_style = space +indent_size = 2 diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..2c877ff --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# Contributor Covenant Code of Conduct + +Translations: [Español](https://github.com/avajs/ava-docs/blob/master/es_ES/code-of-conduct.md), [Français](https://github.com/avajs/ava-docs/blob/master/fr_FR/code-of-conduct.md), [Italiano](https://github.com/avajs/ava-docs/blob/master/it_IT/code-of-conduct.md), [日本語](https://github.com/avajs/ava-docs/blob/master/ja_JP/code-of-conduct.md), [Português](https://github.com/avajs/ava-docs/blob/master/pt_BR/code-of-conduct.md), [Русский](https://github.com/avajs/ava-docs/blob/master/ru_RU/code-of-conduct.md), [简体中文](https://github.com/avajs/ava-docs/blob/master/zh_CN/code-of-conduct.md) + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +nationality, personal appearance, race, religion, or sexual identity and +orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or +advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at sindresorhus@gmail.com. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..fc6a725 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,7 @@ +# Contributing to AVA + +✨ Thanks for contributing to AVA! ✨ + +Please note that this project is released with a [Contributor Code of Conduct](CODE_OF_CONDUCT.md). By participating in this project you agree to abide by its terms. + +This repository is a part of AVA. Start with reading AVA's [contributing guide](https://github.com/avajs/ava/blob/master/.github/CONTRIBUTING.md). Issue labels may be a little different in this repository but otherwise the same applies. diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md new file mode 100644 index 0000000..f77ff69 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -0,0 +1,15 @@ +--- +name: Bug Report +about: If something isn't working the way you expect it to +labels: needs triage +--- + +Please provide details about: + +* What you're trying to do +* What happened +* What you expected to happen + +Please share relevant sample code. Or better yet, provide a link to a [minimal reproducible example](https://stackoverflow.com/help/minimal-reproducible-example). + +We'll also need your AVA configuration (in `package.json` or `ava.config.*` configuration files) and how you're invoking AVA. Share the installed AVA version (get it by running `npx ava --version`) and `@ava/babel` version (from your `package.json` file). diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..6f91954 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: true +contact_links: + - name: AVA on Spectrum + url: https://spectrum.chat/ava + about: Ask questions and discuss in our Spectrum community + - name: Stack Overflow + url: https://stackoverflow.com/questions/tagged/ava + about: Tag your question on Stack Overflow diff --git a/.github/ISSUE_TEMPLATE/feature.md b/.github/ISSUE_TEMPLATE/feature.md new file mode 100644 index 0000000..5c5701d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.md @@ -0,0 +1,11 @@ +--- +name: Feature Request +about: Suggestions for new or different behavior +labels: question +--- + +Please provide details about: + +* What you're trying to do +* Why you can't use AVA's Babel support for this +* And maybe how you think AVA could handle this diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..02e439e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,29 @@ +name: Install and test @ava/typescript +on: + push: + branches: + - master + pull_request: + paths-ignore: + - '*.md' +jobs: + nodejs: + name: Node.js + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + node-version: [^10.18.0, ^12.14.0, ^13.5.0] + os: [ubuntu-latest, windows-latest] + steps: + - uses: actions/checkout@v1 + with: + fetch-depth: 1 + - uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + - run: npm install --no-audit + - run: npm test + - uses: codecov/codecov-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4f52d76 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/.nyc_output +/coverage +/node_modules diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..43c97e7 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/README.md b/README.md new file mode 100644 index 0000000..4b3da26 --- /dev/null +++ b/README.md @@ -0,0 +1,61 @@ +# @ava/typescript + +Adds rudimentary [TypeScript](https://www.typescriptlang.org/) support to [AVA](https://avajs.dev). + +This is designed to work for projects that precompile TypeScript. It allows AVA to load the compiled JavaScript, while configuring AVA to treat the TypeScript files as test files. + +In other words, say you have a test file at `src/test.ts`. You've configured TypeScript to output to `build/`. Using `@ava/typescript` you can run the test using `npx ava src/test.ts`. + +## Enabling TypeScript support + +Add this package to your project: + +```console +npm install --save-dev @ava/typescript +``` + +Then, enable TypeScript support either in `package.json` or `ava.config.*`: + +**`package.json`:** + +```json +{ + "ava": { + "typescript": { + "rewritePaths": { + "src/": "build/" + } + } + } +} +``` + +Both keys and values of the `rewritePaths` object must end with a `/`. Paths are relative to your project directory. + +Output files are expected to have the `.js` extension. + +## Add additional extensions + +You can configure AVA to recognize additional file extensions. To add (partial†) JSX support: + +**`package.json`:** + +```json +{ + "ava": { + "typescript": { + "extensions": [ + "ts", + "tsx" + ], + "rewritePaths": { + "src/": "build/" + } + } + } +} +``` + +See also AVA's [`extensions` option](https://github.com/avajs/ava/blob/master/docs/06-configuration.md#options). + +† Note that the [*preserve* mode for JSX](https://www.typescriptlang.org/docs/handbook/jsx.html) is not (yet) supported. diff --git a/index.js b/index.js new file mode 100644 index 0000000..98c9adc --- /dev/null +++ b/index.js @@ -0,0 +1,93 @@ +'use strict'; +const path = require('path'); + +const escapeStringRegexp = require('escape-string-regexp'); +const isPlainObject = require('is-plain-object'); + +const pkg = require('./package.json'); + +function isValidExtensions(extensions) { + return Array.isArray(extensions) && + extensions.length > 0 && + extensions.every(ext => typeof ext === 'string' && ext !== '') && + new Set(extensions).size === extensions.length; +} + +function isValidRewritePaths(rewritePaths) { + if (!isPlainObject(rewritePaths)) { + return false; + } + + return Object.entries(rewritePaths).every(([from, to]) => { + return from.endsWith('/') && typeof to === 'string' && to.endsWith('/'); + }); +} + +module.exports = ({negotiateProtocol}) => { + const protocol = negotiateProtocol(['ava-3'], {version: pkg.version}); + if (protocol === null) { + return; + } + + return { + main({config}) { + let valid = false; + if (isPlainObject(config)) { + const keys = Object.keys(config); + if (keys.every(key => key === 'extensions' || key === 'rewritePaths')) { + valid = + (config.extensions === undefined || isValidExtensions(config.extensions)) && + isValidRewritePaths(config.rewritePaths); + } + } + + if (!valid) { + throw new Error(`Unexpected Typescript configuration for AVA. See https://github.com/avajs/typescript/blob/v${pkg.version}/README.md for allowed values.`); + } + + const { + extensions = ['ts'], + rewritePaths + } = config; + + return { + async compile() { + return { + extensions: extensions.slice(), + rewritePaths: Object.entries(rewritePaths).map(([from, to]) => [ + path.join(protocol.projectDir, from), + path.join(protocol.projectDir, to) + ]) + }; + }, + + get extensions() { + return extensions.slice(); + } + }; + }, + + worker({extensionsToLoadAsModules, state: {extensions, rewritePaths}}) { + const testFileExtension = new RegExp(`\\.(${extensions.map(ext => escapeStringRegexp(ext)).join('|')})$`); + + return { + canLoad(ref) { + return testFileExtension.test(ref) && rewritePaths.some(([from]) => ref.startsWith(from)); + }, + + async load(ref, {requireFn}) { + for (const extension of extensionsToLoadAsModules) { + if (ref.endsWith(`.${extension}`)) { + throw new Error('@ava/typescript cannot yet load ESM files'); + } + } + + const [from, to] = rewritePaths.find(([from]) => ref.startsWith(from)); + // TODO: Support JSX preserve mode — https://www.typescriptlang.org/docs/handbook/jsx.html + const rewritten = `${to}${ref.slice(from.length)}`.replace(testFileExtension, '.js'); + return requireFn(rewritten); + } + }; + } + }; +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..1cda601 --- /dev/null +++ b/package.json @@ -0,0 +1,38 @@ +{ + "name": "@ava/typescript", + "version": "0.0.0", + "description": "TypeScript provider for AVA", + "engines": { + "node": ">=10.18.0 <11 || >=12.14.0 <13 || >=13.5.0" + }, + "files": [ + "index.js" + ], + "author": "Mark Wubben (https://novemberborn.net)", + "repository": "avajs/typescript", + "license": "MIT", + "keywords": [ + "ava", + "typescript" + ], + "scripts": { + "test": "xo && nyc ava" + }, + "dependencies": { + "escape-string-regexp": "^2.0.0", + "is-plain-object": "^3.0.0" + }, + "devDependencies": { + "ava": "^3.0.0", + "execa": "^4.0.0", + "nyc": "^15.0.0", + "xo": "^0.25.3" + }, + "nyc": { + "reporter": [ + "html", + "lcov", + "text" + ] + } +} diff --git a/test/base.js b/test/base.js new file mode 100644 index 0000000..d3344e5 --- /dev/null +++ b/test/base.js @@ -0,0 +1,7 @@ +const test = require('ava'); +const makeProvider = require('..'); + +test('bails when negotiating protocol returns `null`', t => { + const provider = makeProvider({negotiateProtocol: () => null}); + t.is(provider, undefined); +}); diff --git a/test/fixtures/file.js b/test/fixtures/file.js new file mode 100644 index 0000000..05c09e7 --- /dev/null +++ b/test/fixtures/file.js @@ -0,0 +1 @@ +console.log('logged in file.js'); diff --git a/test/fixtures/install-and-load.js b/test/fixtures/install-and-load.js new file mode 100644 index 0000000..fce29b1 --- /dev/null +++ b/test/fixtures/install-and-load.js @@ -0,0 +1,19 @@ +const path = require('path'); +const makeProvider = require('../..'); + +const provider = makeProvider({ + negotiateProtocol() { + return {identifier: process.argv[2], ava: {version: '3.0.0'}, projectDir: path.resolve(__dirname, '..')}; + } +}); + +const worker = provider.worker({ + extensionsToLoadAsModules: [], + state: JSON.parse(process.argv[3]) +}); + +const ref = path.resolve(process.argv[4]); + +if (worker.canLoad(ref)) { + worker.load(ref, {requireFn: require}); +} diff --git a/test/protocol-ava-3.js b/test/protocol-ava-3.js new file mode 100644 index 0000000..5839f6e --- /dev/null +++ b/test/protocol-ava-3.js @@ -0,0 +1,101 @@ +const path = require('path'); +const test = require('ava'); +const execa = require('execa'); +const pkg = require('../package.json'); +const makeProvider = require('..'); + +const withProvider = (t, run) => run(t, makeProvider({ + negotiateProtocol(identifiers, {version}) { + t.true(identifiers.includes('ava-3')); + t.is(version, pkg.version); + return { + ava: {version: '3.0.0'}, + identifier: 'ava-3', + normalizeGlobPatterns: patterns => patterns, + async findFiles({patterns}) { + return patterns.map(file => path.join(__dirname, file)); + }, + projectDir: __dirname + }; + } +})); + +const validateConfig = (t, provider, config) => { + const error = t.throws(() => provider.main({config})); + error.message = error.message.replace(`v${pkg.version}`, 'v${pkg.version}'); // eslint-disable-line no-template-curly-in-string + t.snapshot(error); +}; + +test('negotiates ava-3 protocol', withProvider, t => t.plan(2)); + +test('main() config validation: throw when config is not a plain object', withProvider, (t, provider) => { + validateConfig(t, provider, false); + validateConfig(t, provider, true); + validateConfig(t, provider, null); + validateConfig(t, provider, []); +}); + +test('main() config validation: throw when config contains keys other than \'extensions\' or \'rewritePaths\'', withProvider, (t, provider) => { + validateConfig(t, provider, {foo: 1}); +}); + +test('main() config validation: throw when config.extensions contains empty strings', withProvider, (t, provider) => { + validateConfig(t, provider, {extensions: ['']}); +}); + +test('main() config validation: throw when config.extensions contains non-strings', withProvider, (t, provider) => { + validateConfig(t, provider, {extensions: [1]}); +}); + +test('main() config validation: throw when config.extensions contains duplicates', withProvider, (t, provider) => { + validateConfig(t, provider, {extensions: ['ts', 'ts']}); +}); + +test('main() config validation: config may not be an empty object', withProvider, (t, provider) => { + validateConfig(t, provider, {}); +}); + +test('main() config validation: rewrite paths must end in a /', withProvider, (t, provider) => { + validateConfig(t, provider, {rewritePaths: {src: 'build/'}}); + validateConfig(t, provider, {rewritePaths: {'src/': 'build'}}); +}); + +test('main() extensions: defaults to [\'ts\']', withProvider, (t, provider) => { + t.deepEqual(provider.main({config: {rewritePaths: {'src/': 'build/'}}}).extensions, ['ts']); +}); + +test('main() extensions: returns configured extensions', withProvider, (t, provider) => { + const extensions = ['tsx']; + t.deepEqual(provider.main({config: {extensions, rewritePaths: {'src/': 'build/'}}}).extensions, extensions); +}); + +test('main() extensions: always returns new arrays', withProvider, (t, provider) => { + const main = provider.main({config: {rewritePaths: {'src/': 'build/'}}}); + t.not(main.extensions, main.extensions); +}); + +const compile = async provider => { + return { + state: await provider.main({ + config: { + rewritePaths: { + 'typescript/': 'fixtures/' + } + } + }).compile() + }; +}; + +test('worker(): load rewritten paths files', withProvider, async (t, provider) => { + const {state} = await compile(provider); + const {stdout, stderr} = await execa.node( + path.join(__dirname, 'fixtures/install-and-load'), + ['ava-3', JSON.stringify(state), path.join(__dirname, 'typescript', 'file.ts')], + {cwd: path.join(__dirname, 'fixtures')} + ); + if (stderr.length > 0) { + t.log(stderr); + } + + t.snapshot(stdout); +}); diff --git a/test/snapshots/protocol-ava-3.js.md b/test/snapshots/protocol-ava-3.js.md new file mode 100644 index 0000000..43ba7b2 --- /dev/null +++ b/test/snapshots/protocol-ava-3.js.md @@ -0,0 +1,91 @@ +# Snapshot report for `test/protocol-ava-3.js` + +The actual snapshot is saved in `protocol-ava-3.js.snap`. + +Generated by [AVA](https://avajs.dev). + +## main() config validation: config may not be an empty object + +> Snapshot 1 + + Error { + message: 'Unexpected Typescript configuration for AVA. See https://github.com/avajs/typescript/blob/v${pkg.version}/README.md for allowed values.', + } + +## main() config validation: rewrite paths must end in a / + +> Snapshot 1 + + Error { + message: 'Unexpected Typescript configuration for AVA. See https://github.com/avajs/typescript/blob/v${pkg.version}/README.md for allowed values.', + } + +> Snapshot 2 + + Error { + message: 'Unexpected Typescript configuration for AVA. See https://github.com/avajs/typescript/blob/v${pkg.version}/README.md for allowed values.', + } + +## main() config validation: throw when config contains keys other than 'extensions' or 'rewritePaths' + +> Snapshot 1 + + Error { + message: 'Unexpected Typescript configuration for AVA. See https://github.com/avajs/typescript/blob/v${pkg.version}/README.md for allowed values.', + } + +## main() config validation: throw when config is not a plain object + +> Snapshot 1 + + Error { + message: 'Unexpected Typescript configuration for AVA. See https://github.com/avajs/typescript/blob/v${pkg.version}/README.md for allowed values.', + } + +> Snapshot 2 + + Error { + message: 'Unexpected Typescript configuration for AVA. See https://github.com/avajs/typescript/blob/v${pkg.version}/README.md for allowed values.', + } + +> Snapshot 3 + + Error { + message: 'Unexpected Typescript configuration for AVA. See https://github.com/avajs/typescript/blob/v${pkg.version}/README.md for allowed values.', + } + +> Snapshot 4 + + Error { + message: 'Unexpected Typescript configuration for AVA. See https://github.com/avajs/typescript/blob/v${pkg.version}/README.md for allowed values.', + } + +## main() config validation: throw when config.extensions contains duplicates + +> Snapshot 1 + + Error { + message: 'Unexpected Typescript configuration for AVA. See https://github.com/avajs/typescript/blob/v${pkg.version}/README.md for allowed values.', + } + +## main() config validation: throw when config.extensions contains empty strings + +> Snapshot 1 + + Error { + message: 'Unexpected Typescript configuration for AVA. See https://github.com/avajs/typescript/blob/v${pkg.version}/README.md for allowed values.', + } + +## main() config validation: throw when config.extensions contains non-strings + +> Snapshot 1 + + Error { + message: 'Unexpected Typescript configuration for AVA. See https://github.com/avajs/typescript/blob/v${pkg.version}/README.md for allowed values.', + } + +## worker(): load rewritten paths files + +> Snapshot 1 + + 'logged in file.js' diff --git a/test/snapshots/protocol-ava-3.js.snap b/test/snapshots/protocol-ava-3.js.snap new file mode 100644 index 0000000000000000000000000000000000000000..35da63a659bef20a1e9520489a35b29d70dd71a7 GIT binary patch literal 494 zcmVzN>U3Nr(P_15 zAR~KjYH@L5dMcF9)Xor^ms(Mfnp~2aq7YJ9kXoEvlvz-sker{FmYH5!lvt9PpQn(P zU!>p|=BTF-oSLeTQBqP+Y^ATCo>`JnnxvPUpR1o(mY7wnUjo;zpOllIq+g~|U67rw zSC(2-3^b%xKgiY5#n)9YHwA27VopweIndIw#GKO9V!csMj(T#`lcSy-_2j50M?E>} k$r0p9Rt7;v!JPc`bf8=&(CW;bRK2WX0I&pHj_e8m0O4WldjJ3c literal 0 HcmV?d00001