Skip to content

Commit

Permalink
Support node16
Browse files Browse the repository at this point in the history
- use node https for fetching mime type data if fetch is not defined
- use JSON.parse(JSON.stringify(...)) for structuredClone if it is not defined
  • Loading branch information
floodfx committed Oct 2, 2022
1 parent c6a8540 commit c6f6985
Show file tree
Hide file tree
Showing 13 changed files with 626 additions and 397 deletions.
7 changes: 7 additions & 0 deletions .changeset/rich-carrots-jam.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"liveviewjs": patch
"@liveviewjs/examples": patch
"@liveviewjs/express": patch
---

Support Node16+ (add polyfills for fetch and structuredClone)
757 changes: 391 additions & 366 deletions packages/core/coverage/clover.xml

Large diffs are not rendered by default.

9 changes: 5 additions & 4 deletions packages/core/coverage/coverage-final.json

Large diffs are not rendered by default.

29 changes: 28 additions & 1 deletion packages/core/dist/liveview.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1416,12 +1416,33 @@ interface CreateLiveComponentParams<TContext extends LiveContext = AnyLiveContex
*/
declare const createLiveComponent: <TContext extends LiveContext = AnyLiveContext, TEvents extends LiveEvent = AnyLiveEvent, TInfos extends LiveInfo = AnyLiveInfo>(params: CreateLiveComponentParams<TContext, TEvents, TInfos>) => LiveComponent<TContext, TEvents, TInfos>;

/**
* Maps a route to a LiveView.
* e.g. `"/users": UserListLiveView`
* Routes can be optionally contain parameters which LiveViewJS will automatically
* extract from the URL path and pass to the LiveView's `mount` method as part
* of the `params` object.
* e.g. `"/users/:id": UserLiveView` => `{ id: "123" }`
*/
interface LiveViewRouter {
[key: string]: LiveView<AnyLiveContext, AnyLiveEvent, AnyLiveInfo>;
}
/**
* Type representing parameters extracted from a URL path.
*/
declare type PathParams = {
[key: string]: string;
};
/**
* Helper function that returns a tuple containing the `LiveView` and
* the `MatchResult` object containing the parameters extracted from the URL path
* if there is a match. Returns `undefined` if there is no match.
* Used internally to match a URL path to a LiveView class for both HTTP and WS
* requests.
* @param router the `LiveViewRouter` object
* @param path the URL path to match
* @returns a tuple containing the `LiveView` and the `MatchResult` object or `undefined`
*/
declare function matchRoute(router: LiveViewRouter, path: string): [LiveView<AnyLiveContext, AnyLiveEvent, AnyLiveInfo>, MatchResult<PathParams>] | undefined;

interface LiveViewTemplate extends HtmlSafeString {
Expand Down Expand Up @@ -1597,6 +1618,12 @@ declare class Mime {
get loaded(): boolean;
load(): Promise<void>;
}
/**
* Fallback implementation of getting JSON from a URL for Node <18.
* @param url the url to fetch
* @returns the JSON object returned from the URL
*/
declare function nodeHttpFetch<T>(url: string): Promise<T>;
declare const mime: Mime;

/**
Expand All @@ -1608,4 +1635,4 @@ interface LiveViewServerAdaptor<THttpMiddleware, TWsMiddleware> {
wsMiddleware(): TWsMiddleware;
}

export { AnyLiveContext, AnyLiveEvent, AnyLiveInfo, AnyLivePushEvent, BaseLiveComponent, BaseLiveView, ConsumeUploadedEntriesMeta, CsrfGenerator, Event, FileSystemAdaptor, FlashAdaptor, HtmlSafeString, HttpLiveComponentSocket, HttpLiveViewSocket, HttpRequestAdaptor, IdGenerator, Info, JS, LiveComponent, LiveComponentMeta, LiveComponentSocket, LiveContext, LiveEvent, LiveInfo, LiveTitleOptions, LiveView, LiveViewChangeset, LiveViewChangesetErrors, LiveViewChangesetFactory, LiveViewHtmlPageTemplate, LiveViewManager, LiveViewMeta, LiveViewMountParams, LiveViewRouter, LiveViewServerAdaptor, LiveViewSocket, LiveViewTemplate, LiveViewWrapperTemplate, MimeSource, Parts, PathParams, PubSub, Publisher, SerDe, SessionData, SessionFlashAdaptor, SingleProcessPubSub, Subscriber, SubscriberFunction, SubscriberId, UploadConfig, UploadConfigOptions, UploadEntry, WsAdaptor, WsLiveComponentSocket, WsLiveViewSocket, WsMessageRouter, createLiveComponent, createLiveView, deepDiff, diffArrays, diffArrays2, error_tag, escapehtml, form_for, handleHttpLiveView, html, join, live_file_input, live_img_preview, live_patch, live_title_tag, matchRoute, mime, newChangesetFactory, options_for_select, safe, submit, telephone_input, text_input };
export { AnyLiveContext, AnyLiveEvent, AnyLiveInfo, AnyLivePushEvent, BaseLiveComponent, BaseLiveView, ConsumeUploadedEntriesMeta, CsrfGenerator, Event, FileSystemAdaptor, FlashAdaptor, HtmlSafeString, HttpLiveComponentSocket, HttpLiveViewSocket, HttpRequestAdaptor, IdGenerator, Info, JS, LiveComponent, LiveComponentMeta, LiveComponentSocket, LiveContext, LiveEvent, LiveInfo, LiveTitleOptions, LiveView, LiveViewChangeset, LiveViewChangesetErrors, LiveViewChangesetFactory, LiveViewHtmlPageTemplate, LiveViewManager, LiveViewMeta, LiveViewMountParams, LiveViewRouter, LiveViewServerAdaptor, LiveViewSocket, LiveViewTemplate, LiveViewWrapperTemplate, MimeSource, Parts, PathParams, PubSub, Publisher, SerDe, SessionData, SessionFlashAdaptor, SingleProcessPubSub, Subscriber, SubscriberFunction, SubscriberId, UploadConfig, UploadConfigOptions, UploadEntry, WsAdaptor, WsLiveComponentSocket, WsLiveViewSocket, WsMessageRouter, createLiveComponent, createLiveView, deepDiff, diffArrays, diffArrays2, error_tag, escapehtml, form_for, handleHttpLiveView, html, join, live_file_input, live_img_preview, live_patch, live_title_tag, matchRoute, mime, newChangesetFactory, nodeHttpFetch, options_for_select, safe, submit, telephone_input, text_input };
68 changes: 63 additions & 5 deletions packages/core/dist/liveview.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,16 @@ const createLiveView = (params) => {
};

const matchFns = {};
/**
* Helper function that returns a tuple containing the `LiveView` and
* the `MatchResult` object containing the parameters extracted from the URL path
* if there is a match. Returns `undefined` if there is no match.
* Used internally to match a URL path to a LiveView class for both HTTP and WS
* requests.
* @param router the `LiveViewRouter` object
* @param path the URL path to match
* @returns a tuple containing the `LiveView` and the `MatchResult` object or `undefined`
*/
function matchRoute(router, path) {
for (const route in router) {
let matchFn = matchFns[route];
Expand Down Expand Up @@ -244,13 +254,20 @@ class Mime {
if (this.loaded)
return;
try {
const res = await fetch(MIME_DB_URL);
// istanbul ignore next
if (!res.ok) {
if (globalThis && !globalThis.fetch) {
// only Node 18+ and Deno have fetch so fall back to https
// implementation if globalThis.fetch is not defined.
this.db = await nodeHttpFetch(MIME_DB_URL);
}
else {
const res = await fetch(MIME_DB_URL);
// istanbul ignore next
throw new Error(`Failed to load mime-db: ${res.status} ${res.statusText}`);
if (!res.ok) {
// istanbul ignore next
throw new Error(`Failed to load mime-db: ${res.status} ${res.statusText}`);
}
this.db = await res.json();
}
this.db = await res.json();
// build a reverse lookup table for extensions to mime types
Object.keys(this.db).forEach((mimeType, i) => {
const exts = this.lookupExtensions(mimeType);
Expand All @@ -271,6 +288,29 @@ class Mime {
}
}
}
/**
* Fallback implementation of getting JSON from a URL for Node <18.
* @param url the url to fetch
* @returns the JSON object returned from the URL
*/
function nodeHttpFetch(url) {
return new Promise((resolve, reject) => {
const https = require("https");
https.get(url, (res) => {
if (res.statusCode !== 200) {
res.resume(); // ignore response body
reject(res.statusCode);
}
let data = "";
res.on("data", (chunk) => {
data += chunk;
});
res.on("close", () => {
resolve(JSON.parse(data));
});
});
});
}
const mime = new Mime();

class UploadEntry {
Expand Down Expand Up @@ -342,6 +382,22 @@ class UploadEntry {
}
}

/**
* Checks if globalThis has a `structuredClone` function and if not, adds one
* that uses `JSON.parse(JSON.stringify())` as a fallback. This is needed
* for Node version <17.
*/
function maybeAddStructuredClone() {
/**
* Really bad implementation of structured clone algorithm to backfill for
* Node 16 (and below).
*/
if (globalThis && !globalThis.structuredClone) {
globalThis.structuredClone = (value, transfer) => JSON.parse(JSON.stringify(value));
}
}

maybeAddStructuredClone();
class BaseLiveViewSocket {
_context;
_tempContext = {}; // values to reset the context to post render cycle
Expand Down Expand Up @@ -1611,6 +1667,7 @@ const newHeartbeatReply = (incoming) => {
];
};

maybeAddStructuredClone();
/**
* The `LiveViewComponentManager` is responsible for managing the lifecycle of a `LiveViewComponent`
* including routing of events, the state (i.e. context), and other aspects of the component. The
Expand Down Expand Up @@ -2808,6 +2865,7 @@ exports.live_title_tag = live_title_tag;
exports.matchRoute = matchRoute;
exports.mime = mime;
exports.newChangesetFactory = newChangesetFactory;
exports.nodeHttpFetch = nodeHttpFetch;
exports.options_for_select = options_for_select;
exports.safe = safe;
exports.submit = submit;
Expand Down
69 changes: 63 additions & 6 deletions packages/core/dist/liveview.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,16 @@ const createLiveView = (params) => {
};

const matchFns = {};
/**
* Helper function that returns a tuple containing the `LiveView` and
* the `MatchResult` object containing the parameters extracted from the URL path
* if there is a match. Returns `undefined` if there is no match.
* Used internally to match a URL path to a LiveView class for both HTTP and WS
* requests.
* @param router the `LiveViewRouter` object
* @param path the URL path to match
* @returns a tuple containing the `LiveView` and the `MatchResult` object or `undefined`
*/
function matchRoute(router, path) {
for (const route in router) {
let matchFn = matchFns[route];
Expand Down Expand Up @@ -237,13 +247,20 @@ class Mime {
if (this.loaded)
return;
try {
const res = await fetch(MIME_DB_URL);
// istanbul ignore next
if (!res.ok) {
if (globalThis && !globalThis.fetch) {
// only Node 18+ and Deno have fetch so fall back to https
// implementation if globalThis.fetch is not defined.
this.db = await nodeHttpFetch(MIME_DB_URL);
}
else {
const res = await fetch(MIME_DB_URL);
// istanbul ignore next
throw new Error(`Failed to load mime-db: ${res.status} ${res.statusText}`);
if (!res.ok) {
// istanbul ignore next
throw new Error(`Failed to load mime-db: ${res.status} ${res.statusText}`);
}
this.db = await res.json();
}
this.db = await res.json();
// build a reverse lookup table for extensions to mime types
Object.keys(this.db).forEach((mimeType, i) => {
const exts = this.lookupExtensions(mimeType);
Expand All @@ -264,6 +281,29 @@ class Mime {
}
}
}
/**
* Fallback implementation of getting JSON from a URL for Node <18.
* @param url the url to fetch
* @returns the JSON object returned from the URL
*/
function nodeHttpFetch(url) {
return new Promise((resolve, reject) => {
const https = require("https");
https.get(url, (res) => {
if (res.statusCode !== 200) {
res.resume(); // ignore response body
reject(res.statusCode);
}
let data = "";
res.on("data", (chunk) => {
data += chunk;
});
res.on("close", () => {
resolve(JSON.parse(data));
});
});
});
}
const mime = new Mime();

class UploadEntry {
Expand Down Expand Up @@ -335,6 +375,22 @@ class UploadEntry {
}
}

/**
* Checks if globalThis has a `structuredClone` function and if not, adds one
* that uses `JSON.parse(JSON.stringify())` as a fallback. This is needed
* for Node version <17.
*/
function maybeAddStructuredClone() {
/**
* Really bad implementation of structured clone algorithm to backfill for
* Node 16 (and below).
*/
if (globalThis && !globalThis.structuredClone) {
globalThis.structuredClone = (value, transfer) => JSON.parse(JSON.stringify(value));
}
}

maybeAddStructuredClone();
class BaseLiveViewSocket {
_context;
_tempContext = {}; // values to reset the context to post render cycle
Expand Down Expand Up @@ -1604,6 +1660,7 @@ const newHeartbeatReply = (incoming) => {
];
};

maybeAddStructuredClone();
/**
* The `LiveViewComponentManager` is responsible for managing the lifecycle of a `LiveViewComponent`
* including routing of events, the state (i.e. context), and other aspects of the component. The
Expand Down Expand Up @@ -2769,4 +2826,4 @@ class WsMessageRouter {
}
}

export { BaseLiveComponent, BaseLiveView, HtmlSafeString, HttpLiveComponentSocket, HttpLiveViewSocket, JS, LiveViewManager, SessionFlashAdaptor, SingleProcessPubSub, UploadConfig, UploadEntry, WsLiveComponentSocket, WsLiveViewSocket, WsMessageRouter, createLiveComponent, createLiveView, deepDiff, diffArrays, diffArrays2, error_tag, escapehtml, form_for, handleHttpLiveView, html, join, live_file_input, live_img_preview, live_patch, live_title_tag, matchRoute, mime, newChangesetFactory, options_for_select, safe, submit, telephone_input, text_input };
export { BaseLiveComponent, BaseLiveView, HtmlSafeString, HttpLiveComponentSocket, HttpLiveViewSocket, JS, LiveViewManager, SessionFlashAdaptor, SingleProcessPubSub, UploadConfig, UploadEntry, WsLiveComponentSocket, WsLiveViewSocket, WsMessageRouter, createLiveComponent, createLiveView, deepDiff, diffArrays, diffArrays2, error_tag, escapehtml, form_for, handleHttpLiveView, html, join, live_file_input, live_img_preview, live_patch, live_title_tag, matchRoute, mime, newChangesetFactory, nodeHttpFetch, options_for_select, safe, submit, telephone_input, text_input };
4 changes: 0 additions & 4 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,5 @@
"ts-jest": "^28.0.5",
"ts-node": "^10.7.0",
"typescript": "^4.6.4"
},
"engines": {
"npm": ">=8.5.0",
"node": ">=18.0.0"
}
}
43 changes: 38 additions & 5 deletions packages/core/src/server/mime/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,19 @@ class Mime {
async load() {
if (this.loaded) return;
try {
const res = await fetch(MIME_DB_URL);
// istanbul ignore next
if (!res.ok) {
if (globalThis && !globalThis.fetch) {
// only Node 18+ and Deno have fetch so fall back to https
// implementation if globalThis.fetch is not defined.
this.db = await nodeHttpFetch<MimeDB>(MIME_DB_URL);
} else {
const res = await fetch(MIME_DB_URL);
// istanbul ignore next
throw new Error(`Failed to load mime-db: ${res.status} ${res.statusText}`);
if (!res.ok) {
// istanbul ignore next
throw new Error(`Failed to load mime-db: ${res.status} ${res.statusText}`);
}
this.db = await res.json();
}
this.db = await res.json();

// build a reverse lookup table for extensions to mime types
Object.keys(this.db).forEach((mimeType, i) => {
Expand All @@ -92,6 +98,33 @@ class Mime {
}
}

/**
* Fallback implementation of getting JSON from a URL for Node <18.
* @param url the url to fetch
* @returns the JSON object returned from the URL
*/
export function nodeHttpFetch<T>(url: string): Promise<T> {
return new Promise((resolve, reject) => {
const https = require("https");

https.get(url, (res: any) => {
if (res.statusCode !== 200) {
res.resume(); // ignore response body
reject(res.statusCode);
}

let data = "";
res.on("data", (chunk: any) => {
data += chunk;
});

res.on("close", () => {
resolve(JSON.parse(data) as T);
});
});
});
}

const mime = new Mime();

export { mime };
7 changes: 6 additions & 1 deletion packages/core/src/server/mime/mime.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { mime } from ".";
import { mime, nodeHttpFetch } from ".";

describe("test mime", () => {
beforeAll(async () => {
Expand All @@ -13,4 +13,9 @@ describe("test mime", () => {
expect(mime.loaded).toBeTruthy();
expect(mime.lookupExtensions("application/pdf")).toContain("pdf");
});

it("http requests success", async () => {
const res = await nodeHttpFetch("https://cdn.jsdelivr.net/gh/jshttp/mime-db@master/db.json");
expect(res).toBeTruthy();
});
});
2 changes: 2 additions & 0 deletions packages/core/src/server/socket/liveSocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { FileSystemAdaptor } from "../adaptor";
import { AnyLiveContext, AnyLiveInfo, AnyLivePushEvent, LiveContext, LiveInfo } from "../live";
import { UploadConfig, UploadEntry } from "../upload";
import { UploadConfigOptions } from "../upload/uploadConfig";
import { maybeAddStructuredClone } from "./structuredClone";
maybeAddStructuredClone();

/**
* Type that enables Info events to be passed as plain strings
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/server/socket/liveViewManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { deepDiff } from "../templates/diff";
import { UploadConfig, UploadEntry } from "../upload";
import { BinaryUploadSerDe } from "../upload/binaryUploadSerDe";
import { ConsumeUploadedEntriesMeta, Info, WsLiveViewSocket } from "./liveSocket";
import { maybeAddStructuredClone } from "./structuredClone";
import {
PhxAllowUploadIncoming,
PhxBlurPayload,
Expand All @@ -48,7 +49,7 @@ import {
PhxProtocol,
} from "./types";
import { newHeartbeatReply, newPhxReply } from "./util";

maybeAddStructuredClone();
/**
* Data kept for each `LiveComponent` instance.
*/
Expand Down
Loading

0 comments on commit c6f6985

Please sign in to comment.