diff --git a/README.md b/README.md index 9e65273..8d02fa9 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,25 @@ export const config: Config = { Stencil will write a `web-types.json` to your project's root directory the next time the Stencil [build task](https://stenciljs.com/docs/cli#stencil-build) is run. -## Usage +## Configuration + +The `webTypesOutputTarget` output target takes an optional argument, an object literal to configure the output target. +The following are properties on that configuration object. + +### `outFile` + +Defaults to `StencilConfig#{rootDir}/web-types.json`. + +Since v0.3.0. + +Description: A string that represents the directory to place the output file. +Users may specify either a directory (e.g. '../'), a filename (e.g. 'my-types.json') or both (e.g. '../my-types.json'). +If no filename ending is '.json' is provided, the output target assumes that a filename must be added to the path. +In such cases, the default 'web-types.json' will be added to the path. + +It is not recommended that users use absolute paths for this setting, as this can cause errors in projects shared by more than one developer. + +## Using Web Types Once web types have been written to disk, they need to be picked up by the IDE. Web types for your project can be picked by JetBrains IDEs by setting the `web-types` property at the root level of your project's `package.json` file: diff --git a/src/index.test.ts b/src/index.test.ts index 53136fe..32cd21a 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -1,26 +1,85 @@ import { describe, expect, it } from 'vitest'; -import { webTypesOutputTarget } from './index.js'; -import { Config } from '@stencil/core/internal'; +import { type WebTypesConfig, webTypesOutputTarget } from './index.js'; +import { type Config } from '@stencil/core/internal'; +import { join, normalize } from 'path'; describe('webTypesOutputTarget', () => { describe('validate', () => { - it('does not throw when all required fields are set', () => { - const config: Config = { - rootDir: '/some/mocked/field', - }; + describe('WebTypesConfig field validation', () => { + describe('outFile', () => { + it.each([normalize('/user/defined/directory/web-types.json'), normalize('/user/defined/directory/types.json')])( + "does not override a user-provided, absolute path, '%s'", + (expectedDir) => { + const outputTargetConfig: WebTypesConfig = { outFile: expectedDir }; - expect(() => webTypesOutputTarget().validate!(config, [])).not.toThrowError(); + webTypesOutputTarget(outputTargetConfig).validate!({ rootDir: '' }, []); + + expect(outputTargetConfig.outFile).toBe(expectedDir); + }, + ); + + it.each([ + [normalize('./web-types.json'), normalize('~/one/two/web-types.json')], + [normalize('../web-types.json'), normalize('~/one/web-types.json')], + [normalize('../../web-types.json'), normalize('~/web-types.json')], + ])("normalizes relative path '%s' against rootDir", (relPath, expected) => { + const outputTargetConfig: WebTypesConfig = { outFile: relPath }; + + webTypesOutputTarget(outputTargetConfig).validate!({ rootDir: normalize('~/one/two/') }, []); + + expect(outputTargetConfig.outFile).toBe(expected); + }); + + it.each([ + '~', + '/', + '.', + normalize('./.'), + normalize('~/user/defined/directory/'), + normalize('~/user/defined.directory/'), + normalize('~/user.defined.directory/'), + normalize('~/user.defined.directory/.'), + normalize('/user/defined/directory/types.txt'), + normalize('/user/defined/directory/no-trailing-slash'), + ])("sets the filename if none is detected for '%s'", (dirName) => { + const expectedDir = join(`${dirName}`, 'web-types.json'); + const outputTargetConfig: WebTypesConfig = { outFile: dirName }; + + webTypesOutputTarget(outputTargetConfig).validate!({ rootDir: '' }, []); + + expect(outputTargetConfig.outFile).toBe(expectedDir); + }); + + it('provides a reasonable default if the user does not provide a directory', () => { + const outputTargetConfig: WebTypesConfig = {}; + + webTypesOutputTarget(outputTargetConfig).validate!({ rootDir: '' }, []); + + expect(outputTargetConfig.outFile).toBe('web-types.json'); + }); + }); }); - describe('no rootDir set', () => { - const EXPECTED_ERR_MSG = 'Unable to determine the Stencil root directory. Exiting without generating web types.'; + describe('Stencil Config validation', () => { + it('does not throw when all required fields are set', () => { + const config: Config = { + rootDir: normalize('/some/mocked/field'), + }; - it('throws an error when the root dir is set to undefined', () => { - expect(() => webTypesOutputTarget().validate!({ rootDir: undefined }, [])).toThrowError(EXPECTED_ERR_MSG); + expect(() => webTypesOutputTarget().validate!(config, [])).not.toThrowError(); }); - it('throws an error when the root dir is missing', () => { - expect(() => webTypesOutputTarget().validate!({}, [])).toThrowError(EXPECTED_ERR_MSG); + describe('no rootDir set', () => { + const EXPECTED_ERR_MSG = + 'Unable to determine the Stencil root directory. Exiting without generating web types.'; + + it('throws an error when the root dir is set to undefined', () => { + expect(() => webTypesOutputTarget().validate!({ rootDir: undefined }, [])).toThrowError(EXPECTED_ERR_MSG); + }); + + it('throws an error when the root dir is missing', () => { + expect(() => webTypesOutputTarget().validate!({}, [])).toThrowError(EXPECTED_ERR_MSG); + }); }); }); }); diff --git a/src/index.ts b/src/index.ts index 5e9f012..5fe87d6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,16 @@ -import type { BuildCtx, CompilerCtx, OutputTargetCustom, Config } from '@stencil/core/internal'; +import type { BuildCtx, CompilerCtx, OutputTargetCustom, Config, Diagnostic } from '@stencil/core/internal'; import { generateWebTypes } from './generate-web-types.js'; -import { Diagnostic } from '@stencil/core/internal/stencil-public-compiler'; +import { extname, isAbsolute, join, sep } from 'path'; + +/** + * A representation of the configuration object that this output target accepts at compile time + */ +export type WebTypesConfig = { + /** + * The output location of the generated JSON file. + */ + outFile?: string; +}; /** * A Stencil output target for generating [web-types](https://github.com/JetBrains/web-types) for a Stencil project. @@ -16,16 +26,25 @@ import { Diagnostic } from '@stencil/core/internal/stencil-public-compiler'; * * For more information on using this output target, please see the project's README file. */ -export const webTypesOutputTarget = (): OutputTargetCustom => ({ +export const webTypesOutputTarget = (outputTargetConfig: WebTypesConfig = {}): OutputTargetCustom => ({ type: 'custom', name: 'web-types-output-target', - validate(config: Config, _diagnostics: Diagnostic[]) { + validate(config: Config, _diagnostics: Diagnostic[]): void { if (typeof config.rootDir === 'undefined') { // defer to Stencil to create & load ths into its diagnostics, rather than us having to generate one ourselves throw new Error('Unable to determine the Stencil root directory. Exiting without generating web types.'); } + + if (!outputTargetConfig.outFile) { + outputTargetConfig.outFile = 'web-types.json'; + } else if (extname(outputTargetConfig.outFile) !== '.json') { + outputTargetConfig.outFile = join(outputTargetConfig.outFile, 'web-types.json'); + } + if (!isAbsolute(outputTargetConfig.outFile)) { + outputTargetConfig.outFile = join(config.rootDir, outputTargetConfig.outFile); + } }, - async generator(config: Config, compilerCtx: CompilerCtx, buildCtx: BuildCtx) { + async generator(config: Config, compilerCtx: CompilerCtx, buildCtx: BuildCtx): Promise { const timespan = buildCtx.createTimeSpan('generate web-types started', true); /** @@ -37,7 +56,7 @@ export const webTypesOutputTarget = (): OutputTargetCustom => ({ const stencilRootDirectory = config.rootDir!; const webTypes = generateWebTypes(buildCtx, stencilRootDirectory); - await compilerCtx.fs.writeFile('web-types.json', JSON.stringify(webTypes, null, 2)); + await compilerCtx.fs.writeFile(outputTargetConfig.outFile!, JSON.stringify(webTypes, null, 2)); timespan.finish('generate web-types finished'); },