diff --git a/README.md b/README.md
index 5e0e01f..4233127 100644
--- a/README.md
+++ b/README.md
@@ -122,7 +122,19 @@ Both attributes are required:
The type of the props can be defined in a separate file. Types imported using relative imports are resolved, the imported files are downloaded from URL relative to the file that imports them. When the import does not specify file extension, `.ts` and `.tsx` files are tried, first as a file, then as an `index` file in a directory.
-The requests for the tried files that do not exist fail. They can be avoided by reading the type of the props from a single file that has no relative imports, e.g. bundled type declarations (`*.d.ts`) of a library. Such a file contains no component, so `name` must be the type of the props, and the props are presented without default values.
+The requests for the tried files that do not exist fail. They can be avoided by the `resolveRelativeImports` option, the URL of a single file that contains all the types, e.g. bundled type declarations (`*.d.ts`) of a library. The relative imports of the file in the `src` attribute are then not downloaded, the imported types are looked up by their names among the exports of that file instead, regardless of the path they are imported from. When the file cannot be downloaded, e.g. during development before it is built, the relative imports are downloaded as usual. The default values of the props are still read from the component:
+
+```html
+
+```
+
+The type of the props can also be read from such a file directly. It contains no component, so `name` must be the type of the props, and the props are presented without default values.
Types imported from packages are not resolved, only their name is presented, unless the package is listed in the `resolvePackages` option. The option defines where the types of the package are placed, which is either:
@@ -197,6 +209,7 @@ window.docoffConfig = {
mergeLiteralUnions: true,
mergeObjectIntersections: true,
resolvePackages: {},
+ resolveRelativeImports: undefined,
},
};
```
@@ -248,7 +261,7 @@ When the site is not deployed at the root of the domain, URLs starting with a sl
* the CSS file of live previews defined by the `--docoff-preview-css` custom property,
* the `src` attribute of the `docoff-react-props` element,
-* the URLs in its `resolvePackages` option.
+* the URLs in its `resolvePackages` and `resolveRelativeImports` options.
Define `window.docoffConfig` **before** the Docoff bundle is loaded:
diff --git a/src/DocoffReactProps/_helpers/getPropsTable.js b/src/DocoffReactProps/_helpers/getPropsTable.js
index f8a4a05..5764557 100644
--- a/src/DocoffReactProps/_helpers/getPropsTable.js
+++ b/src/DocoffReactProps/_helpers/getPropsTable.js
@@ -37,7 +37,9 @@ export const getPropsTable = async (src, name) => {
try {
const packageUrls = getPackageUrls(document.baseURI, basePath, config.resolvePackages);
- const props = await parseProps(await response.text(), url, name, packageUrls);
+ const relativeImportsUrl = config.resolveRelativeImports
+ && resolveUrl(document.baseURI, basePath, config.resolveRelativeImports);
+ const props = await parseProps(await response.text(), url, name, packageUrls, relativeImportsUrl);
const properties = getTypeProperties(props.types.map((tsType) => evaluateTSType(tsType, config)));
return properties.length > 0
diff --git a/src/DocoffReactProps/_helpers/parseProps/_helpers/findComponent.js b/src/DocoffReactProps/_helpers/parseProps/_helpers/findComponent.js
index e78ff2b..c2e8e6f 100644
--- a/src/DocoffReactProps/_helpers/parseProps/_helpers/findComponent.js
+++ b/src/DocoffReactProps/_helpers/parseProps/_helpers/findComponent.js
@@ -5,12 +5,14 @@ import { parseDefinitions } from './parseDefinitions';
* @param {string} url The absolute URL of the file
* @param {string} name The name of the component
* @param {Object} resolvePackages The URLs of the packages to resolve by the names of the packages
+ * @param {Object} relativeImportUrls The URLs of the files to look up relative imports in, by the URLs of the files
+ * with the imports
* @param {Map} modules The files that have already been downloaded by their URL, filled in with the imported files
* @returns {Promise} The component as described by `react-docgen`, `null` when the file has no
* component of that name
*/
-export const findComponent = async (source, url, name, resolvePackages, modules) => {
- const definitions = await parseDefinitions(source, url, resolvePackages, modules);
+export const findComponent = async (source, url, name, resolvePackages, relativeImportUrls, modules) => {
+ const definitions = await parseDefinitions(source, url, resolvePackages, relativeImportUrls, modules);
return definitions.find((definition) => definition.displayName === name) ?? null;
};
diff --git a/src/DocoffReactProps/_helpers/parseProps/_helpers/findType.js b/src/DocoffReactProps/_helpers/parseProps/_helpers/findType.js
index c7cc9b0..5f35c99 100644
--- a/src/DocoffReactProps/_helpers/parseProps/_helpers/findType.js
+++ b/src/DocoffReactProps/_helpers/parseProps/_helpers/findType.js
@@ -12,10 +12,12 @@ const TYPE_NAME = 'DocoffReactPropsTypeProps';
* @param {string} url The absolute URL of the file
* @param {string} name The name of the type exported from the file, or `default`
* @param {Object} resolvePackages The URLs of the packages to resolve by the names of the packages
+ * @param {Object} relativeImportUrls The URLs of the files to look up relative imports in, by the URLs of the files
+ * with the imports
* @param {Map} modules The files that have already been downloaded by their URL, including the file itself
* @returns {Promise} The made-up component as described by `react-docgen`
*/
-export const findType = async (url, name, resolvePackages, modules) => {
+export const findType = async (url, name, resolvePackages, relativeImportUrls, modules) => {
const typeComponentSource = `
import type { ${name} as ${TYPE_NAME} } from './${url.split('/').pop()}';
@@ -33,6 +35,7 @@ export const findType = async (url, name, resolvePackages, modules) => {
typeComponentSource,
new URL(TYPE_COMPONENT_FILE_NAME, url).href,
resolvePackages,
+ relativeImportUrls,
downloadedModules,
);
diff --git a/src/DocoffReactProps/_helpers/parseProps/_helpers/parseDefinitions/_helpers/parseWithImports/_helpers/createUrlImporter/_helpers/getModuleUrl.js b/src/DocoffReactProps/_helpers/parseProps/_helpers/parseDefinitions/_helpers/parseWithImports/_helpers/createUrlImporter/_helpers/getModuleUrl.js
index d5e28a3..540e0d7 100644
--- a/src/DocoffReactProps/_helpers/parseProps/_helpers/parseDefinitions/_helpers/parseWithImports/_helpers/createUrlImporter/_helpers/getModuleUrl.js
+++ b/src/DocoffReactProps/_helpers/parseProps/_helpers/parseDefinitions/_helpers/parseWithImports/_helpers/createUrlImporter/_helpers/getModuleUrl.js
@@ -4,12 +4,15 @@ import { RESOLVE_EXTENSIONS } from '../../../constants';
* @param {string} source What is imported, e.g. `./Button.types` or `@scope/package/src/components/Button`
* @param {string} filename The URL of the file with the import
* @param {Object} resolvePackages The URLs of the packages to resolve by the names of the packages
+ * @param {Object} relativeImportUrls The URLs of the files to look up relative imports in, by the URLs of the files
+ * with the imports
* @returns {string|null} The URL of the import, typically without file extension. `null` when the import
* is not to be resolved.
*/
-export const getModuleUrl = (source, filename, resolvePackages) => {
+export const getModuleUrl = (source, filename, resolvePackages, relativeImportUrls = {}) => {
if (source.startsWith('.')) {
- return new URL(source, filename).href;
+ // A file contains everything the relative imports point to, so it is used regardless of the imported path
+ return relativeImportUrls[filename] ?? new URL(source, filename).href;
}
const packageName = Object.keys(resolvePackages)
diff --git a/src/DocoffReactProps/_helpers/parseProps/_helpers/parseDefinitions/_helpers/parseWithImports/_helpers/createUrlImporter/_helpers/getModuleUrl.test.js b/src/DocoffReactProps/_helpers/parseProps/_helpers/parseDefinitions/_helpers/parseWithImports/_helpers/createUrlImporter/_helpers/getModuleUrl.test.js
index f38fb34..bd2aa6e 100644
--- a/src/DocoffReactProps/_helpers/parseProps/_helpers/parseDefinitions/_helpers/parseWithImports/_helpers/createUrlImporter/_helpers/getModuleUrl.test.js
+++ b/src/DocoffReactProps/_helpers/parseProps/_helpers/parseDefinitions/_helpers/parseWithImports/_helpers/createUrlImporter/_helpers/getModuleUrl.test.js
@@ -7,6 +7,17 @@ describe('functionality', () => {
expect(getModuleUrl('../../types', FILENAME, {})).toEqual('https://example.com/types');
});
+ it('looks up relative import in the file given for the file with the import', () => {
+ expect(getModuleUrl('../../types', FILENAME, {}, { [FILENAME]: 'https://example.com/library.d.ts' }))
+ .toEqual('https://example.com/library.d.ts');
+ });
+
+ it('resolves relative import of another file against it', () => {
+ expect(getModuleUrl('./Button', 'https://example.com/components/Button/index.ts', {}, {
+ [FILENAME]: 'https://example.com/library.d.ts',
+ })).toEqual('https://example.com/components/Button/Button');
+ });
+
it('does not resolve import from package that is not listed', () => {
expect(getModuleUrl('react', FILENAME, { '@scope/package': 'https://cdn.test/package' })).toBeNull();
});
diff --git a/src/DocoffReactProps/_helpers/parseProps/_helpers/parseDefinitions/_helpers/parseWithImports/_helpers/createUrlImporter/createUrlImporter.js b/src/DocoffReactProps/_helpers/parseProps/_helpers/parseDefinitions/_helpers/parseWithImports/_helpers/createUrlImporter/createUrlImporter.js
index 54cb4cf..64ed9b0 100644
--- a/src/DocoffReactProps/_helpers/parseProps/_helpers/parseDefinitions/_helpers/parseWithImports/_helpers/createUrlImporter/createUrlImporter.js
+++ b/src/DocoffReactProps/_helpers/parseProps/_helpers/parseDefinitions/_helpers/parseWithImports/_helpers/createUrlImporter/createUrlImporter.js
@@ -13,16 +13,22 @@ import { setUniqueTypeName } from './_helpers/setUniqueTypeName';
* @param {Map} modules The downloaded files by URL of the import, `null` if the file could not be downloaded
* @param {Set} missingModuleUrls The URLs of the imports that have not been downloaded yet, filled in by the importer
* @param {Object} resolvePackages The URLs of the packages to resolve by the names of the packages
+ * @param {Object} relativeImportUrls The URLs of the files to look up relative imports in, by the URLs of the files
+ * with the imports
* @returns {Function} The importer
*/
-export const createUrlImporter = (modules, missingModuleUrls, resolvePackages) => {
+export const createUrlImporter = (modules, missingModuleUrls, resolvePackages, relativeImportUrls) => {
let importedTypesCount = 0;
const resolveImportedValue = (path, name, file, seen = new Set()) => {
const source = path.node.source?.value;
const { filename } = file.opts;
- const moduleUrl = source && filename ? getModuleUrl(source, filename, resolvePackages) : null;
+ // When the file to look up relative imports in cannot be downloaded, the imported files are downloaded instead
+ const availableRelativeImportUrls = modules.get(relativeImportUrls[filename]) === null ? {} : relativeImportUrls;
+ const moduleUrl = source && filename
+ ? getModuleUrl(source, filename, resolvePackages, availableRelativeImportUrls)
+ : null;
if (moduleUrl === null) {
return null;
}
diff --git a/src/DocoffReactProps/_helpers/parseProps/_helpers/parseDefinitions/_helpers/parseWithImports/parseWithImports.js b/src/DocoffReactProps/_helpers/parseProps/_helpers/parseDefinitions/_helpers/parseWithImports/parseWithImports.js
index ca1b94b..f2585c6 100644
--- a/src/DocoffReactProps/_helpers/parseProps/_helpers/parseDefinitions/_helpers/parseWithImports/parseWithImports.js
+++ b/src/DocoffReactProps/_helpers/parseProps/_helpers/parseDefinitions/_helpers/parseWithImports/parseWithImports.js
@@ -13,15 +13,17 @@ import { restoreTypeNames } from './_helpers/restoreTypeNames';
* @param {string} url The absolute URL of the source file
* @param {Object} options The options of `react-docgen`
* @param {Object} resolvePackages The URLs of the packages to resolve by the names of the packages
+ * @param {Object} relativeImportUrls The URLs of the files to look up relative imports in, by the URLs of the files
+ * with the imports
* @param {Map} modules The files that have already been downloaded by their URL
* @returns {Promise} The components found in the source code as described by `react-docgen`
*/
-export const parseWithImports = async (source, url, options, resolvePackages, modules) => {
+export const parseWithImports = async (source, url, options, resolvePackages, relativeImportUrls, modules) => {
const missingModuleUrls = new Set();
const optionsWithImporter = {
...options,
filename: url,
- importer: createUrlImporter(modules, missingModuleUrls, resolvePackages),
+ importer: createUrlImporter(modules, missingModuleUrls, resolvePackages, relativeImportUrls),
};
const parse = async () => {
diff --git a/src/DocoffReactProps/_helpers/parseProps/_helpers/parseDefinitions/parseDefinitions.js b/src/DocoffReactProps/_helpers/parseProps/_helpers/parseDefinitions/parseDefinitions.js
index 5150b05..edd13b7 100644
--- a/src/DocoffReactProps/_helpers/parseProps/_helpers/parseDefinitions/parseDefinitions.js
+++ b/src/DocoffReactProps/_helpers/parseProps/_helpers/parseDefinitions/parseDefinitions.js
@@ -9,10 +9,12 @@ import { propsTypeHandler } from './_helpers/propsTypeHandler';
* @param {string} source The source code
* @param {string} url The absolute URL of the source file
* @param {Object} resolvePackages The URLs of the packages to resolve by the names of the packages
+ * @param {Object} relativeImportUrls The URLs of the files to look up relative imports in, by the URLs of the files
+ * with the imports
* @param {Map} modules The files that have already been downloaded by their URL
* @returns {Promise} The components as described by `react-docgen`, each with `propsTypes`
*/
-export const parseDefinitions = async (source, url, resolvePackages, modules = new Map()) => {
+export const parseDefinitions = async (source, url, resolvePackages, relativeImportUrls, modules = new Map()) => {
const options = {
babelOptions: {
// The source code is parsed on its own, Babel config files must never be looked up
@@ -28,7 +30,7 @@ export const parseDefinitions = async (source, url, resolvePackages, modules = n
};
try {
- return await parseWithImports(source, url, options, resolvePackages, modules);
+ return await parseWithImports(source, url, options, resolvePackages, relativeImportUrls, modules);
} catch (error) {
// A file with no component, e.g. a file with types only, is not an error
if (error.code === docgen.ERROR_CODES.MISSING_DEFINITION) {
diff --git a/src/DocoffReactProps/_helpers/parseProps/parseProps.js b/src/DocoffReactProps/_helpers/parseProps/parseProps.js
index 5d89185..45a8188 100644
--- a/src/DocoffReactProps/_helpers/parseProps/parseProps.js
+++ b/src/DocoffReactProps/_helpers/parseProps/parseProps.js
@@ -10,10 +10,14 @@ import { normalizeType } from './_helpers/normalizeType';
* @param {string} url The absolute URL of the file
* @param {string} name The name of the component or of the type
* @param {Object} resolvePackages The URLs of the packages to resolve by the names of the packages
+ * @param {string|null} resolveRelativeImports The URL of the file to look up the relative imports of the file in
* @returns {Promise<{ defaultValues: Object, types: Object[] }>} The default values of the props by their names,
* and the types that define the props as described by `react-docgen`
*/
-export const parseProps = async (source, url, name, resolvePackages = {}) => {
+export const parseProps = async (source, url, name, resolvePackages = {}, resolveRelativeImports = null) => {
+ // Only the imports of the file itself are looked up there. The made-up component that presents a type exported
+ // from the file must import the file itself.
+ const relativeImportUrls = resolveRelativeImports ? { [url]: resolveRelativeImports } : {};
// Files are downloaded once, even when both a component and a type are looked for
const modules = new Map([[
url,
@@ -22,8 +26,8 @@ export const parseProps = async (source, url, name, resolvePackages = {}) => {
url,
},
]]);
- const definition = await findComponent(source, url, name, resolvePackages, modules)
- ?? await findType(url, name, resolvePackages, modules);
+ const definition = await findComponent(source, url, name, resolvePackages, relativeImportUrls, modules)
+ ?? await findType(url, name, resolvePackages, relativeImportUrls, modules);
return {
defaultValues: getDefaultValues(definition),
diff --git a/src/DocoffReactProps/_helpers/parseProps/parseProps.test.js b/src/DocoffReactProps/_helpers/parseProps/parseProps.test.js
index ced00d6..da85dd1 100644
--- a/src/DocoffReactProps/_helpers/parseProps/parseProps.test.js
+++ b/src/DocoffReactProps/_helpers/parseProps/parseProps.test.js
@@ -231,6 +231,22 @@ const files = {
{level}{icon?.name}
);
`,
+ 'http://localhost/components/Switch/Switch.tsx': `
+ import React from 'react';
+ import type { ToggleProps } from '../Toggle';
+
+ export const Switch = ({ disabled = false, label }: ToggleProps) => {label} ;
+ `,
+ 'http://localhost/components/Switch/Switch.types.ts': `
+ import type { ToggleLabelPosition } from '../Toggle';
+
+ export type SwitchProps = {
+ /**
+ * Side of the label.
+ */
+ labelPosition?: ToggleLabelPosition;
+ };
+ `,
'http://localhost/components/Toggle/Toggle.types.ts': `
import type { ToggleProps as LibraryToggleProps } from '@library/ui/src/components/Toggle';
@@ -309,8 +325,8 @@ const FORM_LAYOUT_TYPES_URL = 'http://localhost/components/FormLayout/FormLayout
const TOGGLE_TYPES_URL = 'http://localhost/components/Toggle/Toggle.types.ts';
// Presents the props the way they are presented in the table
-const getProps = async (url, name, resolvePackages) => {
- const props = await parseProps(files[url], url, name, resolvePackages);
+const getProps = async (url, name, resolvePackages, resolveRelativeImports) => {
+ const props = await parseProps(files[url], url, name, resolvePackages, resolveRelativeImports);
const properties = getTypeProperties(props.types.map((tsType) => evaluateTSType(tsType)));
return Object.fromEntries(properties.map((property) => [
@@ -488,6 +504,58 @@ describe('functionality', () => {
expect(global.fetch).not.toHaveBeenCalled();
});
+ it('looks up relative imports of the component in the given file instead of downloading them', async () => {
+ expect(await getProps(
+ 'http://localhost/components/Switch/Switch.tsx',
+ 'Switch',
+ {},
+ 'https://cdn.test/library.d.ts',
+ )).toEqual({
+ disabled: {
+ defaultValue: 'false',
+ description: 'If `true`, the input will be disabled.',
+ required: false,
+ type: 'boolean',
+ },
+ label: {
+ defaultValue: undefined,
+ description: 'Label of the input.',
+ required: true,
+ type: 'ReactNode',
+ },
+ labelPosition: {
+ defaultValue: undefined,
+ description: 'Placement of the label.',
+ required: false,
+ type: '\'before\' | \'after\'',
+ },
+ });
+ expect(global.fetch.mock.calls.map(([url]) => url)).toEqual(['https://cdn.test/library.d.ts']);
+ });
+
+ it('downloads relative imports when the given file cannot be downloaded', async () => {
+ expect(Object.keys(await getProps(BUTTON_URL, 'Button', {}, 'https://cdn.test/missing.d.ts')))
+ .toEqual(['color', 'icon', 'label', 'size']);
+ expect(getFetchedUrls()).toContain(BUTTON_TYPES_URL);
+ });
+
+ it('looks up relative imports of the file with the type in the given file', async () => {
+ expect(await getProps(
+ 'http://localhost/components/Switch/Switch.types.ts',
+ 'SwitchProps',
+ {},
+ 'https://cdn.test/library.d.ts',
+ )).toEqual({
+ labelPosition: {
+ defaultValue: undefined,
+ description: 'Side of the label.',
+ required: false,
+ type: '\'before\' | \'after\'',
+ },
+ });
+ expect(global.fetch.mock.calls.map(([url]) => url)).toEqual(['https://cdn.test/library.d.ts']);
+ });
+
it('resolves types that `react-docgen` does not recognize', async () => {
const props = await getProps(FORM_LAYOUT_TYPES_URL, 'FormLayoutProps');