@dbx-tools/appkit-web-search 0.3.21 → 0.3.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -5
- package/src/allowlist.ts +1 -1
- package/src/config.ts +15 -10
- package/src/fetch.ts +1 -2
- package/src/runtime.ts +5 -1
- package/src/schema.ts +1 -3
- package/src/scrape.ts +3 -1
- package/src/search.ts +12 -31
- package/test/allowlist.test.ts +150 -0
- package/test/tsconfig.json +14 -0
package/package.json
CHANGED
|
@@ -17,17 +17,17 @@
|
|
|
17
17
|
"@mastra/core": "^1.47.0",
|
|
18
18
|
"got-scraping": "^4.2.1",
|
|
19
19
|
"zod": "^4.3.6",
|
|
20
|
-
"@dbx-tools/
|
|
21
|
-
"@dbx-tools/model": "0.3.
|
|
22
|
-
"@dbx-tools/
|
|
23
|
-
"@dbx-tools/shared-model": "0.3.
|
|
20
|
+
"@dbx-tools/path": "0.3.23",
|
|
21
|
+
"@dbx-tools/model": "0.3.23",
|
|
22
|
+
"@dbx-tools/shared-core": "0.3.23",
|
|
23
|
+
"@dbx-tools/shared-model": "0.3.23"
|
|
24
24
|
},
|
|
25
25
|
"main": "index.ts",
|
|
26
26
|
"license": "UNLICENSED",
|
|
27
27
|
"publishConfig": {
|
|
28
28
|
"access": "public"
|
|
29
29
|
},
|
|
30
|
-
"version": "0.3.
|
|
30
|
+
"version": "0.3.23",
|
|
31
31
|
"types": "index.ts",
|
|
32
32
|
"type": "module",
|
|
33
33
|
"exports": {
|
package/src/allowlist.ts
CHANGED
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
* @module
|
|
30
30
|
*/
|
|
31
31
|
|
|
32
|
-
import { object } from "@dbx-tools/shared-core";
|
|
33
32
|
import { match, type PathMatcher } from "@dbx-tools/path";
|
|
33
|
+
import { object } from "@dbx-tools/shared-core";
|
|
34
34
|
|
|
35
35
|
/** A compiled URL allow-list. Build one with {@link toUrlAllowList}. */
|
|
36
36
|
export interface UrlAllowList {
|
package/src/config.ts
CHANGED
|
@@ -222,7 +222,7 @@ function resolvePositiveInt(value: number | undefined, envKey: string, fallback:
|
|
|
222
222
|
|
|
223
223
|
/** Parse the `WEB_SEARCH_TOOLS` env var (JSON), else `{}`. Bad JSON is ignored. */
|
|
224
224
|
function parseToolsEnv(): Record<string, unknown> {
|
|
225
|
-
const raw = process.env
|
|
225
|
+
const raw = process.env.WEB_SEARCH_TOOLS;
|
|
226
226
|
if (!raw) return {};
|
|
227
227
|
try {
|
|
228
228
|
const parsed = JSON.parse(raw) as unknown;
|
|
@@ -241,18 +241,23 @@ function parseToolsEnv(): Record<string, unknown> {
|
|
|
241
241
|
export function resolveWebSearchConfig(
|
|
242
242
|
config: WebSearchPluginConfig = {},
|
|
243
243
|
): ResolvedWebSearchConfig {
|
|
244
|
-
const patterns = parseAllowedUrls(config.allowedUrls ?? process.env
|
|
245
|
-
const model = config.model ?? process.env
|
|
246
|
-
const fallbacks = toStringList(config.modelFallbacks ?? process.env
|
|
247
|
-
const fuzzyThresholdRaw =
|
|
244
|
+
const patterns = parseAllowedUrls(config.allowedUrls ?? process.env.WEB_SEARCH_ALLOWED_URLS);
|
|
245
|
+
const model = config.model ?? process.env.WEB_SEARCH_MODEL;
|
|
246
|
+
const fallbacks = toStringList(config.modelFallbacks ?? process.env.WEB_SEARCH_MODEL_FALLBACKS);
|
|
247
|
+
const fuzzyThresholdRaw =
|
|
248
|
+
config.modelFuzzyThreshold ?? Number(process.env.WEB_SEARCH_FUZZY_THRESHOLD);
|
|
248
249
|
return {
|
|
249
250
|
...(model ? { model } : {}),
|
|
250
251
|
modelFallbacks: fallbacks.length > 0 ? fallbacks : DEFAULT_MODEL_FALLBACKS,
|
|
251
252
|
webSearchTools: { ...parseToolsEnv(), ...(config.webSearchTools ?? {}) },
|
|
252
|
-
fuzzy:
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
maxCitations: resolvePositiveInt(
|
|
253
|
+
fuzzy: config.modelFuzzyMatch ?? object.toBoolean(process.env.WEB_SEARCH_FUZZY) ?? true,
|
|
254
|
+
fuzzyThreshold:
|
|
255
|
+
Number.isFinite(fuzzyThresholdRaw) && fuzzyThresholdRaw ? Number(fuzzyThresholdRaw) : 0.4,
|
|
256
|
+
maxCitations: resolvePositiveInt(
|
|
257
|
+
config.maxCitations,
|
|
258
|
+
"WEB_SEARCH_MAX_CITATIONS",
|
|
259
|
+
DEFAULT_MAX_CITATIONS,
|
|
260
|
+
),
|
|
256
261
|
fetchMaxLength: resolvePositiveInt(
|
|
257
262
|
config.fetchMaxLength,
|
|
258
263
|
"WEB_SEARCH_FETCH_MAX_LENGTH",
|
|
@@ -260,7 +265,7 @@ export function resolveWebSearchConfig(
|
|
|
260
265
|
),
|
|
261
266
|
timeoutMs: resolvePositiveInt(config.timeoutMs, "WEB_SEARCH_TIMEOUT_MS", DEFAULT_TIMEOUT_MS),
|
|
262
267
|
scrapeFallback:
|
|
263
|
-
config.scrapeFallback ?? object.toBoolean(process.env
|
|
268
|
+
config.scrapeFallback ?? object.toBoolean(process.env.WEB_SEARCH_SCRAPE_FALLBACK) ?? true,
|
|
264
269
|
allowList: toUrlAllowList(patterns),
|
|
265
270
|
approval: config.approval ?? false,
|
|
266
271
|
};
|
package/src/fetch.ts
CHANGED
|
@@ -94,8 +94,7 @@ export async function runWebFetch(
|
|
|
94
94
|
const body = typeof response.body === "string" ? response.body : String(response.body ?? "");
|
|
95
95
|
const contentType = response.headers["content-type"];
|
|
96
96
|
const isHtml = !contentType || /html|xml/i.test(contentType);
|
|
97
|
-
const rawContent =
|
|
98
|
-
request.format === "html" || !isHtml ? body : htmlToText(body);
|
|
97
|
+
const rawContent = request.format === "html" || !isHtml ? body : htmlToText(body);
|
|
99
98
|
const { content, truncated } = truncate(rawContent, cap);
|
|
100
99
|
const title = isHtml ? extractTitle(body) : undefined;
|
|
101
100
|
|
package/src/runtime.ts
CHANGED
|
@@ -11,7 +11,11 @@
|
|
|
11
11
|
* @module
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
resolveWebSearchConfig,
|
|
16
|
+
type ResolvedWebSearchConfig,
|
|
17
|
+
type WebSearchPluginConfig,
|
|
18
|
+
} from "./config";
|
|
15
19
|
|
|
16
20
|
/** The shared resolved config. */
|
|
17
21
|
export interface WebSearchRuntime {
|
package/src/schema.ts
CHANGED
|
@@ -58,9 +58,7 @@ export type WebSearchCitation = z.infer<typeof webSearchCitationSchema>;
|
|
|
58
58
|
/** Schema for the `web_search` tool output. */
|
|
59
59
|
export const webSearchResultSchema = z.object({
|
|
60
60
|
query: z.string().describe("Echo of the query that was searched."),
|
|
61
|
-
answer: z
|
|
62
|
-
.string()
|
|
63
|
-
.describe("The model's answer, synthesized from live web results."),
|
|
61
|
+
answer: z.string().describe("The model's answer, synthesized from live web results."),
|
|
64
62
|
citations: z
|
|
65
63
|
.array(webSearchCitationSchema)
|
|
66
64
|
.describe(
|
package/src/scrape.ts
CHANGED
|
@@ -104,7 +104,9 @@ export async function runScrapeSearch(
|
|
|
104
104
|
const answer =
|
|
105
105
|
citations.length > 0
|
|
106
106
|
? `Web results for "${request.query}" (no Databricks web-search model is deployed in this workspace, so these are unsynthesized search results - read the citations):\n\n` +
|
|
107
|
-
citations
|
|
107
|
+
citations
|
|
108
|
+
.map((c, n) => `${n + 1}. ${c.title}${c.snippet ? ` - ${c.snippet}` : ""} (${c.url})`)
|
|
109
|
+
.join("\n")
|
|
108
110
|
: `No web results found for "${request.query}".`;
|
|
109
111
|
logger.debug("scraped", {
|
|
110
112
|
query: request.query,
|
package/src/search.ts
CHANGED
|
@@ -21,8 +21,9 @@
|
|
|
21
21
|
* @module
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
-
import { error, log } from "@dbx-tools/shared-core";
|
|
25
24
|
import { resolve, serving } from "@dbx-tools/model";
|
|
25
|
+
import { error, log } from "@dbx-tools/shared-core";
|
|
26
|
+
import { openaiResponses } from "@dbx-tools/shared-model";
|
|
26
27
|
import type { ResolvedWebSearchConfig } from "./config";
|
|
27
28
|
import { detectWebSearchProvider, supportsWebSearch, webSearchToolSpec } from "./provider";
|
|
28
29
|
import type { WebSearchCitation, WebSearchRequest, WebSearchResult } from "./schema";
|
|
@@ -118,36 +119,16 @@ function str(v: unknown): string {
|
|
|
118
119
|
}
|
|
119
120
|
|
|
120
121
|
/**
|
|
121
|
-
* Extract answer text + citations from an OpenAI Responses API payload
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
* `output_text` is the convenience aggregate when present.
|
|
122
|
+
* Extract answer text + citations from an OpenAI Responses API payload, via the
|
|
123
|
+
* shared reader in `@dbx-tools/shared-model` (the same module the model-proxy
|
|
124
|
+
* uses to translate the Responses wire format in the other direction).
|
|
125
125
|
*/
|
|
126
126
|
function fromResponsesPayload(payload: Record<string, unknown>): {
|
|
127
127
|
answer: string;
|
|
128
128
|
citations: WebSearchCitation[];
|
|
129
129
|
} {
|
|
130
|
-
const citations
|
|
131
|
-
|
|
132
|
-
const output = Array.isArray(payload["output"]) ? (payload["output"] as unknown[]) : [];
|
|
133
|
-
for (const item of output) {
|
|
134
|
-
const content = (item as { content?: unknown }).content;
|
|
135
|
-
if (!Array.isArray(content)) continue;
|
|
136
|
-
for (const part of content) {
|
|
137
|
-
const p = part as { text?: unknown; annotations?: unknown };
|
|
138
|
-
const text = str(p.text);
|
|
139
|
-
if (text) texts.push(text);
|
|
140
|
-
if (Array.isArray(p.annotations)) {
|
|
141
|
-
for (const ann of p.annotations) {
|
|
142
|
-
const a = ann as { url?: unknown; title?: unknown };
|
|
143
|
-
const url = str(a.url);
|
|
144
|
-
if (url) citations.push({ url, ...(str(a.title) ? { title: str(a.title) } : {}) });
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
const answer = str(payload["output_text"]) || texts.join("\n").trim();
|
|
150
|
-
return { answer, citations };
|
|
130
|
+
const { text, citations } = openaiResponses.readResponsesOutput(payload);
|
|
131
|
+
return { answer: text, citations };
|
|
151
132
|
}
|
|
152
133
|
|
|
153
134
|
/**
|
|
@@ -160,26 +141,26 @@ function fromChatPayload(payload: Record<string, unknown>): {
|
|
|
160
141
|
answer: string;
|
|
161
142
|
citations: WebSearchCitation[];
|
|
162
143
|
} {
|
|
163
|
-
const choices = Array.isArray(payload
|
|
144
|
+
const choices = Array.isArray(payload.choices) ? (payload.choices as unknown[]) : [];
|
|
164
145
|
const message = (choices[0] as { message?: Record<string, unknown> })?.message ?? {};
|
|
165
|
-
const answer = str(message
|
|
146
|
+
const answer = str(message.content);
|
|
166
147
|
const citations: WebSearchCitation[] = [];
|
|
167
148
|
// Best-effort grounding extraction: walk any nested object for {uri|url,title}.
|
|
168
149
|
const seen = new Set<string>();
|
|
169
150
|
const visit = (v: unknown, depth: number): void => {
|
|
170
151
|
if (depth > 6 || v === null || typeof v !== "object") return;
|
|
171
152
|
const o = v as Record<string, unknown>;
|
|
172
|
-
const url = str(o
|
|
153
|
+
const url = str(o.url) || str(o.uri);
|
|
173
154
|
if (url && !seen.has(url)) {
|
|
174
155
|
seen.add(url);
|
|
175
|
-
citations.push({ url, ...(str(o
|
|
156
|
+
citations.push({ url, ...(str(o.title) ? { title: str(o.title) } : {}) });
|
|
176
157
|
}
|
|
177
158
|
for (const val of Object.values(o)) {
|
|
178
159
|
if (Array.isArray(val)) val.forEach((x) => visit(x, depth + 1));
|
|
179
160
|
else if (val && typeof val === "object") visit(val, depth + 1);
|
|
180
161
|
}
|
|
181
162
|
};
|
|
182
|
-
visit(message
|
|
163
|
+
visit(message.grounding_metadata ?? message.groundingMetadata, 0);
|
|
183
164
|
return { answer, citations };
|
|
184
165
|
}
|
|
185
166
|
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
assertUrlAllowed,
|
|
5
|
+
normalizeUrlPattern,
|
|
6
|
+
parseAllowedUrls,
|
|
7
|
+
toUrlAllowList,
|
|
8
|
+
} from "../src/allowlist";
|
|
9
|
+
import { approvalMatches, resolveWebSearchConfig } from "../src/config";
|
|
10
|
+
import { htmlToText } from "../src/fetch";
|
|
11
|
+
import {
|
|
12
|
+
detectWebSearchProvider,
|
|
13
|
+
supportsWebSearch,
|
|
14
|
+
webSearchToolSpec,
|
|
15
|
+
WEB_SEARCH_PROVIDERS,
|
|
16
|
+
} from "../src/provider";
|
|
17
|
+
|
|
18
|
+
describe("web-search allow-list", () => {
|
|
19
|
+
it("strips the scheme from entries, leaving host / path verbatim", () => {
|
|
20
|
+
assert.equal(normalizeUrlPattern("https://*.databricks.com"), "*.databricks.com");
|
|
21
|
+
assert.equal(normalizeUrlPattern("docs.example.com/api/**"), "docs.example.com/api/**");
|
|
22
|
+
assert.equal(normalizeUrlPattern("databricks.com"), "databricks.com");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("parses CSV and array forms, trimming + de-duping", () => {
|
|
26
|
+
assert.deepEqual(parseAllowedUrls("https://databricks.com, docs.example.com"), [
|
|
27
|
+
"databricks.com",
|
|
28
|
+
"docs.example.com",
|
|
29
|
+
]);
|
|
30
|
+
assert.deepEqual(parseAllowedUrls(["databricks.com", "databricks.com"]), ["databricks.com"]);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("permits everything when unconfigured", () => {
|
|
34
|
+
const list = toUrlAllowList(parseAllowedUrls(undefined));
|
|
35
|
+
assert.equal(list.restricted, false);
|
|
36
|
+
assert.equal(list.allows("https://anything.example.com/x"), true);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("a bare host permits the host and its subdomains, blocks others", () => {
|
|
40
|
+
const list = toUrlAllowList(parseAllowedUrls(["databricks.com", "*.trusted.io"]));
|
|
41
|
+
assert.equal(list.restricted, true);
|
|
42
|
+
assert.equal(list.allows("https://databricks.com/"), true);
|
|
43
|
+
assert.equal(list.allows("https://docs.databricks.com/aws/en/index.html"), true);
|
|
44
|
+
assert.equal(list.allows("https://api.trusted.io/v1"), true);
|
|
45
|
+
assert.equal(list.allows("https://evil.example.com/"), false);
|
|
46
|
+
assert.equal(list.allows("https://notdatabricks.com/"), false);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("a path entry constrains the pathname", () => {
|
|
50
|
+
const list = toUrlAllowList(parseAllowedUrls(["docs.example.com/api/**"]));
|
|
51
|
+
assert.equal(list.allows("https://docs.example.com/api/v1/x"), true);
|
|
52
|
+
assert.equal(list.allows("https://docs.example.com/blog/x"), false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("assertUrlAllowed throws for a disallowed URL, passes an allowed one", () => {
|
|
56
|
+
const list = toUrlAllowList(parseAllowedUrls(["databricks.com"]));
|
|
57
|
+
assert.throws(() => assertUrlAllowed("https://evil.example.com/", list), /not permitted/);
|
|
58
|
+
assert.doesNotThrow(() => assertUrlAllowed("https://databricks.com/", list));
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe("web-search provider detection", () => {
|
|
63
|
+
it("maps GPT ids to openai and Gemini ids to gemini", () => {
|
|
64
|
+
assert.equal(detectWebSearchProvider("databricks-gpt-5"), "openai");
|
|
65
|
+
assert.equal(detectWebSearchProvider("databricks-gpt-5-mini"), "openai");
|
|
66
|
+
assert.equal(detectWebSearchProvider("databricks-gemini-3-pro"), "gemini");
|
|
67
|
+
assert.equal(detectWebSearchProvider("databricks-gemini-2-5-flash"), "gemini");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("treats gpt-oss and non-web families as unsupported", () => {
|
|
71
|
+
assert.equal(detectWebSearchProvider("databricks-gpt-oss-120b"), null);
|
|
72
|
+
assert.equal(detectWebSearchProvider("databricks-claude-sonnet-4-6"), null);
|
|
73
|
+
assert.equal(detectWebSearchProvider("databricks-llama-3-70b"), null);
|
|
74
|
+
assert.equal(supportsWebSearch("databricks-claude-sonnet-4-6"), false);
|
|
75
|
+
assert.equal(supportsWebSearch("databricks-gpt-5"), true);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("uses the built-in tool spec per provider", () => {
|
|
79
|
+
assert.deepEqual(webSearchToolSpec("openai").tool, { type: "web_search" });
|
|
80
|
+
assert.equal(webSearchToolSpec("openai").api, "responses");
|
|
81
|
+
assert.deepEqual(webSearchToolSpec("gemini").tool, { google_search: {} });
|
|
82
|
+
assert.equal(webSearchToolSpec("gemini").api, "chat");
|
|
83
|
+
assert.deepEqual(WEB_SEARCH_PROVIDERS.openai.tool, { type: "web_search" });
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("merges an operator override over the built-in map", () => {
|
|
87
|
+
const overrides = { gemini: { tool: { google_search_retrieval: {} } } };
|
|
88
|
+
const spec = webSearchToolSpec("gemini", overrides);
|
|
89
|
+
assert.deepEqual(spec.tool, { google_search_retrieval: {} });
|
|
90
|
+
// api falls through to the built-in when not overridden
|
|
91
|
+
assert.equal(spec.api, "chat");
|
|
92
|
+
// other providers keep their defaults
|
|
93
|
+
assert.deepEqual(webSearchToolSpec("openai", overrides).tool, { type: "web_search" });
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("web-search config", () => {
|
|
98
|
+
it("defaults model fallbacks to Gemini-then-GPT and approval to none", () => {
|
|
99
|
+
const c = resolveWebSearchConfig();
|
|
100
|
+
assert.equal(c.approval, false);
|
|
101
|
+
assert.equal(c.model, undefined);
|
|
102
|
+
assert.match(c.modelFallbacks[0]!, /gemini/);
|
|
103
|
+
assert.ok(c.modelFallbacks.some((m) => m.includes("gpt")));
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("merges webSearchTools override into resolved config", () => {
|
|
107
|
+
const c = resolveWebSearchConfig({ webSearchTools: { gemini: { tool: { x: {} } } } });
|
|
108
|
+
assert.deepEqual((c.webSearchTools as { gemini: unknown }).gemini, { tool: { x: {} } });
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("enables the scrape fallback by default, honors an explicit off", () => {
|
|
112
|
+
assert.equal(resolveWebSearchConfig().scrapeFallback, true);
|
|
113
|
+
assert.equal(resolveWebSearchConfig({ scrapeFallback: false }).scrapeFallback, false);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe("web-search approval gate", () => {
|
|
118
|
+
it("boolean gates pass through", () => {
|
|
119
|
+
assert.equal(approvalMatches(true, ["https://x.com"]), true);
|
|
120
|
+
assert.equal(approvalMatches(false, ["https://x.com"]), false);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("pattern gate matches only candidate URLs it covers", () => {
|
|
124
|
+
assert.equal(
|
|
125
|
+
approvalMatches("*.internal.example.com", ["https://api.internal.example.com/x"]),
|
|
126
|
+
true,
|
|
127
|
+
);
|
|
128
|
+
assert.equal(
|
|
129
|
+
approvalMatches("*.internal.example.com", ["https://public.example.com/x"]),
|
|
130
|
+
false,
|
|
131
|
+
);
|
|
132
|
+
assert.equal(approvalMatches(["a.com", "b.com"], ["https://b.com/y"]), true);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe("web-search html-to-text", () => {
|
|
137
|
+
it("strips script/style and unwraps tags", () => {
|
|
138
|
+
const html =
|
|
139
|
+
"<html><head><title>Hi</title><style>.x{}</style></head><body><p>One</p><script>bad()</script><p>Two</p></body></html>";
|
|
140
|
+
const text = htmlToText(html);
|
|
141
|
+
assert.match(text, /One/);
|
|
142
|
+
assert.match(text, /Two/);
|
|
143
|
+
assert.doesNotMatch(text, /bad\(\)/);
|
|
144
|
+
assert.doesNotMatch(text, /\.x\{\}/);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("decodes common entities", () => {
|
|
148
|
+
assert.equal(htmlToText("<p>a & b <c></p>"), "a & b <c>");
|
|
149
|
+
});
|
|
150
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// ~~ Generated by projen. To modify, edit .projenrc.js and run "pnpm exec projen".
|
|
2
|
+
{
|
|
3
|
+
"extends": "../tsconfig.json",
|
|
4
|
+
"compilerOptions": {
|
|
5
|
+
"noEmit": true,
|
|
6
|
+
"rootDir": ".."
|
|
7
|
+
},
|
|
8
|
+
"include": [
|
|
9
|
+
"**/*.ts"
|
|
10
|
+
],
|
|
11
|
+
"exclude": [
|
|
12
|
+
"node_modules"
|
|
13
|
+
]
|
|
14
|
+
}
|