diff --git a/.env.test.example b/.env.test.example new file mode 100644 index 0000000..a6b9a54 --- /dev/null +++ b/.env.test.example @@ -0,0 +1,9 @@ +############################################################################### +# The following environment variables are used to run tests. +# Create a specific account for testing to avoid issues. +############################################################################### + +# The email address for your Prismic account. +E2E_PRISMIC_EMAIL= +# The password to your Prismic account. +E2E_PRISMIC_PASSWORD= diff --git a/.github/workflows/prerelease-canary.yml b/.github/workflows/prerelease-canary.yml index decf71b..c1c14f6 100644 --- a/.github/workflows/prerelease-canary.yml +++ b/.github/workflows/prerelease-canary.yml @@ -13,6 +13,8 @@ jobs: publish: if: github.repository_owner == 'prismicio' runs-on: ubuntu-latest + env: + MODE: production steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 diff --git a/.github/workflows/prerelease-pr.yml b/.github/workflows/prerelease-pr.yml index 715055e..88cc9fa 100644 --- a/.github/workflows/prerelease-pr.yml +++ b/.github/workflows/prerelease-pr.yml @@ -11,6 +11,8 @@ jobs: publish: if: github.repository_owner == 'prismicio' runs-on: ubuntu-latest + env: + MODE: production steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 59e85ac..3bcdfa5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,6 +14,8 @@ on: jobs: release-please: runs-on: ubuntu-latest + env: + MODE: production steps: - uses: googleapis/release-please-action@v4 id: release diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index b423b0c..dea4c33 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -16,6 +16,8 @@ jobs: build: runs-on: ubuntu-latest + env: + MODE: production steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 diff --git a/package-lock.json b/package-lock.json index 0e06f3a..28d5b93 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,14 +15,16 @@ "@prismicio/types-internal": "3.16.1", "@sentry/node-core": "10.42.0", "@types/node": "25.0.9", - "@vitest/coverage-v8": "4.0.17", "change-case": "5.4.4", + "concurrently": "^9.2.1", + "cross-env": "^10.1.0", "dedent": "^1.7.2", "detect-indent": "^7.0.2", "magicast": "0.5.1", "oxfmt": "^0.24.0", "oxlint": "1.39.0", "prismic-ts-codegen": "^0.1.28", + "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tsdown": "0.19.0", "typescript": "5.9.3", @@ -30,7 +32,7 @@ "zod": "^4.3.6" }, "engines": { - "node": ">=24" + "node": ">=20" } }, "node_modules/@babel/code-frame": { @@ -122,16 +124,6 @@ "node": ">=6.9.0" } }, - "node_modules/@bcoe/v8-coverage": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", - "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, "node_modules/@emnapi/core": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", @@ -166,6 +158,13 @@ "tslib": "^2.4.0" } }, + "node_modules/@epic-web/invariant": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz", + "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==", + "dev": true, + "license": "MIT" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", @@ -1837,37 +1836,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@vitest/coverage-v8": { - "version": "4.0.17", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.17.tgz", - "integrity": "sha512-/6zU2FLGg0jsd+ePZcwHRy3+WpNTBBhDY56P4JTRqUN/Dp6CvOEa9HrikcQ4KfV2b2kAHUFB4dl1SuocWXSFEw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@bcoe/v8-coverage": "^1.0.2", - "@vitest/utils": "4.0.17", - "ast-v8-to-istanbul": "^0.3.10", - "istanbul-lib-coverage": "^3.2.2", - "istanbul-lib-report": "^3.0.1", - "istanbul-reports": "^3.2.0", - "magicast": "^0.5.1", - "obug": "^2.1.1", - "std-env": "^3.10.0", - "tinyrainbow": "^3.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@vitest/browser": "4.0.17", - "vitest": "4.0.17" - }, - "peerDependenciesMeta": { - "@vitest/browser": { - "optional": true - } - } - }, "node_modules/@vitest/expect": { "version": "4.0.17", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.17.tgz", @@ -2002,6 +1970,32 @@ "acorn": "^8" } }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/ansis": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz", @@ -2049,18 +2043,6 @@ "url": "https://github.com/sponsors/sxzz" } }, - "node_modules/ast-v8-to-istanbul": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz", - "integrity": "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.31", - "estree-walker": "^3.0.3", - "js-tokens": "^10.0.0" - } - }, "node_modules/birpc": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/birpc/-/birpc-4.0.0.tgz", @@ -2149,6 +2131,23 @@ "node": ">=18" } }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/change-case": { "version": "5.4.4", "resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz", @@ -2163,6 +2162,41 @@ "dev": true, "license": "MIT" }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, "node_modules/common-tags": { "version": "1.8.2", "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", @@ -2173,6 +2207,80 @@ "node": ">=4.0.0" } }, + "node_modules/concurrently": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz", + "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.8.3", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/cross-env": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", + "integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@epic-web/invariant": "^1.0.0", + "cross-spawn": "^7.0.6" + }, + "bin": { + "cross-env": "dist/bin/cross-env.js", + "cross-env-shell": "dist/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", @@ -2271,6 +2379,13 @@ } } }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, "node_modules/empathic": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", @@ -2340,6 +2455,16 @@ "@esbuild/win32-x64": "0.27.3" } }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/estree-walker": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", @@ -2487,6 +2612,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-tsconfig": { "version": "4.13.6", "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", @@ -2566,13 +2701,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true, - "license": "MIT" - }, "node_modules/imgix-url-builder": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/imgix-url-builder/-/imgix-url-builder-0.0.6.tgz", @@ -2681,6 +2809,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -2714,44 +2852,12 @@ "node": ">=0.10.0" } }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-reports": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", - "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } + "license": "ISC" }, "node_modules/jiti": { "version": "1.21.7", @@ -2777,13 +2883,6 @@ "@sideway/pinpoint": "^2.0.0" } }, - "node_modules/js-tokens": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", - "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", - "dev": true, - "license": "MIT" - }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -2879,22 +2978,6 @@ "source-map-js": "^1.2.1" } }, - "node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/map-obj": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", @@ -3234,6 +3317,16 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -3512,6 +3605,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", @@ -3678,6 +3781,16 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/semver": { "version": "7.7.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", @@ -3691,6 +3804,42 @@ "node": ">=10" } }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/siginfo": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", @@ -3758,6 +3907,34 @@ "dev": true, "license": "MIT" }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-indent": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-4.1.1.tgz", @@ -4579,6 +4756,22 @@ "node": ">= 8" } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/why-is-node-running": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", @@ -4596,6 +4789,34 @@ "node": ">=8" } }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -4603,6 +4824,25 @@ "dev": true, "license": "ISC" }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/yargs-parser": { "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", diff --git a/package.json b/package.json index ba888d4..88a3c09 100644 --- a/package.json +++ b/package.json @@ -30,36 +30,38 @@ "prepare": "npm run build", "lint": "oxlint --deny-warnings", "types": "tsc --noEmit", - "unit": "vitest run --coverage", - "unit:watch": "vitest watch", - "test": "npm run lint && npm run types && npm run unit && npm run build" + "unit": "cross-env MODE=test tsdown --logLevel=warn && vitest run", + "unit:watch": "concurrently --raw --kill-others \"cross-env MODE=test tsdown --watch --logLevel=warn\" \"vitest watch\"", + "test": "npm run lint && npm run types && npm run unit" }, "devDependencies": { "@prismicio/types-internal": "3.16.1", "@sentry/node-core": "10.42.0", "@types/node": "25.0.9", - "@vitest/coverage-v8": "4.0.17", "change-case": "5.4.4", + "concurrently": "^9.2.1", + "cross-env": "^10.1.0", "dedent": "^1.7.2", "detect-indent": "^7.0.2", "magicast": "0.5.1", "oxfmt": "^0.24.0", "oxlint": "1.39.0", "prismic-ts-codegen": "^0.1.28", + "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tsdown": "0.19.0", "typescript": "5.9.3", "vitest": "4.0.17", "zod": "^4.3.6" }, - "engines": { - "node": ">=20" - }, "devEngines": { "runtime": { "name": "node", "version": ">=24", "onFail": "warn" } + }, + "engines": { + "node": ">=20" } } diff --git a/src/commands/init.ts b/src/commands/init.ts index 9e85f90..6e0aa24 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -28,6 +28,7 @@ USAGE FLAGS -r, --repo string Repository name + --no-browser Skip opening the browser automatically during login -h, --help Show help for command EXAMPLES @@ -43,6 +44,7 @@ export async function init(): Promise { options: { help: { type: "boolean", short: "h" }, repo: { type: "string", short: "r" }, + "no-browser": { type: "boolean" }, }, }); @@ -87,9 +89,13 @@ export async function init(): Promise { console.info("Not logged in. Starting login..."); const { email } = await createLoginSession({ onReady: (url) => { - console.info("Opening browser to complete login..."); - console.info(`If the browser doesn't open, visit: ${url}`); - openBrowser(url); + if (values["no-browser"]) { + console.info(`Open this URL to log in: ${url}`); + } else { + console.info("Opening browser to complete login..."); + console.info(`If the browser doesn't open, visit: ${url}`); + openBrowser(url); + } }, }); console.info(`Logged in as ${email}`); diff --git a/src/commands/login.ts b/src/commands/login.ts index a4618ea..f369fac 100644 --- a/src/commands/login.ts +++ b/src/commands/login.ts @@ -10,7 +10,8 @@ USAGE prismic login [flags] FLAGS - -h, --help Show help for command + --no-browser Skip opening the browser automatically + -h, --help Show help for command LEARN MORE Use \`prismic --help\` for more information about a command. @@ -19,7 +20,10 @@ LEARN MORE export async function login(): Promise { const { values } = parseArgs({ args: process.argv.slice(3), - options: { help: { type: "boolean", short: "h" } }, + options: { + help: { type: "boolean", short: "h" }, + "no-browser": { type: "boolean" }, + }, }); if (values.help) { @@ -29,9 +33,13 @@ export async function login(): Promise { const { email } = await createLoginSession({ onReady: (url) => { - console.info("Opening browser to complete login..."); - console.info(`If the browser doesn't open, visit: ${url}`); - openBrowser(url); + if (values["no-browser"]) { + console.info(`Open this URL to log in: ${url}`); + } else { + console.info("Opening browser to complete login..."); + console.info(`If the browser doesn't open, visit: ${url}`); + openBrowser(url); + } }, }); diff --git a/src/env.ts b/src/env.ts index 714c41b..17ddce5 100644 --- a/src/env.ts +++ b/src/env.ts @@ -7,6 +7,7 @@ const Env = z.object({ MODE: z.string(), DEV: z.stringbool(), PROD: z.stringbool(), + TEST: z.stringbool(), PRISMIC_SENTRY_DSN: z._default(z.httpUrl(), DEFAULT_PRISMIC_SENTRY_DSN), PRISMIC_SENTRY_ENVIRONMENT: z.optional(z.string()), PRISMIC_SENTRY_ENABLED: z.optional(z.stringbool()), @@ -14,8 +15,9 @@ const Env = z.object({ }); export const env = z.parse(Env, { - MODE: process.env.MODE, - DEV: JSON.stringify(process.env.MODE !== "production"), - PROD: JSON.stringify(process.env.MODE === "production"), ...process.env, + MODE: process.env.MODE, + DEV: process.env.DEV, + PROD: process.env.PROD, + TEST: process.env.TEST, }); diff --git a/src/index.ts b/src/index.ts index 7662256..3767c34 100644 --- a/src/index.ts +++ b/src/index.ts @@ -54,7 +54,7 @@ const SKIP_REFRESH_COMMANDS = new Set(["login", "logout"]); const { positionals, - values: { version }, + values: { version, help }, } = parseArgs({ options: { help: { type: "boolean", short: "h" }, @@ -84,7 +84,7 @@ if (version) { sentrySetTag("framework", framework.id); } - if (command && !SKIP_REFRESH_COMMANDS.has(command)) { + if (command && !help && !SKIP_REFRESH_COMMANDS.has(command)) { // Refreesh the token and identify the user in the background. refreshToken() .then(async (token) => { diff --git a/src/lib/segment.ts b/src/lib/segment.ts index 3336d21..b42ad41 100644 --- a/src/lib/segment.ts +++ b/src/lib/segment.ts @@ -5,6 +5,7 @@ import { homedir } from "node:os"; import { join } from "node:path"; import packageJson from "../../package.json" with { type: "json" }; +import { env } from "../env"; const SEGMENT_WRITE_KEY = process.env.PRISMIC_ENV && process.env.PRISMIC_ENV !== "production" @@ -28,6 +29,9 @@ const identifyQueue: Array> = []; export async function initSegment(): Promise { try { + if (env.TEST) { + return; + } enabled = await isTelemetryEnabled(); if (!enabled) { return; diff --git a/test/index.test-d.ts b/test/index.test-d.ts deleted file mode 100644 index 5623cef..0000000 --- a/test/index.test-d.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { expectTypeOf } from "vitest"; - -import { it } from "./it"; - -// TODO: Test the package's public types -// See: https://vitest.dev/guide/testing-types.html - -it("placeholder test", () => { - expectTypeOf(true).toBeBoolean(); -}); diff --git a/test/index.test.ts b/test/index.test.ts index 62dd9f1..f214244 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -1,10 +1,13 @@ -import { expect } from "vitest"; - import { it } from "./it"; -// TODO: Test the package's exports -// See: https://vitest.dev/api/ +it("supports --help", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("", ["--help"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("USAGE"); +}); -it("placeholder test", () => { - expect(true).toBe(true); +it("prints help text by default", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic(""); + expect(exitCode).toBe(0); + expect(stdout).toContain("USAGE"); }); diff --git a/test/init.test.ts b/test/init.test.ts new file mode 100644 index 0000000..93dcd64 --- /dev/null +++ b/test/init.test.ts @@ -0,0 +1,105 @@ +import { readFile, writeFile, access, rm } from "node:fs/promises"; + +import { captureOutput, it } from "./it"; + +it("supports --help", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("init", ["--help"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("USAGE"); +}); + +it("fails if prismic.config.json already exists", async ({ expect, prismic }) => { + const { exitCode, stderr } = await prismic("init", ["--repo", "test"]); + expect(exitCode).toBe(1); + expect(stderr).toContain("already initialized"); +}); + +it("fails if --repo is not provided and no legacy config exists", async ({ + expect, + project, + prismic, +}) => { + await rm(new URL("prismic.config.json", project)); + const { exitCode, stderr } = await prismic("init"); + expect(exitCode).toBe(1); + expect(stderr).toContain("Missing required flag"); +}); + +it("initializes a project with --repo when logged in", async ({ + expect, + project, + prismic, + repo, +}) => { + await rm(new URL("prismic.config.json", project)); + + const { exitCode, stdout } = await prismic("init", ["--repo", repo]); + expect(exitCode).toBe(0); + expect(stdout).toContain(`Initialized Prismic for repository "${repo}"`); + + const configRaw = await readFile(new URL("prismic.config.json", project), "utf-8"); + const config = JSON.parse(configRaw); + expect(config.repositoryName).toBe(repo); +}, 60_000); + +it("triggers login flow when not logged in, then initializes", async ({ + expect, + project, + prismic, + token, + logout, + repo, +}) => { + await rm(new URL("prismic.config.json", project)); + await logout(); + + const proc = prismic("init", ["--repo", repo, "--no-browser"]); + const output = captureOutput(proc); + + // Wait for the login server to start and print the URL with port + await expect.poll(output, { timeout: 15_000 }).toMatch(/port=(\d+)/); + + const port = output().match(/port=(\d+)/)![1]; + await fetch(`http://localhost:${port}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + cookies: [`prismic-auth=${token}; path=/`], + email: process.env.E2E_PRISMIC_EMAIL, + }), + }); + + await expect + .poll(output, { timeout: 30_000 }) + .toContain(`Initialized Prismic for repository "${repo}"`); +}, 60_000); + +it("fails if repo is not in the user's account", async ({ expect, project, prismic }) => { + await rm(new URL("prismic.config.json", project)); + const { exitCode, stderr } = await prismic("init", ["--repo", "nonexistent-repo-xyz-12345"]); + expect(exitCode).toBe(1); + expect(stderr).toContain("not found in your account"); +}, 30_000); + +it("migrates slicemachine.config.json", async ({ expect, project, prismic, repo }) => { + await rm(new URL("prismic.config.json", project)); + await writeFile( + new URL("slicemachine.config.json", project), + JSON.stringify({ + repositoryName: repo, + libraries: ["./src/slices"], + }), + ); + + const { exitCode, stdout } = await prismic("init"); + expect(exitCode).toBe(0); + expect(stdout).toContain("Migrated slicemachine.config.json"); + + const configRaw = await readFile(new URL("prismic.config.json", project), "utf-8"); + const config = JSON.parse(configRaw); + expect(config.repositoryName).toBe(repo); + expect(config.libraries).toEqual(["./src/slices"]); + + // Verify legacy config was deleted + await expect(access(new URL("slicemachine.config.json", project))).rejects.toThrow(); +}, 60_000); diff --git a/test/it.ts b/test/it.ts index aa5f7e6..b0ae5cb 100644 --- a/test/it.ts +++ b/test/it.ts @@ -1,16 +1,99 @@ -import { test } from "vitest"; +import type { Result } from "tinyexec"; + +import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import { x } from "tinyexec"; +import { inject, test } from "vitest"; + +const BIN = fileURLToPath(new URL("../dist/index.mjs", import.meta.url)); + +const E2E_PRISMIC_EMAIL = process.env.E2E_PRISMIC_EMAIL!; +const PRISMIC_HOST = process.env.PRISMIC_HOST ?? "prismic.io"; export type Fixtures = { - // TODO: Add custom fixtures - // See: https://vitest.dev/guide/test-context.html#test-extend - foo: string; + home: URL; + project: URL; + prismic: typeof x; + login: () => Promise<{ token: string; email: string }>; + logout: () => Promise; + token: string; + repo: string; + setupPackageJson: (args: { dependencies?: Record }) => Promise; }; export const it = test.extend({ - // TODO: Add custom fixtures - // See: https://vitest.dev/guide/test-context.html#test-extend // oxlint-disable-next-line no-empty-pattern - foo: async ({}, use) => { - await use("bar"); + home: async ({}, use) => { + const dir = await mkdtemp(join(tmpdir(), "prismic-test-")); + await use(pathToFileURL(dir + "/")); + await rm(dir, { recursive: true, force: true }); + }, + project: async ({ home }, use) => { + const projectPath = new URL("project/", home); + await mkdir(projectPath, { recursive: true }); + await use(projectPath); + }, + setupPackageJson: async ({ project }, use) => { + const packageJsonPath = new URL("package.json", project); + await use(async ({ dependencies }) => { + await writeFile(packageJsonPath, JSON.stringify({ dependencies })); + }); + }, + // oxlint-disable-next-line no-empty-pattern + token: async ({}, use) => { + await use(inject("token")); + }, + login: async ({ token, home }, use) => { + await use(async () => { + await writeFile(new URL(".prismic", home), JSON.stringify({ token, host: PRISMIC_HOST })); + return { token, email: E2E_PRISMIC_EMAIL }; + }); + }, + logout: async ({ home }, use) => { + await use(async () => { + await rm(new URL(".prismic", home), { recursive: true, force: true }); + }); + }, + prismic: async ({ home, project, login, setupPackageJson, repo }, use) => { + await login(); + await setupPackageJson({ dependencies: { next: "latest" } }); + await writeFile( + new URL("prismic.config.json", project), + JSON.stringify({ repositoryName: repo }), + ); + const procs: Result[] = []; + await use((command, args = [], options) => { + const env = { + ...process.env, + ...options?.nodeOptions?.env, + HOME: fileURLToPath(home), + }; + const proc = x("node", [BIN, command, ...args].filter(Boolean), { + ...options, + nodeOptions: { + cwd: fileURLToPath(project), + ...options?.nodeOptions, + env, + }, + }); + procs.push(proc); + return proc; + }); + for (const proc of procs) { + if (proc.exitCode === undefined) proc.kill(); + } + }, + // oxlint-disable-next-line no-empty-pattern + repo: async ({}, use) => { + await use(inject("repo")); }, }); + +export function captureOutput(proc: Result): () => string { + let output = ""; + proc.process?.stdout?.on("data", (c: Buffer) => (output += c.toString())); + proc.process?.stderr?.on("data", (c: Buffer) => (output += c.toString())); + return () => output; +} diff --git a/test/login.test.ts b/test/login.test.ts new file mode 100644 index 0000000..01783e9 --- /dev/null +++ b/test/login.test.ts @@ -0,0 +1,31 @@ +import { readFile } from "node:fs/promises"; + +import { captureOutput, it } from "./it"; + +it("supports --help", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("login", ["--help"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("USAGE"); +}); + +it("logs in and writes token", async ({ expect, home, prismic, logout }) => { + await logout(); + const proc = prismic("login", ["--no-browser"]); + const output = captureOutput(proc); + + await expect.poll(output, { timeout: 10_000 }).toMatch(/port=(\d+)/); + const port = output().match(/port=(\d+)/)![1]; + await fetch(`http://localhost:${port}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + cookies: ["prismic-auth=test-token-123; path=/"], + email: "test@example.com", + }), + }); + await expect.poll(output).toContain("Logged in to Prismic as test@example.com"); + + const authFile = await readFile(new URL(".prismic", home), "utf-8"); + const { token } = JSON.parse(authFile); + expect(token).toBe("test-token-123"); +}); diff --git a/test/logout.test.ts b/test/logout.test.ts new file mode 100644 index 0000000..ca4e85e --- /dev/null +++ b/test/logout.test.ts @@ -0,0 +1,23 @@ +import { readFile } from "node:fs/promises"; + +import { it } from "./it"; + +it("supports --help", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("logout", ["--help"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("USAGE"); +}); + +it("logs out and deletes auth file", async ({ expect, home, prismic }) => { + const { stdout, exitCode } = await prismic("logout"); + expect(exitCode).toBe(0); + expect(stdout).toContain("Logged out of Prismic"); + await expect(readFile(new URL(".prismic", home), "utf-8")).rejects.toThrow(); +}); + +it("succeeds when not logged in", async ({ expect, prismic, logout }) => { + await logout(); + const { stdout, exitCode } = await prismic("logout"); + expect(exitCode).toBe(0); + expect(stdout).toContain("Logged out of Prismic"); +}); diff --git a/test/prismic.ts b/test/prismic.ts new file mode 100644 index 0000000..054268a --- /dev/null +++ b/test/prismic.ts @@ -0,0 +1,78 @@ +const DEFAULT_HOST = "prismic.io"; + +type HostConfig = { host?: string }; +type AuthConfig = { token: string; host?: string }; +type RepoConfig = { repo: string; token: string; host?: string }; + +export async function login(email: string, password: string, config?: HostConfig): Promise { + const host = config?.host ?? DEFAULT_HOST; + const url = new URL("login", `https://auth.${host}/`); + const res = await fetch(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email, password }), + }); + if (!res.ok) throw new Error(`Login failed: ${res.status} ${await res.text()}`); + return await res.text(); +} + +export async function createRepository(domain: string, config: AuthConfig): Promise { + const host = config.host ?? DEFAULT_HOST; + const url = new URL("app/dashboard/repositories", `https://${host}/`); + const res = await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + Cookie: `prismic-auth=${config.token}`, + }, + body: JSON.stringify({ domain, framework: "nextjs", plan: "personal" }), + }); + if (!res.ok) + throw new Error(`Failed to create repository ${domain}: ${res.status} ${await res.text()}`); +} + +export async function deleteRepository( + domain: string, + config: AuthConfig & { password: string }, +): Promise { + const host = config.host ?? DEFAULT_HOST; + const url = new URL("app/settings/delete", `https://${domain}.${host}/`); + const headers = { "Content-Type": "application/json", Cookie: `prismic-auth=${config.token}` }; + const body = JSON.stringify({ confirm: domain, password: config.password }); + const res = await fetch(url, { method: "POST", headers, body }); + if (!res.ok) { + // Sometimes deletion returns 500 but actually succeeds — retry once + const retry = await fetch(url, { method: "POST", headers, body }); + if (!retry.ok) throw new Error(`Failed to delete repository ${domain}`); + } +} + +export async function insertCustomType(customType: object, config: RepoConfig): Promise { + const host = config.host ?? DEFAULT_HOST; + const url = new URL("customtypes/insert", `https://customtypes.${host}/`); + const res = await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${config.token}`, + repository: config.repo, + }, + body: JSON.stringify(customType), + }); + if (!res.ok) throw new Error(`Failed to insert custom type: ${res.status} ${await res.text()}`); +} + +export async function insertSlice(slice: object, config: RepoConfig): Promise { + const host = config.host ?? DEFAULT_HOST; + const url = new URL("slices/insert", `https://customtypes.${host}/`); + const res = await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${config.token}`, + repository: config.repo, + }, + body: JSON.stringify(slice), + }); + if (!res.ok) throw new Error(`Failed to insert slice: ${res.status} ${await res.text()}`); +} diff --git a/test/setup.global.ts b/test/setup.global.ts index f344b5f..e69b268 100644 --- a/test/setup.global.ts +++ b/test/setup.global.ts @@ -1,9 +1,42 @@ -export async function setup(): Promise { - // TODO: Add global setup - // See: https://vitest.dev/config/globalsetup.html#globalsetup +import type { Vitest } from "vitest/node"; + +import { createRepository, deleteRepository, login } from "./prismic"; + +declare module "vitest" { + export interface ProvidedContext { + token: string; + repo: string; + } } -export async function teardown(): Promise { - // TODO: Add global teardown - // See: https://vitest.dev/config/globalsetup.html#globalsetup +export default async function ({ provide }: Vitest): Promise<() => Promise> { + try { + process.loadEnvFile(".env.test.local"); + } catch { + // .env.test.local is optional + } + + const email = process.env.E2E_PRISMIC_EMAIL; + if (!email) throw new Error("E2E_PRISMIC_EMAIL is required"); + const password = process.env.E2E_PRISMIC_PASSWORD; + if (!password) throw new Error("E2E_PRISMIC_PASSWORD is required"); + const host = process.env.PRISMIC_HOST ?? "prismic.io"; + + console.info(`Logging in to ${host} with ${email}`); + const token = await login(email, password, { host }); + provide("token", token); + + const domain = `prismic-cli-test-${crypto.randomUUID().replace(/-/g, "").slice(0, 8)}`; + console.info(`Creating shared test repository: ${domain}`); + await createRepository(domain, { token, host }); + provide("repo", domain); + + return async () => { + try { + console.info(`Deleting shared test repository: ${domain}`); + await deleteRepository(domain, { token, password, host }); + } catch { + console.warn(`Warning: failed to delete test repository ${domain}`); + } + }; } diff --git a/test/setup.ts b/test/setup.ts deleted file mode 100644 index ba5a1d1..0000000 --- a/test/setup.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { afterEach, expect, vi } from "vitest"; - -afterEach(() => { - vi.resetAllMocks(); -}); - -expect.extend({ - // TODO: Add custom matchers - // See: https://vitest.dev/guide/extending-matchers -}); diff --git a/test/sync.test.ts b/test/sync.test.ts new file mode 100644 index 0000000..7390d91 --- /dev/null +++ b/test/sync.test.ts @@ -0,0 +1,97 @@ +import type { CustomType, SharedSlice } from "@prismicio/types-internal/lib/customtypes"; + +import { readFile } from "node:fs/promises"; + +import { captureOutput, it } from "./it"; +import { insertCustomType, insertSlice } from "./prismic"; + +const PRISMIC_HOST = process.env.PRISMIC_HOST ?? "prismic.io"; + +it("supports --help", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("sync", ["--help"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("USAGE"); +}); + +it("syncs slices and custom types from remote", async ({ + expect, + project, + prismic, + repo, + token, +}) => { + const customType = buildCustomType(); + const slice = buildSlice(); + + await insertCustomType(customType, { repo, token, host: PRISMIC_HOST }); + await insertSlice(slice, { repo, token, host: PRISMIC_HOST }); + + const { exitCode, stdout } = await prismic("sync", ["--repo", repo]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Sync complete"); + + const customTypeModel = JSON.parse( + await readFile(new URL(`customtypes/${customType.id}/index.json`, project), "utf-8"), + ); + expect(customTypeModel.id).toBe(customType.id); + + const sliceModel = JSON.parse( + await readFile(new URL(`slices/${slice.name}/model.json`, project), "utf-8"), + ); + expect(sliceModel.id).toBe(slice.id); +}, 60_000); + +it("watches for changes and syncs", async ({ expect, project, prismic, repo, token }) => { + const customType = buildCustomType(); + const slice = buildSlice(); + + const proc = prismic("sync", ["--repo", repo, "--watch"]); + const output = captureOutput(proc); + + await expect.poll(output, { timeout: 30_000 }).toContain("Watching for changes"); + + await insertCustomType(customType, { repo, token, host: PRISMIC_HOST }); + await insertSlice(slice, { repo, token, host: PRISMIC_HOST }); + + await expect.poll(output, { timeout: 30_000 }).toContain("Changes detected"); + + const customTypeModel = JSON.parse( + await readFile(new URL(`customtypes/${customType.id}/index.json`, project), "utf-8"), + ); + expect(customTypeModel.id).toBe(customType.id); + + const sliceModel = JSON.parse( + await readFile(new URL(`slices/${slice.name}/model.json`, project), "utf-8"), + ); + expect(sliceModel.id).toBe(slice.id); +}, 60_000); + +function buildCustomType(): CustomType { + const id = crypto.randomUUID().split("-")[0]; + return { + id: `type-T${id}`, + label: `TypeT${id}`, + repeatable: true, + status: true, + json: {}, + }; +} + +function buildSlice(): SharedSlice { + const id = crypto.randomUUID().split("-")[0]; + return { + id: `slice-S${id}`, + type: "SharedSlice", + name: `SliceS${id}`, + variations: [ + { + id: "default", + name: "Default", + docURL: "", + version: "initial", + description: "Default", + imageUrl: "", + }, + ], + }; +} diff --git a/test/whoami.test.ts b/test/whoami.test.ts new file mode 100644 index 0000000..4134373 --- /dev/null +++ b/test/whoami.test.ts @@ -0,0 +1,20 @@ +import { it } from "./it"; + +it("supports --help", async ({ expect, prismic }) => { + const { stdout, exitCode } = await prismic("whoami", ["--help"]); + expect(exitCode).toBe(0); + expect(stdout).toContain("USAGE"); +}); + +it("prints email of logged-in user", async ({ expect, prismic, login }) => { + const { email } = await login(); + const { stdout, exitCode } = await prismic("whoami"); + expect(exitCode).toBe(0); + expect(stdout).toContain(email); +}); + +it("fails when not logged in", async ({ expect, prismic, logout }) => { + await logout(); + const { exitCode } = await prismic("whoami"); + expect(exitCode).not.toBe(0); +}); diff --git a/tsdown.config.ts b/tsdown.config.ts index 6dcdada..4b4a6d4 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -1,16 +1,19 @@ import { defineConfig } from "tsdown"; -const MODE = process.env.MODE || "production"; +const MODE = process.env.MODE || "development"; +const TEST = MODE === "test"; export default defineConfig({ entry: "./src/index.ts", format: "esm", platform: "node", - minify: true, + unbundle: TEST, + minify: !TEST, envPrefix: "PRISMIC_", define: { "process.env.MODE": JSON.stringify(MODE), - "process.env.DEV": JSON.stringify(MODE !== "production"), - "process.env.PROD": JSON.stringify(MODE === "production"), + "process.env.DEV": JSON.stringify(String(MODE !== "production")), + "process.env.PROD": JSON.stringify(String(MODE === "production")), + "process.env.TEST": JSON.stringify(String(TEST)), }, }); diff --git a/vitest.config.ts b/vitest.config.ts index 5edd767..1ee9357 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -3,14 +3,12 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ test: { globalSetup: ["./test/setup.global.ts"], - setupFiles: ["./test/setup.ts"], + forceRerunTriggers: ["**/dist/index.mjs"], typecheck: { enabled: true, }, - coverage: { - provider: "v8", - reporter: ["lcovonly", "text"], - include: ["src"], + sequence: { + concurrent: true, }, }, });