Compare commits

..

47 commits
0.0.2 ... main

Author SHA1 Message Date
Kirill Gavrilov
53d2692fe0 build(deps): tech stack upgrades 2026-06-07 14:30:30 +03:00
Kirill Gavrilov
f212cbebd0 build(e2e, deps): wdio-electron-service -> @wdio/electron-service and the latest version of WDIO 2026-06-07 14:30:30 +03:00
Kirill Gavrilov
bc35cb11ee ci: single pnpm install in repo root is enough now
That's because both packages (plugin itself and E2E-tests) belong to single pnpm-workspace now.
2026-06-07 14:30:30 +03:00
Kirill Gavrilov
5e2ec97973 build: upgrade to the latest pnpm v11.5.2 2026-06-07 14:30:30 +03:00
Kirill Gavrilov
8a4f0ec4a9 ci: use envinfo package to check environment info on CI 2026-06-07 14:30:30 +03:00
Kirill Gavrilov
1e40cd1979 chore: use lowercase values from tsconfig docs 2026-05-03 16:10:00 +03:00
Kirill Gavrilov
a16e9f483a build: upgrade all dependencies to latest versions 2026-05-03 16:10:00 +03:00
Kirill Gavrilov
c5fcbee18a build(e2e): upgrade WDIO to the latest v9.27.1 2026-05-03 16:10:00 +03:00
Kirill Gavrilov
7c1f5894f8 chore(tsconfig): add explicit rootDir 2026-05-03 16:10:00 +03:00
Kirill Gavrilov
8a2d7a05fb chore(tsconfig): set moduleResolution to bundler
Since the project gets assembled with esbuild
2026-05-03 16:10:00 +03:00
Kirill Gavrilov
1b2350ee99 build: upgrade to TypeScript v6 and latest ESLint 2026-05-03 16:10:00 +03:00
Kirill Gavrilov
e451c18e33 fix(e2e): remove unnecessary await after expect-webdriverio upgrade
Fixes the following ESLint error:

> Unexpected `await` of a non-Promise (non-"Thenable") value

Related discussion: webdriverio/expect-webdriverio#2068
2026-04-06 03:06:16 +03:00
Kirill Gavrilov
8ce6c12ebf test(e2e): upgrade WDIO to the latest v9.27.0 2026-04-06 02:53:33 +03:00
Kirill Gavrilov
e276168617 fix: resolve TS7006 in Vitest config after Vitest upgrade
Fixes:

> error TS7006: Parameter 'id' implicitly has an 'any' type.
2026-03-27 02:10:27 +03:00
Kirill Gavrilov
64cbaa3f08 chore: typo fix 2026-03-27 01:57:48 +03:00
Kirill Gavrilov
e99d127bc6 fix: augment require to fix eslint warnings on require('electron')
Fixes #8
2026-03-27 01:52:53 +03:00
Kirill Gavrilov
a97c023338 chore: remove unsued assignment
Fixes new ESLint warning
2026-03-27 01:52:12 +03:00
Kirill Gavrilov
7f0d379928 build: upgrade all dependencies to latest versions 2026-03-27 01:51:59 +03:00
Kirill Gavrilov
a8aaaa0d37 ci: add missing build argument passing Obsidian version 2026-03-27 01:22:27 +03:00
Kirill Gavrilov
1bf2109df0 ci: upgrade Obsidian to the latest v1.12.7 2026-03-26 22:23:31 +03:00
Kirill Gavrilov
0a68c244c1 ci: upgrade Obsidian to the latest v1.12.7 2026-03-26 22:22:03 +03:00
Kirill Gavrilov
46b76af4fd test(e2e): fix EPERM error during tests execution on Mac
Fixes #7
2026-03-26 22:21:53 +03:00
Kirill Gavrilov
d026ed5f38 build: upgrade 'obsidian' and 'electron' dependencies 2026-03-26 22:21:53 +03:00
Kirill Gavrilov
ebf9b7b49b build: upgrade pnpm to v10.33.0 2026-03-26 21:58:09 +03:00
Kirill Gavrilov
320d93bb4f ci: the latest version of Obsidian (v1.10.6) for E2E testing 2025-12-20 01:54:18 +03:00
Kirill Gavrilov
825ca9f060 ci: switch to NodeJS v24 LTS 2025-12-20 01:50:22 +03:00
Kirill Gavrilov
a2fb2d7c4f test: fix "Failed to parse ... TimecodesPlugin.ts" error
Fix "Failed to parse ... TimecodesPlugin.ts" error by creating virtual module for 'obsidian' package.

Related discussion: vitest-dev/vitest#9298
2025-12-20 01:45:44 +03:00
Kirill Gavrilov
3980070aae build: upgrade to Vitest v4 2025-12-20 01:41:44 +03:00
Kirill Gavrilov
7ec2f52c57 build: upgrade dependencies 2025-12-18 03:19:04 +03:00
Kirill Gavrilov
6e2d657048 chore(eslint): remove no longer existing property 2025-12-18 03:19:03 +03:00
Kirill Gavrilov
17a7228a47 ci: enforce colored output 2025-10-24 13:47:06 +03:00
Kirill Gavrilov
c8646b6070 ci: more specific version of CI runner image 2025-10-22 00:29:22 +03:00
Kirill Gavrilov
ef7f208370 ci: add obsidian version to CI runner image 2025-10-22 00:26:17 +03:00
Kirill Gavrilov
0c5a41e14a build: upgrade all dependencies to latest versions 2025-10-20 01:55:16 +03:00
Kirill Gavrilov
45f5dfc1af ci: publish E2E-tests coverage to SonarQube
#5
2025-10-20 01:45:46 +03:00
Kirill Gavrilov
da38f73330 chore(e2e): collect coverage upon E2E test suite completion
#5
2025-10-20 01:45:41 +03:00
Kirill Gavrilov
8c674b5dcf build: add task producing instrumented build for measuring code coverage
#5
2025-10-20 01:45:31 +03:00
Kirill Gavrilov
56f5a6362c ci(e2e): start executing e2e tests in CI
Closes #3
2025-10-19 23:32:30 +03:00
Kirill Gavrilov
075a8849dd ci: execute CI pipeline inside of special Docker container supporting e2e testing
#3
2025-10-19 23:32:30 +03:00
Kirill Gavrilov
6eb9cf0a69 build: adjust CI Dockerfile to make it possible to run on Apple Silicon Mac 2025-10-19 23:32:30 +03:00
Kirill Gavrilov
e27b30584a ci: automate build of Docker image for running CI
#3
2025-10-19 23:32:30 +03:00
Kirill Gavrilov
6369bb2d62 build: prepare image for running tests inside of Docker container
#3
2025-10-19 23:32:30 +03:00
Kirill Gavrilov
2b3ab72f49 chore(e2e): make obsidian path and sandbox Electron option configurable
Will be needed for running E2E tests in CI #3
2025-10-19 23:32:29 +03:00
Kirill Gavrilov
99ed8adf07 chore: remove explicit version of Electron from WDIO config
Now E2E tests work even without it
2025-10-07 13:28:45 +03:00
Kirill Gavrilov
7a2c1ba2e3 build: upgrade Electron version to match the latest Obsidian v1.9.14 2025-10-06 01:46:48 +03:00
Kirill Gavrilov
34e0d0009c build: upgrade all dependencies to latest versions where possible
Not latest versions for:
- `wdio-electron-service` (because of the webdriverio-community/wdio-electron-service#1191 bug)
- `electron` (because it matches the version currently used by Obsidian)
2025-10-06 01:46:48 +03:00
Kirill Gavrilov
938b95aab0 build: the latest pnpm v10.18.0 2025-10-06 01:46:48 +03:00
19 changed files with 360 additions and 74 deletions

View file

@ -0,0 +1,68 @@
name: Docker image for CI
on:
push:
branches:
- main
paths:
- '.github/workflows/build-ci-runner-image.yml'
- 'docker/ci-runner.Dockerfile'
workflow_dispatch:
inputs:
nodejs_version:
description: 'Node.js version to use'
type: string
required: true
obsidian_version:
description: 'Obsidian version to test with'
type: string
required: true
debian_version:
description: 'Debian codename version to use'
type: string
required: true
env:
NODEJS_VERSION: ${{ inputs.nodejs_version || '24' }}
OBSIDIAN_VERSION: ${{ inputs.obsidian_version || '1.12.7' }}
DEBIAN_VERSION: ${{ inputs.debian_version || 'trixie' }}
IMAGE_NAME: ghcr.io/${{ github.repository }}/ci-runner
DOCKERFILE_PATH: docker/ci-runner.Dockerfile
jobs:
build:
runs-on: ubuntu-latest
permissions:
packages: write
steps:
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Prepare metadata for tags and labels
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.IMAGE_NAME }}
tags: |
type=raw,value=node-${{ env.NODEJS_VERSION }}-obsidian-${{ env.OBSIDIAN_VERSION }}-${{ env.DEBIAN_VERSION }}-slim
labels: |
org.opencontainers.image.title: Obsidian plugins CI runner
org.opencontainers.image.description=Docker image for building and testing obsidian plugins (with E2E tests support)
- name: Build and push
uses: docker/build-push-action@v6
with:
file: ${{ env.DOCKERFILE_PATH }}
build-args: |
NODEJS_VERSION=${{ env.NODEJS_VERSION }}
OBSIDIAN_VERSION=${{ env.OBSIDIAN_VERSION }}
DEBIAN_VERSION=${{ env.DEBIAN_VERSION }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

View file

@ -7,32 +7,41 @@ on:
pull_request:
types: [opened, synchronize, reopened]
env:
FORCE_COLOR: 1
jobs:
Build:
runs-on: ubuntu-latest
container:
image: ghcr.io/${{ github.repository }}/ci-runner:node-24-obsidian-1.12.7-trixie-slim
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 1
- name: Install pnpm
uses: pnpm/action-setup@v4
- uses: actions/setup-node@v5
with:
node-version: '22'
package-manager-cache: false
- name: Environment details
run: pnpm version
run: pnpm dlx envinfo --system --binaries
- name: Install dependencies
run: pnpm install
- name: Build
run: pnpm run build
- name: Test
run: pnpm run test:unit
- name: Upload coverage data
- name: E2E tests
run: xvfb-run pnpm run test
working-directory: e2e
- name: Upload unit-test coverage data
uses: actions/upload-artifact@v4
with:
name: lcov.info
name: lcov-unit.info
path: out/coverage/lcov.info
- name: Upload E2E-test coverage data
uses: actions/upload-artifact@v4
with:
name: lcov-e2e.info
path: e2e/out/coverage/lcov.info
SonarScan:
needs: Build
@ -41,11 +50,16 @@ jobs:
- uses: actions/checkout@v5
with:
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
- name: Download coverage data
- name: Download unit-tests coverage data
uses: actions/download-artifact@v5
with:
name: lcov.info
path: coverage/
name: lcov-unit.info
path: coverage/unit/
- name: Download E2E-tests coverage data
uses: actions/download-artifact@v5
with:
name: lcov-e2e.info
path: coverage/e2e/
- name: SonarQube Scan
uses: SonarSource/sonarqube-scan-action@v6
env:

View file

@ -20,10 +20,10 @@ jobs:
uses: pnpm/action-setup@v4
- uses: actions/setup-node@v5
with:
node-version: '22'
node-version: '24'
package-manager-cache: false
- name: Environment details
run: pnpm version
run: pnpm dlx envinfo --system --binaries
- name: Build
id: build
run: |

View file

@ -0,0 +1,49 @@
ARG NODEJS_VERSION=22
ARG DEBIAN_VERSION=trixie
ARG BASE_IMAGE=node:${NODEJS_VERSION}-${DEBIAN_VERSION}-slim
ARG TARGET_IMAGE_PLATFORM=linux/amd64
ARG OBSIDIAN_VERSION=1.9.14
ARG OBSIDIAN_APPIMAGE_FILE=Obsidian-${OBSIDIAN_VERSION}.AppImage
FROM busybox:latest AS obsidian-downloader
ARG OBSIDIAN_VERSION
ARG OBSIDIAN_APPIMAGE_FILE
RUN wget https://github.com/obsidianmd/obsidian-releases/releases/download/v${OBSIDIAN_VERSION}/${OBSIDIAN_APPIMAGE_FILE}
FROM ${BASE_IMAGE} AS obsidian-extractor-amd64
ARG OBSIDIAN_APPIMAGE_FILE
COPY --from=obsidian-downloader /${OBSIDIAN_APPIMAGE_FILE} /${OBSIDIAN_APPIMAGE_FILE}
RUN chmod +x ${OBSIDIAN_APPIMAGE_FILE}
RUN ./${OBSIDIAN_APPIMAGE_FILE} --appimage-extract
# `--appimage-extract` won't work on Apple Silicon, hence using a special extraction stage
FROM --platform=${TARGET_IMAGE_PLATFORM} ${BASE_IMAGE} AS obsidian-extractor-arm64
ARG OBSIDIAN_APPIMAGE_FILE
RUN apt-get update && \
apt-get install -y squashfs-tools && \
rm -rf /var/lib/apt/lists/*
COPY --from=obsidian-downloader /${OBSIDIAN_APPIMAGE_FILE} /${OBSIDIAN_APPIMAGE_FILE}
RUN offset=$(grep -aob 'hsqs' ${OBSIDIAN_APPIMAGE_FILE} | tail -n1 | cut -d: -f1) && \
dd if=${OBSIDIAN_APPIMAGE_FILE} of=image.squashfs bs=4M iflag=skip_bytes,count_bytes skip=$offset status=progress && \
unsquashfs image.squashfs
FROM obsidian-extractor-${TARGETARCH} AS obsidian-extractor
FROM --platform=${TARGET_IMAGE_PLATFORM} ${BASE_IMAGE}
RUN apt-get update && \
apt-get install -y --no-install-recommends --no-install-suggests \
libgtk-3-0 libnss3 libgbm1 libasound2 \
xvfb xauth && \
rm -rf /var/lib/apt/lists/*
COPY --from=obsidian-extractor /squashfs-root /obsidian-appimage-extracted
ENV OBSIDIAN_BINARY_PATH=/obsidian-appimage-extracted/obsidian
ENV OBSIDIAN_NO_SANDBOX=true
RUN corepack enable pnpm
# for running locally as: docker run --rm -it -v ${PWD}:/plugin local-ci-runner
WORKDIR /plugin
CMD ["sh", "-c", "pnpm i && cd e2e && xvfb-run pnpm run test"]

View file

@ -1,2 +1,4 @@
import * as path from 'node:path'
export const TIMECODES_PLUGIN_ID = 'timecodes'
export const TEST_VAULT_DIR = '.e2e_test_vault'
export const TEST_VAULT_DIR = path.resolve(process.cwd(), '.e2e_test_vault')

View file

@ -3,19 +3,27 @@
"type": "module",
"private": true,
"scripts": {
"pretest": "pnpm --dir .. run build",
"pretest": "pnpm --dir .. run build:coverage",
"test": "wdio run wdio.conf.ts"
},
"devDependencies": {
"@wdio/cli": "9.11.0",
"@wdio/globals": "9.11.0",
"@wdio/local-runner": "9.11.0",
"@wdio/mocha-framework": "9.11.0",
"@wdio/spec-reporter": "9.11.0",
"electron": "32.2.5",
"expect-webdriverio": "5.1.0",
"obsidian": "1.8.7",
"wdio-electron-service": "8.0.1",
"webdriverio": "9.11.0"
"@types/istanbul-lib-coverage": "2.0.6",
"@types/istanbul-lib-report": "3.0.3",
"@types/istanbul-lib-source-maps": "4.0.4",
"@types/istanbul-reports": "3.0.4",
"@wdio/cli": "9.27.2",
"@wdio/electron-service": "^10.0.0",
"@wdio/globals": "9.27.2",
"@wdio/local-runner": "9.27.2",
"@wdio/mocha-framework": "9.27.2",
"@wdio/spec-reporter": "9.27.2",
"electron": "39.8.3",
"expect-webdriverio": "5.6.7",
"istanbul-lib-coverage": "3.2.2",
"istanbul-lib-report": "3.0.1",
"istanbul-lib-source-maps": "5.0.6",
"istanbul-reports": "3.2.0",
"obsidian": "1.12.3",
"webdriverio": "9.27.2"
}
}

View file

@ -12,7 +12,7 @@ describe('Markdown view post-processor', () => {
const link = $('//div[contains(@class, "markdown-preview-section")]//a[text()="22:53"]')
await expect(link).toBePresent()
const href = await link.getAttribute('href')
await expect(href).toBe('https://youtu.be/k_ItB5btREU?t=1373')
expect(href).toBe('https://youtu.be/k_ItB5btREU?t=1373')
})
})
@ -28,7 +28,7 @@ describe('Markdown view post-processor', () => {
const link = $('//div[contains(@class, "markdown-preview-section")]//a[text()="22:53"]')
await expect(link).toBePresent()
const href = await link.getAttribute('href')
await expect(href).toBe('https://youtu.be/k_ItB5btREU?t=1373')
expect(href).toBe('https://youtu.be/k_ItB5btREU?t=1373')
})
})
@ -42,7 +42,7 @@ describe('Markdown view post-processor', () => {
const link = $('//div[contains(@class, "markdown-preview-section")]//a[text()="22:53"]')
await expect(link).toBePresent()
const href = await link.getAttribute('href')
await expect(href).toBe('https://youtu.be/k_ItB5btREU?t=1373')
expect(href).toBe('https://youtu.be/k_ItB5btREU?t=1373')
})
})
@ -58,7 +58,7 @@ describe('Markdown view post-processor', () => {
const link = $('//div[contains(@class, "markdown-preview-section")]//a[text()="22:53"]')
await expect(link).toBePresent()
const href = await link.getAttribute('href')
await expect(href).toBe('https://youtu.be/k_ItB5btREU?t=1373')
expect(href).toBe('https://youtu.be/k_ItB5btREU?t=1373')
})
})
@ -78,7 +78,7 @@ referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
const link = $('//div[contains(@class, "markdown-preview-section")]//a[text()="22:53"]')
await expect(link).toBePresent()
const href = await link.getAttribute('href')
await expect(href).toBe('https://youtu.be/k_ItB5btREU?t=1373')
expect(href).toBe('https://youtu.be/k_ItB5btREU?t=1373')
})
})
@ -102,17 +102,17 @@ https://www.youtube.com/watch?v=k_ItB5btREU
let link = $('//div[contains(@class, "markdown-preview-section")]//a[text()="14:27"]')
await expect(link).toBePresent()
let href = await link.getAttribute('href')
await expect(href).toBe('https://youtu.be/Z5n9VK3sOnI?t=867')
expect(href).toBe('https://youtu.be/Z5n9VK3sOnI?t=867')
link = $('//div[contains(@class, "markdown-preview-section")]//a[text()="4:07"]')
await expect(link).toBePresent()
href = await link.getAttribute('href')
await expect(href).toBe('https://youtu.be/Lipf5piizZc?t=247')
expect(href).toBe('https://youtu.be/Lipf5piizZc?t=247')
link = $('//div[contains(@class, "markdown-preview-section")]//a[text()="22:53"]')
await expect(link).toBePresent()
href = await link.getAttribute('href')
await expect(href).toBe('https://youtu.be/k_ItB5btREU?t=1373')
expect(href).toBe('https://youtu.be/k_ItB5btREU?t=1373')
})
})
@ -140,17 +140,17 @@ referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
let link = $('//div[contains(@class, "markdown-preview-section")]//a[text()="14:27"]')
await expect(link).toBePresent()
let href = await link.getAttribute('href')
await expect(href).toBe('https://youtu.be/Z5n9VK3sOnI?t=867')
expect(href).toBe('https://youtu.be/Z5n9VK3sOnI?t=867')
link = $('//div[contains(@class, "markdown-preview-section")]//a[text()="4:07"]')
await expect(link).toBePresent()
href = await link.getAttribute('href')
await expect(href).toBe('https://youtu.be/Lipf5piizZc?t=247')
expect(href).toBe('https://youtu.be/Lipf5piizZc?t=247')
link = $('//div[contains(@class, "markdown-preview-section")]//a[text()="22:53"]')
await expect(link).toBePresent()
href = await link.getAttribute('href')
await expect(href).toBe('https://youtu.be/k_ItB5btREU?t=1373')
expect(href).toBe('https://youtu.be/k_ItB5btREU?t=1373')
})
})
@ -176,7 +176,7 @@ https://www.youtube.com/watch?v=k_ItB5btREU
const textLinesFromAllNoteParagraphs = $$('.markdown-preview-section p:not(.mod-ui)')
.map(p => p.getText())
const textFromAllNoteParagraphs = (await textLinesFromAllNoteParagraphs).join('\n\n')
await expect(textFromAllNoteParagraphs).toBe(noteContent)
expect(textFromAllNoteParagraphs).toBe(noteContent)
})
})
})

6
e2e/types/require-electron.d.ts vendored Normal file
View file

@ -0,0 +1,6 @@
namespace NodeJS {
interface Require {
// eslint-disable-next-line @typescript-eslint/prefer-function-type
(module: 'electron'): typeof import('electron')
}
}

44
e2e/utils/coverage.ts Normal file
View file

@ -0,0 +1,44 @@
import type { CoverageMapData } from 'istanbul-lib-coverage'
import * as fs from 'node:fs'
import * as path from 'node:path'
import { createCoverageMap } from 'istanbul-lib-coverage'
import { createContext } from 'istanbul-lib-report'
import { createSourceMapStore } from 'istanbul-lib-source-maps'
import * as reports from 'istanbul-reports'
export async function exportCoverageToLcov() {
const coverageMap = createCoverageMap(await collectCoverageData())
const remappedCoverageMap = await createSourceMapStore().transformCoverage(coverageMap)
const context = createContext({
coverageMap: remappedCoverageMap,
dir: 'out/coverage',
sourceFinder: (filePath: string) => {
return fs.readFileSync(restorePathRelativeToMainPluginProject(filePath), 'utf-8')
},
})
reports.create('lcov').execute(context)
}
const cwd = process.cwd()
function restorePathRelativeToMainPluginProject(filePath: string) {
const relativePart = path.relative(cwd, filePath)
return path.join(path.resolve(cwd, '..'), relativePart)
}
async function collectCoverageData() {
const coverageData = await browser.execute(() => {
return window.__coverage__
})
if (!coverageData) throw new Error('No coverage data found!')
return coverageData
}
declare global {
interface Window {
__coverage__?: CoverageMapData
}
}

View file

@ -1,9 +1,13 @@
/// <reference types="wdio-electron-service" />
/// <reference types="@wdio/electron-service" />
import ObsidianApp from './specs/pageobjects/obsidian-app.page'
import { exportCoverageToLcov } from './utils/coverage'
const debug = process.env.DEBUG
const ONE_DAY = 24 * 60 * 60 * 1000
const obsidianBinaryPath = process.env.OBSIDIAN_BINARY_PATH
const obsidianNoSandbox = process.env.OBSIDIAN_NO_SANDBOX === 'true'
export const config: WebdriverIO.Config = {
runner: 'local',
specs: ['./specs/*.e2e.ts'],
@ -12,11 +16,10 @@ export const config: WebdriverIO.Config = {
capabilities: [
{
browserName: 'electron',
browserVersion: '32.2.5',
'wdio:electronServiceOptions': {
// custom application args
appBinaryPath: '/Applications/Obsidian.app/Contents/MacOS/Obsidian',
appArgs: [],
appBinaryPath: obsidianBinaryPath ?? '/Applications/Obsidian.app/Contents/MacOS/Obsidian',
appArgs: [obsidianNoSandbox ? '--no-sandbox' : '--sandbox'],
},
},
],
@ -38,4 +41,7 @@ export const config: WebdriverIO.Config = {
await ObsidianApp.createAndOpenFreshVault()
await ObsidianApp.activateTimecodesPlugin()
},
afterSuite: async () => {
await exportCoverageToLcov()
},
}

View file

@ -12,7 +12,6 @@ export default tseslint.config(
tseslint.configs.strictTypeChecked,
tseslint.configs.stylisticTypeChecked,
stylistic.configs.customize({
flat: true,
jsx: false,
indent: 2,
quotes: 'single',
@ -51,17 +50,17 @@ export default tseslint.config(
type: 'natural',
order: 'asc',
ignoreCase: false,
newlinesBetween: 'always',
newlinesBetween: 1,
environment: 'node',
groups: [
'type',
'builtin',
'external',
'internal-type',
'internal',
['parent-type', 'sibling-type', 'index-type'],
['parent', 'sibling', 'index'],
'object',
'type-import',
'value-builtin',
'value-external',
'type-internal',
'value-internal',
['type-parent', 'type-sibling', 'type-index'],
['value-parent', 'value-sibling', 'value-index'],
'ts-equals-import',
'unknown',
],
}],

View file

@ -5,6 +5,7 @@
"scripts": {
"dev": "node scripts/dev.js",
"build": "tsc -noEmit -skipLibCheck && node scripts/esbuild.build.js production",
"build:coverage": "node scripts/esbuild.build.js coverage",
"test:eslint": "eslint --cache --cache-location out/.eslintcache",
"test:unit": "vitest --coverage"
},
@ -15,22 +16,27 @@
],
"author": "Kirill Gavrilov",
"license": "MIT",
"devDependencies": {
"@eslint/js": "9.21.0",
"@stylistic/eslint-plugin": "4.2.0",
"@types/node": "22.10.8",
"@vitest/coverage-v8": "3.0.7",
"enquirer": "2.4.1",
"esbuild": "0.25.0",
"eslint": "9.21.0",
"eslint-plugin-perfectionist": "4.9.0",
"eslint-plugin-wdio": "9.9.1",
"globals": "16.0.0",
"obsidian": "1.8.7",
"obsidian-utils": "0.10.2",
"typescript": "5.8.2",
"typescript-eslint": "8.26.0",
"vitest": "3.0.7"
"engines": {
"node": ">=24 <25"
},
"packageManager": "pnpm@10.5.2"
"devDependencies": {
"@eslint/js": "10.0.1",
"@stylistic/eslint-plugin": "5.10.0",
"@types/istanbul-lib-instrument": "1.7.8",
"@types/node": "24.13.1",
"@vitest/coverage-v8": "4.1.8",
"enquirer": "2.4.1",
"esbuild": "0.28.0",
"eslint": "10.4.1",
"eslint-plugin-perfectionist": "5.9.0",
"eslint-plugin-wdio": "9.27.2",
"globals": "17.6.0",
"istanbul-lib-instrument": "6.0.3",
"obsidian": "1.12.3",
"obsidian-utils": "0.10.2",
"typescript": "6.0.3",
"typescript-eslint": "8.60.1",
"vitest": "4.1.8"
},
"packageManager": "pnpm@11.5.2"
}

9
pnpm-workspace.yaml Normal file
View file

@ -0,0 +1,9 @@
packages:
- 'e2e'
allowBuilds:
edgedriver: false
electron: false
esbuild: false
geckodriver: false
minimumReleaseAgeExclude:
- '@types/node@24.13.1'

View file

@ -5,21 +5,30 @@ import process from 'node:process'
import esbuild from 'esbuild'
import { sharedEsbuildConfig } from './esbuild.config.js'
import { instrumentWithSourceMaps } from './utils/instrument.js'
const config = sharedEsbuildConfig
const prod = process.argv[2] === 'production'
const coverage = process.argv[2] === 'coverage'
const context = await esbuild.context({
...config,
...prod
? { minify: true, sourcemap: false }
: {},
...coverage
? { minify: false, sourcemap: 'inline' }
: {},
})
if (prod) {
await context.rebuild()
process.exit(0)
} else if (coverage) {
await context.rebuild()
instrumentWithSourceMaps('out/main.js', 'out/main.js')
process.exit(0)
} else {
await context.watch()
}

View file

@ -0,0 +1,47 @@
// @ts-check
import * as fs from 'node:fs'
import { createInstrumenter } from 'istanbul-lib-instrument'
/**
* Instrument a JavaScript file with Istanbul coverage instrumentation
* while preserving and remapping original sourcemaps
* @param {string} inputFile
* @param {string} outputFile
*/
export function instrumentWithSourceMaps(inputFile, outputFile) {
const code = fs.readFileSync(inputFile, 'utf8')
// Extract the original sourcemap from the bundled file
const sourceMapMatch
= code.match(/\/\/# sourceMappingURL=data:application\/json;base64,(.+)/)
if (!sourceMapMatch) throw new Error(`No sourcemap found in ${inputFile}`)
const sourceMapJson = Buffer.from(sourceMapMatch[1].trim(), 'base64').toString('utf8')
const originalSourceMap = JSON.parse(sourceMapJson)
const instrumenter = createInstrumenter({
esModules: false, // esbuild outputs Obsidian plugins as CommonJS
compact: false,
preserveComments: true,
produceSourceMap: true,
autoWrap: true,
})
const instrumentedCode = instrumenter.instrumentSync(
code,
inputFile,
originalSourceMap, // <- This is critical for sourcemaps remapping!
)
const sourceMap = instrumenter.lastSourceMap()
let output = instrumentedCode
if (!sourceMap) throw new Error('No source map found!')
const base64SourceMap = Buffer.from(JSON.stringify(sourceMap)).toString('base64')
output += `\n//# sourceMappingURL=data:application/json;base64,${base64SourceMap}`
fs.writeFileSync(outputFile, output, 'utf8')
console.log(`✓ Instrumented: ${inputFile} -> ${outputFile}`)
}

View file

@ -6,4 +6,4 @@ sonar.exclusions=**/*.test.ts
sonar.tests=src
sonar.test.inclusions=**/*.test.ts
sonar.javascript.lcov.reportPaths=./coverage/lcov.info
sonar.javascript.lcov.reportPaths=./coverage/unit/lcov.info,./coverage/e2e/lcov.info

View file

@ -24,7 +24,7 @@ export function turnRawTimecodesIntoClickableLinks(
}
latestNoteBeingProcessed = currentNoteSourcePath
let node: Node | null = null
let node: Node | null
const nodesWithTimecodes: Text[] = []
const nodeWalker = document.createTreeWalker(root, NodeFilter.SHOW_ALL)
while ((node = nodeWalker.nextNode()) != null) {
@ -38,7 +38,7 @@ export function turnRawTimecodesIntoClickableLinks(
textForFindingVideoLink = elementNode.src
}
if (elementNode instanceof HTMLMediaElement) {
latestRequiredEnricher = localMedicaEnricherFor(elementNode)
latestRequiredEnricher = localMediaEnricherFor(elementNode)
}
} else if (node.nodeType === Node.TEXT_NODE) {
const textNode = node as Text
@ -123,7 +123,7 @@ const youtubeEnricherFor = (videoId: string): TextTimecodeEnricher => ({
},
})
const localMedicaEnricherFor = (media: HTMLMediaElement): TextTimecodeEnricher => ({
const localMediaEnricherFor = (media: HTMLMediaElement): TextTimecodeEnricher => ({
composeLinkElementWithTimecode: (raw, timecode) => {
const link = document.createElement('a')
link.href = '#'

View file

@ -1,8 +1,9 @@
{
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "node",
"target": "ES6",
"module": "esnext",
"moduleResolution": "bundler",
"rootDir": ".",
"target": "es6",
"noImplicitAny": true,
"isolatedModules": true,
"strict": true,

View file

@ -1,6 +1,24 @@
import { defineConfig } from 'vitest/config'
export default defineConfig({
plugins: [
{
name: 'virtual-obsidian',
enforce: 'pre',
resolveId(id: string) {
if (id === 'obsidian') {
return '\0obsidian'
}
},
load(id: string) {
if (id === '\0obsidian') {
return `
export class Plugin {}
export default {}`
}
},
},
],
test: {
isolate: false,
pool: 'threads',