diff --git a/.github/workflows/unit-ci.yml b/.github/workflows/unit-ci.yml index fdc569d4..d6453cac 100644 --- a/.github/workflows/unit-ci.yml +++ b/.github/workflows/unit-ci.yml @@ -91,16 +91,34 @@ jobs: - name: Checkout uses: actions/checkout@v4 + - name: Detect LiveSync-owned integration tests + id: integration_tests + shell: bash + run: | + git ls-files -- ':(glob)**/*.integration.spec.ts' ':(glob)**/*.integration.test.ts' > "$RUNNER_TEMP/livesync-integration-tests.txt" + if [[ -s "$RUNNER_TEMP/livesync-integration-tests.txt" ]]; then + echo 'present=true' >> "$GITHUB_OUTPUT" + else + echo 'present=false' >> "$GITHUB_OUTPUT" + fi + + - name: Record delegated integration coverage + if: ${{ steps.integration_tests.outputs.present != 'true' }} + run: echo 'No LiveSync-owned integration tests are present. Commonlib integration tests run in the Commonlib package CI.' >> "$GITHUB_STEP_SUMMARY" + - name: Setup Node.js + if: ${{ steps.integration_tests.outputs.present == 'true' }} uses: actions/setup-node@v4 with: node-version: '24.x' cache: 'npm' - name: Install dependencies + if: ${{ steps.integration_tests.outputs.present == 'true' }} run: npm ci - name: Create environment configuration files + if: ${{ steps.integration_tests.outputs.present == 'true' }} run: | cat < .env BUILD_MODE=dev @@ -118,16 +136,19 @@ jobs: EOF - name: Start CouchDB container + if: ${{ steps.integration_tests.outputs.present == 'true' }} run: npm run test:docker-couchdb:start - name: Start MinIO container + if: ${{ steps.integration_tests.outputs.present == 'true' }} run: npm run test:docker-s3:start - name: Run integration tests + if: ${{ steps.integration_tests.outputs.present == 'true' }} run: npm run test:integration - name: Stop containers - if: always() + if: ${{ always() && steps.integration_tests.outputs.present == 'true' }} run: | npm run test:docker-couchdb:stop || true npm run test:docker-s3:stop || true diff --git a/package-lock.json b/package-lock.json index 99f8ef1a..2bfba016 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15154,12 +15154,16 @@ "license": "MIT" }, "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", + "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], "license": "MIT", "bin": { - "uuid": "dist/bin/uuid" + "uuid": "dist/esm/bin/uuid" } }, "node_modules/vite": { diff --git a/package.json b/package.json index 976df6c0..f82136dc 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "pretty": "npm run prettyNoWrite -- --write --log-level error", "prettyCheck": "npm run prettyNoWrite -- --check", "prettyNoWrite": "prettier --config ./.prettierrc.mjs \"**/*.js\" \"**/*.ts\" \"**/*.json\" ", + "precheck:compatibility": "npm run build", "check:compatibility": "node utils/check-compatibility.js --file main.js --ios 15", "check": "npm run tsc-check && npm run tsc-check:apps && npm run lint && npm run lint:community -- --quiet && npm run svelte-check && npm run check:compatibility", "unittest": "deno test -A --no-check --coverage=cov_profile --v8-flags=--expose-gc --trace-leaks ./src/", @@ -169,6 +170,14 @@ "qrcode-generator": "^1.4.4", "xxhash-wasm-102": "npm:xxhash-wasm@^1.0.2" }, + "overrides": { + "pouchdb-core": { + "uuid": "11.1.1" + }, + "pouchdb-utils": { + "uuid": "11.1.1" + } + }, "workspaces": [ "src/apps/cli", "src/apps/webpeer", diff --git a/src/apps/webapp/obsidianMockExports.unit.spec.ts b/src/apps/webapp/obsidianMockExports.unit.spec.ts index cf0e7407..d82799da 100644 --- a/src/apps/webapp/obsidianMockExports.unit.spec.ts +++ b/src/apps/webapp/obsidianMockExports.unit.spec.ts @@ -6,21 +6,33 @@ const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)) const dependencyFacadePath = path.resolve(repositoryRoot, "src/deps.ts"); const obsidianMockPath = path.resolve(repositoryRoot, "test/harness/obsidian-mock.ts"); +function parseSourceFile(filePath: string): ts.SourceFile { + const source = ts.sys.readFile(filePath); + if (source === undefined) throw new Error(`Could not read ${filePath}`); + return ts.createSourceFile(filePath, source, ts.ScriptTarget.ESNext, true, ts.ScriptKind.TS); +} + +function hasExportModifier(statement: ts.Statement): boolean { + return ts.getModifiers(statement)?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) ?? false; +} + +function addBindingNames(name: ts.BindingName, names: Set): void { + if (ts.isIdentifier(name)) { + names.add(name.text); + return; + } + for (const element of name.elements) { + if (!ts.isOmittedExpression(element)) addBindingNames(element.name, names); + } +} + describe("Webapp Obsidian mock exports", () => { it("provides every value re-exported from Obsidian", () => { - const program = ts.createProgram([dependencyFacadePath, obsidianMockPath], { - module: ts.ModuleKind.ESNext, - moduleResolution: ts.ModuleResolutionKind.Bundler, - skipLibCheck: true, - target: ts.ScriptTarget.ESNext, - }); - const dependencyFacade = program.getSourceFile(dependencyFacadePath); - const obsidianMock = program.getSourceFile(obsidianMockPath); - expect(dependencyFacade).toBeDefined(); - expect(obsidianMock).toBeDefined(); + const dependencyFacade = parseSourceFile(dependencyFacadePath); + const obsidianMock = parseSourceFile(obsidianMockPath); const requiredExports = new Set(); - for (const statement of dependencyFacade!.statements) { + for (const statement of dependencyFacade.statements) { if ( ts.isExportDeclaration(statement) && statement.moduleSpecifier && @@ -38,10 +50,33 @@ describe("Webapp Obsidian mock exports", () => { } } - const checker = program.getTypeChecker(); - const mockSymbol = checker.getSymbolAtLocation(obsidianMock!); - expect(mockSymbol).toBeDefined(); - const availableExports = new Set(checker.getExportsOfModule(mockSymbol!).map((symbol) => symbol.name)); + const availableExports = new Set(); + for (const statement of obsidianMock.statements) { + if ( + ts.isExportDeclaration(statement) && + !statement.isTypeOnly && + statement.exportClause && + ts.isNamedExports(statement.exportClause) + ) { + for (const element of statement.exportClause.elements) { + if (!element.isTypeOnly) availableExports.add(element.name.text); + } + continue; + } + if (!hasExportModifier(statement)) continue; + if ( + (ts.isClassDeclaration(statement) || + ts.isFunctionDeclaration(statement) || + ts.isEnumDeclaration(statement)) && + statement.name + ) { + availableExports.add(statement.name.text); + } else if (ts.isVariableStatement(statement)) { + for (const declaration of statement.declarationList.declarations) { + addBindingNames(declaration.name, availableExports); + } + } + } const missingExports = [...requiredExports].filter((name) => !availableExports.has(name)).sort(); expect(missingExports).toEqual([]);