set-cookie-parser icon indicating copy to clipboard operation
set-cookie-parser copied to clipboard

distribute ESM version

Open benmccann opened this issue 3 years ago • 7 comments

I was wondering if you'd be open to a PR to convert to ESM and what your thoughts are about ESM vs CJS, etc.

benmccann avatar Apr 14 '22 18:04 benmccann

Yeah, I suppose we ought to do that. Fair warning, though: I just went through a similar transition with a different project, and was more work than it sounded on the surface.

The main thing I want is to keep backwards compatibility for existing users, so keep a CJS version and no require(...).default or other nonsense.

I suspect that the best way to do it is probably a thin ESM wrapper that just re-exports the existing CJS module and provides a named and/or default export.

nfriedly avatar Apr 15 '22 18:04 nfriedly

@nfriedly Hi 👋 would you consider adding a small script to generate the ESM version? Somewhat similar to this PR in cookie for the same purpose: https://github.com/jshttp/cookie/pull/154

It doesn't add dependencies or anything, it just concatenates the CJS version with an extra bit of code in an .mjs file.

frandiox avatar Apr 12 '24 11:04 frandiox

Hum, that's an interesting idea, I like the simplicity.

What I had been thinking about was converting it to typescript, and then having tsc run twice to output both esm and cjs versions of the library, but I've done that with a few other libraries and it's a decent amount of work each time.

If you want to send in a PR to add the conversion script, I'll go ahead and merge it in, and I'll punt typescript for some other day (or maybe never).

nfriedly avatar Apr 12 '24 15:04 nfriedly

A couple of thoughts if we go with a generation script:

  1. It should add a comment to the top of the file along the lines of // this file is generated, see scripts/build-esm.js
  2. There should be at least a small .mjs test that imports from the generated file and ensures it works

nfriedly avatar Apr 12 '24 15:04 nfriedly

I also wonder if it should go in the opposite direction. Start with esm source and then generate the cjs version. Esm will be around longer-term and eventually everyone will be dropping cjs. That would make it easier for this library in the future as then you can just drop the generation script when the time comes and don't have to update the source

benmccann avatar Apr 12 '24 15:04 benmccann

Start with esm source and then generate the cjs version.

Yeah, that makes sense.

nfriedly avatar Apr 12 '24 15:04 nfriedly

Here's the ESM version in case anyone still needs this. I was having trouble bundling this package with Vite and decided to copy-paste code and transform it to Typescript.

File: types.ts

export type ParseOptions = {
    /**
     * Calls decodeURIComponent on each value
     * @default true
     */
    decodeValues?: boolean;
    /**
     * Return an object instead of an array
     * @default false
     */
    map?: boolean;
    /**
     * Suppress the warning that is loged when called on a request instead of a response
     * @default false
     */
    silent?: boolean;
};

export interface ParsedCookie {
    name: string;
    /**
     * cookie value
     */
    value: string;
    /**
     * cookie path
     */
    path?: string;
    /**
     * absolute expiration date for the cookie
     */
    expires?: Date;
    /**
     * relative max age of the cookie in seconds from when the client receives it (integer or undefined)
     * Note: when using with express's res.cookie() method, multiply maxAge by 1000 to convert to milliseconds
     */
    maxAge?: number;
    /**
     * Domain for the cookie, may begin with "." to indicate the named
     * domain or any subdomain of it
     */
    domain?: string;
    /**
     * indicates that this cookie should only be sent over HTTPs
     */
    secure?: boolean;
    /**
     * indicates that this cookie should not be accessible to client-side JavaScript
     */
    httpOnly?: boolean;
    /**
     * indicates a cookie ought not to be sent along with cross-site requests
     */
    sameSite?: string;
}

export interface ParsedCookieMap {
    [name: string]: ParsedCookie;
}

file: cookies.util.ts

import type { ParsedCookie, ParsedCookieMap, ParseOptions } from './types';

export class CookiesUtil {
    private static readonly defaultParseOptions: ParseOptions = {
        decodeValues: true,
        map: false,
        silent: false
    };

    static parse(
        input: string | string[] | undefined,
        options: ParseOptions & { map: true }
    ): ParsedCookieMap;

    static parse(
        input: string | string[] | undefined,
        options?: ParseOptions & { map?: false | undefined }
    ): ParsedCookie[];

    static parse(
        input: string | string[] | undefined,
        options?: ParseOptions
    ): ParsedCookie[] | ParsedCookieMap {
        options = { ...this.defaultParseOptions, ...(options ?? {}) };

        if (!input) {
            return options.map ? {} : [];
        }

        if (!Array.isArray(input)) {
            input = [input];
        }

        if (!options.map) {
            return input
                .filter((part) => part.trim().length)
                .map((str) => this.parseString(str, options));
        }

        const cookies: Record<string, ParsedCookie> = {};

        return input
            .filter((part) => part.trim().length)
            .reduce((cookies, str) => {
                const cookie = this.parseString(str, options);

                cookies[cookie.name] = cookie;

                return cookies;
            }, cookies);
    }

    static parseString(
        setCookieValue: string,
        options?: ParseOptions
    ): ParsedCookie {
        options = { ...this.defaultParseOptions, ...(options ?? {}) };

        const parts = setCookieValue
            .split(';')
            .filter((part) => part.trim().length);

        const nameValuePairStr = parts.shift()!;
        const parsed = this.__parseNameValuePair(nameValuePairStr);
        const { name, value: initialValue } = parsed;

        let value = initialValue;
        try {
            value = options.decodeValues ? decodeURIComponent(value) : value;
        } catch (error) {
            console.error(
                `Encountered an error while decoding a cookie with value '${value}'. Set options.decodeValues to false to disable this feature.`,
                error
            );
        }

        const cookie: ParsedCookie = { name, value };

        parts.forEach((part) => {
            const sides = part.split('=');
            const key = sides.shift()!.trimStart().toLowerCase();
            const val = sides.join('=');

            if (key === 'expires') {
                cookie.expires = new Date(val);
            } else if (key === 'max-age') {
                cookie.maxAge = parseInt(val, 10);
            } else if (key === 'secure') {
                cookie.secure = true;
            } else if (key === 'httponly') {
                cookie.httpOnly = true;
            } else if (key === 'samesite') {
                cookie.sameSite = val;
            } else {
                cookie[key] = val;
            }
        });

        return cookie;
    }

    static splitCookiesString(cookiesString: string | string[]): string[] {
        if (Array.isArray(cookiesString)) {
            return cookiesString;
        }

        const cookiesStrings: string[] = [];
        let pos = 0;
        let start: number;
        let ch: string;
        let lastComma: number;
        let nextStart: number;
        let cookiesSeparatorFound: boolean;

        const skipWhitespace = (): boolean => {
            while (
                pos < cookiesString.length &&
                /\s/.test(cookiesString.charAt(pos))
            ) {
                pos += 1;
            }

            return pos < cookiesString.length;
        };

        const notSpecialChar = (): boolean => {
            ch = cookiesString.charAt(pos);

            return ch !== '=' && ch !== ';' && ch !== ',';
        };

        while (pos < cookiesString.length) {
            start = pos;
            cookiesSeparatorFound = false;

            while (skipWhitespace()) {
                ch = cookiesString.charAt(pos);
                if (ch === ',') {
                    lastComma = pos;
                    pos += 1;

                    skipWhitespace();
                    nextStart = pos;

                    while (pos < cookiesString.length && notSpecialChar()) {
                        pos += 1;
                    }

                    if (
                        pos < cookiesString.length &&
                        cookiesString.charAt(pos) === '='
                    ) {
                        cookiesSeparatorFound = true;
                        pos = nextStart;
                        cookiesStrings.push(
                            cookiesString.substring(start, lastComma)
                        );
                        start = pos;
                    } else {
                        pos = lastComma + 1;
                    }
                } else {
                    pos += 1;
                }
            }

            if (!cookiesSeparatorFound || pos >= cookiesString.length) {
                cookiesStrings.push(cookiesString.substring(start));
            }
        }

        return cookiesStrings;
    }

    private static __parseNameValuePair(nameValuePairStr: string): {
        name: string;
        value: string;
    } {
        const nameValueArr = nameValuePairStr.split('=');
        const name = nameValueArr.shift()!;
        const value = nameValueArr.join('=');

        return { name, value };
    }
}

renatoaraujoc avatar Jun 27 '24 20:06 renatoaraujoc