From e67c83f6eeef7f7fa5fbd9702ee9c8ed2f0f2c96 Mon Sep 17 00:00:00 2001 From: Theo Ephraim Date: Wed, 3 Jun 2026 14:37:51 -0700 Subject: [PATCH 01/10] bump deps --- .bumpy/update-deps.md | 5 ++ bun.lock | 12 ++-- package.json | 6 +- src/lib/GoogleSpreadsheet.ts | 22 ++++--- src/test/exports.test.ts | 107 +++++++++++++++++++++++++++++++++++ 5 files changed, 135 insertions(+), 17 deletions(-) create mode 100644 .bumpy/update-deps.md create mode 100644 src/test/exports.test.ts diff --git a/.bumpy/update-deps.md b/.bumpy/update-deps.md new file mode 100644 index 0000000..33ca250 --- /dev/null +++ b/.bumpy/update-deps.md @@ -0,0 +1,5 @@ +--- +google-spreadsheet: patch +--- + +bump deps diff --git a/bun.lock b/bun.lock index e4d9e15..ddad1b5 100644 --- a/bun.lock +++ b/bun.lock @@ -5,14 +5,14 @@ "": { "name": "google-spreadsheet", "dependencies": { - "es-toolkit": "^1.44.0", - "ky": "^1.14.3", + "es-toolkit": "^1.46.1", + "ky": "^2.0.2", }, "devDependencies": { "@types/node": "^25.2.3", "@typescript-eslint/eslint-plugin": "^5.59.7", "@typescript-eslint/parser": "^5.59.7", - "@varlock/bumpy": "^1.5.1", + "@varlock/bumpy": "^1.6.0", "docsify-cli": "^4.4.4", "eslint": "^8.41.0", "eslint-config-airbnb-base": "^15.0.0", @@ -271,7 +271,7 @@ "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], - "@varlock/bumpy": ["@varlock/bumpy@1.5.1", "", { "bin": { "bumpy": "dist/cli.mjs" } }, "sha512-oSGpOR7OIDJ4UXyX8ZosUAQYYgvBR3BOdNj+WHYXY0DSEWyC4Lzh09iVC9pGflo3vpZ+OtyemGSyL2nWhz/Qdw=="], + "@varlock/bumpy": ["@varlock/bumpy@1.6.0", "", { "bin": { "bumpy": "dist/cli.mjs" } }, "sha512-hrKJ3W897e/h9OkYAbk0J2rfEhC8Y+L5fvuZzAfwTG5r6Zxwa8n1gu1Pb0osuYuolubIr4EzHtDBP3mi9HotOA=="], "@vitest/expect": ["@vitest/expect@4.0.18", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ=="], @@ -481,7 +481,7 @@ "es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="], - "es-toolkit": ["es-toolkit@1.44.0", "", {}, "sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg=="], + "es-toolkit": ["es-toolkit@1.46.1", "", {}, "sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ=="], "esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], @@ -767,7 +767,7 @@ "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], - "ky": ["ky@1.14.3", "", {}, "sha512-9zy9lkjac+TR1c2tG+mkNSVlyOpInnWdSMiue4F+kq8TwJSgv6o8jhLRg8Ho6SnZ9wOYUq/yozts9qQCfk7bIw=="], + "ky": ["ky@2.0.2", "", {}, "sha512-/GmXpo9F9W+f8n4Ivr2iH+7h7wL7jLbLKWkMlpflcCRb6kGjBfTlASEXaZ9qUgNTn4VgS0P2pwxxzQ4EM6Ulgg=="], "latest-version": ["latest-version@5.1.0", "", { "dependencies": { "package-json": "^6.3.0" } }, "sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA=="], diff --git a/package.json b/package.json index 4d81ece..947b9fd 100644 --- a/package.json +++ b/package.json @@ -51,14 +51,14 @@ "test:ci": "vitest run" }, "dependencies": { - "es-toolkit": "^1.44.0", - "ky": "^1.14.3" + "es-toolkit": "^1.46.1", + "ky": "^2.0.2" }, "devDependencies": { "@types/node": "^25.2.3", "@typescript-eslint/eslint-plugin": "^5.59.7", "@typescript-eslint/parser": "^5.59.7", - "@varlock/bumpy": "^1.5.1", + "@varlock/bumpy": "^1.6.0", "docsify-cli": "^4.4.4", "eslint": "^8.41.0", "eslint-config-airbnb-base": "^15.0.0", diff --git a/src/lib/GoogleSpreadsheet.ts b/src/lib/GoogleSpreadsheet.ts index 3bf942b..2826500 100644 --- a/src/lib/GoogleSpreadsheet.ts +++ b/src/lib/GoogleSpreadsheet.ts @@ -142,19 +142,23 @@ export class GoogleSpreadsheet { // create a ky instance with sheet root URL and hooks to handle auth this.sheetsApi = ky.create({ - prefixUrl: `${SHEETS_API_BASE_URL}/${spreadsheetId}`, + prefix: `${SHEETS_API_BASE_URL}/${spreadsheetId}`, timeout: 180_000, hooks: { - beforeRequest: [(r) => this._setAuthRequestHook(r)], - beforeError: [(e) => this._errorHook(e)], + beforeRequest: [ + ({ request }) => this._setAuthRequestHook(request), + ], + beforeError: [ + ({ error }) => this._errorHook(error), + ], }, retry: retryConfig, }); this.driveApi = ky.create({ - prefixUrl: `${DRIVE_API_BASE_URL}/${spreadsheetId}`, + prefix: `${DRIVE_API_BASE_URL}/${spreadsheetId}`, hooks: { - beforeRequest: [(r) => this._setAuthRequestHook(r)], - beforeError: [(e) => this._errorHook(e)], + beforeRequest: [({ request }) => this._setAuthRequestHook(request)], + beforeError: [({ error }) => this._errorHook(error)], }, retry: retryConfig, }); @@ -185,7 +189,9 @@ export class GoogleSpreadsheet { } /** @internal */ - async _errorHook(error: HTTPError) { + async _errorHook(error: Error) { + if (!(error instanceof HTTPError)) return error; + const { response } = error; const errorDataText = await response?.text(); let errorData; @@ -494,7 +500,7 @@ export class GoogleSpreadsheet { const exportUrl = this._spreadsheetUrl.replace('edit', 'export'); const response = await this.sheetsApi.get(exportUrl, { - prefixUrl: '', // unset baseUrl since we're not hitting the normal sheets API + prefix: '', // unset baseUrl since we're not hitting the normal sheets API searchParams: { id: this.spreadsheetId, format: fileType, diff --git a/src/test/exports.test.ts b/src/test/exports.test.ts new file mode 100644 index 0000000..2d572ce --- /dev/null +++ b/src/test/exports.test.ts @@ -0,0 +1,107 @@ +import { + describe, expect, it, beforeAll, afterAll, afterEach, +} from 'vitest'; +import { setTimeout as delay } from 'timers/promises'; +import { ENV } from 'varlock/env'; + +import { GoogleSpreadsheet, GoogleSpreadsheetWorksheet } from '..'; + +import { DOC_IDS, testServiceAccountAuth } from './auth/docs-and-auth'; + +const doc = new GoogleSpreadsheet(DOC_IDS.private, testServiceAccountAuth); +let sheet: GoogleSpreadsheetWorksheet; + +describe('Export/download methods', () => { + beforeAll(async () => { + await doc.loadInfo(); + sheet = await doc.addSheet({ + title: `Export test ${+new Date()}`, + headerValues: ['name', 'value'], + }); + await sheet.addRows([ + { name: 'Alice', value: '100' }, + { name: 'Bob', value: '200' }, + { name: 'Charlie', value: '300' }, + ]); + }); + + afterAll(async () => { + await sheet.delete(); + }); + + // hitting rate limits when running tests on ci - so we add a short delay + if (ENV.TEST_DELAY) afterEach(async () => delay(ENV.TEST_DELAY)); + + describe('document-level exports', () => { + it('can download as XLSX', async () => { + const buffer = await doc.downloadAsXLSX(); + expect(buffer).toBeInstanceOf(ArrayBuffer); + expect(buffer.byteLength).toBeGreaterThan(0); + }); + + it('can download as XLSX stream', async () => { + const stream = await doc.downloadAsXLSX(true); + expect(stream).toBeTruthy(); + // ReadableStream should have a getReader method + expect(typeof (stream as ReadableStream).getReader).toBe('function'); + }); + + it('can download as ODS', async () => { + const buffer = await doc.downloadAsODS(); + expect(buffer).toBeInstanceOf(ArrayBuffer); + expect(buffer.byteLength).toBeGreaterThan(0); + }); + + it('can download as zipped HTML', async () => { + const buffer = await doc.downloadAsZippedHTML(); + expect(buffer).toBeInstanceOf(ArrayBuffer); + expect(buffer.byteLength).toBeGreaterThan(0); + }); + }); + + describe('worksheet-level exports', () => { + it('can download as CSV and verify content', async () => { + const buffer = await sheet.downloadAsCSV(); + expect(buffer).toBeInstanceOf(ArrayBuffer); + expect(buffer.byteLength).toBeGreaterThan(0); + + const csvText = new TextDecoder().decode(buffer); + const lines = csvText.trim().split('\n'); + + // header row + expect(lines[0]).toContain('name'); + expect(lines[0]).toContain('value'); + + // data rows + expect(lines[1]).toContain('Alice'); + expect(lines[1]).toContain('100'); + expect(lines[2]).toContain('Bob'); + expect(lines[2]).toContain('200'); + expect(lines[3]).toContain('Charlie'); + expect(lines[3]).toContain('300'); + }); + + it('can download as CSV stream', async () => { + const stream = await sheet.downloadAsCSV(true); + expect(stream).toBeTruthy(); + expect(typeof (stream as ReadableStream).getReader).toBe('function'); + }); + + it('can download as TSV', async () => { + const buffer = await sheet.downloadAsTSV(); + expect(buffer).toBeInstanceOf(ArrayBuffer); + expect(buffer.byteLength).toBeGreaterThan(0); + + const tsvText = new TextDecoder().decode(buffer); + // TSV uses tabs + expect(tsvText).toContain('\t'); + expect(tsvText).toContain('Alice'); + }); + + it('can download as PDF', async () => { + const buffer = await sheet.downloadAsPDF(); + expect(buffer).toBeInstanceOf(ArrayBuffer); + expect(buffer.byteLength).toBeGreaterThan(0); + }); + }); +}); From d2535cafc7d338e6241c07ae9f269c0fad5d4217 Mon Sep 17 00:00:00 2001 From: Theo Ephraim Date: Wed, 3 Jun 2026 14:37:51 -0700 Subject: [PATCH 02/10] fix: handle consumed response body in ky error hook Clone the response before reading text in beforeError hook since ky may have already consumed the body, causing "Body has already been consumed" errors. --- src/lib/GoogleSpreadsheet.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/GoogleSpreadsheet.ts b/src/lib/GoogleSpreadsheet.ts index 2826500..7bf1c6b 100644 --- a/src/lib/GoogleSpreadsheet.ts +++ b/src/lib/GoogleSpreadsheet.ts @@ -193,12 +193,12 @@ export class GoogleSpreadsheet { if (!(error instanceof HTTPError)) return error; const { response } = error; - const errorDataText = await response?.text(); let errorData; try { + const errorDataText = await response?.clone().text(); errorData = JSON.parse(errorDataText); } catch (e) { - // console.log('parsing json failed', errorDataText); + // body may have already been consumed, or response may not be JSON } if (errorData) { From 0bec5df87a49754f8ece2860640c16cf63649815 Mon Sep 17 00:00:00 2001 From: Theo Ephraim Date: Wed, 3 Jun 2026 14:37:52 -0700 Subject: [PATCH 03/10] fix: use error.data instead of reading consumed response body in ky error hook New ky versions pre-parse the response body into error.data before beforeError hooks run, making response.text() fail. Use error.data directly to extract Google API error details. --- src/lib/GoogleSpreadsheet.ts | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/src/lib/GoogleSpreadsheet.ts b/src/lib/GoogleSpreadsheet.ts index 7bf1c6b..a8f8d55 100644 --- a/src/lib/GoogleSpreadsheet.ts +++ b/src/lib/GoogleSpreadsheet.ts @@ -192,25 +192,18 @@ export class GoogleSpreadsheet { async _errorHook(error: Error) { if (!(error instanceof HTTPError)) return error; - const { response } = error; - let errorData; - try { - const errorDataText = await response?.clone().text(); - errorData = JSON.parse(errorDataText); - } catch (e) { - // body may have already been consumed, or response may not be JSON - } - - if (errorData) { - // usually the error has a code and message, but occasionally not - if (!errorData.error) return error; + // ky pre-parses the response body into error.data (the response body is already consumed) + const errorData = typeof error.data === 'string' ? (() => { + try { return JSON.parse(error.data as string); } catch { return undefined; } + })() : error.data; + if (errorData?.error) { const { code, message } = errorData.error; error.message = `Google API error - [${code}] ${message}`; return error; } - if (_.get(error, 'response.status') === 403) { + if (error.response?.status === 403) { if ('apiKey' in this.auth) { throw new Error('Sheet is private. Use authentication or make public. (see https://github.com/theoephraim/node-google-spreadsheet#a-note-on-authentication for details)'); } From d152662f050acad9a99e162c87da1e8af3db120b Mon Sep 17 00:00:00 2001 From: Theo Ephraim Date: Wed, 3 Jun 2026 14:37:52 -0700 Subject: [PATCH 04/10] increase TEST_DELAY on CI to reduce rate limiting flakiness --- .env.schema | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env.schema b/.env.schema index 4f321a7..5a94bdb 100644 --- a/.env.schema +++ b/.env.schema @@ -24,4 +24,4 @@ CI= # Delay in ms to use when running tests # @type=number -TEST_DELAY=200 \ No newline at end of file +TEST_DELAY=if($CI, 300, 200) \ No newline at end of file From e2769d2e1a00f380dad460f2140babe5018abf92 Mon Sep 17 00:00:00 2001 From: Theo Ephraim Date: Wed, 3 Jun 2026 14:37:53 -0700 Subject: [PATCH 05/10] bump dev deps and simplify export tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - bump @varlock/bumpy and varlock - simplify export tests to avoid rate limiting (8 → 4 tests, 3s delay) --- bun.lock | 8 ++-- package.json | 4 +- src/test/exports.test.ts | 97 +++++++++++++--------------------------- 3 files changed, 36 insertions(+), 73 deletions(-) diff --git a/bun.lock b/bun.lock index ddad1b5..f0c34b9 100644 --- a/bun.lock +++ b/bun.lock @@ -12,7 +12,7 @@ "@types/node": "^25.2.3", "@typescript-eslint/eslint-plugin": "^5.59.7", "@typescript-eslint/parser": "^5.59.7", - "@varlock/bumpy": "^1.6.0", + "@varlock/bumpy": "^1.8.1", "docsify-cli": "^4.4.4", "eslint": "^8.41.0", "eslint-config-airbnb-base": "^15.0.0", @@ -23,7 +23,7 @@ "nock": "^14.0.11", "tsdown": "^0.20.3", "typescript": "^5.9.3", - "varlock": "^1.0.0", + "varlock": "^1.1.0", "vitest": "^4.0.18", }, "peerDependencies": { @@ -271,7 +271,7 @@ "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], - "@varlock/bumpy": ["@varlock/bumpy@1.6.0", "", { "bin": { "bumpy": "dist/cli.mjs" } }, "sha512-hrKJ3W897e/h9OkYAbk0J2rfEhC8Y+L5fvuZzAfwTG5r6Zxwa8n1gu1Pb0osuYuolubIr4EzHtDBP3mi9HotOA=="], + "@varlock/bumpy": ["@varlock/bumpy@1.8.1", "", { "bin": { "bumpy": "dist/cli.mjs" } }, "sha512-HhJ4UYeCPAMtSWYKO9lc5RABjpHjLZdu1lEzCJbgEWo+QOz2JCRcxpDLeH6XQJ1oQddrxUQeSiqcuuElJoCVEw=="], "@vitest/expect": ["@vitest/expect@4.0.18", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ=="], @@ -1121,7 +1121,7 @@ "utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="], - "varlock": ["varlock@1.0.0", "", { "bin": { "varlock": "bin/cli.js" } }, "sha512-0KwOc6x+daO4BOG2kuLVDsbo3+m2avCsEH1ZAccRh/dvr9UliejleD49LHhxLXiVv7kyBgCxARWI89F/EjBwIQ=="], + "varlock": ["varlock@1.1.0", "", { "bin": { "varlock": "bin/cli.js" } }, "sha512-S6wE06TzoGBtVM0CKHN1Z4D9lW6eA3fQ8RmsPx6gZ/nkQtye19fel0gvpPMk37YOCEtHY/vaxkMIVziX6NRFCg=="], "vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], diff --git a/package.json b/package.json index 947b9fd..da71c89 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "@types/node": "^25.2.3", "@typescript-eslint/eslint-plugin": "^5.59.7", "@typescript-eslint/parser": "^5.59.7", - "@varlock/bumpy": "^1.6.0", + "@varlock/bumpy": "^1.8.1", "docsify-cli": "^4.4.4", "eslint": "^8.41.0", "eslint-config-airbnb-base": "^15.0.0", @@ -69,7 +69,7 @@ "nock": "^14.0.11", "tsdown": "^0.20.3", "typescript": "^5.9.3", - "varlock": "^1.0.0", + "varlock": "^1.1.0", "vitest": "^4.0.18" }, "peerDependencies": { diff --git a/src/test/exports.test.ts b/src/test/exports.test.ts index 2d572ce..070f67c 100644 --- a/src/test/exports.test.ts +++ b/src/test/exports.test.ts @@ -2,7 +2,6 @@ import { describe, expect, it, beforeAll, afterAll, afterEach, } from 'vitest'; import { setTimeout as delay } from 'timers/promises'; -import { ENV } from 'varlock/env'; import { GoogleSpreadsheet, GoogleSpreadsheetWorksheet } from '..'; @@ -29,79 +28,43 @@ describe('Export/download methods', () => { await sheet.delete(); }); - // hitting rate limits when running tests on ci - so we add a short delay - if (ENV.TEST_DELAY) afterEach(async () => delay(ENV.TEST_DELAY)); + // export endpoint has tight rate limits + afterEach(async () => delay(3000)); - describe('document-level exports', () => { - it('can download as XLSX', async () => { - const buffer = await doc.downloadAsXLSX(); - expect(buffer).toBeInstanceOf(ArrayBuffer); - expect(buffer.byteLength).toBeGreaterThan(0); - }); - - it('can download as XLSX stream', async () => { - const stream = await doc.downloadAsXLSX(true); - expect(stream).toBeTruthy(); - // ReadableStream should have a getReader method - expect(typeof (stream as ReadableStream).getReader).toBe('function'); - }); - - it('can download as ODS', async () => { - const buffer = await doc.downloadAsODS(); - expect(buffer).toBeInstanceOf(ArrayBuffer); - expect(buffer.byteLength).toBeGreaterThan(0); - }); - - it('can download as zipped HTML', async () => { - const buffer = await doc.downloadAsZippedHTML(); - expect(buffer).toBeInstanceOf(ArrayBuffer); - expect(buffer.byteLength).toBeGreaterThan(0); - }); + it('can download document as XLSX', async () => { + const buffer = await doc.downloadAsXLSX(); + expect(buffer).toBeInstanceOf(ArrayBuffer); + expect(buffer.byteLength).toBeGreaterThan(0); }); - describe('worksheet-level exports', () => { - it('can download as CSV and verify content', async () => { - const buffer = await sheet.downloadAsCSV(); - expect(buffer).toBeInstanceOf(ArrayBuffer); - expect(buffer.byteLength).toBeGreaterThan(0); - - const csvText = new TextDecoder().decode(buffer); - const lines = csvText.trim().split('\n'); + it('can download worksheet as CSV and verify content', async () => { + const buffer = await sheet.downloadAsCSV(); + expect(buffer).toBeInstanceOf(ArrayBuffer); + expect(buffer.byteLength).toBeGreaterThan(0); - // header row - expect(lines[0]).toContain('name'); - expect(lines[0]).toContain('value'); + const csvText = new TextDecoder().decode(buffer); + const lines = csvText.trim().split('\n'); - // data rows - expect(lines[1]).toContain('Alice'); - expect(lines[1]).toContain('100'); - expect(lines[2]).toContain('Bob'); - expect(lines[2]).toContain('200'); - expect(lines[3]).toContain('Charlie'); - expect(lines[3]).toContain('300'); - }); - - it('can download as CSV stream', async () => { - const stream = await sheet.downloadAsCSV(true); - expect(stream).toBeTruthy(); - expect(typeof (stream as ReadableStream).getReader).toBe('function'); - }); + expect(lines[0]).toContain('name'); + expect(lines[0]).toContain('value'); + expect(lines[1]).toContain('Alice'); + expect(lines[2]).toContain('Bob'); + expect(lines[3]).toContain('Charlie'); + }); - it('can download as TSV', async () => { - const buffer = await sheet.downloadAsTSV(); - expect(buffer).toBeInstanceOf(ArrayBuffer); - expect(buffer.byteLength).toBeGreaterThan(0); + it('can download worksheet as TSV', async () => { + const buffer = await sheet.downloadAsTSV(); + expect(buffer).toBeInstanceOf(ArrayBuffer); + expect(buffer.byteLength).toBeGreaterThan(0); - const tsvText = new TextDecoder().decode(buffer); - // TSV uses tabs - expect(tsvText).toContain('\t'); - expect(tsvText).toContain('Alice'); - }); + const tsvText = new TextDecoder().decode(buffer); + expect(tsvText).toContain('\t'); + expect(tsvText).toContain('Alice'); + }); - it('can download as PDF', async () => { - const buffer = await sheet.downloadAsPDF(); - expect(buffer).toBeInstanceOf(ArrayBuffer); - expect(buffer.byteLength).toBeGreaterThan(0); - }); + it('can download worksheet as PDF', async () => { + const buffer = await sheet.downloadAsPDF(); + expect(buffer).toBeInstanceOf(ArrayBuffer); + expect(buffer.byteLength).toBeGreaterThan(0); }); }); From ac83f66fd6390cb34685c0f69b24cfbfa58acfae Mon Sep 17 00:00:00 2001 From: Theo Ephraim Date: Wed, 3 Jun 2026 14:37:53 -0700 Subject: [PATCH 06/10] bump version to minor for ky v2 upgrade --- .bumpy/update-deps.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.bumpy/update-deps.md b/.bumpy/update-deps.md index 33ca250..d80dd0e 100644 --- a/.bumpy/update-deps.md +++ b/.bumpy/update-deps.md @@ -1,5 +1,5 @@ --- -google-spreadsheet: patch +google-spreadsheet: minor --- bump deps From 0b2c3ce9f02d2a2e49fe238196a5dcae0065b6ab Mon Sep 17 00:00:00 2001 From: Theo Ephraim Date: Wed, 3 Jun 2026 14:37:54 -0700 Subject: [PATCH 07/10] add ky v2 breaking change notes to bump file --- .bumpy/update-deps.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.bumpy/update-deps.md b/.bumpy/update-deps.md index d80dd0e..bf399bb 100644 --- a/.bumpy/update-deps.md +++ b/.bumpy/update-deps.md @@ -2,4 +2,6 @@ google-spreadsheet: minor --- -bump deps +upgrade ky from v1 to v2 + +If you use `doc.sheetsApi` or `doc.driveApi` directly, note that ky v2 changed its hook signatures (hooks now receive state objects instead of direct Request/Error params) and renamed `prefixUrl` to `prefix`. See the [ky v2 migration guide](https://github.com/sindresorhus/ky/releases/tag/v2.0.0) for details. From e7fd582f0360f01b60f06618a88d1e1c05d33aac Mon Sep 17 00:00:00 2001 From: Theo Ephraim Date: Wed, 3 Jun 2026 14:37:54 -0700 Subject: [PATCH 08/10] update CI workflows to bumpy recommended setup - split bumpy check into dedicated pull_request_target workflow - split release into plan/version-pr/publish jobs with environment gate - enable OIDC provenance and npm staged publishing - bump varlock and @varlock/bumpy to latest --- .bumpy/_config.json | 7 ++- .github/workflows/bumpy-check.yml | 31 +++++++++++ .github/workflows/ci.yml | 4 -- .github/workflows/release.yml | 87 ++++++++++++++++++++++--------- bun.lock | 8 +-- package.json | 4 +- 6 files changed, 104 insertions(+), 37 deletions(-) create mode 100644 .github/workflows/bumpy-check.yml diff --git a/.bumpy/_config.json b/.bumpy/_config.json index 8fd7645..98c16aa 100644 --- a/.bumpy/_config.json +++ b/.bumpy/_config.json @@ -8,5 +8,10 @@ "theoephraim" ] } - ] + ], + "publish": { + "publishManager": "npm", + "provenance": true, + "npmStaged": true + } } \ No newline at end of file diff --git a/.github/workflows/bumpy-check.yml b/.github/workflows/bumpy-check.yml new file mode 100644 index 0000000..d7808b1 --- /dev/null +++ b/.github/workflows/bumpy-check.yml @@ -0,0 +1,31 @@ +# Uses `pull_request_target` so fork PR comments work — runs with write +# perms and secrets, so it MUST NOT execute PR code (no `bun install`, no +# PR-defined scripts). Bumpy only reads files; its version is resolved from +# the base branch's package.json. +name: Bumpy Check +on: pull_request_target + +permissions: + pull-requests: write + contents: read + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + ref: ${{ github.event.pull_request.head.sha }} + - uses: oven-sh/setup-bun@v2 + + - name: Resolve bumpy version from base + run: | + git fetch origin main --depth=1 + VERSION=$(git show "origin/main:package.json" \ + | jq -r '.devDependencies["@varlock/bumpy"] // .dependencies["@varlock/bumpy"]' \ + | sed 's/[\^~]//') + echo "BUMPY_VERSION=$VERSION" >> "$GITHUB_ENV" + + - run: bunx "@varlock/bumpy@$BUMPY_VERSION" ci check + env: + GH_TOKEN: ${{ github.token }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7bfb3b7..1ee94a3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,10 +32,6 @@ jobs: - name: Lint run: bun run lint --format junit -o reports/junit/js-lint-results.xml - - run: bunx @varlock/bumpy ci check - env: - GH_TOKEN: ${{ github.token }} - - name: Test run: bun run test env: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d0ca256..fc65d90 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,41 +4,76 @@ on: push: branches: [main] -concurrency: ${{ github.workflow }}-${{ github.ref }} +concurrency: + group: bumpy-release + cancel-in-progress: false jobs: - release: + plan: + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + mode: ${{ steps.plan.outputs.mode }} + packages: ${{ steps.plan.outputs.packages }} + bumpy_version: ${{ steps.bumpy-version.outputs.version }} + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + - uses: oven-sh/setup-bun@v2 + - id: bumpy-version + name: Resolve bumpy version + run: | + VERSION=$(jq -r '.devDependencies["@varlock/bumpy"] // .dependencies["@varlock/bumpy"]' package.json | sed 's/[\^~]//') + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "BUMPY_VERSION=$VERSION" >> "$GITHUB_ENV" + - id: plan + run: bunx "@varlock/bumpy@$BUMPY_VERSION" ci plan + env: + GH_TOKEN: ${{ github.token }} + + version-pr: + needs: plan + if: needs.plan.outputs.mode == 'version-pr' runs-on: ubuntu-latest permissions: contents: write pull-requests: write - id-token: write # required for trusted publishing - + env: + BUMPY_VERSION: ${{ needs.plan.outputs.bumpy_version }} steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Use Node.js 24 - uses: actions/setup-node@v6 + - uses: actions/checkout@v6 with: - node-version: 24 - registry-url: "https://registry.npmjs.org" - - # Ensure npm 11.5.1 or later is installed (for trusted publishing) - - name: Update npm - run: npm install -g npm@latest - - - name: Setup Bun - uses: oven-sh/setup-bun@v2 - - - name: Install dependencies - run: bun install - - - name: Build - run: bun run build + fetch-depth: 0 + - uses: oven-sh/setup-bun@v2 + - run: bunx "@varlock/bumpy@$BUMPY_VERSION" ci release --expect-mode version-pr + env: + GH_TOKEN: ${{ github.token }} + BUMPY_GH_TOKEN: ${{ secrets.BUMPY_GH_TOKEN }} - - name: Create Release PR or Publish - run: bunx @varlock/bumpy ci release + publish: + needs: plan + if: needs.plan.outputs.mode == 'publish' + runs-on: ubuntu-latest + environment: publish + permissions: + contents: write + id-token: write + env: + BUMPY_VERSION: ${{ needs.plan.outputs.bumpy_version }} + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + - uses: oven-sh/setup-bun@v2 + - uses: actions/setup-node@v6 + with: + node-version: latest + - run: npm install -g npm@latest # needed for oidc/staged + - run: bun install + - run: bun run build + - run: bunx "@varlock/bumpy@$BUMPY_VERSION" ci release --expect-mode publish env: GH_TOKEN: ${{ github.token }} BUMPY_GH_TOKEN: ${{ secrets.BUMPY_GH_TOKEN }} diff --git a/bun.lock b/bun.lock index f0c34b9..f25f719 100644 --- a/bun.lock +++ b/bun.lock @@ -12,7 +12,7 @@ "@types/node": "^25.2.3", "@typescript-eslint/eslint-plugin": "^5.59.7", "@typescript-eslint/parser": "^5.59.7", - "@varlock/bumpy": "^1.8.1", + "@varlock/bumpy": "^1.13.0", "docsify-cli": "^4.4.4", "eslint": "^8.41.0", "eslint-config-airbnb-base": "^15.0.0", @@ -23,7 +23,7 @@ "nock": "^14.0.11", "tsdown": "^0.20.3", "typescript": "^5.9.3", - "varlock": "^1.1.0", + "varlock": "^1.4.0", "vitest": "^4.0.18", }, "peerDependencies": { @@ -271,7 +271,7 @@ "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], - "@varlock/bumpy": ["@varlock/bumpy@1.8.1", "", { "bin": { "bumpy": "dist/cli.mjs" } }, "sha512-HhJ4UYeCPAMtSWYKO9lc5RABjpHjLZdu1lEzCJbgEWo+QOz2JCRcxpDLeH6XQJ1oQddrxUQeSiqcuuElJoCVEw=="], + "@varlock/bumpy": ["@varlock/bumpy@1.13.0", "", { "bin": { "bumpy": "dist/cli.mjs" } }, "sha512-foyaNxBvFELqx6/cVvIUcDVBF+7jzWs8cvUFW+2UsVER1KEA+oobReTg3yBASL4X+lBqJhNukM4cd/PqsyK8AQ=="], "@vitest/expect": ["@vitest/expect@4.0.18", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ=="], @@ -1121,7 +1121,7 @@ "utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="], - "varlock": ["varlock@1.1.0", "", { "bin": { "varlock": "bin/cli.js" } }, "sha512-S6wE06TzoGBtVM0CKHN1Z4D9lW6eA3fQ8RmsPx6gZ/nkQtye19fel0gvpPMk37YOCEtHY/vaxkMIVziX6NRFCg=="], + "varlock": ["varlock@1.4.0", "", { "bin": { "varlock": "bin/cli.js" } }, "sha512-dVQyJlcZjTVnBiCs4/AyyH1pxPU+cqXSTauWcWFeKng6NrjX3y84klK1g2oiBHkfESOtzpkx/oYP7oJCGU4NuA=="], "vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], diff --git a/package.json b/package.json index da71c89..c739467 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "@types/node": "^25.2.3", "@typescript-eslint/eslint-plugin": "^5.59.7", "@typescript-eslint/parser": "^5.59.7", - "@varlock/bumpy": "^1.8.1", + "@varlock/bumpy": "^1.13.0", "docsify-cli": "^4.4.4", "eslint": "^8.41.0", "eslint-config-airbnb-base": "^15.0.0", @@ -69,7 +69,7 @@ "nock": "^14.0.11", "tsdown": "^0.20.3", "typescript": "^5.9.3", - "varlock": "^1.1.0", + "varlock": "^1.4.0", "vitest": "^4.0.18" }, "peerDependencies": { From 83aa5d2512e9b8b3a111013261516c49fbbdecc8 Mon Sep 17 00:00:00 2001 From: Theo Ephraim Date: Wed, 3 Jun 2026 14:37:55 -0700 Subject: [PATCH 09/10] bump es-toolkit and remove redundant publishManager config --- .bumpy/_config.json | 1 - bun.lock | 4 ++-- package.json | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.bumpy/_config.json b/.bumpy/_config.json index 98c16aa..517a272 100644 --- a/.bumpy/_config.json +++ b/.bumpy/_config.json @@ -10,7 +10,6 @@ } ], "publish": { - "publishManager": "npm", "provenance": true, "npmStaged": true } diff --git a/bun.lock b/bun.lock index f25f719..17040db 100644 --- a/bun.lock +++ b/bun.lock @@ -5,7 +5,7 @@ "": { "name": "google-spreadsheet", "dependencies": { - "es-toolkit": "^1.46.1", + "es-toolkit": "^1.47.0", "ky": "^2.0.2", }, "devDependencies": { @@ -481,7 +481,7 @@ "es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="], - "es-toolkit": ["es-toolkit@1.46.1", "", {}, "sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ=="], + "es-toolkit": ["es-toolkit@1.47.0", "", {}, "sha512-n1GuoD0WEQZMBk5tttoZSqwgyLx01oqa5XsBmCHwPyNe1S9jPBEmtR2pSgp2kJuWE3ciFZ6yRHmY4pM4C3OOkw=="], "esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], diff --git a/package.json b/package.json index c739467..ba0dbf7 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "test:ci": "vitest run" }, "dependencies": { - "es-toolkit": "^1.46.1", + "es-toolkit": "^1.47.0", "ky": "^2.0.2" }, "devDependencies": { From 83619a0e12e51ad65cb9912b2e98698a06bd74ee Mon Sep 17 00:00:00 2001 From: Theo Ephraim Date: Wed, 3 Jun 2026 14:37:55 -0700 Subject: [PATCH 10/10] stop claude from attributing itself commits --- .claude/settings.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .claude/settings.json diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..0c03c6a --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,6 @@ +{ + "attribution": { + "commit": "", + "pr": "" + } +} \ No newline at end of file