diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8ff2b65af44c9..204ec883633a7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -79,6 +79,38 @@ jobs: run: npm run test:cov if: ${{ !cancelled() }} + cli-unit-tests-win: + name: CLI (Windows) + runs-on: windows-latest + defaults: + run: + working-directory: ./cli + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Setup typescript-sdk + run: npm ci && npm run build + working-directory: ./open-api/typescript-sdk + + - name: Install deps + run: npm ci + + # Skip linter & formatter in Windows test. + - name: Run tsc + run: npm run check + if: ${{ !cancelled() }} + + - name: Run unit tests & coverage + run: npm run test:cov + if: ${{ !cancelled() }} + web-unit-tests: name: Web runs-on: ubuntu-latest diff --git a/cli/src/utils.spec.ts b/cli/src/utils.spec.ts index 409aa8664e83d..0094b329b8e39 100644 --- a/cli/src/utils.spec.ts +++ b/cli/src/utils.spec.ts @@ -1,4 +1,5 @@ import mockfs from 'mock-fs'; +import { readFileSync } from 'node:fs'; import { CrawlOptions, crawl } from 'src/utils'; interface Test { @@ -9,6 +10,10 @@ interface Test { const cwd = process.cwd(); +const readContent = (path: string) => { + return readFileSync(path).toString(); +}; + const extensions = [ '.jpg', '.jpeg', @@ -256,7 +261,8 @@ const tests: Test[] = [ { test: 'should support ignoring absolute paths', options: { - pathsToCrawl: ['/'], + // Currently, fast-glob has some caveat when dealing with `/`. + pathsToCrawl: ['/*s'], recursive: true, exclusionPattern: '/images/**', }, @@ -276,14 +282,16 @@ describe('crawl', () => { describe('crawl', () => { for (const { test, options, files } of tests) { it(test, async () => { - mockfs(Object.fromEntries(Object.keys(files).map((file) => [file, '']))); + // The file contents is the same as the path. + mockfs(Object.fromEntries(Object.keys(files).map((file) => [file, file]))); const actual = await crawl({ ...options, extensions }); const expected = Object.entries(files) .filter((entry) => entry[1]) .map(([file]) => file); - expect(actual.sort()).toEqual(expected.sort()); + // Compare file's content instead of path since a file can be represent in multiple ways. + expect(actual.map((path) => readContent(path)).sort()).toEqual(expected.sort()); }); } }); diff --git a/cli/src/utils.ts b/cli/src/utils.ts index 4919a2b3cabdd..67948e0bd211a 100644 --- a/cli/src/utils.ts +++ b/cli/src/utils.ts @@ -1,8 +1,9 @@ import { getMyUser, init, isHttpError } from '@immich/sdk'; -import { glob } from 'fast-glob'; +import { convertPathToPattern, glob } from 'fast-glob'; import { createHash } from 'node:crypto'; import { createReadStream } from 'node:fs'; import { readFile, stat, writeFile } from 'node:fs/promises'; +import { platform } from 'node:os'; import { join, resolve } from 'node:path'; import yaml from 'yaml'; @@ -106,6 +107,11 @@ export interface CrawlOptions { exclusionPattern?: string; extensions: string[]; } + +const convertPathToPatternOnWin = (path: string) => { + return platform() === 'win32' ? convertPathToPattern(path) : path; +}; + export const crawl = async (options: CrawlOptions): Promise => { const { extensions: extensionsWithPeriod, recursive, pathsToCrawl, exclusionPattern, includeHidden } = options; const extensions = extensionsWithPeriod.map((extension) => extension.replace('.', '')); @@ -124,11 +130,11 @@ export const crawl = async (options: CrawlOptions): Promise => { if (stats.isFile() || stats.isSymbolicLink()) { crawledFiles.push(absolutePath); } else { - patterns.push(absolutePath); + patterns.push(convertPathToPatternOnWin(absolutePath)); } } catch (error: any) { if (error.code === 'ENOENT') { - patterns.push(currentPath); + patterns.push(convertPathToPatternOnWin(currentPath)); } else { throw error; } diff --git a/cli/vite.config.ts b/cli/vite.config.ts index f5dd7c8e15591..f538a9a357d8c 100644 --- a/cli/vite.config.ts +++ b/cli/vite.config.ts @@ -2,6 +2,7 @@ import { defineConfig } from 'vite'; import tsconfigPaths from 'vite-tsconfig-paths'; export default defineConfig({ + resolve: { alias: { src: '/src' } }, build: { rollupOptions: { input: 'src/index.ts',