diff --git a/deploy-server.js b/deploy-server.js new file mode 100644 index 0000000..a4397ce --- /dev/null +++ b/deploy-server.js @@ -0,0 +1,196 @@ +import { listenAndServe } from "https://deno.land/std@0.111.0/http/server.ts"; +import { MEDIA_TYPES } from './media-type.js'; +import { main as generateImportmap } from './js/importmap-generator.js'; + +const assetMap = { + '/': './deploy.html', + '/README.md': './README.md' +} + +/** + * @param {string} path + * @returns {string} + */ +function removeLeadingSlash(path) { + if (path.startsWith("/")) { + return path.slice(1); + } + return path; +} + +/** + * @param {string} path + * @returns {string} + */ +function removeTrailingSlash(path) { + if (path.endsWith("/")) { + return path.slice(0, -1); + } + return path; +} + +/** + * @param {string} path + * @returns {string} + */ +function removeSlashes(path) { + return removeTrailingSlash(removeLeadingSlash(path)); +} + +/** + * A `maidenPathname` is a `URL.pathname` without any path prefix, relative or absolute. + * e.g. 👇 + * ``` + * https://www.example.com/js/main.js -> js/main.js + * /js/main.js -> js/main.js + * ./js/main.js -> js/main.js + * js/main.js -> js/main.js + * ``` + * @param {string} maidenPathname + * @returns {string} + */ +function getFileExtensionFromPath(maidenPathname) { + const [fileExtension] = maidenPathname.split('.').reverse(); + return `.${fileExtension}`; +} + +/** +* @param {Request} request +* @returns {Promise} +*/ +async function requestHandler(request) { + const { pathname } = new URL(request.url); + + const { sessionStorage, localStorage } = globalThis; + + const storage = sessionStorage || localStorage; + + if (storage) { + const storedFileKey = removeSlashes(pathname); + const storedFile = storage.getItem(storedFileKey); + + if (storedFile) { + return new Response(storedFile, { + // @ts-ignore + headers: { + // @ts-ignore + "content-type": MEDIA_TYPES[getFileExtensionFromPath(storedFileKey)], + "x-cache": 'HIT' + } + }); + } + } + + const assetPath = assetMap[pathname]; + + if (assetPath) { + try { + + const mode = request.headers.get('sec-fetch-mode'); + const dest = request.headers.get('sec-fetch-dest'); + const contentTypeHTML = mode === 'navigate' || dest === 'document'; + + if (contentTypeHTML) { + const content = await Deno.readTextFile(assetPath); + const importMap = await generateImportmap(); + + const [beforeImportmap, afterImportmap] = content.split("//__importmap"); + const html = `${beforeImportmap}${importMap}${afterImportmap}`; + + return new Response(html, { + headers: { + "content-type": MEDIA_TYPES['.html'], + } + }); + } + + const content = Deno.readFile(assetPath); + const textDecodedContent = new TextDecoder().decode(fileContent); + const maidenPathname = removeSlashes(assetPath); + + return new Response(textDecodedContent, { + headers: { + "content-type": MEDIA_TYPES[getFileExtensionFromPath(maidenPathname)], + } + }); + } catch (error) { + return new Response(error.message || error.toString(), { status: 500 }); + } + } + + + const site = request.headers.get('sec-fetch-site'); + const contentTypeCompiledJSX = dest === 'script' && mode === 'cors' && site === 'same-origin' && pathname.endsWith(".jsx.js"); + + if (contentTypeCompiledJSX) { + try { + const { files, diagnostics } = await Deno.emit(`.${pathname}`.slice(0, -3)); + + if (diagnostics.length) { + // there is something that impacted the emit + console.warn(Deno.formatDiagnostics(diagnostics)); + } + // @ts-ignore + const [, content] = Object.entries(files).find(([fileName]) => { + const cwd = toFileUrl(Deno.cwd()).href; + const commonPath = common([ + cwd, + fileName, + ]); + const shortFileName = fileName.replace(commonPath, `/`); + + return shortFileName === pathname; + }); + return new Response(content); + } catch (error) { + return new Response(error.message || error.toString(), { status: 500 }) + } + } + + if (pathname.endsWith(".jsx")) { + try { + if (storage) { + const { files, diagnostics } = await Deno.emit(`.${pathname}`); + + if (diagnostics.length) { + // there is something that impacted the emit + console.warn(Deno.formatDiagnostics(diagnostics)); + } + + for (const [fileName, text] of Object.entries(files)) { + + const cwd = toFileUrl(Deno.cwd()).href; + const commonPath = common([ + cwd, + fileName, + ]); + const maidenPathname = fileName.replace(commonPath, ``); + + storage.setItem(maidenPathname, text); + } + } + + return new Response(pathname, { + status: 303, + headers: { + "location": `${request.url}.js`, + }, + }); + } catch (error) { + return new Response(error.message || error.toString(), { status: 500 }) + } + } + + return new Response(null, { + status: 404, + }); +} + +if (import.meta.main) { + const PORT = Deno.env.get("PORT") || 1729; + const timestamp = Date.now(); + const humanReadableDateTime = new Date(timestamp).toLocaleString(); + console.log('Current Date: ', humanReadableDateTime) + console.info(`Server Listening on http://localhost:${PORT}`); + listenAndServe(`:${PORT}`, requestHandler); +} diff --git a/deploy.html b/deploy.html new file mode 100644 index 0000000..b6faab8 --- /dev/null +++ b/deploy.html @@ -0,0 +1,25 @@ + + + + + + + @fusionstrings/river + + + + + +

@fusionstrings/river

+ + + + \ No newline at end of file diff --git a/dev-server.js b/dev-server.js index dfd0f69..74771fe 100644 --- a/dev-server.js +++ b/dev-server.js @@ -2,11 +2,11 @@ import { listenAndServe } from "https://deno.land/std@0.113.0/http/server.ts"; import { serveFile } from "https://deno.land/std@0.113.0/http/file_server.ts"; import { common, - parse, + // parse, extname, toFileUrl, } from "https://deno.land/std@0.113.0/path/mod.ts"; -import { ensureDir } from "https://deno.land/std@0.113.0/fs/mod.ts"; +//import { ensureDir } from "https://deno.land/std@0.113.0/fs/mod.ts"; import { MEDIA_TYPES } from "./media-type.js"; const staticAssets = { @@ -18,7 +18,7 @@ const staticAssets = { /** * @param {string} path - * @returns string + * @returns {string} */ function removeLeadingSlash(path) { if (path.startsWith("/")) { @@ -29,7 +29,7 @@ function removeLeadingSlash(path) { /** * @param {string} path - * @returns + * @returns {string} */ function removeTrailingSlash(path) { if (path.endsWith("/")) { @@ -40,7 +40,7 @@ function removeTrailingSlash(path) { /** * @param {string} path - * @returns + * @returns {string} */ function removeSlashes(path) { return removeTrailingSlash(removeLeadingSlash(path)); @@ -48,6 +48,7 @@ function removeSlashes(path) { /** * @param {Request} request +* @returns {Promise} */ async function requestHandler(request) { const mode = request.headers.get('sec-fetch-mode'); @@ -142,11 +143,11 @@ async function requestHandler(request) { const shortFileName = fileName.replace(commonPath, ``); sessionStorage.setItem(shortFileName, text); - const outputFileName = `./dist/${shortFileName}`; + // const outputFileName = `./dist/${shortFileName}`; - const { dir } = parse(outputFileName); - await ensureDir(dir); - await Deno.writeTextFile(outputFileName, text); + // const { dir } = parse(outputFileName); + // await ensureDir(dir); + // await Deno.writeTextFile(outputFileName, text); } return new Response(pathname, { diff --git a/js/deploy.js b/js/deploy.js new file mode 100644 index 0000000..b75352e --- /dev/null +++ b/js/deploy.js @@ -0,0 +1 @@ +export { main } from './dom.jsx'; diff --git a/js/importmap-generator.js b/js/importmap-generator.js new file mode 100644 index 0000000..695a97d --- /dev/null +++ b/js/importmap-generator.js @@ -0,0 +1,21 @@ +import { Generator } from 'https://cdn.jsdelivr.net/gh/fusionstrings/dependencies@c59ae9dde8d5d339b3c0433d4b4f46e72739fafe/dist/browser/jspm.js'; + +async function main(subpath = "./js/main.js") { + const generator = new Generator(); + + await generator.install([ + { + alias: "@fusionstrings/river", + target: "./", + subpath, + }, + ]); + const importMap = JSON.stringify(generator.getMap(), null, 2); + return importMap; +} + +if (import.meta.main) { + main(); +} + +export { main } \ No newline at end of file