Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

distribute ESM version #50

Open
benmccann opened this issue Apr 14, 2022 · 7 comments
Open

distribute ESM version #50

benmccann opened this issue Apr 14, 2022 · 7 comments

Comments

@benmccann
Copy link

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.

@nfriedly
Copy link
Owner

nfriedly commented Apr 15, 2022

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.

@frandiox
Copy link
Contributor

@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: jshttp/cookie#154

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

@nfriedly
Copy link
Owner

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
Copy link
Owner

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

@benmccann
Copy link
Author

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

@nfriedly
Copy link
Owner

Start with esm source and then generate the cjs version.

Yeah, that makes sense.

@renatoaraujoc
Copy link

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 };
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants