diff --git a/docs/1.docs/50.configuration.md b/docs/1.docs/50.configuration.md index 2e312a0558..d8a561df41 100644 --- a/docs/1.docs/50.configuration.md +++ b/docs/1.docs/50.configuration.md @@ -105,6 +105,26 @@ export default defineConfig({ > [!NOTE] > The `srcDir` option is deprecated. Use `serverDir` instead. +## Source extensions + +Nitro scans built-in JS and TS server files by default. Use `sourceExtensions` to add other script-like source extensions that your bundler can transform, such as `.civet` or `.res`. + +```ts [nitro.config.ts] +import { defineConfig } from "nitro"; + +export default defineConfig({ + sourceExtensions: [".civet", ".res"], +}) +``` + +This affects server file scanning, entry resolution, generated route types, and development reload detection. Nitro still expects your builder or framework to provide the actual transform for the custom extension. + +If you also use custom source extensions for Nitro `modules`, those files must be supported by the current JS runtime. For example, with Civet you can register the loader before starting Nitro: + +```bash +NODE_OPTIONS="--import @danielx/civet/register" pnpm dev +``` + ## Environment variables Certain Nitro behaviors can be configured using environment variables: diff --git a/src/build/config.ts b/src/build/config.ts index 495b580260..cf1a8b3265 100644 --- a/src/build/config.ts +++ b/src/build/config.ts @@ -2,12 +2,16 @@ import type { Nitro, NitroImportMeta } from "nitro/types"; import { defineEnv } from "unenv"; import { pkgDir } from "nitro/meta"; import { pathRegExp, toPathRegExp } from "../utils/regex.ts"; +import { + getSourceExtensionPattern, + getSourceExtensions, + TS_SOURCE_EXTENSIONS, +} from "../utils/source-extensions.ts"; export type BaseBuildConfig = ReturnType; export function baseBuildConfig(nitro: Nitro) { - // prettier-ignore - const extensions: string[] = [".ts", ".mjs", ".js", ".json", ".node", ".tsx", ".jsx" ]; + const extensions: string[] = [...getSourceExtensions(nitro.options), ".json", ".node"]; const isNodeless = nitro.options.node === false; @@ -66,8 +70,9 @@ export function baseBuildConfig(nitro: Nitro) { } function getNoExternals(nitro: Nitro): RegExp[] { + const extensionPattern = getSourceExtensionPattern(nitro.options, TS_SOURCE_EXTENSIONS); const noExternal: RegExp[] = [ - /\.[mc]?tsx?$/, + new RegExp(String.raw`\.(?:${extensionPattern})$`), /^(?:[\0#~.]|virtual:)/, new RegExp("^" + pathRegExp(pkgDir) + "(?!.*node_modules)"), ...[ diff --git a/src/build/rolldown/dev.ts b/src/build/rolldown/dev.ts index 4b41eac4d7..4a9179266e 100644 --- a/src/build/rolldown/dev.ts +++ b/src/build/rolldown/dev.ts @@ -4,6 +4,7 @@ import { watch as chokidarWatch } from "chokidar"; import { basename, join } from "pathe"; import { debounce } from "perfect-debounce"; import { scanHandlers } from "../../scan.ts"; +import { getSourceExtensionPattern } from "../../utils/source-extensions.ts"; import { writeTypes } from "../types.ts"; import { formatCompatibilityDate } from "compatx"; @@ -40,7 +41,8 @@ export async function watchDev(nitro: Nitro, config: RolldownOptions) { } }); - const serverEntryRe = /^server\.[mc]?[jt]sx?$/; + const sourceExtensionPattern = getSourceExtensionPattern(nitro.options); + const serverEntryRe = new RegExp(String.raw`^server(?:\.node)?\.(?:${sourceExtensionPattern})$`); const rootDirWatcher = chokidarWatch(nitro.options.rootDir, { ignoreInitial: true, depth: 0, diff --git a/src/build/rollup/dev.ts b/src/build/rollup/dev.ts index 20f1fb5eb3..bdc6765911 100644 --- a/src/build/rollup/dev.ts +++ b/src/build/rollup/dev.ts @@ -5,6 +5,7 @@ import { defu } from "defu"; import { basename, join } from "pathe"; import { debounce } from "perfect-debounce"; import { scanHandlers } from "../../scan.ts"; +import { getSourceExtensionPattern } from "../../utils/source-extensions.ts"; import { formatRollupError } from "./error.ts"; import { writeTypes } from "../types.ts"; import { formatCompatibilityDate } from "compatx"; @@ -42,7 +43,8 @@ export async function watchDev(nitro: Nitro, rollupConfig: RollupConfig) { } }); - const serverEntryRe = /^server\.[mc]?[jt]sx?$/; + const sourceExtensionPattern = getSourceExtensionPattern(nitro.options); + const serverEntryRe = new RegExp(String.raw`^server(?:\.node)?\.(?:${sourceExtensionPattern})$`); const rootDirWatcher = chokidarWatch(nitro.options.rootDir, { ignoreInitial: true, depth: 0, diff --git a/src/build/types.ts b/src/build/types.ts index 14d914879f..a4a4e0f506 100644 --- a/src/build/types.ts +++ b/src/build/types.ts @@ -12,6 +12,7 @@ import type { TSConfig } from "pkg-types"; import type { JSValue } from "untyped"; import { generateTypes, resolveSchema } from "untyped"; import { toExports } from "unimport"; +import { getSourceExtensions, stripSourceExtension } from "../utils/source-extensions.ts"; export async function writeTypes(nitro: Nitro) { const types: NitroTypes = { @@ -29,10 +30,10 @@ export async function writeTypes(nitro: Nitro) { if (typeof mw.handler !== "string" || !mw.route) { continue; } - const relativePath = relative( - generatedTypesDir, - resolveNitroPath(mw.handler, nitro.options) - ).replace(/\.(js|mjs|cjs|ts|mts|cts|tsx|jsx)$/, ""); + const relativePath = stripSourceExtension( + relative(generatedTypesDir, resolveNitroPath(mw.handler, nitro.options)), + nitro.options + ); const method = mw.method || "default"; @@ -72,7 +73,7 @@ export async function writeTypes(nitro: Nitro) { from: nitro.options.rootDir, conditions: ["type", "node", "import"], suffixes: ["", "/index"], - extensions: [".mjs", ".cjs", ".js", ".mts", ".cts", ".ts"], + extensions: getSourceExtensions(nitro.options), }); if (resolvedPath) { const { dir, name } = parseNodeModulePath(resolvedPath); diff --git a/src/build/vite/dev.ts b/src/build/vite/dev.ts index ab4f24671b..4823a22d7d 100644 --- a/src/build/vite/dev.ts +++ b/src/build/vite/dev.ts @@ -13,6 +13,7 @@ import { join } from "pathe"; import { debounce } from "perfect-debounce"; import { withBase } from "ufo"; import { scanHandlers } from "../../scan.ts"; +import { getSourceExtensionPattern } from "../../utils/source-extensions.ts"; import { getEnvRunner } from "./env.ts"; // https://vite.dev/guide/api-environment-runtimes.html#modulerunner @@ -112,6 +113,8 @@ export class FetchableDevEnvironment extends DevEnvironment { export async function configureViteDevServer(ctx: NitroPluginContext, server: ViteDevServer) { const nitro = ctx.nitro!; const nitroEnv = server.environments.nitro as FetchableDevEnvironment; + const sourceExtensionPattern = getSourceExtensionPattern(nitro.options); + const serverEntryRe = new RegExp(String.raw`^server(?:\.node)?\.(?:${sourceExtensionPattern})$`); // Restart with nitro.config changes const nitroConfigFile = nitro.options._c12.configFile; @@ -160,7 +163,7 @@ export async function configureViteDevServer(ctx: NitroPluginContext, server: Vi nitro.options.rootDir, { persistent: false }, (_event, filename) => { - if (filename && /^server\.[mc]?[jt]sx?$/.test(filename)) { + if (filename && serverEntryRe.test(filename)) { reload(); } } diff --git a/src/build/vite/env.ts b/src/build/vite/env.ts index 93f2d37a83..e384b4c3b8 100644 --- a/src/build/vite/env.ts +++ b/src/build/vite/env.ts @@ -8,6 +8,7 @@ import { runtimeDependencies, runtimeDir } from "nitro/meta"; import { resolveModulePath } from "exsolve"; import { createFetchableDevEnvironment } from "./dev.ts"; import { isAbsolute } from "pathe"; +import { getSourceExtensions } from "../../utils/source-extensions.ts"; export function createNitroEnvironment(ctx: NitroPluginContext): EnvironmentOptions { const isWorkerdRunner = _isWorkerdRunner(ctx); @@ -89,7 +90,7 @@ export function createServiceEnvironment( }, dev: { createEnvironment: (envName, envConfig) => { - const entry = tryResolve(serviceConfig.entry); + const entry = tryResolve(serviceConfig.entry, ctx.nitro!.options.sourceExtensions); (ctx._viteEnvs ??= new Map()).set(envName, entry); return createFetchableDevEnvironment(envName, envConfig, getEnvRunner(ctx), entry, { preventExternalize: isWorkerdRunner, @@ -237,13 +238,14 @@ function _isWorkerdRunner(ctx: NitroPluginContext): boolean { return runnerName === "miniflare"; } -function tryResolve(id: string) { +function tryResolve(id: string, sourceExtensions: string[] = []) { if (/^[~#/\0]/.test(id) || isAbsolute(id)) { return id; } + const resolvableSourceExtensions = getSourceExtensions({ sourceExtensions }); const resolved = resolveModulePath(id, { suffixes: ["", "/index"], - extensions: ["", ".ts", ".mjs", ".cjs", ".js", ".mts", ".cts"], + extensions: ["", ...resolvableSourceExtensions], try: true, }); return resolved || id; diff --git a/src/build/vite/plugin.ts b/src/build/vite/plugin.ts index cc22511dc0..33a33b175b 100644 --- a/src/build/vite/plugin.ts +++ b/src/build/vite/plugin.ts @@ -29,12 +29,11 @@ import { NitroDevApp } from "../../dev/app.ts"; import { nitroPreviewPlugin } from "./preview.ts"; import assetsPlugin from "@hiogawa/vite-plugin-fullstack/assets"; import type { NitroConfig } from "nitro/types"; +import { getSourceExtensions } from "../../utils/source-extensions.ts"; // https://vite.dev/guide/api-environment-plugins // https://vite.dev/guide/api-environment-frameworks.html -const DEFAULT_EXTENSIONS = [".ts", ".js", ".mts", ".mjs", ".tsx", ".jsx"]; - const debug = process.env.NITRO_DEBUG ? (...args: any[]) => console.log("[nitro]", ...args) : () => {}; @@ -145,7 +144,7 @@ function nitroEnv(ctx: NitroPluginContext): VitePlugin { const resolvedEntry = resolveModulePath(entry, { from: [ctx.nitro!.options.rootDir, ...ctx.nitro!.options.scanDirs], - extensions: DEFAULT_EXTENSIONS, + extensions: getSourceExtensions(ctx.nitro!.options), suffixes: ["", "/index"], try: true, }) || entry; @@ -398,7 +397,7 @@ async function setupNitroContext( from: ["app", "src", ""].flatMap((d) => [ctx.nitro!.options.rootDir, ...ctx.nitro!.options.scanDirs].map((s) => join(s, d) + "/") ), - extensions: DEFAULT_EXTENSIONS, + extensions: getSourceExtensions(ctx.nitro!.options), try: true, }); if (ssrEntry) { @@ -411,7 +410,7 @@ async function setupNitroContext( ssrEntry = resolveModulePath(ssrEntry, { from: [ctx.nitro.options.rootDir, ...ctx.nitro.options.scanDirs], - extensions: DEFAULT_EXTENSIONS, + extensions: getSourceExtensions(ctx.nitro.options), suffixes: ["", "/index"], try: true, }) || ssrEntry; diff --git a/src/config/defaults.ts b/src/config/defaults.ts index 990c9058ef..16d288ee45 100644 --- a/src/config/defaults.ts +++ b/src/config/defaults.ts @@ -12,6 +12,7 @@ export const NitroDefaults: NitroConfig = { // Dirs serverDir: false, scanDirs: [], + sourceExtensions: [], buildDir: `node_modules/.nitro`, output: { dir: "{{ rootDir }}/.output", diff --git a/src/config/resolvers/paths.ts b/src/config/resolvers/paths.ts index c5bbb99db7..370bcf62ec 100644 --- a/src/config/resolvers/paths.ts +++ b/src/config/resolvers/paths.ts @@ -6,8 +6,7 @@ import { findWorkspaceDir } from "pkg-types"; import { NitroDefaults } from "../defaults.ts"; import { resolveModulePath } from "exsolve"; import consola from "consola"; - -const RESOLVE_EXTENSIONS = [".ts", ".js", ".mts", ".mjs", ".tsx", ".jsx"]; +import { getSourceExtensions, normalizeSourceExtensions } from "../../utils/source-extensions.ts"; export async function resolvePathOptions(options: NitroOptions) { options.rootDir = resolve(options.rootDir || ".") + "/"; @@ -30,6 +29,7 @@ export async function resolvePathOptions(options: NitroOptions) { } options.alias ??= {}; + options.sourceExtensions = normalizeSourceExtensions(options.sourceExtensions); // Resolve possibly template paths if (!options.static && !options.entry) { @@ -93,7 +93,7 @@ export async function resolvePathOptions(options: NitroOptions) { const detected = resolveModulePath("./server", { try: true, from: options.rootDir, - extensions: RESOLVE_EXTENSIONS.flatMap((ext) => [ext, `.node${ext}`]), + extensions: getSourceExtensions(options).flatMap((ext) => [ext, `.node${ext}`]), }); if (detected) { options.serverEntry ??= { handler: "" }; @@ -118,7 +118,7 @@ export async function resolvePathOptions(options: NitroOptions) { resolveNitroPath(options.renderer?.handler, options), { from: [options.rootDir, ...options.scanDirs], - extensions: RESOLVE_EXTENSIONS, + extensions: getSourceExtensions(options), } ); } diff --git a/src/module.ts b/src/module.ts index 1af08d41ec..3513892c1c 100644 --- a/src/module.ts +++ b/src/module.ts @@ -1,5 +1,6 @@ import type { Nitro, NitroModule, NitroModuleInput } from "nitro/types"; import { resolveModuleURL } from "exsolve"; +import { getSourceExtensions } from "./utils/source-extensions.ts"; export async function installModules(nitro: Nitro) { const _modules = [...(nitro.options.modules || [])]; @@ -25,7 +26,7 @@ async function _resolveNitroModule( if (typeof mod === "string") { _url = resolveModuleURL(mod, { from: [nitroOptions.rootDir], - extensions: [".mjs", ".cjs", ".js", ".mts", ".cts", ".ts"], + extensions: getSourceExtensions(nitroOptions), }); mod = (await import(_url).then((m: any) => m.default || m)) as NitroModule; } diff --git a/src/presets/cloudflare/entry-exports.ts b/src/presets/cloudflare/entry-exports.ts index e67e4b0d34..6b5ba6d4bc 100644 --- a/src/presets/cloudflare/entry-exports.ts +++ b/src/presets/cloudflare/entry-exports.ts @@ -1,8 +1,7 @@ import type { Nitro } from "nitro/types"; import { resolveModulePath } from "exsolve"; import { prettyPath } from "../../utils/fs.ts"; - -const RESOLVE_EXTENSIONS = [".ts", ".js", ".mts", ".mjs"]; +import { getSourceExtensions } from "../../utils/source-extensions.ts"; export async function setupEntryExports(nitro: Nitro) { const exportsEntry = resolveExportsEntry(nitro); @@ -21,7 +20,7 @@ export async function setupEntryExports(nitro: Nitro) { function resolveExportsEntry(nitro: Nitro) { const entry = resolveModulePath(nitro.options.cloudflare?.exports || "./exports.cloudflare.ts", { from: nitro.options.rootDir, - extensions: RESOLVE_EXTENSIONS, + extensions: getSourceExtensions(nitro.options), try: true, }); diff --git a/src/scan.ts b/src/scan.ts index ca076ed25a..d5a00f396c 100644 --- a/src/scan.ts +++ b/src/scan.ts @@ -2,8 +2,7 @@ import { glob } from "tinyglobby"; import type { Nitro } from "nitro/types"; import { join, relative } from "pathe"; import { withBase, withLeadingSlash, withoutTrailingSlash } from "ufo"; - -export const GLOB_SCAN_PATTERN = "**/*.{js,mjs,cjs,ts,mts,cts,tsx,jsx}"; +import { getScanPattern, stripSourceExtension } from "./utils/source-extensions.ts"; type FileInfo = { path: string; fullPath: string }; const suffixRegex = @@ -85,8 +84,7 @@ export async function scanMiddleware(nitro: Nitro) { export async function scanServerRoutes(nitro: Nitro, dir: string, prefix = "/") { const files = await scanFiles(nitro, dir); return files.map((file) => { - let route = file.path - .replace(/\.[A-Za-z]+$/, "") + let route = stripSourceExtension(file.path, nitro.options) .replace(/\(([^(/\\]+)\)[/\\]/g, "") .replace(/\[\.{3}]/g, "**") .replace(/\[\.{3}([^\]]+)]/g, (_, p) => "**:" + p.replace(/[^\w-]/g, "_")) @@ -123,9 +121,8 @@ export async function scanPlugins(nitro: Nitro) { export async function scanTasks(nitro: Nitro) { const files = await scanFiles(nitro, "tasks"); return files.map((f) => { - const name = f.path + const name = stripSourceExtension(f.path, nitro.options) .replace(/\/index$/, "") - .replace(/\.[A-Za-z]+$/, "") .replace(/\//g, ":"); return { name, handler: f.fullPath }; }); @@ -144,7 +141,7 @@ async function scanFiles(nitro: Nitro, name: string): Promise { } async function scanDir(nitro: Nitro, dir: string, name: string): Promise { - const fileNames = await glob(join(name, GLOB_SCAN_PATTERN), { + const fileNames = await glob(join(name, getScanPattern(nitro.options)), { cwd: dir, dot: true, ignore: nitro.options.ignore, diff --git a/src/types/config.ts b/src/types/config.ts index 14d5ae6863..a18a7e06eb 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -162,6 +162,27 @@ export interface NitroOptions extends PresetOptions { */ scanDirs: string[]; + /** + * Additional script-like source file extensions Nitro should treat like + * built-in JS/TS server files. + * + * These extensions are appended to Nitro's built-in source extensions and + * are used for file scanning, entry resolution, generated route types, and + * development reload detection. + * + * When used for Nitro `modules`, custom extensions must also be supported + * by the current JS runtime. For example: + * `NODE_OPTIONS="--import @danielx/civet/register" pnpm dev` + * + * @example + * ```ts + * sourceExtensions: [".civet", ".res"] + * ``` + * + * @see https://nitro.build/config#sourceextensions + */ + sourceExtensions: string[]; + /** * Directory name to scan for API route handlers. * diff --git a/src/utils/source-extensions.ts b/src/utils/source-extensions.ts new file mode 100644 index 0000000000..5b123b278f --- /dev/null +++ b/src/utils/source-extensions.ts @@ -0,0 +1,59 @@ +import type { NitroOptions } from "nitro/types"; +import { escapeRegExp } from "./regex.ts"; + +type SourceExtensionOptions = Pick; + +const moduleExtensions = { + js: [".js", ".mjs", ".cjs", ".jsx"], + ts: [".ts", ".mts", ".cts", ".tsx"], +}; + +export const TS_SOURCE_EXTENSIONS = [...moduleExtensions.ts]; + +export const BASE_SOURCE_EXTENSIONS = [...moduleExtensions.js, ...moduleExtensions.ts]; + +export function normalizeSourceExtensions(extensions: string[] = []) { + return extensions + .map((ext) => ext.trim()) + .filter((ext) => { + const trimmedExt = ext.trim(); + const isEmpty = trimmedExt.length === 0; + const isInvalid = isEmpty || trimmedExt === "."; + return !isInvalid; + }) + .map((ext) => (ext.startsWith(".") ? ext : `.${ext}`)); +} + +export function getSourceExtensions( + { sourceExtensions }: SourceExtensionOptions, + baseExtensions = BASE_SOURCE_EXTENSIONS +) { + return [...new Set([...baseExtensions, ...normalizeSourceExtensions(sourceExtensions)])]; +} + +export function getSourceExtensionPattern( + { sourceExtensions }: SourceExtensionOptions, + baseExtensions = BASE_SOURCE_EXTENSIONS +) { + return getSourceExtensions({ sourceExtensions }, baseExtensions) + .map((ext) => escapeRegExp(ext.slice(1))) + .join("|"); +} + +export function getScanPattern(options: SourceExtensionOptions) { + const extensionPattern = getSourceExtensions(options) + .map((ext) => ext.slice(1)) + .join(","); + return `**/*.{${extensionPattern}}`; +} + +export function stripSourceExtension( + id: string, + options: SourceExtensionOptions, + baseExtensions = BASE_SOURCE_EXTENSIONS +) { + const ext = getSourceExtensions(options, baseExtensions) + .sort((a, b) => b.length - a.length) + .find((ext) => id.endsWith(ext)); + return ext ? id.slice(0, -ext.length) : id; +} diff --git a/test/fixture/nitro.config.ts b/test/fixture/nitro.config.ts index 6680c45f3c..c0e039cf4e 100644 --- a/test/fixture/nitro.config.ts +++ b/test/fixture/nitro.config.ts @@ -20,6 +20,7 @@ export default defineConfig({ compressPublicAssets: true, compatibilityDate: "latest", serverDir: "server", + sourceExtensions: [".civet"], builder: (process.env.NITRO_BUILDER as any) || "rolldown", // @ts-expect-error __vitePkg__: process.env.NITRO_VITE_PKG, diff --git a/test/fixture/server/routes/api/civet.civet b/test/fixture/server/routes/api/civet.civet new file mode 100644 index 0000000000..6293b7e56b --- /dev/null +++ b/test/fixture/server/routes/api/civet.civet @@ -0,0 +1 @@ +export default () => "civet"; diff --git a/test/tests.ts b/test/tests.ts index 1536367130..ad7786aea2 100644 --- a/test/tests.ts +++ b/test/tests.ts @@ -277,6 +277,11 @@ export function testNitro( expect(paramsData2).toBe("foo/bar/baz"); }); + it("configured source extensions work", async () => { + const { data } = await callHandler({ url: "/api/civet" }); + expect(data).toBe("civet"); + }); + it("group routes", async () => { const { status } = await callHandler({ url: "/route-group" }); expect(status).toBe(200); diff --git a/test/vite/hmr-fixture/api/state-source-extension.civet b/test/vite/hmr-fixture/api/state-source-extension.civet new file mode 100644 index 0000000000..179dafca0a --- /dev/null +++ b/test/vite/hmr-fixture/api/state-source-extension.civet @@ -0,0 +1,3 @@ +import { state } from "../shared.ts"; + +export default () => ({ state }); diff --git a/test/vite/hmr.test.ts b/test/vite/hmr.test.ts index ccccd09d59..627ca1f076 100644 --- a/test/vite/hmr.test.ts +++ b/test/vite/hmr.test.ts @@ -2,6 +2,7 @@ import { join } from "pathe"; import { readFileSync, writeFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; import type { ViteDevServer } from "vite"; +import { nitro } from "nitro/vite"; import { describe, test, expect, beforeAll, afterEach, afterAll } from "vitest"; const { createServer } = (await import( @@ -92,6 +93,55 @@ describe("vite:hmr", { sequential: true }, () => { ); expect(wsMessages).toMatchObject([{ type: "full-reload" }]); }); + + test("editing custom source extension API entry", async () => { + const customAPI = openFileForEditing(join(rootDir, "api/state-source-extension.civet")); + const customMessages: any[] = []; + const customServer = await createServer({ + root: rootDir, + configFile: false, + plugins: [ + { + name: "test:source-extensions", + enforce: "pre", + transform(code: any, id: any) { + if (id.endsWith(".civet")) { + return { code, map: null }; + } + }, + }, + nitro({ serverDir: "./", sourceExtensions: [".civet"] }), + ], + }); + const originalSend = customServer.ws.send.bind(customServer.ws); + customServer.ws.send = function (payload: any) { + customMessages.push(payload); + return originalSend(payload); + }; + + try { + await customServer.listen("0" as unknown as number); + const addr = customServer.httpServer?.address() as { + port: number; + address: string; + family: string; + }; + const customServerURL = `http://${addr.family === "IPv6" ? `[${addr.address}]` : addr.address}:${addr.port}`; + const initialResponse = await fetch(`${customServerURL}/api/state-source-extension`).then( + (r) => r.text() + ); + expect(initialResponse).toContain('"state":1'); + + customAPI.update((content) => + content.replace("({ state })", '({ state: state + " (modified)" })') + ); + await pollResponse(`${customServerURL}/api/state-source-extension`, /modified/); + expect(customMessages).toMatchObject([{ type: "full-reload" }]); + } finally { + customAPI.restore(); + await customServer.close(); + } + }); }); function openFileForEditing(path: string) {