diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..3f7bf98
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,6 @@
+node_modules
+
+/.cache
+/build
+/public/build
+.env
diff --git a/.gitignore b/.gitignore
index 3f7bf98..3d3ba9a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
node_modules
+/.idea
/.cache
/build
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..9173eb0
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,45 @@
+# syntax = docker/dockerfile:1
+
+# Adjust NODE_VERSION as desired
+ARG NODE_VERSION=20.10.0
+FROM node:${NODE_VERSION}-slim as base
+
+LABEL fly_launch_runtime="Remix"
+
+# Remix app lives here
+WORKDIR /app
+
+# Set production environment
+ENV NODE_ENV="production"
+
+
+# Throw-away build stage to reduce size of final image
+FROM base as build
+
+# Install packages needed to build node modules
+RUN apt-get update -qq && \
+ apt-get install -y build-essential pkg-config python-is-python3
+
+# Install node modules
+COPY --link package-lock.json package.json ./
+RUN npm ci --include=dev
+
+# Copy application code
+COPY --link . .
+
+# Build application
+RUN npm run build
+
+# Remove development dependencies
+RUN npm prune --omit=dev
+
+
+# Final stage for app image
+FROM base
+
+# Copy built application
+COPY --from=build /app /app
+
+# Start the server by default, this can be overwritten at runtime
+EXPOSE 3000
+CMD [ "npm", "run", "start" ]
diff --git a/app/entry.client.tsx b/app/entry.client.tsx
index 94d5dc0..beda79c 100644
--- a/app/entry.client.tsx
+++ b/app/entry.client.tsx
@@ -1,18 +1,12 @@
-/**
- * By default, Remix will handle hydrating your app on the client for you.
- * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
- * For more information, see https://remix.run/file-conventions/entry.client
- */
-
import { RemixBrowser } from "@remix-run/react";
import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";
startTransition(() => {
- hydrateRoot(
- document,
-
-
-
- );
+ hydrateRoot(
+ document,
+
+
+ ,
+ );
});
diff --git a/app/entry.server.tsx b/app/entry.server.tsx
index 0c7712b..8a5252f 100644
--- a/app/entry.server.tsx
+++ b/app/entry.server.tsx
@@ -1,9 +1,3 @@
-/**
- * By default, Remix will handle generating the HTTP Response for you.
- * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
- * For more information, see https://remix.run/file-conventions/entry.server
- */
-
import { PassThrough } from "node:stream";
import type { AppLoadContext, EntryContext } from "@remix-run/node";
@@ -15,123 +9,123 @@ import { renderToPipeableStream } from "react-dom/server";
const ABORT_DELAY = 5_000;
export default function handleRequest(
- request: Request,
- responseStatusCode: number,
- responseHeaders: Headers,
- remixContext: EntryContext,
- loadContext: AppLoadContext
+ request: Request,
+ responseStatusCode: number,
+ responseHeaders: Headers,
+ remixContext: EntryContext,
+ loadContext: AppLoadContext,
) {
- return isbot(request.headers.get("user-agent"))
- ? handleBotRequest(
- request,
- responseStatusCode,
- responseHeaders,
- remixContext
- )
- : handleBrowserRequest(
- request,
- responseStatusCode,
- responseHeaders,
- remixContext
- );
+ return isbot(request.headers.get("user-agent"))
+ ? handleBotRequest(
+ request,
+ responseStatusCode,
+ responseHeaders,
+ remixContext,
+ )
+ : handleBrowserRequest(
+ request,
+ responseStatusCode,
+ responseHeaders,
+ remixContext,
+ );
}
function handleBotRequest(
- request: Request,
- responseStatusCode: number,
- responseHeaders: Headers,
- remixContext: EntryContext
+ request: Request,
+ responseStatusCode: number,
+ responseHeaders: Headers,
+ remixContext: EntryContext,
) {
- return new Promise((resolve, reject) => {
- let shellRendered = false;
- const { pipe, abort } = renderToPipeableStream(
- ,
- {
- onAllReady() {
- shellRendered = true;
- const body = new PassThrough();
- const stream = createReadableStreamFromReadable(body);
+ return new Promise((resolve, reject) => {
+ let shellRendered = false;
+ const { pipe, abort } = renderToPipeableStream(
+ ,
+ {
+ onAllReady() {
+ shellRendered = true;
+ const body = new PassThrough();
+ const stream = createReadableStreamFromReadable(body);
- responseHeaders.set("Content-Type", "text/html");
+ responseHeaders.set("Content-Type", "text/html");
- resolve(
- new Response(stream, {
- headers: responseHeaders,
- status: responseStatusCode,
- })
- );
+ resolve(
+ new Response(stream, {
+ headers: responseHeaders,
+ status: responseStatusCode,
+ }),
+ );
- pipe(body);
- },
- onShellError(error: unknown) {
- reject(error);
- },
- onError(error: unknown) {
- responseStatusCode = 500;
- // Log streaming rendering errors from inside the shell. Don't log
- // errors encountered during initial shell rendering since they'll
- // reject and get logged in handleDocumentRequest.
- if (shellRendered) {
- console.error(error);
- }
- },
- }
- );
+ pipe(body);
+ },
+ onShellError(error: unknown) {
+ reject(error);
+ },
+ onError(error: unknown) {
+ responseStatusCode = 500;
+ // Log streaming rendering errors from inside the shell. Don't log
+ // errors encountered during initial shell rendering since they'll
+ // reject and get logged in handleDocumentRequest.
+ if (shellRendered) {
+ console.error(error);
+ }
+ },
+ },
+ );
- setTimeout(abort, ABORT_DELAY);
- });
+ setTimeout(abort, ABORT_DELAY);
+ });
}
function handleBrowserRequest(
- request: Request,
- responseStatusCode: number,
- responseHeaders: Headers,
- remixContext: EntryContext
+ request: Request,
+ responseStatusCode: number,
+ responseHeaders: Headers,
+ remixContext: EntryContext,
) {
- return new Promise((resolve, reject) => {
- let shellRendered = false;
- const { pipe, abort } = renderToPipeableStream(
- ,
- {
- onShellReady() {
- shellRendered = true;
- const body = new PassThrough();
- const stream = createReadableStreamFromReadable(body);
+ return new Promise((resolve, reject) => {
+ let shellRendered = false;
+ const { pipe, abort } = renderToPipeableStream(
+ ,
+ {
+ onShellReady() {
+ shellRendered = true;
+ const body = new PassThrough();
+ const stream = createReadableStreamFromReadable(body);
- responseHeaders.set("Content-Type", "text/html");
+ responseHeaders.set("Content-Type", "text/html");
- resolve(
- new Response(stream, {
- headers: responseHeaders,
- status: responseStatusCode,
- })
- );
+ resolve(
+ new Response(stream, {
+ headers: responseHeaders,
+ status: responseStatusCode,
+ }),
+ );
- pipe(body);
- },
- onShellError(error: unknown) {
- reject(error);
- },
- onError(error: unknown) {
- responseStatusCode = 500;
- // Log streaming rendering errors from inside the shell. Don't log
- // errors encountered during initial shell rendering since they'll
- // reject and get logged in handleDocumentRequest.
- if (shellRendered) {
- console.error(error);
- }
- },
- }
- );
+ pipe(body);
+ },
+ onShellError(error: unknown) {
+ reject(error);
+ },
+ onError(error: unknown) {
+ responseStatusCode = 500;
+ // Log streaming rendering errors from inside the shell. Don't log
+ // errors encountered during initial shell rendering since they'll
+ // reject and get logged in handleDocumentRequest.
+ if (shellRendered) {
+ console.error(error);
+ }
+ },
+ },
+ );
- setTimeout(abort, ABORT_DELAY);
- });
+ setTimeout(abort, ABORT_DELAY);
+ });
}
diff --git a/app/root.tsx b/app/root.tsx
index b46b8fb..a5259f9 100644
--- a/app/root.tsx
+++ b/app/root.tsx
@@ -1,33 +1,33 @@
import { cssBundleHref } from "@remix-run/css-bundle";
import type { LinksFunction } from "@remix-run/node";
import {
- Links,
- LiveReload,
- Meta,
- Outlet,
- Scripts,
- ScrollRestoration,
+ Links,
+ LiveReload,
+ Meta,
+ Outlet,
+ Scripts,
+ ScrollRestoration,
} from "@remix-run/react";
export const links: LinksFunction = () => [
- ...(cssBundleHref ? [{ rel: "stylesheet", href: cssBundleHref }] : []),
+ ...(cssBundleHref ? [{ rel: "stylesheet", href: cssBundleHref }] : []),
];
export default function App() {
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
}
diff --git a/app/routes/_index.tsx b/app/routes/_index.tsx
index 5347369..d251b0f 100644
--- a/app/routes/_index.tsx
+++ b/app/routes/_index.tsx
@@ -1,41 +1,97 @@
-import type { MetaFunction } from "@remix-run/node";
+import type { LinksFunction, MetaFunction } from "@remix-run/node";
+
+import styles from "../styles/index.css";
+
+export const links: LinksFunction = () => [
+ { rel: "stylesheet", href: styles },
+];
export const meta: MetaFunction = () => {
- return [
- { title: "New Remix App" },
- { name: "description", content: "Welcome to Remix!" },
- ];
+ return [
+ { title: "New Remix App" },
+ { name: "description", content: "Welcome to Remix!" },
+ ];
};
export default function Index() {
- return (
-
- );
+ return (
+ <>
+
+
+{` _ _ _ _
+ | | | | (_) | |
+ | | ___ ___ __ _| | _ _ __ ___| |__
+ | |/ _ \\ / __/ _\` | |_____| | '_ \\ / __| '_ \\
+ | | (_) | (_| (_| | |_____| | |_) |\\__ \\ | | |
+ |_|\\___/ \\___\\__,_|_| |_| .__(_)___/_| |_|
+ | |
+ |_|
+`}
+
+
+
+
+
+
+
+
+ local-ip.sh is a magic domain name that provides wildcard DNS
+ for any IP address. It is heavily inspired by local-ip.co,
+ {" "}sslip.io, and xip.io.
+
+
+
+ Quick example, say your LAN IP address is 192.168.1.10.
+ Using local-ip.sh,
+
+
+
+ 192.168.1.10.local-ip.sh resolves to 192.168.1.10
+ dots.192.168.1.10.local-ip.sh resolves to 192.168.1.10
+dashes.192-168-1-10.local-ip.sh resolves to 192.168.1.10`,
+ }} />
+
+
+
+ ...and so on. You can use these domains to access virtual
+ hosts on your development web server from devices on your
+ local network. No configuration required!
+
+
+
+ The best part is, you can serve your content over HTTPS with our TLS certificate
+ for *.local-ip.sh
:
+
+ Be aware that wildcard certificates are not recursive, meaning they don't match "sub-subdomains".
+ In our case, this certificate will only match subdomains of local-ip.sh
such as 192-168-1-10.local-ip.sh
+ {" "}where dashes separate the numbers that make up the IP address.
+
+
+
+
+
+
+
+
+ local-ip.sh runs publicly a custom DNS server.
+ When your computer looks up a local-ip.sh domain, the local-ip.sh
+ DNS server resolves to the IP address it extracts from the domain.
+
+
+
+ The TLS certificate is obtained from Let's Encrypt and renewed up to a month before it expires.
+
+
+
+
+
+
+ >
+ );
}
diff --git a/app/routes/server[.]key.ts b/app/routes/server[.]key.ts
new file mode 100644
index 0000000..40a1889
--- /dev/null
+++ b/app/routes/server[.]key.ts
@@ -0,0 +1,7 @@
+export async function loader() {
+ if (process.env.NODE_ENV === "development") {
+ // return fetch("http://localhost:9229/server.key");
+ }
+
+ return fetch("http://local-ip.internal:9229/server.key");
+}
diff --git a/app/routes/server[.]pem.ts b/app/routes/server[.]pem.ts
new file mode 100644
index 0000000..d0a42ab
--- /dev/null
+++ b/app/routes/server[.]pem.ts
@@ -0,0 +1,7 @@
+export async function loader() {
+ if (process.env.NODE_ENV === "development") {
+ // return fetch("http://localhost:9229/server.pem");
+ }
+
+ return fetch("http://local-ip.internal:9229/server.pem");
+}
diff --git a/app/styles/index.css b/app/styles/index.css
new file mode 100644
index 0000000..a46ce44
--- /dev/null
+++ b/app/styles/index.css
@@ -0,0 +1,94 @@
+html {
+ background: #111;
+}
+
+body {
+ color: #728ea7;
+ display: flex;
+ flex-direction: column;
+ margin-inline: auto;
+ padding-left: 1.5em;
+ padding-right: 1.5em;
+ width: min(100%, 41.5rem);
+ /*margin: 50px auto;*/
+ margin-top: 50px;
+ margin-bottom: 50px;
+
+ font-family: ui-monospace, monospace;
+ font-size: 18px;
+ font-weight: bold;
+ /*line-height: 1.5;*/
+
+ -webkit-font-smoothing: antialiased;
+}
+
+header {
+ color: #7aa6da;
+}
+
+main a {
+ color: #728ea7;
+}
+
+main a:hover {
+ background: #7aa6da;
+ color: #111;
+ text-decoration: none;
+}
+
+section:nth-child(n + 2) {
+ margin-top: 3rem;
+}
+
+section > main {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ row-gap: 2rem;
+}
+
+code {
+ padding: 0.2em 0.4em;
+ margin: 0;
+ font-size: 85%;
+ background-color: rgba(27, 31, 35, 0.95);
+ border-radius: 3px;
+ white-space: nowrap;
+}
+
+header span.dim {
+ color: #728ea7;
+}
+
+section strong {
+ color: #7aa6da;
+}
+
+footer {
+ margin: 5rem auto 0;
+ color: #556a7d;
+}
+
+footer a {
+ color: inherit;
+}
+
+footer a:hover {
+ color: #728ea7;
+}
+
+div.cursor {
+ display: inline-block;
+ background: #111;
+ margin-left: 1px;
+ margin-right: -1px;
+ animation: blink 2s linear 0s infinite;
+}
+
+@keyframes blink {
+ 0% { background: #7aa6da }
+ 47% { background: #728ea7 }
+ 50% { background: #111 }
+ 97% { background: #111 }
+ 100% { background: #728ea7 }
+}
diff --git a/fly.toml b/fly.toml
new file mode 100644
index 0000000..4ed1a8f
--- /dev/null
+++ b/fly.toml
@@ -0,0 +1,22 @@
+# fly.toml app configuration file generated for www-local-ip on 2023-12-12T23:37:13+01:00
+#
+# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
+#
+
+app = "www-local-ip"
+primary_region = "ams"
+
+[build]
+
+[http_service]
+ internal_port = 3000
+ force_https = true
+ auto_stop_machines = true
+ auto_start_machines = true
+ min_machines_running = 0
+ processes = ["app"]
+
+[[vm]]
+ cpu_kind = "shared"
+ cpus = 1
+ memory_mb = 256
diff --git a/package-lock.json b/package-lock.json
index b906a71..6579b4d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,6 +15,7 @@
"react-dom": "^18.2.0"
},
"devDependencies": {
+ "@flydotio/dockerfile": "^0.4.11",
"@remix-run/dev": "^2.3.1",
"@remix-run/eslint-config": "^2.3.1",
"@types/react": "^18.2.20",
@@ -1309,6 +1310,37 @@
"node": ">=14"
}
},
+ "node_modules/@flydotio/dockerfile": {
+ "version": "0.4.11",
+ "resolved": "https://registry.npmjs.org/@flydotio/dockerfile/-/dockerfile-0.4.11.tgz",
+ "integrity": "sha512-L52UAfrOhmAn3T4TxpeRofQOSO+Kctg+uraB4nLzo4mvvh+4Z7HYxSi7Dnq0Kirz+xx6fDIc4OMNT1EdaORecA==",
+ "dev": true,
+ "dependencies": {
+ "chalk": "^5.3.0",
+ "diff": "^5.1.0",
+ "ejs": "^3.1.9",
+ "shell-quote": "^1.8.1",
+ "yargs": "^17.7.2"
+ },
+ "bin": {
+ "dockerfile": "index.js"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/@flydotio/dockerfile/node_modules/chalk": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz",
+ "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==",
+ "dev": true,
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
"node_modules/@humanwhocodes/config-array": {
"version": "0.11.13",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz",
@@ -2635,6 +2667,12 @@
"astring": "bin/astring"
}
},
+ "node_modules/async": {
+ "version": "3.2.5",
+ "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz",
+ "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==",
+ "dev": true
+ },
"node_modules/asynciterator.prototype": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/asynciterator.prototype/-/asynciterator.prototype-1.0.0.tgz",
@@ -3107,6 +3145,57 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "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,
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/cliui/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
+ },
+ "node_modules/cliui/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,
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cliui/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,
+ "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/clone": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
@@ -3585,6 +3674,21 @@
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
},
+ "node_modules/ejs": {
+ "version": "3.1.9",
+ "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz",
+ "integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==",
+ "dev": true,
+ "dependencies": {
+ "jake": "^10.8.5"
+ },
+ "bin": {
+ "ejs": "bin/cli.js"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/electron-to-chromium": {
"version": "1.4.609",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.609.tgz",
@@ -4897,6 +5001,27 @@
"node": "^10.12.0 || >=12.0.0"
}
},
+ "node_modules/filelist": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
+ "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==",
+ "dev": true,
+ "dependencies": {
+ "minimatch": "^5.0.1"
+ }
+ },
+ "node_modules/filelist/node_modules/minimatch": {
+ "version": "5.1.6",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
+ "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
@@ -5139,6 +5264,15 @@
"node": ">=6.9.0"
}
},
+ "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,
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
"node_modules/get-intrinsic": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz",
@@ -6138,6 +6272,46 @@
"@pkgjs/parseargs": "^0.11.0"
}
},
+ "node_modules/jake": {
+ "version": "10.8.7",
+ "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.7.tgz",
+ "integrity": "sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==",
+ "dev": true,
+ "dependencies": {
+ "async": "^3.2.3",
+ "chalk": "^4.0.2",
+ "filelist": "^1.0.4",
+ "minimatch": "^3.1.2"
+ },
+ "bin": {
+ "jake": "bin/cli.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/jake/node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/jake/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/javascript-stringify": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/javascript-stringify/-/javascript-stringify-2.1.0.tgz",
@@ -8840,6 +9014,15 @@
"url": "https://opencollective.com/unified"
}
},
+ "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,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/require-like": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/require-like/-/require-like-0.1.2.tgz",
@@ -9239,6 +9422,15 @@
"node": ">=8"
}
},
+ "node_modules/shell-quote": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz",
+ "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/side-channel": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
@@ -11118,6 +11310,15 @@
"node": ">=0.4"
}
},
+ "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,
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
@@ -11133,6 +11334,53 @@
"node": ">= 14"
}
},
+ "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,
+ "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",
+ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yargs/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
+ },
+ "node_modules/yargs/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,
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
diff --git a/package.json b/package.json
index db53c92..4d65431 100644
--- a/package.json
+++ b/package.json
@@ -20,6 +20,7 @@
"react-dom": "^18.2.0"
},
"devDependencies": {
+ "@flydotio/dockerfile": "^0.4.11",
"@remix-run/dev": "^2.3.1",
"@remix-run/eslint-config": "^2.3.1",
"@types/react": "^18.2.20",
@@ -30,4 +31,4 @@
"engines": {
"node": ">=18.0.0"
}
-}
\ No newline at end of file
+}