From 4a678d8cad5346803fab1270a42a548efa56e221 Mon Sep 17 00:00:00 2001 From: tjcouch-sil Date: Fri, 1 Dec 2023 10:44:43 -0600 Subject: [PATCH] Added ypes to manifest.json, added ExtensionManifest to papi.d.ts so it can have documentation generated, changed a log level --- lib/papi-dts/papi.d.ts | 36 +++++ lib/papi-dts/tsconfig.json | 3 +- .../extension-manifest.model.ts | 34 +++++ .../services/extension.service.ts | 142 ++++++++++-------- 4 files changed, 149 insertions(+), 66 deletions(-) create mode 100644 src/extension-host/extension-types/extension-manifest.model.ts diff --git a/lib/papi-dts/papi.d.ts b/lib/papi-dts/papi.d.ts index d2a9abc75a..521db2ba4b 100644 --- a/lib/papi-dts/papi.d.ts +++ b/lib/papi-dts/papi.d.ts @@ -4765,3 +4765,39 @@ declare module 'extension-host/extension-types/extension.interface' { deactivate?: UnsubscriberAsync; } } +declare module 'extension-host/extension-types/extension-manifest.model' { + /** Information about an extension provided by the extension developer. */ + export type ExtensionManifest = { + /** Name of the extension */ + name: string; + /** + * Extension version - expected to be [semver](https://semver.org/) like `"0.1.3"`. + * + * Note: semver may become a hard requirement in the future, so we recommend using it now. + */ + version: string; + /** + * Path to the JavaScript file to run in the extension host. Relative to the extension's root + * folder. + * + * Must be specified. Can be `null` if the extension does not have any JavaScript to run. + */ + main: string | null; + /** + * Path to the TypeScript type definition file that describes this extension and its interactions + * on the PAPI. Relative to the extension's root folder. + * + * If not provided, Platform.Bible will look in the following locations: + * + * 1. `.d.ts` + * 2. `.d.ts` + * 3. `index.d.ts` + */ + types?: string; + /** + * List of events that occur that should cause this extension to be activated. Not yet + * implemented. + */ + activationEvents: string[]; + }; +} diff --git a/lib/papi-dts/tsconfig.json b/lib/papi-dts/tsconfig.json index 0136fc701a..b05ede7bd1 100644 --- a/lib/papi-dts/tsconfig.json +++ b/lib/papi-dts/tsconfig.json @@ -24,7 +24,8 @@ "../../src/renderer/services/papi-frontend.service.ts", "../../src/renderer/services/papi-frontend-react.service.ts", "../../src/extension-host/services/papi-backend.service.ts", - "../../src/extension-host/extension-types/extension.interface.ts" + "../../src/extension-host/extension-types/extension.interface.ts", + "../../src/extension-host/extension-types/extension-manifest.model.ts" ], "exclude": ["node_modules"], "ts-node": { diff --git a/src/extension-host/extension-types/extension-manifest.model.ts b/src/extension-host/extension-types/extension-manifest.model.ts new file mode 100644 index 0000000000..47271c28c9 --- /dev/null +++ b/src/extension-host/extension-types/extension-manifest.model.ts @@ -0,0 +1,34 @@ +/** Information about an extension provided by the extension developer. */ +export type ExtensionManifest = { + /** Name of the extension */ + name: string; + /** + * Extension version - expected to be [semver](https://semver.org/) like `"0.1.3"`. + * + * Note: semver may become a hard requirement in the future, so we recommend using it now. + */ + version: string; + /** + * Path to the JavaScript file to run in the extension host. Relative to the extension's root + * folder. + * + * Must be specified. Can be `null` if the extension does not have any JavaScript to run. + */ + main: string | null; + /** + * Path to the TypeScript type definition file that describes this extension and its interactions + * on the PAPI. Relative to the extension's root folder. + * + * If not provided, Platform.Bible will look in the following locations: + * + * 1. `.d.ts` + * 2. `.d.ts` + * 3. `index.d.ts` + */ + types?: string; + /** + * List of events that occur that should cause this extension to be activated. Not yet + * implemented. + */ + activationEvents: string[]; +}; diff --git a/src/extension-host/services/extension.service.ts b/src/extension-host/services/extension.service.ts index 3541aacf11..574ec556d7 100644 --- a/src/extension-host/services/extension.service.ts +++ b/src/extension-host/services/extension.service.ts @@ -23,6 +23,7 @@ import UnsubscriberAsyncList from '@shared/utils/unsubscriber-async-list'; import { ExecutionActivationContext } from '@extension-host/extension-types/extension-activation-context.model'; import { debounce } from '@shared/utils/util'; import LogError from '@shared/log-error.model'; +import { ExtensionManifest } from '@extension-host/extension-types/extension-manifest.model'; /** * The way to use `require` directly - provided by webpack because they overwrite normal `require`. @@ -31,29 +32,13 @@ import LogError from '@shared/log-error.model'; // eslint-disable-next-line camelcase, no-underscore-dangle declare const __non_webpack_require__: typeof require; -/** Extension manifest before it is finalized and frozen */ - /** - * Information about an extension provided by the extension developer. This will be transformed and - * frozen into an ExtensionInfo before use + * Information about an extension and extra metadata about it that we generate + * + * This is a transformed and frozen version of the extension's {@link ExtensionManifest} */ -type ExtensionManifest = { - name: string; - version: string; - /** - * The JavaScript file to run in the extension host. - * - * Must be specified. Can be `null` if the extension does not have any JavaScript to run. - */ - main: string | null; - activationEvents: string[]; -}; - -/** Information about an extension and extra metadata about it that we generate */ type ExtensionInfo = Readonly< ExtensionManifest & { - /** We filtered out undefined and null in `getExtensions`, so this should now be defined */ - main: string; /** * Uri to this extension's directory. Not provided in actual manifest, but added while parsing * the manifest @@ -396,47 +381,35 @@ async function getExtensions(): Promise { return extensionInfos; } +/** + * Creates a `DtsInfo` from a Uri + * + * @param declarationUri The uri to the dts file + * @returns `DtsInfo` for the declaration file at the uri specified + */ +function createDtsInfoFromUri(declarationUri: Uri): DtsInfo { + return { + uri: declarationUri, + base: path.parse(declarationUri).base, + }; +} + /** * Caches type definition files for each extension. Gets the type definition file from each * extension and copies it to `extension-types//index.d.ts` - * because that is the path that works. - * - * We look for predetermined files in the following order and copy over the first one found: - * - * 1. `.d.ts` - * 2. `.d.ts` - * 3. `index.d.ts` + * because that is the path that works. If the extension's type definition file does not start with + * ``, the folder created will be named `` instead of the name of + * the extension type declaration file name. * - * If it becomes a need, we can also add a `types` field to the manifest that overrides these and - * points to the location where this types file should be found. However, we need to be sure to - * prevent extensions from overlapping type definition file names, so keep `` at the - * start of the destination file name however we end up getting the types file. - * - * If the type definition file found does not start with ``, it will be renamed to - * `.d.ts`. + * We look first at the location provided by the extension manifest's `types` property. If one is + * not provided, we look for files according to the specification in the JSDoc for + * {@link ExtensionManifest}'s `types` property order and copy over the first one found. * * @param extensionInfos Extension info for extensions whose types to cache */ async function cacheExtensionTypeDefinitions(extensionInfos: ExtensionInfo[]) { return Promise.all( extensionInfos.map(async (extensionInfo) => { - // Get a list of all the dts files - const dtsInfos = ( - await nodeFS.readDir(extensionInfo.dirUri, (entryName) => entryName.endsWith('.d.ts')) - )[nodeFS.EntryType.File].map( - (declarationUri): DtsInfo => ({ - uri: declarationUri, - base: path.parse(declarationUri).base, - }), - ); - - if (dtsInfos.length <= 0) { - logger.debug( - `Extension ${extensionInfo.name} does not seem to have any .d.ts files in its root`, - ); - return; - } - /** The default assumed name for the dts file including `.d.ts` */ const extensionDtsBaseDefault = `${extensionInfo.name}.d.ts`; /** The declaration file uri we are copying for this extension */ @@ -444,26 +417,65 @@ async function cacheExtensionTypeDefinitions(extensionInfos: ExtensionInfo[]) { /** The declaration file name we are creating for this extension including `.d.ts` */ let extensionDtsBaseDestination = extensionDtsBaseDefault; - // Try using a dts file whose name matches the name of the extension - extensionDtsInfo = dtsInfos.find((dtsInfo) => dtsInfo.base === extensionDtsBaseDefault); + // Try using the path to the type declaration file specified in the extension manifest + if (extensionInfo.types) { + const providedDtsUri = joinUriPaths(extensionInfo.dirUri, extensionInfo.types); + const providedDtsStats = await nodeFS.getStats(providedDtsUri); + if (providedDtsStats && providedDtsStats.isFile()) { + // The extension's specified dts exists, so use it + extensionDtsInfo = createDtsInfoFromUri(providedDtsUri); + } else + logger.warn( + `Extension ${extensionInfo.name} specified its type declaration file was at ${extensionInfo.types}, but this path does not seem to exist. Trying other options`, + ); + } - // Try using a dts file whose name starts with the name of the extension in case they suffixed - // with version number or something + // If the extension manifest's specified types didn't work out for some reason, try to find a + // dts file elsewhere if (!extensionDtsInfo) { - extensionDtsInfo = dtsInfos.find((dtsInfo) => dtsInfo.base.startsWith(extensionInfo.name)); - if (extensionDtsInfo) extensionDtsBaseDestination = extensionDtsInfo.base; - } + // Get a list of all the dts files in the extension's root + // Note: checking if the file exists before copying it is generally not great practice as + // it can lead to problems with race conditions. If this ever becomes a problem, we can fix + // this code. + const dtsInfos = ( + await nodeFS.readDir(extensionInfo.dirUri, (entryName) => entryName.endsWith('.d.ts')) + )[nodeFS.EntryType.File].map(createDtsInfoFromUri); + + if (dtsInfos.length <= 0) { + logger.debug( + `Extension ${extensionInfo.name} does not seem to have any .d.ts files in its root`, + ); + return; + } - // Try using a dts file whose name is `index.d.ts` - if (!extensionDtsInfo) - extensionDtsInfo = dtsInfos.find((dtsInfo) => dtsInfo.base === 'index.d.ts'); + // Try using a dts file whose name matches the name of the extension + if (!extensionDtsInfo) + extensionDtsInfo = dtsInfos.find((dtsInfo) => dtsInfo.base === extensionDtsBaseDefault); - if (!extensionDtsInfo) { - logger.debug(`Extension ${extensionInfo.name} did not have a type declaration file with a - fitting name. If you are trying to provide one, try naming it \`${extensionInfo.name}.d.ts\` or \`index.d.ts\``); - return; + // Try using a dts file whose name starts with the name of the extension in case they suffixed + // with version number or something + if (!extensionDtsInfo) + extensionDtsInfo = dtsInfos.find((dtsInfo) => + dtsInfo.base.startsWith(extensionInfo.name), + ); + + // Try using a dts file whose name is `index.d.ts` + if (!extensionDtsInfo) + extensionDtsInfo = dtsInfos.find((dtsInfo) => dtsInfo.base === 'index.d.ts'); + + if (!extensionDtsInfo) { + logger.debug( + `Could not find a type declaration file for extension ${extensionInfo.name}. If you are trying to provide one, try specifying its path relative to your extension root folder in your \`manifest.json\`'s \`types\` or naming it \`${extensionInfo.name}.d.ts\` or \`index.d.ts\``, + ); + return; + } } + // If the dts file has stuff after the extension name, we want to use it so they can suffix a + // version number or something + if (extensionDtsInfo.base.startsWith(extensionInfo.name)) + extensionDtsBaseDestination = extensionDtsInfo.base; + // Put the extension's dts in the types cache in its own folder // Without being put in its own folder, it was being lazy loaded by Intellisense, so its types // weren't being discovered for some reason. So put it in its own folder whose name is the @@ -739,7 +751,7 @@ async function reloadExtensions(shouldDeactivateExtensions: boolean): Promise