From 33d3730dee28cd122b3a0125bc9ab2ed8b5f4f95 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli Date: Sat, 7 May 2022 07:33:54 -0400 Subject: [PATCH 1/3] wip --- .npmrc | 2 +- frontend/app/services/-compile/babel/index.ts | 27 ++- .../app/services/-compile/babel/worker.ts | 34 +--- frontend/ember-cli-build.js | 2 + .../{build.js => broccoli-funnel.js} | 17 ++ packages/transpilation/package.json | 2 +- packages/transpilation/src/babel.ts | 0 packages/transpilation/src/fetch-handler.ts | 190 +++++++++--------- packages/transpilation/src/index.ts | 104 +++++----- 9 files changed, 189 insertions(+), 189 deletions(-) rename packages/transpilation/{build.js => broccoli-funnel.js} (80%) create mode 100644 packages/transpilation/src/babel.ts diff --git a/.npmrc b/.npmrc index 5789e699f..9daa01a03 100644 --- a/.npmrc +++ b/.npmrc @@ -1,5 +1,5 @@ registry = https://registry.npmjs.org -ignore-scripts = false +ignore-scripts = true public-hoist-pattern[]=*@types* diff --git a/frontend/app/services/-compile/babel/index.ts b/frontend/app/services/-compile/babel/index.ts index bec2776e7..cb08259f5 100644 --- a/frontend/app/services/-compile/babel/index.ts +++ b/frontend/app/services/-compile/babel/index.ts @@ -1,19 +1,18 @@ -// import config from 'limber/config/environment'; +import { compileJS as cjs } from 'ember-repl'; +import config from 'limber/config/environment'; -// import { compile as worker } from './worker'; +import { compile as worker } from './worker'; -// import type { ExtractedCode } from '../markdown-to-ember'; -// import type { AsyncReturnType } from 'type-fest'; +import type { AsyncReturnType } from 'type-fest'; -// type CompiledViaWorker = AsyncReturnType; -// type CompiledViaCJS = AsyncReturnType; -// export type CompileOutput = CompiledViaCJS[0] | CompiledViaWorker[0]; +type CompiledViaWorker = AsyncReturnType; +type CompiledViaCJS = AsyncReturnType; +export type CompileOutput = CompiledViaCJS | CompiledViaWorker; -// export async function compileJS(_id: string, js: ExtractedCode[]): Promise { -// if (config.SERVICE_WORKER) { -// return worker(js); -// } +export async function compileJS(js: string): Promise { + if (config.SERVICE_WORKER) { + return worker(js); + } -// // use compileAll from -compile/index -// // return cjs(js); -// } + return cjs(js); +} diff --git a/frontend/app/services/-compile/babel/worker.ts b/frontend/app/services/-compile/babel/worker.ts index b80381a65..06db91ae2 100644 --- a/frontend/app/services/-compile/babel/worker.ts +++ b/frontend/app/services/-compile/babel/worker.ts @@ -2,21 +2,7 @@ import type { ExtractedCode } from '../markdown-to-ember'; let isRegistered = false; -export async function compile(js: ExtractedCode[]) { - let moduleInfos = await compileModules(js); - - let missing = moduleInfos.filter(({ importPath }) => !importPath); - - if (missing.length > 0) { - let first = missing[0]; - - throw new Error(`Component, ${first.name}, failed to compile`); - } - - return moduleInfos; -} - -export async function compileModules(js: ExtractedCode[]) { +export async function compile(js: ExtractedCode) { if (!isRegistered) { await installImportMap(); await setupServiceWorker(); @@ -24,21 +10,17 @@ export async function compileModules(js: ExtractedCode[]) { isRegistered = true; } - let responses = await Promise.all( - js.map(async ({ name, code }) => { - let qps = new URLSearchParams(); + let { name, code } = js; - qps.set('n', name); - qps.set('q', code); + let qps = new URLSearchParams(); - let response = await fetch(`/compile-sw?${qps}`); - let { importPath } = await response.json(); + qps.set('n', name); + qps.set('q', code); - return { name, importPath }; - }) - ); + let response = await fetch(`/compile-sw?${qps}`); + let { importPath } = await response.json(); - return responses; + return { name, importPath }; } async function installImportMap() { diff --git a/frontend/ember-cli-build.js b/frontend/ember-cli-build.js index 898a4de5b..f2bf964bb 100644 --- a/frontend/ember-cli-build.js +++ b/frontend/ember-cli-build.js @@ -44,6 +44,8 @@ module.exports = function (defaults) { return require('@embroider/compat').compatBuild(app, Webpack, { extraPublicTrees: [ + // Service Worker / transpilation + require('@nullvoxpopuli/limber-transpilation/broccoli-funnel')(), // Mobile Editor require('@nullvoxpopuli/limber-codemirror/broccoli-funnel')(), // Desktop Editor diff --git a/packages/transpilation/build.js b/packages/transpilation/broccoli-funnel.js similarity index 80% rename from packages/transpilation/build.js rename to packages/transpilation/broccoli-funnel.js index 9e75196cb..c7da9fe1f 100644 --- a/packages/transpilation/build.js +++ b/packages/transpilation/broccoli-funnel.js @@ -60,3 +60,20 @@ function workersFunnel({ isProduction }) { module.exports = { workersFunnel, }; + +'use strict'; + +const Funnel = require('broccoli-funnel'); + +const SRC_FILES = path.join(__dirname, 'dist'); + +/** + * This broccoli funnel is for copying the built assets to a target + * app's public folder. No building occurs + * + */ +module.exports = function codemirrorFunnel() { + return new Funnel(SRC_FILES, { + destDir: 'transpilation/', + }); +}; diff --git a/packages/transpilation/package.json b/packages/transpilation/package.json index 5611c14e5..a4def3eb6 100644 --- a/packages/transpilation/package.json +++ b/packages/transpilation/package.json @@ -6,7 +6,7 @@ "author": "NullVoxPopuli", "main": "dist/limber-worker.js", "scripts": { - "prepare": "webpack", + "build": "webpack", "lint:fix": "npm-run-all --aggregate-output --parallel 'lint:*:fix'", "lint:js": "eslint . --cache", "lint:js:fix": "eslint . --fix", diff --git a/packages/transpilation/src/babel.ts b/packages/transpilation/src/babel.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/transpilation/src/fetch-handler.ts b/packages/transpilation/src/fetch-handler.ts index 4001858e6..a04cfecce 100644 --- a/packages/transpilation/src/fetch-handler.ts +++ b/packages/transpilation/src/fetch-handler.ts @@ -1,97 +1,97 @@ // import { compileGJS } from './babel'; -// // const CACHE_NAME = 'babel-compilation-and-module-service'; -// const URLS = ['/compile-sw', /^\/module-sw\//]; - -// const COMPILE_CACHE = new Map(); - -// export async function handleFetch(event: FetchEvent): Promise { -// const url = new URL(event.request.url); - -// console.info('handleFetch', url.pathname); - -// if (!URLS.some((matcher) => url.pathname.match(matcher))) { -// return fetch(event.request); -// } - -// if (COMPILE_CACHE.has(url.pathname)) { -// return moduleResponse(url.pathname); -// } - -// if (url.pathname === '/compile-sw') { -// return maybe(() => compile(url)); -// } - -// return error(`Unhandled URL: ${url.pathname}`); -// } - -// async function maybe(op: () => Return | Promise) { -// try { -// return await op(); -// } catch (e) { -// return error(e); -// } -// } - -// function error(msg: Error | string, status = 500) { -// let payload: string | Error | Record; - -// if (typeof msg === 'string') { -// payload = msg; -// } else if (msg instanceof TypeError) { -// payload = { -// ...msg, -// name: msg.name, -// message: msg.message, -// stack: msg.stack, -// }; -// } else { -// payload = msg; -// } - -// return new Response(JSON.stringify({ error: payload }), { -// status, -// headers: { -// 'Content-Type': 'application/json', -// }, -// }); -// } - -// function moduleResponse(pathName: string) { -// let code = COMPILE_CACHE.get(pathName); - -// if (!code) { -// throw new Error(`Code has not been compiled. call /compile-sw with the code`); -// } - -// return new Response(code, { -// headers: { -// 'Content-Type': 'application/javascript', -// }, -// }); -// } - -// async function compile(url: URL) { -// let qps = new URLSearchParams(url.search); -// let name = qps.get('n'); -// let code = qps.get('q'); -// let modulePath = `/module-sw/${name}.js`; - -// if (!name || !code) { -// throw new Error( -// `Both name and code are required. Make sure than the n and q query params are specified` -// ); -// } - -// let compiled = await compileGJS({ name, code }); - -// COMPILE_CACHE.set(modulePath, compiled); - -// let response = new Response(JSON.stringify({ importPath: modulePath }), { -// headers: { -// 'Content-Type': 'application/json', -// }, -// }); - -// return response; -// } +// const CACHE_NAME = 'babel-compilation-and-module-service'; +const URLS = ['/compile-sw', /^\/module-sw\//]; + +const COMPILE_CACHE = new Map(); + +export async function handleFetch(event: FetchEvent): Promise { + const url = new URL(event.request.url); + + console.info('handleFetch', url.pathname); + + if (!URLS.some((matcher) => url.pathname.match(matcher))) { + return fetch(event.request); + } + + if (COMPILE_CACHE.has(url.pathname)) { + return moduleResponse(url.pathname); + } + + if (url.pathname === '/compile-sw') { + return maybe(() => compile(url)); + } + + return error(`Unhandled URL: ${url.pathname}`); +} + +async function maybe(op: () => Return | Promise) { + try { + return await op(); + } catch (e) { + return error(e); + } +} + +function error(msg: Error | string, status = 500) { + let payload: string | Error | Record; + + if (typeof msg === 'string') { + payload = msg; + } else if (msg instanceof TypeError) { + payload = { + ...msg, + name: msg.name, + message: msg.message, + stack: msg.stack, + }; + } else { + payload = msg; + } + + return new Response(JSON.stringify({ error: payload }), { + status, + headers: { + 'Content-Type': 'application/json', + }, + }); +} + +function moduleResponse(pathName: string) { + let code = COMPILE_CACHE.get(pathName); + + if (!code) { + throw new Error(`Code has not been compiled. call /compile-sw with the code`); + } + + return new Response(code, { + headers: { + 'Content-Type': 'application/javascript', + }, + }); +} + +async function compile(url: URL) { + let qps = new URLSearchParams(url.search); + let name = qps.get('n'); + let code = qps.get('q'); + let modulePath = `/module-sw/${name}.js`; + + if (!name || !code) { + throw new Error( + `Both name and code are required. Make sure than the n and q query params are specified` + ); + } + + let compiled = await compileGJS({ name, code }); + + COMPILE_CACHE.set(modulePath, compiled); + + let response = new Response(JSON.stringify({ importPath: modulePath }), { + headers: { + 'Content-Type': 'application/json', + }, + }); + + return response; +} diff --git a/packages/transpilation/src/index.ts b/packages/transpilation/src/index.ts index ffd516c33..fe58eec80 100644 --- a/packages/transpilation/src/index.ts +++ b/packages/transpilation/src/index.ts @@ -1,57 +1,57 @@ -// import { handleFetch } from './fetch-handler'; +import { handleFetch } from './fetch-handler'; -// const worker = self as unknown as ServiceWorkerGlobalScope; +const worker = self as unknown as ServiceWorkerGlobalScope; -// /** -// * For a given glimdown document id, we will compile -// * N components within that glimdown, and return an object -// * map of an arbitrary name of the default export to the URL -// * for which the module may be imported from. -// * -// * Since the set of modules is uniqueish to the glimdown -// * document id, we'll try to keep a history of 10 most recent -// * compiles, so that quick edits don't need to do extra work -// * -// * example: -// * -// * POST /compile.sw -// * id: gmd.id, -// * components: [{ name: string, code: string }] -// * -// * => -// * -// * { -// * [name] => "url/to/import" -// * } -// * -// * which will then turn in to (roughly): -// * -// * for (let [name, importPath] of response) { -// * let module = await import(importPath); -// * -// * owner.register(`component:${name}`, module); -// * } -// * -// * and the <${name} /> will be swapped in to the ember -// * variant of the glimdown for invocation -// * -// */ -// worker.addEventListener('install', () => { -// // force moving on to activation even if another service worker had control -// worker.skipWaiting(); -// }); +/** + * For a given glimdown document id, we will compile + * N components within that glimdown, and return an object + * map of an arbitrary name of the default export to the URL + * for which the module may be imported from. + * + * Since the set of modules is uniqueish to the glimdown + * document id, we'll try to keep a history of 10 most recent + * compiles, so that quick edits don't need to do extra work + * + * example: + * + * POST /compile.sw + * id: gmd.id, + * components: [{ name: string, code: string }] + * + * => + * + * { + * [name] => "url/to/import" + * } + * + * which will then turn in to (roughly): + * + * for (let [name, importPath] of response) { + * let module = await import(importPath); + * + * owner.register(`component:${name}`, module); + * } + * + * and the <${name} /> will be swapped in to the ember + * variant of the glimdown for invocation + * + */ +worker.addEventListener('install', () => { + // force moving on to activation even if another service worker had control + worker.skipWaiting(); +}); -// worker.addEventListener('activate', (event) => { -// // Claim any clients immediately, so that the page will be under SW control without reloading. -// event.waitUntil(worker.clients.claim()); -// console.info(`\ -// Service Worker installed successfully! +worker.addEventListener('activate', (event) => { + // Claim any clients immediately, so that the page will be under SW control without reloading. + event.waitUntil(worker.clients.claim()); + console.info(`\ + Service Worker installed successfully! -// This service worker is used for compiling JavaScript -// and providing modules to the main thread. -// `); -// }); + This service worker is used for compiling JavaScript + and providing modules to the main thread. + `); +}); -// worker.addEventListener('fetch', (event) => { -// event.respondWith(handleFetch(event)); -// }); +worker.addEventListener('fetch', (event) => { + event.respondWith(handleFetch(event)); +}); From 6bb8eef71a5be997fded274573643a5ce491c9f2 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli Date: Sun, 8 May 2022 18:30:59 -0400 Subject: [PATCH 2/3] Why is everything difficult --- frontend/app/index.html | 2 - frontend/app/services/-compile/babel/index.ts | 13 ++-- .../app/services/-compile/babel/worker.ts | 63 +++++++++++------- frontend/app/services/-compile/index.ts | 28 ++++---- frontend/ember-cli-build.js | 12 ++++ packages/transpilation/broccoli-funnel.js | 64 +------------------ packages/transpilation/build.js | 30 +++++++++ packages/transpilation/package.json | 13 ++-- packages/transpilation/src/fetch-handler.ts | 3 +- packages/transpilation/src/index.ts | 2 + packages/transpilation/webpack.config.js | 44 ------------- 11 files changed, 112 insertions(+), 162 deletions(-) create mode 100644 packages/transpilation/build.js delete mode 100644 packages/transpilation/webpack.config.js diff --git a/frontend/app/index.html b/frontend/app/index.html index 4f176f5c0..7276ec22a 100644 --- a/frontend/app/index.html +++ b/frontend/app/index.html @@ -7,8 +7,6 @@ - - {{content-for "head"}} diff --git a/frontend/app/services/-compile/babel/index.ts b/frontend/app/services/-compile/babel/index.ts index cb08259f5..65a9dda5c 100644 --- a/frontend/app/services/-compile/babel/index.ts +++ b/frontend/app/services/-compile/babel/index.ts @@ -1,18 +1,19 @@ import { compileJS as cjs } from 'ember-repl'; -import config from 'limber/config/environment'; +// import config from 'limber/config/environment'; import { compile as worker } from './worker'; import type { AsyncReturnType } from 'type-fest'; +import { ExtractedCode } from '../markdown-to-ember'; type CompiledViaWorker = AsyncReturnType; type CompiledViaCJS = AsyncReturnType; export type CompileOutput = CompiledViaCJS | CompiledViaWorker; -export async function compileJS(js: string): Promise { - if (config.SERVICE_WORKER) { - return worker(js); - } +export async function compileJS(info: ExtractedCode): Promise { + // if (config.SERVICE_WORKER) { + return worker(info); + // } - return cjs(js); + // return cjs(js); } diff --git a/frontend/app/services/-compile/babel/worker.ts b/frontend/app/services/-compile/babel/worker.ts index 06db91ae2..1700f7a1a 100644 --- a/frontend/app/services/-compile/babel/worker.ts +++ b/frontend/app/services/-compile/babel/worker.ts @@ -2,14 +2,22 @@ import type { ExtractedCode } from '../markdown-to-ember'; let isRegistered = false; +class CompileError extends Error { + constructor(public message: string, public response: Response, public code: string, public url: string) { + super(message); + } +} + export async function compile(js: ExtractedCode) { if (!isRegistered) { - await installImportMap(); + // await installImportMap(); await setupServiceWorker(); isRegistered = true; } + await new Promise(resolve => setTimeout(resolve, 5000)); + let { name, code } = js; let qps = new URLSearchParams(); @@ -17,46 +25,53 @@ export async function compile(js: ExtractedCode) { qps.set('n', name); qps.set('q', code); - let response = await fetch(`/compile-sw?${qps}`); + let url = `/compile-sw?${qps}`; + + let response = await fetch(url); + if (response.status !== 200) { + throw new CompileError(`${response.status} | Could not compile code. See console for details.`, response, code, url); + } let { importPath } = await response.json(); return { name, importPath }; } -async function installImportMap() { - let script = document.createElement('script'); +// async function installImportMap() { +// let script = document.createElement('script'); - script.setAttribute('type', 'importmap'); +// script.setAttribute('type', 'importmap'); - // let response = await import( - // /* webpackIgnore: true */ - // 'https://raw.githubusercontent.com/ef4/mho/a4391e53891f3f6321f0a8f36de88ec23511dbee/ember-app/importmap.json' - // ); - // External Import maps are not supported yet - // let response = await fetch( - // 'https://raw.githubusercontent.com/ef4/mho/a4391e53891f3f6321f0a8f36de88ec23511dbee/ember-app/importmap.json' - // ); - // let importmap = await response.text(); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - let importmap = (await import('/mho-importmap.json')).default; +// // let response = await import( +// // /* webpackIgnore: true */ +// // 'https://raw.githubusercontent.com/ef4/mho/a4391e53891f3f6321f0a8f36de88ec23511dbee/ember-app/importmap.json' +// // ); +// // External Import maps are not supported yet +// // let response = await fetch( +// // 'https://raw.githubusercontent.com/ef4/mho/a4391e53891f3f6321f0a8f36de88ec23511dbee/ember-app/importmap.json' +// // ); +// // let importmap = await response.text(); +// // eslint-disable-next-line @typescript-eslint/ban-ts-comment +// // @ts-ignore +// let importmap = (await import('/mho-importmap.json')).default; - console.debug({ importmap }); +// console.debug({ importmap }); - script.innerHTML = JSON.stringify(importmap); - document.body.appendChild(script); -} +// script.innerHTML = JSON.stringify(importmap); +// document.body.appendChild(script); +// } async function setupServiceWorker() { if ('serviceWorker' in navigator) { - let registration = await navigator.serviceWorker.register('/transpilation-worker.js'); + let registration = await navigator.serviceWorker.register('/transpilation/limber-worker.js') - // registration.update(); + registration.update(); console.info('ServiceWorker registration successful with scope: ', registration.scope); await new Promise((resolve) => { let interval = setInterval(() => { + console.log(registration?.active?.state); + if (registration.active?.state === 'activated') { clearInterval(interval); resolve(null); @@ -64,6 +79,8 @@ async function setupServiceWorker() { }, 50); }); + console.info('ServiceWorker activated.'); + return registration; } diff --git a/frontend/app/services/-compile/index.ts b/frontend/app/services/-compile/index.ts index 70084d412..608a873a2 100644 --- a/frontend/app/services/-compile/index.ts +++ b/frontend/app/services/-compile/index.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/ban-types */ -import { compileHBS, compileJS, invocationName } from 'ember-repl'; +import { compileHBS, invocationName } from 'ember-repl'; +import { compileJS } from './babel'; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error import CopyMenu from 'limber/components/limber/copy-menu'; @@ -19,14 +20,14 @@ interface CompilationResult { errorLine?: number; } -export async function compileAll(js: { code: string }[]) { +export async function compileAll(js: ExtractedCode[]) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore let { COMPONENT_MAP } = await import('/ember-repl/component-map.js'); let modules = await Promise.all( - js.map(async ({ code }) => { - return await compileJS(code, COMPONENT_MAP); + js.map(async (info) => { + return await compileJS(info); }) ); @@ -69,15 +70,15 @@ export async function compile(glimdownInput: string): Promise await Promise.all( compiled.map(async (info) => { - // using web worker + import maps is not available yet (need firefox support) - // (and to somehow be able to point at npm) - // - // if ('importPath' in info) { - // return scope.push({ - // moduleName: name, - // component: await import(/* webpackIgnore: true */ info.importPath), - // }); - // } + // using web worker + import maps is not available yet (need firefox support) + // (and to somehow be able to point at npm) + + if ('importPath' in info) { + return scope.push({ + moduleName: name, + component: await import(/* webpackIgnore: true */ info.importPath), + }); + } return scope.push(info); }) @@ -88,7 +89,6 @@ export async function compile(glimdownInput: string): Promise scope.push(compileHBS(code)); } } catch (error) { - console.info({ scope }); console.error(error); return { error, rootTemplate }; diff --git a/frontend/ember-cli-build.js b/frontend/ember-cli-build.js index f2bf964bb..6fdff5e14 100644 --- a/frontend/ember-cli-build.js +++ b/frontend/ember-cli-build.js @@ -18,6 +18,18 @@ module.exports = function (defaults) { `); let config = { + contentFor(_, __, type) { + if (type === 'head' && isProduction) { + return ` + + ` + } + + this._super.call(this, ...arguments); + }, 'ember-cli-terser': { enabled: MINIFY, }, diff --git a/packages/transpilation/broccoli-funnel.js b/packages/transpilation/broccoli-funnel.js index c7da9fe1f..a5ac508ad 100644 --- a/packages/transpilation/broccoli-funnel.js +++ b/packages/transpilation/broccoli-funnel.js @@ -1,68 +1,6 @@ 'use strict'; const path = require('path'); -const os = require('os'); -const fs = require('fs'); -const esbuild = require('esbuild'); - -const appFolder = path.join(__dirname, '..', 'app'); -const workerRoot = path.join(appFolder, 'workers'); - -function detectWorkers() { - let workers = {}; - let dir = fs.readdirSync(workerRoot); - - for (let i = 0; i < dir.length; i++) { - let name = dir[i]; - - workers[name] = path.join(workerRoot, name, 'index.js'); - } - - return workers; -} - -function configureWorkerTree({ isProduction, buildDir }) { - return ([name, entryPath]) => { - esbuild.buildSync({ - loader: { '.ts': 'ts', '.js': 'js' }, - entryPoints: [entryPath], - bundle: true, - outfile: path.join(buildDir, `${name}.js`), - format: 'esm', - minify: isProduction, - sourcemap: !isProduction, - // incremental: true, - // tsconfig: path.join(appFolder, 'tsconfig.json'), - }); - }; -} - -function buildWorkers(env) { - let inputs = detectWorkers(); - let workerBuilder = configureWorkerTree(env); - - // separate build from ember, will be detached, won't watch - Object.entries(inputs).map(workerBuilder); -} - -function workersFunnel({ isProduction }) { - let buildDir = fs.mkdtempSync(path.join(os.tmpdir(), 'limber--workers')); - - let options = { - isProduction, - buildDir, - }; - - // outputs {buildDir}/highlighting.js - buildWorkers(options); -} - -module.exports = { - workersFunnel, -}; - -'use strict'; - const Funnel = require('broccoli-funnel'); const SRC_FILES = path.join(__dirname, 'dist'); @@ -72,7 +10,7 @@ const SRC_FILES = path.join(__dirname, 'dist'); * app's public folder. No building occurs * */ -module.exports = function codemirrorFunnel() { +module.exports = function distWatcher() { return new Funnel(SRC_FILES, { destDir: 'transpilation/', }); diff --git a/packages/transpilation/build.js b/packages/transpilation/build.js new file mode 100644 index 000000000..21cda1288 --- /dev/null +++ b/packages/transpilation/build.js @@ -0,0 +1,30 @@ +const path = require('path'); +const esbuild = require('esbuild'); + +const entry = path.join(__dirname, 'src', 'index.ts'); +const buildDir = path.join(__dirname, 'dist'); + +const isWatching = process.argv.includes('--watch') +const isProduction = !isWatching; + +esbuild.build({ + loader: { '.ts': 'ts', '.js': 'js' }, + entryPoints: [entry], + bundle: true, + outfile: path.join(buildDir, `limber-worker.js`), + format: 'esm', + minify: isProduction, + sourcemap: isProduction, + ...( + isWatching ? { + incremental: true, + watch: { + onRebuild(error, result) { + if (error) console.error('watch build failed:', error) + else console.log('watch build succeeded:', result) + }, + }, + } + : {} + ) +}); diff --git a/packages/transpilation/package.json b/packages/transpilation/package.json index a4def3eb6..463daee3a 100644 --- a/packages/transpilation/package.json +++ b/packages/transpilation/package.json @@ -6,13 +6,11 @@ "author": "NullVoxPopuli", "main": "dist/limber-worker.js", "scripts": { - "build": "webpack", + "start": "node build.js --watch", + "build": "node build.js", "lint:fix": "npm-run-all --aggregate-output --parallel 'lint:*:fix'", "lint:js": "eslint . --cache", - "lint:js:fix": "eslint . --fix", - "start": "webpack --watch", - "cp": "cp ./dist/limber-worker.js ../limber/public/transpilation-worker.js", - "prod": "PRODUCTION=true webpack" + "lint:js:fix": "eslint . --fix" }, "browser": { "path": "path-browserify", @@ -45,10 +43,7 @@ "@types/htmlbars-inline-precompile": "^3.0.0", "babel-loader": "^8.2.3", "esbuild": "0.14.27", - "typescript": "^4.6.2", - "webpack": "^5.70.0", - "webpack-cli": "^4.9.2", - "webpack-node-externals": "^3.0.0" + "typescript": "^4.6.2" }, "engines": { "node": ">= v16.14.1" diff --git a/packages/transpilation/src/fetch-handler.ts b/packages/transpilation/src/fetch-handler.ts index a04cfecce..244299f83 100644 --- a/packages/transpilation/src/fetch-handler.ts +++ b/packages/transpilation/src/fetch-handler.ts @@ -83,7 +83,8 @@ async function compile(url: URL) { ); } - let compiled = await compileGJS({ name, code }); + // let compiled = await compileGJS({ name, code }); + let compiled = ''; COMPILE_CACHE.set(modulePath, compiled); diff --git a/packages/transpilation/src/index.ts b/packages/transpilation/src/index.ts index fe58eec80..776ae7289 100644 --- a/packages/transpilation/src/index.ts +++ b/packages/transpilation/src/index.ts @@ -43,6 +43,8 @@ worker.addEventListener('install', () => { worker.addEventListener('activate', (event) => { // Claim any clients immediately, so that the page will be under SW control without reloading. + // + // https://developer.mozilla.org/en-US/docs/Web/API/Clients/claim event.waitUntil(worker.clients.claim()); console.info(`\ Service Worker installed successfully! diff --git a/packages/transpilation/webpack.config.js b/packages/transpilation/webpack.config.js deleted file mode 100644 index 6f3c12153..000000000 --- a/packages/transpilation/webpack.config.js +++ /dev/null @@ -1,44 +0,0 @@ -'use strict'; - -const path = require('path'); -const webpack = require('webpack'); - -module.exports = { - mode: process.env.PRODUCTION != null ? 'production' : 'development', - entry: { - worker: './src/index.ts', - }, - target: 'webworker', - devtool: process.env.PRODUCTION != null ? false : 'inline-source-map', - plugins: [ - new webpack.ProvidePlugin({ - process: 'process', - Buffer: 'buffer', - }), - ], - module: { - rules: [ - { - test: /\.ts$/i, - use: ['babel-loader'], - }, - ], - }, - output: { - filename: 'limber-[name].js', - }, - resolve: { - extensions: ['.ts', '.js', '.json'], - fallback: { - fs: false, - path: require.resolve('path-browserify'), - '@ember/template-compilation': require.resolve( - 'ember-source/dist/ember-template-compiler.js' - ), - }, - alias: { - // this prevents complaining about require.extensions - // handlebars: 'handlebars/dist/cjs/handlebars.js', - }, - }, -}; From 88339cff7fcf7bf83dc3b19788889301c0564f95 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli Date: Sun, 8 May 2022 22:17:58 -0400 Subject: [PATCH 3/3] fetch thing works --- .../app/services/-compile/babel/worker.ts | 51 ++++++++++++++++--- frontend/app/services/-compile/index.ts | 18 +++---- packages/transpilation/broccoli-funnel.js | 3 +- packages/transpilation/build.js | 2 +- packages/transpilation/src/babel.ts | 25 +++++++++ packages/transpilation/src/fetch-handler.ts | 28 +++++++--- packages/transpilation/src/index.ts | 4 +- 7 files changed, 104 insertions(+), 27 deletions(-) diff --git a/frontend/app/services/-compile/babel/worker.ts b/frontend/app/services/-compile/babel/worker.ts index 1700f7a1a..d19c09de0 100644 --- a/frontend/app/services/-compile/babel/worker.ts +++ b/frontend/app/services/-compile/babel/worker.ts @@ -3,7 +3,12 @@ import type { ExtractedCode } from '../markdown-to-ember'; let isRegistered = false; class CompileError extends Error { - constructor(public message: string, public response: Response, public code: string, public url: string) { + constructor( + public message: string, + public response: Response, + public code: string, + public url: string + ) { super(message); } } @@ -12,12 +17,11 @@ export async function compile(js: ExtractedCode) { if (!isRegistered) { // await installImportMap(); await setupServiceWorker(); + await establishRequireJSEntries(); isRegistered = true; } - await new Promise(resolve => setTimeout(resolve, 5000)); - let { name, code } = js; let qps = new URLSearchParams(); @@ -27,10 +31,19 @@ export async function compile(js: ExtractedCode) { let url = `/compile-sw?${qps}`; - let response = await fetch(url); + let response = await fetch(url, { + headers: { 'Content-Type': 'application/json' }, + }); + if (response.status !== 200) { - throw new CompileError(`${response.status} | Could not compile code. See console for details.`, response, code, url); + throw new CompileError( + `${response.status} | Could not compile code. See console for details.`, + response, + code, + url + ); } + let { importPath } = await response.json(); return { name, importPath }; @@ -62,7 +75,7 @@ export async function compile(js: ExtractedCode) { async function setupServiceWorker() { if ('serviceWorker' in navigator) { - let registration = await navigator.serviceWorker.register('/transpilation/limber-worker.js') + let registration = await navigator.serviceWorker.register('/transpile.js'); registration.update(); @@ -70,8 +83,6 @@ async function setupServiceWorker() { await new Promise((resolve) => { let interval = setInterval(() => { - console.log(registration?.active?.state); - if (registration.active?.state === 'activated') { clearInterval(interval); resolve(null); @@ -86,3 +97,27 @@ async function setupServiceWorker() { throw new Error(`ServiceWorker is required`); } + +async function establishRequireJSEntries() { + await fetch('/populate-sw', { + method: 'POST', + // Require JS is private API, but we need to swap out imports in the code + // snippets for accessing the same requirejs modules that are in this app. + // + // This is to + // - reduce overall shipped JS + // + // However, if we were to make the rendering area an entirely different app, + // we could then isolate compile errors and not have fatal problems that require + // a browser page refresh -- this may be the best thing to do in the long term. + // + // But for now, we need to at least try the requirejs stuff, because it's a requirement + // for design-system REPLs + // + // eslint-disable-next-line @typescript-eslint/no-explicit-any + body: JSON.stringify(Object.keys((window.requirejs as any).entries)), + headers: { + 'Content-Type': 'application/json', + }, + }); +} diff --git a/frontend/app/services/-compile/index.ts b/frontend/app/services/-compile/index.ts index 608a873a2..330e985b8 100644 --- a/frontend/app/services/-compile/index.ts +++ b/frontend/app/services/-compile/index.ts @@ -70,15 +70,15 @@ export async function compile(glimdownInput: string): Promise await Promise.all( compiled.map(async (info) => { - // using web worker + import maps is not available yet (need firefox support) - // (and to somehow be able to point at npm) + // using web worker + import maps is not available yet (need firefox support) + // (and to somehow be able to point at npm) - if ('importPath' in info) { - return scope.push({ - moduleName: name, - component: await import(/* webpackIgnore: true */ info.importPath), - }); - } + if ('importPath' in info) { + return scope.push({ + moduleName: name, + component: await import(/* webpackIgnore: true */ info.importPath), + }); + } return scope.push(info); }) @@ -89,8 +89,6 @@ export async function compile(glimdownInput: string): Promise scope.push(compileHBS(code)); } } catch (error) { - console.error(error); - return { error, rootTemplate }; } } diff --git a/packages/transpilation/broccoli-funnel.js b/packages/transpilation/broccoli-funnel.js index a5ac508ad..8fce569b9 100644 --- a/packages/transpilation/broccoli-funnel.js +++ b/packages/transpilation/broccoli-funnel.js @@ -12,6 +12,7 @@ const SRC_FILES = path.join(__dirname, 'dist'); */ module.exports = function distWatcher() { return new Funnel(SRC_FILES, { - destDir: 'transpilation/', + // dist or whatever the root of the output directory is + destDir: '/', }); }; diff --git a/packages/transpilation/build.js b/packages/transpilation/build.js index 21cda1288..83922e255 100644 --- a/packages/transpilation/build.js +++ b/packages/transpilation/build.js @@ -11,7 +11,7 @@ esbuild.build({ loader: { '.ts': 'ts', '.js': 'js' }, entryPoints: [entry], bundle: true, - outfile: path.join(buildDir, `limber-worker.js`), + outfile: path.join(buildDir, `transpile.js`), format: 'esm', minify: isProduction, sourcemap: isProduction, diff --git a/packages/transpilation/src/babel.ts b/packages/transpilation/src/babel.ts index e69de29bb..7da5e621e 100644 --- a/packages/transpilation/src/babel.ts +++ b/packages/transpilation/src/babel.ts @@ -0,0 +1,25 @@ +interface Info { + name: string; + code: string; +} + +/** + * Multi-phase "compiling"! + * 1. Swap out imports from requireJS as const-definitions in the via + * `import { tracked } from '@glimmer/tracking';` + * -> const { tracked } = requirejs('@glimmer/tracking'); + * `import Component from '@ember/component';` + * -> const { default: Component } = requirejs('@ember/component'); + * + * Outstanding question: where does '@glimmer/component' live? + * where would we get it from? + * + * 2. Run template transform + * 3. Compile out the decorators + * + */ +export function compileGJS({ name, code }: Info, requireJSList: Set) { + console.log(name, code, requireJSList); + + return ' '; +} diff --git a/packages/transpilation/src/fetch-handler.ts b/packages/transpilation/src/fetch-handler.ts index 244299f83..9d2baea55 100644 --- a/packages/transpilation/src/fetch-handler.ts +++ b/packages/transpilation/src/fetch-handler.ts @@ -1,23 +1,28 @@ -// import { compileGJS } from './babel'; +import { compileGJS } from './babel'; // const CACHE_NAME = 'babel-compilation-and-module-service'; -const URLS = ['/compile-sw', /^\/module-sw\//]; +const URLS = ['/compile-sw', /^\/module-sw\//, '/populate-sw']; const COMPILE_CACHE = new Map(); +let REQUIRE_JS_LIST: Set; export async function handleFetch(event: FetchEvent): Promise { const url = new URL(event.request.url); - console.info('handleFetch', url.pathname); - if (!URLS.some((matcher) => url.pathname.match(matcher))) { return fetch(event.request); } + console.info('handleFetch', url.pathname); + if (COMPILE_CACHE.has(url.pathname)) { return moduleResponse(url.pathname); } + if (url.pathname === '/populate-sw') { + return maybe(() => populateRequireJSModules(event)); + } + if (url.pathname === '/compile-sw') { return maybe(() => compile(url)); } @@ -57,6 +62,18 @@ function error(msg: Error | string, status = 500) { }); } +async function populateRequireJSModules(event: FetchEvent) { + let list = await event.request.json(); + + REQUIRE_JS_LIST = new Set(list); + + return new Response('{ "status": "done" }', { + headers: { + 'Content-Type': 'application/javascript', + }, + }); +} + function moduleResponse(pathName: string) { let code = COMPILE_CACHE.get(pathName); @@ -83,8 +100,7 @@ async function compile(url: URL) { ); } - // let compiled = await compileGJS({ name, code }); - let compiled = ''; + let compiled = await compileGJS({ name, code }, REQUIRE_JS_LIST); COMPILE_CACHE.set(modulePath, compiled); diff --git a/packages/transpilation/src/index.ts b/packages/transpilation/src/index.ts index 776ae7289..3271fe980 100644 --- a/packages/transpilation/src/index.ts +++ b/packages/transpilation/src/index.ts @@ -55,5 +55,7 @@ worker.addEventListener('activate', (event) => { }); worker.addEventListener('fetch', (event) => { - event.respondWith(handleFetch(event)); + let response = handleFetch(event); + + event.respondWith(response); });