diff --git a/packages/ember-cookies/package.json b/packages/ember-cookies/package.json index bbe3b63b..acd50191 100644 --- a/packages/ember-cookies/package.json +++ b/packages/ember-cookies/package.json @@ -18,7 +18,7 @@ "default": "./dist/*.js" }, "./test-support": { - "types": "./declarations/*.d.ts", + "types": "./declarations/test-support/index.d.ts", "default": "./dist/test-support/index.js" }, "./addon-main.js": "./addon-main.cjs" @@ -82,6 +82,9 @@ "rollup": "4.29.1", "typescript": "^5.7.2" }, + "peerDependencies": { + "ember-source": ">=4.0" + }, "engines": { "node": ">= 16.*" }, diff --git a/packages/ember-cookies/src/services/cookies.js b/packages/ember-cookies/src/services/cookies.ts similarity index 53% rename from packages/ember-cookies/src/services/cookies.js rename to packages/ember-cookies/src/services/cookies.ts index 6d3db012..69c7f91a 100644 --- a/packages/ember-cookies/src/services/cookies.js +++ b/packages/ember-cookies/src/services/cookies.ts @@ -1,21 +1,61 @@ import { isNone, isPresent, isEmpty } from '@ember/utils'; -import { get } from '@ember/object'; import { assert } from '@ember/debug'; import { getOwner } from '@ember/application'; import Service from '@ember/service'; import { serializeCookie } from '../utils/serialize-cookie.ts'; -const { keys } = Object; + const DEFAULTS = { raw: false }; const MAX_COOKIE_BYTE_LENGTH = 4096; +type ReadOptions = { + raw?: boolean; + domain?: never; + expires?: never; + maxAge?: never; + path?: never; +}; + +export type WriteOptions = { + domain?: string; + path?: string; + secure?: boolean; + raw?: boolean; + sameSite?: 'Strict' | 'Lax' | 'None'; + signed?: never; + httpOnly?: boolean; +} & ({ expires?: Date; maxAge?: never } | { maxAge?: number; expires?: never }); + +type ClearOptions = { + domain?: string; + path?: string; + secure?: boolean; + + expires?: never; + maxAge?: never; + raw?: never; +}; + +type CookiePair = [string, string]; + +type FastbootCookies = Record; + export default class CookiesService extends Service { + _fastBoot: + | any + | { + request?: { + cookies?: Record; + }; + }; + _fastBootCookiesCache?: FastbootCookies; + + protected _document: Document = window.document; + constructor() { super(...arguments); - this._document = this._document || window.document; - if (typeof this._fastBoot === 'undefined') { - let owner = getOwner(this); - + let owner = getOwner(this); + if (typeof this._fastBoot === 'undefined' && owner) { this._fastBoot = owner.lookup('service:fastboot'); } } @@ -24,31 +64,42 @@ export default class CookiesService extends Service { let all = this._document.cookie.split(';'); let filtered = this._filterDocumentCookies(all); - return filtered.reduce((acc, cookie) => { - if (!isEmpty(cookie)) { - let [key, value] = cookie; - acc[key.trim()] = (value || '').trim(); - } - return acc; - }, {}); + return filtered.reduce( + (acc, cookie) => { + if (!isEmpty(cookie)) { + let [key, value] = cookie; + acc[key.trim()] = (value || '').trim(); + } + return acc; + }, + {} as Record + ); } _getFastBootCookies() { - let fastBootCookies = this._fastBoot.request.cookies; - fastBootCookies = keys(fastBootCookies).reduce((acc, name) => { - let value = fastBootCookies[name]; - acc[name] = { value }; - return acc; - }, {}); + const cookies = this._fastBoot.request.cookies; + const fastBootCookies = Object.keys(cookies).reduce((acc, name) => { + const value = cookies[name]; + + if (typeof value === 'object') { + acc[name] = value; + } else { + acc[name] = { value }; + } - let fastBootCookiesCache = this._fastBootCookiesCache || {}; - fastBootCookies = Object.assign({}, fastBootCookies, fastBootCookiesCache); - this._fastBootCookiesCache = fastBootCookies; + return acc; + }, {} as FastbootCookies); + const fastBootCookiesCache = this._fastBootCookiesCache || {}; - return this._filterCachedFastBootCookies(fastBootCookies); + const mergedFastBootCookies = Object.assign({}, fastBootCookies, fastBootCookiesCache); + this._fastBootCookiesCache = mergedFastBootCookies; + return this._filterCachedFastBootCookies(mergedFastBootCookies); } - read(name, options = {}) { + read( + name?: string, + options: ReadOptions = {} + ): string | undefined | Record { options = Object.assign({}, DEFAULTS, options || {}); assert( 'Domain, Expires, Max-Age, and Path options cannot be set when reading cookies', @@ -58,7 +109,7 @@ export default class CookiesService extends Service { isEmpty(options.path) ); - let all; + let all: Record = {}; if (this._isFastBoot()) { all = this._getFastBootCookies(); } else { @@ -68,12 +119,12 @@ export default class CookiesService extends Service { if (name) { return this._decodeValue(all[name], options.raw); } else { - keys(all).forEach(name => (all[name] = this._decodeValue(all[name], options.raw))); + Object.keys(all).forEach(name => (all[name] = this._decodeValue(all[name], options.raw))); return all; } } - write(name, value, options = {}) { + write(name: string, value: unknown, options?: WriteOptions) { options = Object.assign({}, DEFAULTS, options || {}); assert( "Cookies cannot be set as signed as signed cookies would not be modifyable in the browser as it has no knowledge of the express server's signing key!", @@ -84,11 +135,11 @@ export default class CookiesService extends Service { isEmpty(options.expires) || isEmpty(options.maxAge) ); - value = this._encodeValue(value, options.raw); + value = this._encodeValue(value as string, options.raw); assert( `Cookies larger than ${MAX_COOKIE_BYTE_LENGTH} bytes are not supported by most browsers!`, - this._isCookieSizeAcceptable(value) + typeof value === 'string' && this._isCookieSizeAcceptable(value) ); if (this._isFastBoot()) { @@ -101,19 +152,19 @@ export default class CookiesService extends Service { } } - clear(name, options = {}) { + clear(name: string, options?: ClearOptions) { options = Object.assign({}, options || {}); assert( 'Expires, Max-Age, and raw options cannot be set when clearing cookies', isEmpty(options.expires) && isEmpty(options.maxAge) && isEmpty(options.raw) ); - options.expires = new Date('1970-01-01'); + options.expires = new Date('1970-01-01') as never; options.path = options.path || this._normalizedDefaultPath(); this.write(name, null, options); } - exists(name) { + exists(name: string) { let all; if (this._isFastBoot()) { all = this._getFastBootCookies(); @@ -124,20 +175,20 @@ export default class CookiesService extends Service { return Object.prototype.hasOwnProperty.call(all, name); } - _writeDocumentCookie(name, value, options = {}) { + _writeDocumentCookie(name: string, value: unknown, options: WriteOptions = {}) { let serializedCookie = this._serializeCookie(name, value, options); this._document.cookie = serializedCookie; } - _writeFastBootCookie(name, value, options = {}) { + _writeFastBootCookie(name: string, value: unknown, options: WriteOptions = {}) { let responseHeaders = this._fastBoot.response.headers; - let serializedCookie = this._serializeCookie(...arguments); + let serializedCookie = this._serializeCookie(name, value, options); - if (!isEmpty(options.maxAge)) { + if (options.maxAge) { options.maxAge *= 1000; } - this._cacheFastBootCookie(...arguments); + this._cacheFastBootCookie(name, value, options); let replaced = false; let existing = responseHeaders.getAll('set-cookie'); @@ -155,52 +206,54 @@ export default class CookiesService extends Service { } } - _cacheFastBootCookie(name, value, options = {}) { + _cacheFastBootCookie(name: string, value: unknown, options: WriteOptions = {}) { let fastBootCache = this._fastBootCookiesCache || {}; - let cachedOptions = Object.assign({}, options); + let cachedOptions: WriteOptions = Object.assign({}, options); - if (cachedOptions.maxAge) { + if (cachedOptions.maxAge && options.maxAge) { let expires = new Date(); expires.setSeconds(expires.getSeconds() + options.maxAge); - cachedOptions.expires = expires; + delete cachedOptions.maxAge; + (cachedOptions as WriteOptions & { expires?: Date; maxAge?: never }).expires = expires; } - fastBootCache[name] = { value, options: cachedOptions }; + fastBootCache[name] = { value: value as string, options: cachedOptions }; this._fastBootCookiesCache = fastBootCache; } - _filterCachedFastBootCookies(fastBootCookies) { + _filterCachedFastBootCookies(fastBootCookies: FastbootCookies) { let { path: requestPath } = this._fastBoot.request; - // cannot use deconstruct here - // eslint-disable-next-line ember/no-get - let host = get(this._fastBoot, 'request.host'); + let host = this._fastBoot?.request?.host; - return keys(fastBootCookies).reduce((acc, name) => { - let { value, options } = fastBootCookies[name]; - options = options || {}; + return Object.keys(fastBootCookies).reduce( + (acc, name) => { + let { value, options } = fastBootCookies[name] as { value: string; options: ReadOptions }; + options = options || {}; - let { path: optionsPath, domain, expires } = options; + let { path: optionsPath, domain, expires } = options; - if (optionsPath && requestPath.indexOf(optionsPath) !== 0) { - return acc; - } + if (optionsPath && requestPath.indexOf(optionsPath) !== 0) { + return acc; + } - if (domain && host.indexOf(domain) + domain.length !== host.length) { - return acc; - } + if (domain && host.indexOf(domain) + (domain as string).length !== host.length) { + return acc; + } - if (expires && expires < new Date()) { - return acc; - } + if (expires && (expires as Date) < new Date()) { + return acc; + } - acc[name] = value; - return acc; - }, {}); + acc[name] = value; + return acc; + }, + {} as Record + ); } - _encodeValue(value, raw) { + _encodeValue(value: string | undefined, raw?: boolean) { if (isNone(value)) { return ''; } else if (raw) { @@ -210,7 +263,7 @@ export default class CookiesService extends Service { } } - _decodeValue(value, raw) { + _decodeValue(value: string | undefined, raw?: boolean) { if (isNone(value) || raw) { return value; } else { @@ -218,20 +271,20 @@ export default class CookiesService extends Service { } } - _filterDocumentCookies(unfilteredCookies) { + _filterDocumentCookies(unfilteredCookies: string[]): CookiePair[] { return unfilteredCookies - .map(c => { + .map(c => { let separatorIndex = c.indexOf('='); return [c.substring(0, separatorIndex), c.substring(separatorIndex + 1)]; }) .filter(c => c.length === 2 && isPresent(c[0])); } - _serializeCookie(name, value, options = {}) { + _serializeCookie(name: string, value: unknown, options: WriteOptions = {}) { return serializeCookie(name, value, options); } - _isCookieSizeAcceptable(value) { + _isCookieSizeAcceptable(value: string) { // Counting bytes varies Pre-ES6 and in ES6 // This snippet counts the bytes in the value // about to be stored as the cookie: @@ -240,9 +293,7 @@ export default class CookiesService extends Service { let i = 0; let c; while ((c = value.charCodeAt(i++))) { - /* eslint-disable no-bitwise */ _byteCount += c >> 11 ? 3 : c >> 7 ? 2 : 1; - /* eslint-enable no-bitwise */ } return _byteCount < MAX_COOKIE_BYTE_LENGTH; diff --git a/packages/ember-cookies/src/test-support/clear-all-cookies.js b/packages/ember-cookies/src/test-support/clear-all-cookies.ts similarity index 71% rename from packages/ember-cookies/src/test-support/clear-all-cookies.js rename to packages/ember-cookies/src/test-support/clear-all-cookies.ts index 2ea59556..00eba5ff 100644 --- a/packages/ember-cookies/src/test-support/clear-all-cookies.js +++ b/packages/ember-cookies/src/test-support/clear-all-cookies.ts @@ -1,8 +1,9 @@ import { assert } from '@ember/debug'; import { isEmpty } from '@ember/utils'; import { serializeCookie } from '../utils/serialize-cookie.ts'; +import type { WriteOptions } from '../services/cookies.ts'; -export default function clearAllCookies(options = {}) { +export default function clearAllCookies(options: WriteOptions = {}) { assert('Cookies cannot be set to be HTTP-only from a browser!', !options.httpOnly); assert( 'Expires, Max-Age, and raw options cannot be set when clearing cookies', @@ -16,6 +17,9 @@ export default function clearAllCookies(options = {}) { cookies.forEach(cookie => { let cookieName = cookie.split('=')[0]; - document.cookie = serializeCookie(cookieName, '', options); + + if (typeof cookieName === 'string') { + document.cookie = serializeCookie(cookieName, '', options); + } }); } diff --git a/packages/ember-cookies/src/test-support/index.js b/packages/ember-cookies/src/test-support/index.js deleted file mode 100644 index 6d1cba98..00000000 --- a/packages/ember-cookies/src/test-support/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default as clearAllCookies } from './clear-all-cookies'; diff --git a/packages/ember-cookies/src/test-support/index.ts b/packages/ember-cookies/src/test-support/index.ts new file mode 100644 index 00000000..61a407db --- /dev/null +++ b/packages/ember-cookies/src/test-support/index.ts @@ -0,0 +1 @@ +export { default as clearAllCookies } from './clear-all-cookies.ts'; diff --git a/packages/ember-cookies/src/utils/serialize-cookie.ts b/packages/ember-cookies/src/utils/serialize-cookie.ts index fe17ed12..d3c25284 100644 --- a/packages/ember-cookies/src/utils/serialize-cookie.ts +++ b/packages/ember-cookies/src/utils/serialize-cookie.ts @@ -11,7 +11,7 @@ interface Options { partitioned?: boolean; } -export const serializeCookie = (name: string, value: string, options: Options = {}) => { +export const serializeCookie = (name: string, value: string | unknown, options: Options = {}) => { let cookie = `${name}=${value}`; if (!isEmpty(options.domain)) { diff --git a/packages/ember-cookies/unpublished-development-types/index.d.ts b/packages/ember-cookies/unpublished-development-types/index.d.ts deleted file mode 100644 index 507d4797..00000000 --- a/packages/ember-cookies/unpublished-development-types/index.d.ts +++ /dev/null @@ -1 +0,0 @@ -import '@types/ember__utils'; diff --git a/packages/test-app/app/app.js b/packages/test-app/app/app.ts similarity index 88% rename from packages/test-app/app/app.js rename to packages/test-app/app/app.ts index 1ba93424..6360ec36 100644 --- a/packages/test-app/app/app.js +++ b/packages/test-app/app/app.ts @@ -5,7 +5,6 @@ import config from 'test-app/config/environment'; export default class App extends Application { modulePrefix = config.modulePrefix; - podModulePrefix = config.podModulePrefix; Resolver = Resolver; } diff --git a/packages/test-app/app/resolver.js b/packages/test-app/app/resolver.ts similarity index 100% rename from packages/test-app/app/resolver.js rename to packages/test-app/app/resolver.ts diff --git a/packages/test-app/app/router.js b/packages/test-app/app/router.ts similarity index 100% rename from packages/test-app/app/router.js rename to packages/test-app/app/router.ts diff --git a/packages/test-app/package.json b/packages/test-app/package.json index 42cedca3..e1222dcd 100644 --- a/packages/test-app/package.json +++ b/packages/test-app/package.json @@ -38,6 +38,8 @@ "@glint/environment-ember-loose": "^1.5.0", "@glint/environment-ember-template-imports": "^1.5.0", "@tsconfig/ember": "^3.0.8", + "@types/ember-qunit": "^6.1.3", + "@types/qunit": "^2.19.12", "@typescript-eslint/eslint-plugin": "^8.18.1", "@typescript-eslint/parser": "^8.18.1", "broccoli-asset-rev": "3.0.0", diff --git a/packages/test-app/tests/helpers/resolver.js b/packages/test-app/tests/helpers/resolver.js deleted file mode 100644 index 319b45fc..00000000 --- a/packages/test-app/tests/helpers/resolver.js +++ /dev/null @@ -1,11 +0,0 @@ -import Resolver from '../../resolver'; -import config from '../../config/environment'; - -const resolver = Resolver.create(); - -resolver.namespace = { - modulePrefix: config.modulePrefix, - podModulePrefix: config.podModulePrefix, -}; - -export default resolver; diff --git a/packages/test-app/tests/test-helper.js b/packages/test-app/tests/test-helper.ts similarity index 62% rename from packages/test-app/tests/test-helper.js rename to packages/test-app/tests/test-helper.ts index 7827a6af..d9e95dcb 100644 --- a/packages/test-app/tests/test-helper.js +++ b/packages/test-app/tests/test-helper.ts @@ -1,5 +1,5 @@ -import Application from '../app'; -import config from '../config/environment'; +import Application from 'test-app/app'; +import config from 'test-app/config/environment'; import { setApplication } from '@ember/test-helpers'; import { start } from 'ember-qunit'; diff --git a/packages/test-app/tests/unit/clear-all-cookies-test.js b/packages/test-app/tests/unit/clear-all-cookies-test.ts similarity index 100% rename from packages/test-app/tests/unit/clear-all-cookies-test.js rename to packages/test-app/tests/unit/clear-all-cookies-test.ts diff --git a/packages/test-app/types/config/environment.d.ts b/packages/test-app/types/config/environment.d.ts new file mode 100644 index 00000000..945b1bca --- /dev/null +++ b/packages/test-app/types/config/environment.d.ts @@ -0,0 +1,10 @@ +declare module 'test-app/config/environment' { + type Config = { + modulePrefix: string; + APP: Record; + locationType: string; + rootURL: string; + }; + + export default {} as Config; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 859b760f..7a32f8db 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: '@embroider/addon-shim': specifier: ^1.7.1 version: 1.8.9 + ember-source: + specifier: '>=4.0' + version: 5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.0)(rsvp@4.8.5)(webpack@5.95.0) devDependencies: '@babel/core': specifier: 7.25.8 @@ -141,6 +144,12 @@ importers: '@tsconfig/ember': specifier: ^3.0.8 version: 3.0.8 + '@types/ember-qunit': + specifier: ^6.1.3 + version: 6.1.3(@ember/test-helpers@3.3.1(@babel/core@7.25.8)(@glint/template@1.5.0)(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.0)(rsvp@4.8.5)(webpack@5.95.0))(webpack@5.95.0))(@glint/template@1.5.0)(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.0)(rsvp@4.8.5)(webpack@5.95.0))(qunit@2.23.1) + '@types/qunit': + specifier: ^2.19.12 + version: 2.19.12 '@typescript-eslint/eslint-plugin': specifier: ^8.18.1 version: 8.18.1(@typescript-eslint/parser@8.18.1(eslint@9.12.0)(typescript@5.7.2))(eslint@9.12.0)(typescript@5.7.2) @@ -1625,6 +1634,10 @@ packages: '@types/cors@2.8.17': resolution: {integrity: sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==} + '@types/ember-qunit@6.1.3': + resolution: {integrity: sha512-cX28wQZ5n66YTdXAC0wktzaxL0yRm/FVPJMEnPC/CySMxFiDplBkelrYfonGo9vKQ0iL7W12OQjNkq5AGUJdNg==} + deprecated: This is a stub types definition. ember-qunit provides its own type definitions, so you do not need this installed. + '@types/ember@4.0.11': resolution: {integrity: sha512-v7VIex0YILK8fP87LkIfzeeYKNnu74+xwf6U56v6MUDDGfSs9q/6NCxiUfwkxD+z5nQiUcwvfKVokX8qzZFRLw==} @@ -1724,6 +1737,9 @@ packages: '@types/qs@6.9.16': resolution: {integrity: sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A==} + '@types/qunit@2.19.12': + resolution: {integrity: sha512-II+C1wgzUia0g+tGAH+PBb4XiTm8/C/i6sN23r21NNskBYOYrv+qnW0tFQ/IxZzKVwrK4CTglf8YO3poJUclQA==} + '@types/range-parser@1.2.7': resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} @@ -8670,6 +8686,16 @@ snapshots: dependencies: '@types/node': 22.5.5 + '@types/ember-qunit@6.1.3(@ember/test-helpers@3.3.1(@babel/core@7.25.8)(@glint/template@1.5.0)(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.0)(rsvp@4.8.5)(webpack@5.95.0))(webpack@5.95.0))(@glint/template@1.5.0)(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.0)(rsvp@4.8.5)(webpack@5.95.0))(qunit@2.23.1)': + dependencies: + ember-qunit: 8.1.1(@ember/test-helpers@3.3.1(@babel/core@7.25.8)(@glint/template@1.5.0)(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.0)(rsvp@4.8.5)(webpack@5.95.0))(webpack@5.95.0))(@glint/template@1.5.0)(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.0)(rsvp@4.8.5)(webpack@5.95.0))(qunit@2.23.1) + transitivePeerDependencies: + - '@ember/test-helpers' + - '@glint/template' + - ember-source + - qunit + - supports-color + '@types/ember@4.0.11': dependencies: '@types/ember__application': 4.0.11(@babel/core@7.25.8) @@ -8687,7 +8713,7 @@ snapshots: '@types/ember__string': 3.0.15 '@types/ember__template': 4.0.7 '@types/ember__test': 4.0.6(@babel/core@7.25.8) - '@types/ember__utils': 4.0.7(@babel/core@7.25.8) + '@types/ember__utils': 4.0.7 '@types/rsvp': 4.0.9 '@types/ember@4.0.11(@babel/core@7.25.8)': @@ -8808,6 +8834,10 @@ snapshots: - '@babel/core' - supports-color + '@types/ember__utils@4.0.7': + dependencies: + '@types/ember': 4.0.11 + '@types/ember__utils@4.0.7(@babel/core@7.25.8)': dependencies: '@types/ember': 4.0.11(@babel/core@7.25.8) @@ -8869,6 +8899,8 @@ snapshots: '@types/qs@6.9.16': {} + '@types/qunit@2.19.12': {} + '@types/range-parser@1.2.7': {} '@types/responselike@1.0.3':