Merge branch 'develop'

This commit is contained in:
Simon Cambier
2023-01-14 08:58:46 +01:00
16 changed files with 230 additions and 89 deletions

View File

@@ -1,17 +1,12 @@
---
name: Bug report
about: Create a report to help us improve
title: "[BUG]"
title: "[BUG] "
labels: ''
assignees: ''
---
<!--
Please, if possible and before filing an issue,
make sure that you have the latest available version of Omnisearch.
-->
**Problem description:**
<!-- Describe your problem in details. -->
@@ -20,8 +15,16 @@ make sure that you have the latest available version of Omnisearch.
**Your environment:**
<!-- Please, if possible and before filing an issue, -->
<!-- make sure that you have the latest available version of Omnisearch. -->
- Omnisearch version:
- Obsidian version:
- Operating system:
- Number of notes in your vault (approx.):
- Other plugins that may be related to the issue:
- Number of indexed documents in your vault (approx.):
**Things to try:**
- Does the problem occur when Obsidian is the only active community plugin:
- Does the problem occur when you _don't_ index PDFs, images, or other non-notes files:
- Does the problem occur after a cache reset:

View File

@@ -18,13 +18,13 @@ jobs:
- uses: pnpm/action-setup@v2.1.0
with:
version: 6.32.4
version: 7.17.0
run_install: true
- name: Use Node.js
uses: actions/setup-node@v1
with:
node-version: "14.x"
node-version: "18.x"
- name: Build
id: build

View File

@@ -119,16 +119,6 @@ There are several CSS classes you can use to customize the appearance of Omnisea
.omnisearch-input-field
```
For example, if you'd like the usual yellow highlight on search matches, you can add this code inside a CSS snippet
file:
```css
.omnisearch-highlight {
color: var(--text-normal);
background-color: var(--text-highlight-bg);
}
```
See [styles.css](./assets/styles.css) for more information.
## Issues & Solutions

View File

@@ -6,5 +6,9 @@
"description": "A search engine that just works",
"author": "Simon Cambier",
"authorUrl": "https://github.com/scambier/obsidian-omnisearch",
"isDesktopOnly": false
"isDesktopOnly": false,
"fundingUrl": {
"Github": "https://github.com/sponsors/scambier",
"Ko-fi": "https://ko-fi.com/scambier"
}
}

View File

@@ -1,13 +1,13 @@
{
"name": "scambier.obsidian-search",
"version": "1.9.1",
"version": "1.10.0-beta.4",
"description": "A search engine for Obsidian",
"main": "dist/main.js",
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
"check": "svelte-check --tsconfig ./tsconfig.json",
"version": "node version-bump.mjs && git add manifest.json versions.json package.json",
"version": "node version-bump.mjs",
"test": "jest"
},
"keywords": [],
@@ -43,7 +43,7 @@
"dexie": "^3.2.2",
"lodash-es": "4.17.21",
"minisearch": "6.0.0-beta.1",
"obsidian-text-extract": "1.0.4",
"obsidian-text-extract": "^1.0.4",
"pure-md5": "^0.1.14"
},
"pnpm": {

9
pnpm-lock.yaml generated
View File

@@ -22,7 +22,7 @@ specifiers:
lodash-es: 4.17.21
minisearch: 6.0.0-beta.1
obsidian: latest
obsidian-text-extract: 1.0.4
obsidian-text-extract: ^1.0.4
prettier: ^2.8.1
prettier-plugin-svelte: ^2.8.1
pure-md5: ^0.1.14
@@ -56,7 +56,7 @@ devDependencies:
esbuild-plugin-copy: 1.3.0_esbuild@0.14.0
esbuild-svelte: 0.7.1_wvi5wuag3veo5vm52k3h7pgaae
jest: 27.5.1
obsidian: 0.16.3
obsidian: 1.1.1
prettier: 2.8.1
prettier-plugin-svelte: 2.8.1_sro2v6ld777payjtkjtiuogcxi
svelte: 3.54.0
@@ -4138,6 +4138,7 @@ packages:
/obsidian-text-extract/1.0.4:
resolution: {integrity: sha512-lJ7HaEPGUoGVYr6iCSEU8oPrnA8xRBQgSU9KryWteq3wO+Yiw2Zue70G0rZf9Yj+DkgR92PtuR8aQZQxTy/uLA==}
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
dependencies:
dexie: 3.2.2
p-limit: 4.0.0
@@ -4149,8 +4150,8 @@ packages:
- supports-color
dev: false
/obsidian/0.16.3:
resolution: {integrity: sha512-hal9qk1A0GMhHSeLr2/+o3OpLmImiP+Y+sx2ewP13ds76KXsziG96n+IPFT0mSkup1zSwhEu+DeRhmbcyCCXWw==}
/obsidian/1.1.1:
resolution: {integrity: sha512-GcxhsHNkPEkwHEjeyitfYNBcQuYGeAHFs1pEpZIv0CnzSfui8p8bPLm2YKLgcg20B764770B1sYGtxCvk9ptxg==}
peerDependencies:
'@codemirror/state': ^6.0.0
'@codemirror/view': ^6.0.0

View File

@@ -1,12 +1,15 @@
import { Notice } from 'obsidian'
import type { DocumentRef, IndexedDocument } from './globals'
import {
type DocumentRef,
getTextExtractor,
type IndexedDocument,
} from './globals'
import { database } from './database'
import type { AsPlainObject } from 'minisearch'
import type MiniSearch from 'minisearch'
import {
extractHeadingsFromCache,
getAliasesFromMetadata,
getTagsFromMetadata,
isFileCanvas,
isFileImage,
isFilePDF,
isFilePlaintext,
@@ -14,20 +17,72 @@ import {
removeDiacritics,
} from './tools/utils'
import { getImageText, getPdfText } from 'obsidian-text-extract'
import type { CanvasData } from 'obsidian/canvas'
import type { AsPlainObject } from 'minisearch'
import type MiniSearch from 'minisearch'
async function getIndexedDocument(path: string): Promise<IndexedDocument> {
/**
* This function is responsible for extracting the text from a file and
* returning it as an `IndexedDocument` object.
* @param path
*/
async function getAndMapIndexedDocument(
path: string
): Promise<IndexedDocument> {
const file = app.vault.getFiles().find(f => f.path === path)
if (!file) throw new Error(`Invalid file path: "${path}"`)
let content: string
let content: string | null = null
const extractor = getTextExtractor()
// ** Plain text **
// Just read the file content
if (isFilePlaintext(path)) {
content = await app.vault.cachedRead(file)
} else if (isFilePDF(path)) {
}
// ** Canvas **
// Extract the text fields from the json
else if (isFileCanvas(path)) {
const canvas = JSON.parse(await app.vault.cachedRead(file)) as CanvasData
let texts: string[] = []
// Concatenate text from the canvas fields
for (const node of canvas.nodes) {
if (node.type === 'text') {
texts.push(node.text)
} else if (node.type === 'file') {
texts.push(node.file)
}
}
for (const edge of canvas.edges.filter(e => !!e.label)) {
texts.push(edge.label!)
}
content = texts.join('\r\n')
}
// a) ** Image or PDF ** with Text Extractor
else if (extractor) {
if (extractor.canFileBeExtracted(path)) {
content = await extractor.extractText(file)
} else {
throw new Error('Invalid file format: ' + file.path)
}
}
// b) ** Image or PDF ** without the text-extractor plugin
else {
if (isFilePDF(path)) {
content = await getPdfText(file)
} else if (isFileImage(file.path)) {
content = await getImageText(file)
} else {
throw new Error('Invalid file format: ' + file.path)
}
}
if (content === null || content === undefined) {
// This shouldn't happen
console.warn(`Omnisearch: ${content} content for file`, file.path)
content = ''
}
content = removeDiacritics(content)
const metadata = app.metadataCache.getFileCache(file)
@@ -81,8 +136,12 @@ class CacheManager {
private documents: Map<string, IndexedDocument> = new Map()
public async addToLiveCache(path: string): Promise<void> {
const doc = await getIndexedDocument(path)
try {
const doc = await getAndMapIndexedDocument(path)
this.documents.set(path, doc)
} catch (e) {
console.warn('Omnisearch: Error while adding to live cache', e)
}
}
public removeFromLiveCache(path: string): void {

View File

@@ -11,6 +11,10 @@
let elInput: HTMLInputElement
const dispatch = createEventDispatcher()
export function setInputValue(v:string): void {
value = v
}
$: {
if (initialValue && !initialSet && !value) {
initialSet = true
@@ -19,12 +23,15 @@
}
}
async function selectInput() {
await tick()
function selectInput(_?: HTMLElement): void {
tick()
.then(() => {
elInput.focus()
await tick()
return tick()
})
.then(() => {
elInput.select()
await tick()
})
}
const debouncedOnInput = debounce(() => {

View File

@@ -23,6 +23,7 @@
let query: Query
let indexingStepDesc = ''
let searching = true
let refInput: InputSearch|undefined
$: selectedNote = resultNotes[selectedIndex]
$: searchQuery = searchQuery ?? previousQuery
@@ -31,6 +32,8 @@
searching = true
updateResults().then(() => {
searching = false
}).catch((e) => {
console.error(e)
})
} else {
searching = false
@@ -85,7 +88,8 @@
if (++historySearchIndex >= history.length) {
historySearchIndex = 0
}
previousQuery = history[historySearchIndex]
searchQuery = history[historySearchIndex]
refInput?.setInputValue(searchQuery)
}
async function nextSearchHistory() {
@@ -93,7 +97,8 @@
if (--historySearchIndex < 0) {
historySearchIndex = history.length ? history.length - 1 : 0
}
previousQuery = history[historySearchIndex]
searchQuery = history[historySearchIndex]
refInput?.setInputValue(searchQuery)
}
async function updateResults() {
@@ -228,6 +233,7 @@
<InputSearch
initialValue="{searchQuery}"
bind:this={refInput}
on:input="{e => (searchQuery = e.detail)}"
placeholder="Omnisearch - Vault">
{#if settings.showCreateButton}

View File

@@ -1,6 +1,7 @@
import { EventBus } from './tools/event-bus'
import { writable } from 'svelte/store'
import { settings } from './settings'
import type { TFile } from 'obsidian'
export const regexLineSplit = /\r?\n|\r|((\.|\?|!)( |\r?\n|\r))/g
export const regexYaml = /^---\s*\n(.*?)\n?^---\s?/ms
@@ -76,9 +77,27 @@ export function isInputComposition(): boolean {
return inComposition
}
/**
* Plugin dependency - Chs Patch for Chinese word segmentation
* @returns
*/
export function getChsSegmenter(): any | undefined {
return (app as any).plugins.plugins['cm-chs-patch']
}
export type TextExtractorApi = {
extractText: (file: TFile) => Promise<string>
canFileBeExtracted: (filePath: string) => boolean
}
/**
* Plugin dependency - Text Extractor
* @returns
*/
export function getTextExtractor(): TextExtractorApi | undefined {
return (app as any).plugins?.plugins?.['text-extractor']?.api
}
export const SPACE_OR_PUNCTUATION =
/[|\n\r -#%-*,-/:;?@[-\]_{}\u00A0\u00A1\u00A7\u00AB\u00B6\u00B7\u00BB\u00BF\u037E\u0387\u055A-\u055F\u0589\u058A\u05BE\u05C0\u05C3\u05C6\u05F3\u05F4\u0609\u060A\u060C\u060D\u061B\u061E\u061F\u066A-\u066D\u06D4\u0700-\u070D\u07F7-\u07F9\u0830-\u083E\u085E\u0964\u0965\u0970\u09FD\u0A76\u0AF0\u0C77\u0C84\u0DF4\u0E4F\u0E5A\u0E5B\u0F04-\u0F12\u0F14\u0F3A-\u0F3D\u0F85\u0FD0-\u0FD4\u0FD9\u0FDA\u104A-\u104F\u10FB\u1360-\u1368\u1400\u166E\u1680\u169B\u169C\u16EB-\u16ED\u1735\u1736\u17D4-\u17D6\u17D8-\u17DA\u1800-\u180A\u1944\u1945\u1A1E\u1A1F\u1AA0-\u1AA6\u1AA8-\u1AAD\u1B5A-\u1B60\u1BFC-\u1BFF\u1C3B-\u1C3F\u1C7E\u1C7F\u1CC0-\u1CC7\u1CD3\u2000-\u200A\u2010-\u2029\u202F-\u2043\u2045-\u2051\u2053-\u205F\u207D\u207E\u208D\u208E\u2308-\u230B\u2329\u232A\u2768-\u2775\u27C5\u27C6\u27E6-\u27EF\u2983-\u2998\u29D8-\u29DB\u29FC\u29FD\u2CF9-\u2CFC\u2CFE\u2CFF\u2D70\u2E00-\u2E2E\u2E30-\u2E4F\u3000-\u3003\u3008-\u3011\u3014-\u301F\u3030\u303D\u30A0\u30FB\uA4FE\uA4FF\uA60D-\uA60F\uA673\uA67E\uA6F2-\uA6F7\uA874-\uA877\uA8CE\uA8CF\uA8F8-\uA8FA\uA8FC\uA92E\uA92F\uA95F\uA9C1-\uA9CD\uA9DE\uA9DF\uAA5C-\uAA5F\uAADE\uAADF\uAAF0\uAAF1\uABEB\uFD3E\uFD3F\uFE10-\uFE19\uFE30-\uFE52\uFE54-\uFE61\uFE63\uFE68\uFE6A\uFE6B\uFF01-\uFF03\uFF05-\uFF0A\uFF0C-\uFF0F\uFF1A\uFF1B\uFF1F\uFF20\uFF3B-\uFF3D\uFF3F\uFF5B\uFF5D\uFF5F-\uFF65]+/u

View File

@@ -46,7 +46,9 @@ export default class OmnisearchPlugin extends Plugin {
id: 'show-modal-infile',
name: 'In-file search',
editorCallback: (_editor, view) => {
if (view.file) {
new OmnisearchInFileModal(app, view.file).open()
}
},
})
@@ -190,15 +192,13 @@ async function cleanOldCacheFiles() {
}
function executeFirstLaunchTasks(plugin: Plugin) {
const code = '1.8.0-beta.3'
const code = '1.10.0-beta.1'
if (settings.welcomeMessage !== code) {
const welcome = new DocumentFragment()
welcome.createSpan({}, span => {
span.innerHTML = `<strong>Omnisearch has been updated</strong>
You can now enable "Images Indexing" to use Optical Character Recognition on your scanned documents
🔎🖼`
span.innerHTML = `🔎 Omnisearch will soon require the <strong>Text Extractor</strong> plugin to index PDF and images. See Omnisearch settings for more information.`
})
new Notice(welcome, 30000)
new Notice(welcome, 20_000)
}
settings.welcomeMessage = code

View File

@@ -104,8 +104,7 @@ export class Omnisearch {
documents.filter(d => this.indexedDocuments.has(d.path)).map(d => d.path)
)
// Split the documents in smaller chunks to regularly save the cache.
// If the user shuts off Obsidian mid-indexing, we at least saved some
// Split the documents in smaller chunks to add them to minisearch
const chunkedDocs = chunkArray(documents, 500)
for (const docs of chunkedDocs) {
// Update the list of indexed docs
@@ -146,7 +145,10 @@ export class Omnisearch {
let results = this.minisearch.search(query.segmentsToStr(), {
prefix: term => term.length >= options.prefixLength,
fuzzy: 0.2,
// length <= 3: no fuzziness
// length <= 5: fuzziness of 10%
// length > 5: fuzziness of 20%
fuzzy: term => (term.length <= 3 ? 0 : term.length <= 5 ? 0.1 : 0.2),
combineWith: 'AND',
boost: {
basename: settings.weightBasename,

View File

@@ -8,6 +8,7 @@ import {
} from 'obsidian'
import { writable } from 'svelte/store'
import { database } from './database'
import { getTextExtractor } from './globals'
import type OmnisearchPlugin from './main'
interface WeightingSettings {
@@ -83,16 +84,30 @@ export class SettingsTab extends PluginSettingTab {
new Setting(containerEl).setName('Indexing').setHeading()
if (getTextExtractor()) {
const desc = new DocumentFragment()
desc.createSpan({}, span => {
span.innerHTML = `👍 You have installed <a href="https://github.com/scambier/obsidian-text-extractor">Text Extractor</a>, Omnisearch will use it to index PDFs and images.
<br />Text extraction only works on desktop, but the cache can be synchronized with your mobile device.`
})
new Setting(containerEl).setDesc(desc)
} else {
const label = new DocumentFragment()
label.createSpan({}, span => {
span.innerHTML = `⚠️ Omnisearch will soon require <a href="https://github.com/scambier/obsidian-text-extractor">Text Extractor</a> to index PDFs and images.
You can already install it to get a head start.`
})
new Setting(containerEl).setDesc(label)
}
// PDF Indexing
if (!Platform.isMobileApp) {
if (!Platform.isMobileApp || getTextExtractor()) {
const indexPDFsDesc = new DocumentFragment()
indexPDFsDesc.createSpan({}, span => {
span.innerHTML = `Omnisearch will include PDFs in search results.<br>
⚠️ PDFs first need to be processed. This can take anywhere from a few seconds to 2 minutes, then the resulting text is cached.</li>
<strong style="color: var(--text-accent)">Needs a restart to fully take effect.</strong>`
span.innerHTML = `Include PDFs in search results - Will soon depend on Text Extractor.`
})
new Setting(containerEl)
.setName('PDF Indexing')
.setName(`PDFs Indexing`)
.setDesc(indexPDFsDesc)
.addToggle(toggle =>
toggle.setValue(settings.PDFIndexing).onChange(async v => {
@@ -100,21 +115,14 @@ export class SettingsTab extends PluginSettingTab {
await saveSettings(this.plugin)
})
)
}
// Images Indexing
if (!Platform.isMobileApp) {
const indexImagesDesc = new DocumentFragment()
indexImagesDesc.createSpan({}, span => {
span.innerHTML = `Omnisearch will use <a href="https://en.wikipedia.org/wiki/Tesseract_(software)">Tesseract</a> to index images from their text.
<ul>
<li>Only English is supported at the moment.</li>
<li>Not all images can be correctly read by the OCR, this feature works best with scanned documents.</li>
</ul>
<strong style="color: var(--text-accent)">Needs a restart to fully take effect.</strong>`
span.innerHTML = `Include images in search results - Will soon depend on Text Extractor.`
})
new Setting(containerEl)
.setName('BETA - Images Indexing')
.setName(`Images Indexing`)
.setDesc(indexImagesDesc)
.addToggle(toggle =>
toggle.setValue(settings.imagesIndexing).onChange(async v => {
@@ -123,6 +131,7 @@ export class SettingsTab extends PluginSettingTab {
})
)
}
// Additional files to index
const indexedFileTypesDesc = new DocumentFragment()
indexedFileTypesDesc.createSpan({}, span => {
@@ -169,7 +178,8 @@ export class SettingsTab extends PluginSettingTab {
const diacriticsDesc = new DocumentFragment()
diacriticsDesc.createSpan({}, span => {
span.innerHTML = `Normalize diacritics in search terms. Words like "brûlée" or "žluťoučký" will be indexed as "brulee" and "zlutoucky".<br/>
⚠️<span style="color: var(--text-accent)">You probably should NOT disable this.</span><br>
⚠️ <span style="color: var(--text-accent)">You probably should <strong>NOT</strong> disable this.</span><br>
⚠️ <span style="color: var(--text-accent)">Changing this setting will clear the cache.</span><br>
<strong style="color: var(--text-accent)">Needs a restart to fully take effect.</strong>
`
})
@@ -178,6 +188,7 @@ export class SettingsTab extends PluginSettingTab {
.setDesc(diacriticsDesc)
.addToggle(toggle =>
toggle.setValue(settings.ignoreDiacritics).onChange(async v => {
await database.clearCache()
settings.ignoreDiacritics = v
await saveSettings(this.plugin)
})
@@ -349,7 +360,7 @@ export class SettingsTab extends PluginSettingTab {
}
weightSlider(cb: SliderComponent, key: keyof WeightingSettings): void {
cb.setLimits(1, 3, 0.1)
cb.setLimits(1, 5, 0.1)
.setValue(settings[key])
.setDynamicTooltip()
.onChange(v => {

View File

@@ -1,6 +1,7 @@
import type { ResultNote } from '../globals'
import { Query } from '../search/query'
import { searchEngine } from '../search/omnisearch'
import { makeExcerpt } from './utils'
type ResultNoteApi = {
score: number
@@ -8,6 +9,7 @@ type ResultNoteApi = {
basename: string
foundWords: string[]
matches: SearchMatchApi[]
excerpt: string
}
export type SearchMatchApi = {
@@ -17,7 +19,10 @@ export type SearchMatchApi = {
function mapResults(results: ResultNote[]): ResultNoteApi[] {
return results.map(result => {
const { score, path, basename, foundWords, matches } = result
const { score, path, basename, foundWords, matches, content } = result
const excerpt = makeExcerpt(content, matches[0]?.offset ?? -1)
return {
score,
path,
@@ -29,14 +34,18 @@ function mapResults(results: ResultNote[]): ResultNoteApi[] {
offset: match.offset,
}
}),
excerpt,
}
})
}
async function search(q: string): Promise<ResultNoteApi[]> {
async function search(
q: string,
options: Partial<{ excerpt: boolean }> = {}
): Promise<ResultNoteApi[]> {
const query = new Query(q)
const raw = await searchEngine.getSuggestions(query)
return mapResults(raw)
}
export default {search}
export default { search }

View File

@@ -5,11 +5,11 @@ import {
parseFrontMatterAliases,
Platform,
} from 'obsidian'
import type { SearchMatch } from '../globals'
import { getTextExtractor, type SearchMatch } from '../globals'
import {
getChsSegmenter,
excerptAfter,
excerptBefore,
getChsSegmenter,
highlightClass,
isSearchMatch,
regexLineSplit,
@@ -195,14 +195,40 @@ export function getAliasesFromMetadata(
}
export function getTagsFromMetadata(metadata: CachedMetadata | null): string[] {
return metadata ? getAllTags(metadata) ?? [] : []
let tags = metadata ? getAllTags(metadata) ?? [] : []
// This will "un-nest" tags that are in the form of "#tag/subtag"
// A tag like "#tag/subtag" will be split into 3 tags: '#tag/subtag", "#tag" and "#subtag"
// https://github.com/scambier/obsidian-omnisearch/issues/146
tags = [
...new Set(
tags.reduce((acc, tag) => {
return [
...acc,
...tag
.split('/')
.filter(t => t)
.map(t => (t.startsWith('#') ? t : `#${t}`)),
tag,
]
}, [] as string[])
),
]
return tags
}
/**
* https://stackoverflow.com/a/37511463
*/
export function removeDiacritics(str: string): string {
return str.normalize('NFD').replace(/\p{Diacritic}/gu, '')
if (str === null || str === undefined) {
return ''
}
// Keep backticks for code blocks, because otherwise they are removed by the .normalize() function
// https://stackoverflow.com/a/36100275
str = str.replaceAll('`', '[__omnisearch__backtick__]')
str = str.normalize('NFD').replace(/\p{Diacritic}/gu, '')
str = str.replaceAll('[__omnisearch__backtick__]', '`')
return str
}
export function getCtrlKeyLabel(): 'ctrl' | '⌘' {
@@ -210,10 +236,13 @@ export function getCtrlKeyLabel(): 'ctrl' | '⌘' {
}
export function isFileIndexable(path: string): boolean {
const canIndexPDF = (!Platform.isMobileApp || !!getTextExtractor()) && settings.PDFIndexing
const canIndexImages = (!Platform.isMobileApp || !!getTextExtractor()) && settings.imagesIndexing
return (
isFilePlaintext(path) ||
(!Platform.isMobileApp && settings.PDFIndexing && isFilePDF(path)) ||
(!Platform.isMobileApp && settings.imagesIndexing && isFileImage(path))
isFileCanvas(path) ||
(canIndexPDF && isFilePDF(path)) ||
(canIndexImages && isFileImage(path))
)
}
@@ -228,11 +257,11 @@ export function isFilePDF(path: string): boolean {
}
export function isFilePlaintext(path: string): boolean {
return getPlaintextExtensions().some(t => path.endsWith(`.${t}`))
return [...settings.indexedFileTypes, 'md'].some(t => path.endsWith(`.${t}`))
}
export function getPlaintextExtensions(): string[] {
return [...settings.indexedFileTypes, 'md']
export function isFileCanvas(path: string): boolean {
return path.endsWith('.canvas')
}
export function getExtension(path: string): string {

View File

@@ -1,6 +1,7 @@
import { readFileSync, writeFileSync } from 'fs'
const targetVersion = process.env.npm_package_version
console.log(`Bumping version to ${targetVersion}`)
// read minAppVersion from manifest.json and bump version to target version
const manifest = JSON.parse(readFileSync('manifest-beta.json', 'utf8'))