diff --git a/.eslintignore b/.eslintignore index b9ef98ff5..cf040d311 100644 --- a/.eslintignore +++ b/.eslintignore @@ -6,3 +6,4 @@ vendor/ public/ coverage/ *.ember-try +tests-self/ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 23b6e4cdb..b05102401 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -84,7 +84,7 @@ jobs: tests: - name: "Ember Tests" + name: "Browser Tests" strategy: matrix: # os: [ubuntu-latest, macOS-latest, windows-latest] @@ -109,7 +109,7 @@ jobs: echo "NPM: $( npm --version )" echo "pnpm: $( pnpm --version )" - name: Test - run: pnpm turbo test:ember + run: pnpm turbo test:chrome test:firefox env: CI_BROWSER: ${{ matrix.ci_browser }} diff --git a/.gitignore b/.gitignore index 17b9a529d..4989d8ffb 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ # compiled output dist/ declarations/ +__screenshots__/ /tmp/ /out/ diff --git a/.node-version b/.node-version index 10fef252a..2bd5a0a98 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -20.18 +22 diff --git a/apps/repl/package.json b/apps/repl/package.json index 6a006dd6c..f9271c08a 100644 --- a/apps/repl/package.json +++ b/apps/repl/package.json @@ -24,7 +24,8 @@ "start": "ember serve -p 4201", "start:iframe": "pnpx http-server ./public -i -p 4204", "test:browserstack": "./scripts/browserstack.sh", - "test:ember": "ember test --test-port 0", + "test:chrome": "CI_BROWSER=Chrome ember test --test-port 0", + "test:firefox": "CI_BROWSER=Firefox ember test --test-port 0", "lint": "pnpm -w exec lint", "lint:js": "pnpm -w exec lint js", "lint:js:fix": "pnpm -w exec lint js:fix", diff --git a/apps/tutorial/package.json b/apps/tutorial/package.json index c0b7f3d3a..e2a456150 100644 --- a/apps/tutorial/package.json +++ b/apps/tutorial/package.json @@ -17,7 +17,8 @@ "lint:fix": "pnpm -w exec lint fix", "cf": "cd dist && npx wrangler pages dev --port 42000 ./", "start": "ember serve", - "test:ember": "ember test --test-port 0", + "test:chrome": "CI_BROWSER=Chrome ember test --test-port 0", + "test:firefox": "CI_BROWSER=Firefox ember test --test-port 0", "lint:js": "pnpm -w exec lint js", "lint:js:fix": "pnpm -w exec lint js:fix", "lint:hbs": "pnpm -w exec lint hbs", diff --git a/dev/package.json b/dev/package.json index d80c7b572..50b71cb22 100644 --- a/dev/package.json +++ b/dev/package.json @@ -46,5 +46,6 @@ "directory": "dev" }, "license": "MIT", - "author": "NullVoxPopuli" + "author": "NullVoxPopuli", + "version": "0.0.0" } diff --git a/package.json b/package.json index 943fc9bf4..d23ab3ece 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "@glimmer/component": "^2.0.0", "@ember/test-waiters": "^4.0.0", "ember-element-helper": "^0.8.5", - "ember-auto-import": "^2.9.0", + "ember-auto-import": "^2.10.0", "ember-repl": "workspace:*", "ember-source": ">= 6.0.1", "webpack": ">= 5.92.0", diff --git a/packages/app-support/limber-ui/test-app/package.json b/packages/app-support/limber-ui/test-app/package.json index 468478352..c4f190f03 100644 --- a/packages/app-support/limber-ui/test-app/package.json +++ b/packages/app-support/limber-ui/test-app/package.json @@ -12,7 +12,8 @@ "scripts": { "lint:fix": "pnpm -w exec lint fix", "start": "ember serve", - "test:ember": "ember test --test-port 0", + "test:chrome": "CI_BROWSER=Chrome ember test --test-port 0", + "test:firefox": "CI_BROWSER=Firefox ember test --test-port 0", "lint": "pnpm -w exec lint", "lint:js": "pnpm -w exec lint js", "lint:js:fix": "pnpm -w exec lint js:fix", diff --git a/packages/ember-repl/addon/package.json b/packages/ember-repl/addon/package.json index 225d94965..aab120a03 100644 --- a/packages/ember-repl/addon/package.json +++ b/packages/ember-repl/addon/package.json @@ -12,44 +12,22 @@ }, "license": "MIT", "author": "NullVoxPopuli", - "typesVersions": { - "*": { - "test-support": [ - "declarations/test-support/index.d.ts" - ], - "markdown/parse": [ - "./declarations/compile/markdown-to-ember.d.ts" - ], - "*": [ - "declarations/*", - "declarations/*/index.d.ts" - ] - } - }, "exports": { ".": { - "types": "./declarations/index.d.ts", - "default": "./dist/index.js" - }, - "./formats/markdown": { - "types": "./declarations/compile/formats/markdown.d.ts", - "default": "./dist/compile/formats/markdown.js" - }, - "./formats/hbs": { - "types": "./declarations/compile/formats/hbs.d.ts", - "default": "./dist/compile/formats/hbs.js" + "types": "./declarations/browser/index.d.ts", + "default": "./dist/browser/index.js" }, - "./formats/gjs": { - "types": "./declarations/compile/formats/gjs/index.d.ts", - "default": "./dist/compile/formats/gjs/index.js" + "./workers/compiler": { + "types": "./declarations/compiler-worker/index.d.ts", + "default": "./dist/compiler-worker/index.js" }, - "./test-support": { - "types": "./declarations/test-support/index.d.ts", - "default": "./dist/test-support/index.js" + "./workers/markdown": { + "types": "./declarations/markdown-worker/index.d.ts", + "default": "./dist/markdown-worker/index.js" }, - "./__PRIVATE__DO_NOT_USE__": { - "types": "./declarations/__PRIVATE__.d.ts", - "default": "./dist/__PRIVATE__.js" + "./sw": { + "types": "./declarations/service-worker/index.d.ts", + "default": "./dist/service-worker/index.js" }, "./addon-main.js": "./addon-main.cjs" }, @@ -60,10 +38,12 @@ "addon-main.cjs" ], "scripts": { - "build": "rollup --config", + "build": "rm -rf dist declarations && concurrently 'pnpm:build:*'", + "build:browser": "rollup --config", + "build:types": "tsc --emitDeclarationOnly --noEmit false", + "start": "vite build --watch", "lint:types": "tsc --noEmit", "lint:fix": "pnpm -w exec lint fix", - "start": "rollup --config --watch", "lint": "pnpm -w exec lint", "lint:package": "pnpm publint", "lint:js": "pnpm -w exec lint js", @@ -76,8 +56,10 @@ "dependencies": { "@babel/helper-plugin-utils": "^7.25.7", "@babel/standalone": "^7.25.7", - "@embroider/addon-shim": "1.8.9", + "@ember/test-waiters": "^3.1.0", + "@embroider/addon-shim": "1.9.0", "@embroider/macros": "1.16.9", + "@rollup/plugin-alias": "^5.1.1", "babel-import-util": "^3.0.0", "babel-plugin-ember-template-compilation": "^2.3.0", "broccoli-file-creator": "^2.1.1", @@ -87,17 +69,19 @@ "decorator-transforms": "^2.3.0", "ember-resources": "^7.0.3", "line-column": "^1.0.2", - "magic-string": "^0.30.6", + "magic-string": "^0.30.14", "mdast": "^3.0.0", "parse-static-imports": "^1.1.0", - "rehype-raw": "^6.1.1", - "rehype-stringify": "^9.0.4", - "remark-gfm": "^3.0.1", - "remark-parse": "^10.0.2", - "remark-rehype": "^10.1.0", - "unified": "^10.1.2", + "promise-worker-bi": "^4.1.1", + "register-service-worker": "^1.7.2", + "rehype-raw": "^7.0.0", + "rehype-stringify": "^10.0.1", + "remark-gfm": "^4.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.1", + "unified": "^11.0.5", "unist-util-visit": "^5.0.0", - "uuid": "^10.0.0", + "uuid": "^11.0.3", "vfile": "^6.0.1" }, "devDependencies": { @@ -123,6 +107,7 @@ "@nullvoxpopuli/limber-untyped": "workspace:*", "@rollup/plugin-babel": "^6.0.4", "@rollup/plugin-commonjs": "^28.0.0", + "@rollup/plugin-node-resolve": "^15.2.3", "@tsconfig/ember": "^3.0.7", "@types/babel__core": "^7.20.5", "@types/babel__standalone": "^7.1.7", diff --git a/packages/ember-repl/addon/rollup.config.mjs b/packages/ember-repl/addon/rollup.config.mjs index d5485af32..92000b6fc 100644 --- a/packages/ember-repl/addon/rollup.config.mjs +++ b/packages/ember-repl/addon/rollup.config.mjs @@ -1,36 +1,98 @@ import { Addon } from "@embroider/addon-dev/rollup"; +import alias from "@rollup/plugin-alias"; import { babel } from "@rollup/plugin-babel"; -import cjs from "@rollup/plugin-commonjs"; -import { execaCommand } from "execa"; +import commonjs from "@rollup/plugin-commonjs"; +import { nodeResolve } from "@rollup/plugin-node-resolve"; import { defineConfig } from "rollup"; -const addon = new Addon({ - srcDir: "src", - destDir: "dist", -}); - -export default defineConfig({ - output: addon.output(), - external: ["@glimmer/compiler", "@glimmer/syntax"], - plugins: [ - addon.publicEntrypoints(["**/*.js"]), - addon.appReexports([]), - babel({ - extensions: [".js", ".gjs", ".ts", ".gts"], - babelHelpers: "bundled", - }), - addon.dependencies(), - // line-column... - cjs(), - addon.keepAssets(["build/**/*"]), - addon.clean(), +const browser = new Addon({ srcDir: "src/browser", destDir: "dist/browser" }); +const compiler = new Addon({ srcDir: "src/compiler-worker", destDir: "dist/compiler-worker" }); +const sw = new Addon({ srcDir: "src/service-worker", destDir: "dist/service-worker" }); +function ember(name) { + return `./node_modules/ember-source/dist/${name}`; +} + +function pkg(name) { + return ember(`packages/${name}`); +} + +const lookup = { + "@ember/debug": pkg("@ember/debug/index.js"), + "@ember/template-factory": pkg("@ember/template-factory/index.js"), + "@ember/helper": pkg("@ember/helper/index.js"), + "@ember/modifier": pkg("@ember/modifier/index.js"), + "@ember/component": pkg("@ember/component/index.js"), + "@ember/component/template-only": pkg("@ember/component/template-only.js"), + "ember-template-compiler": ember("ember-template-compiler.js"), +}; + +function bundledEmber() { + return alias([ { - async closeBundle() { - await execaCommand("tsc --emitDeclarationOnly --noEmit false", { stdio: "inherit" }); - console.info("Declarations built successfully"); + find: /(@?ember.+)/, + replacement: "$1", + customResolver(source) { + return lookup[source]; }, }, - ], -}); + ]); +} + +function worker(input, out) { + return { + input: input, + external: [], + output: { + sourcemap: true, + format: "es", + hoistTransitiveImports: false, + inlineDynamicImports: true, + file: out, + }, + plugins: [ + babel({ + extensions: [".js", ".gjs", ".ts", ".gts"], + babelHelpers: "bundled", + }), + bundledEmber(), + nodeResolve(), + // unified>extends + commonjs(), + ], + }; +} + +export default defineConfig([ + { + output: browser.output(), + plugins: [ + browser.publicEntrypoints(["**/*.js"]), + browser.appReexports(["./services/ember-repl/compiler.js"]), + babel({ + extensions: [".js", ".gjs", ".ts", ".gts"], + babelHelpers: "bundled", + }), + browser.dependencies(), + ], + }, + worker("src/compiler-worker/index.ts", "dist/compiler-worker.js"), + worker("src/markdown-worker/index.ts", "dist/markdown-worker.js"), + { + input: "src/service-worker/index.ts", + output: { + sourcemap: true, + format: "es", + hoistTransitiveImports: false, + file: "dist/service-worker.js", + }, + plugins: [ + babel({ + extensions: [".js", ".gjs", ".ts", ".gts"], + babelHelpers: "bundled", + }), + bundledEmber(), + ], + }, +]); diff --git a/packages/ember-repl/addon/src/__PRIVATE__.ts b/packages/ember-repl/addon/src/__PRIVATE__.ts deleted file mode 100644 index 91f5800b3..000000000 --- a/packages/ember-repl/addon/src/__PRIVATE__.ts +++ /dev/null @@ -1 +0,0 @@ -export { CACHE } from './compile/index.ts'; diff --git a/packages/ember-repl/addon/src/browser/index.ts b/packages/ember-repl/addon/src/browser/index.ts new file mode 100644 index 000000000..201a97ca5 --- /dev/null +++ b/packages/ember-repl/addon/src/browser/index.ts @@ -0,0 +1 @@ +export { Compiled } from './resource.ts'; diff --git a/packages/ember-repl/addon/src/browser/resource.ts b/packages/ember-repl/addon/src/browser/resource.ts new file mode 100644 index 000000000..fd3acc923 --- /dev/null +++ b/packages/ember-repl/addon/src/browser/resource.ts @@ -0,0 +1,102 @@ +import { assert } from '@ember/debug'; + +import { cell, resource, resourceFactory } from 'ember-resources'; + +import Compiler from './services/ember-repl/compiler.ts'; + +import type { EvalImportMap, Format, ScopeMap, UnifiedPlugin } from '../types.ts'; +import type { ComponentLike } from '@glint/template'; + +export const CACHE = new Map(); + +type Input = string | undefined | null; + +type ExtraOptions = + | { + format: 'glimdown'; + remarkPlugins?: UnifiedPlugin[]; + rehypePlugins?: UnifiedPlugin[]; + importMap?: EvalImportMap; + CopyComponent?: string; + ShadowComponent?: string; + topLevelScope?: ScopeMap; + } + | { + format: 'hbs'; + topLevelScope?: ScopeMap; + } + | { + format: 'gjs'; + importMap?: EvalImportMap; + }; + +/** + * @internal + */ +export interface Value { + isReady: boolean; + error: string | null; + component: ComponentLike; +} + +export function Compiled(markdownText: Input | (() => Input)): Value; +export function Compiled(markdownText: Input | (() => Input), options?: Format): Value; +export function Compiled(markdownText: Input | (() => Input), options?: () => Format): Value; +export function Compiled(markdownText: Input | (() => Input), options?: ExtraOptions): Value; +export function Compiled(markdownText: Input | (() => Input), options?: () => ExtraOptions): Value; + +/** + * By default, this compiles to `glimdown`. A Markdown format which + * extracts `live` tagged code snippets and compiles them to components. + */ +export function Compiled( + markdownText: Input | (() => Input), + maybeOptions?: Format | (() => Format) | ExtraOptions | (() => ExtraOptions) +): Value { + return resource(({ owner }) => { + let maybeObject = typeof maybeOptions === 'function' ? maybeOptions() : maybeOptions; + let format = + (typeof maybeObject === 'string' ? maybeObject : maybeObject?.format) || 'glimdown'; + let options = (typeof maybeObject === 'string' ? {} : maybeObject) || {}; + + let input = typeof markdownText === 'function' ? markdownText() : markdownText; + let ready = cell(false); + let error = cell(); + let result = cell(); + + let compiler = owner.lookup('service:ember-repl/compiler'); + + assert(`Expected to find the compiler service at 'ember-repl/compiler'`, compiler); + assert( + `Expcected the service at 'ember-repl/compiler' to be an instance of the Compiler Service`, + compiler instanceof Compiler + ); + + if (input) { + compiler.compile(input, { + // narrowing is hard here, but this is an implementation detail + format: format as any, + onSuccess: async (component) => { + result.current = component; + ready.set(true); + error.set(null); + }, + onError: async (e) => { + error.set(e); + }, + onCompileStart: async () => { + ready.set(false); + }, + ...options, + }); + } + + return () => ({ + isReady: ready.current, + error: error.current, + component: result.current, + }); + }); +} + +resourceFactory(Compiled); diff --git a/packages/ember-repl/addon/src/browser/services/ember-repl/compiler.ts b/packages/ember-repl/addon/src/browser/services/ember-repl/compiler.ts new file mode 100644 index 000000000..911d7f917 --- /dev/null +++ b/packages/ember-repl/addon/src/browser/services/ember-repl/compiler.ts @@ -0,0 +1,194 @@ +import { assert } from '@ember/debug'; +import Service from '@ember/service'; +import { waitFor } from '@ember/test-waiters'; + +import { PWBHost } from 'promise-worker-bi'; +import { register } from 'register-service-worker'; + +import { + type CompileResult, + type GJSOptions, + type GlimdownOptions, + type HBSOptions, + SUPPORTED_FORMATS, +} from '../../../types.ts'; +import { nameFor } from '../../utils.ts'; + +import type { ComponentLike } from '@glint/template'; + +/** + * TODO: + * - Check if service worker is registered + * - Check if worker exists + * + * Workflow: + * - compile + * -> send to worker for initial work + * -> output ESM + * -> any imports here that are pre-defined (manually specified), should be swapped out with + * "globalThis" references -- maybe not directly on globalThis + * -> all other imports are changed to use `/ember-repl/import:${path or module}` + * so that the service worker can intercept it + * + * -> fetch the blob of that output `/ember-repl/compiled:${blobtext}` (type application/javascript) + * -> service-worker intercepts and returns the decoded blobtext + * via Response + * -> the browser parses the returned JS module, and any remaining imports + * are handled be the service worker (in particular the `/ember-repl/import:...` imports) + * -> if an extension is present, we go back out to the worker to compiler probably + * -> if no extension, we fetch from esm.sh, and pass the response to the worker + * to repeat the initial process. + * -> traverse all imports until the module graph is complete + * this assures that if we need to compile something to be browser-compatible, + * we can. + * + * Questions: do we need to compress here? + * (probably not? + * -- single files should not be that huge + * and the browser URL already has the compressed + * representation, which is more important for sharing + * and all that + * ) + * + */ +export default class Compiler extends Service { + #workers = { + compiler: null, + } as { + compiler: Worker | null; + }; + #promiseWorkers = { + compiler: null, + } as { compiler: PWBHost | null }; + /** + * Configures the workers for compiling + */ + setup(urls: { serviceWorker: string; compiler: string }) { + register(urls.serviceWorker, { + ready: () => console.log('hello there'), + registered(registration) { + console.log('Service worker has been registered.'); + }, + cached(registration) { + console.log('Content has been cached for offline use.'); + }, + updatefound(registration) { + console.log('New content is downloading.'); + }, + updated(registration) { + console.log('New content is available; please refresh.'); + }, + offline() { + console.log('No internet connection found. App is running in offline mode.'); + }, + error(error) { + console.error('Error during service worker registration:', error); + }, + }); + + this.#workers.compiler = new Worker(urls.compiler); + this.#promiseWorkers.compiler = new PWBHost(this.#workers.compiler); + + this.#promiseWorkers.compiler.register(() => { + // TODO: what is supposed to go here + return 'registered from app service'; + }); + } + + #cache = new Map(); + + /** + * Compile a stateless component using just the template + */ + + compile(text: string, options: HBSOptions): Promise; + + /** + * Compile GJS + */ + compile(text: string, options: GJSOptions): Promise; + + /** + * Compile GitHub-flavored Markdown with GJS support + * and optionally render gjs-snippets via a `live` meta tag + * on the code fences. + */ + compile(text: string, options: GlimdownOptions): Promise; + + @waitFor + async compile( + text: string, + options: GlimdownOptions | GJSOptions | HBSOptions + ): Promise { + let { onSuccess, onError, onCompileStart } = options; + let id = nameFor(`${options.format}:${text}`); + + let existing = this.#cache.get(id); + + if (existing) { + onSuccess(existing); + + return existing; + } + + if (!SUPPORTED_FORMATS.includes(options.format)) { + await onError( + `Unsupported format: ${options.format}. Supported formats: ${SUPPORTED_FORMATS}` + ); + + return; + } + + await onCompileStart(); + + if (!text) { + await onError('No Input Document yet'); + + return; + } + + let file = await this.#compile({ + text, + format: options.format, + options, + }); + /** + * 1. convert to blob + * 2. import('/repl/module:blob') + * - default export is a CompileResult + */ + let blob = new Blob([file]); + // handled by the service worker's fetch handler + let module = await import(`/repl/module:${blob}`); + let result = module.default as CompileResult; + + if (result.error) { + await onError(result.error.message || `${result.error}`); + + return; + } + + this.#cache.set(id, result.component as ComponentLike); + + await onSuccess(result.component as ComponentLike); + + return result.component; + } + + get #promileCompiler() { + assert(`Cannot compile without the compiler-worker.`, this.#promiseWorkers.compiler); + + return this.#promiseWorkers.compiler; + } + + async #compile(data: { + text: string; + format: string; + options: GlimdownOptions | HBSOptions | GJSOptions; + }): Promise { + return this.#promileCompiler.postMessage({ + command: 'compile', + ...data, + }); + } +} diff --git a/packages/ember-repl/addon/src/compile/utils.ts b/packages/ember-repl/addon/src/browser/utils.ts similarity index 100% rename from packages/ember-repl/addon/src/compile/utils.ts rename to packages/ember-repl/addon/src/browser/utils.ts diff --git a/packages/ember-repl/addon/src/compile/formats/gjs/eval.ts b/packages/ember-repl/addon/src/compile/formats/gjs/eval.ts deleted file mode 100644 index 767d4630d..000000000 --- a/packages/ember-repl/addon/src/compile/formats/gjs/eval.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -import { modules } from './known-modules.ts'; - -import type Component from '@glimmer/component'; - -export function evalSnippet( - compiled: string, - extraModules: Record = {} -): { - default: Component; - services?: { [key: string]: unknown }; -} { - const exports = {}; - - function require(moduleName: keyof typeof modules): unknown { - let preConfigured = modules[moduleName] || extraModules[moduleName]; - - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - return preConfigured || window.require(moduleName); - } - - eval(compiled); - - return Object.assign(exports, { require }) as { - default: Component; - services?: { [key: string]: unknown }; - require: unknown; - }; -} diff --git a/packages/ember-repl/addon/src/compile/formats/gjs/known-modules.ts b/packages/ember-repl/addon/src/compile/formats/gjs/known-modules.ts deleted file mode 100644 index 9aa55fe4f..000000000 --- a/packages/ember-repl/addon/src/compile/formats/gjs/known-modules.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * We need to import and hang on to these references so that they - * don't get optimized away during deploy - */ -import _GlimmerComponent from '@glimmer/component'; -import * as _tracking from '@glimmer/tracking'; -import * as _application from '@ember/application'; -import * as _array from '@ember/array'; -import * as _EmberComponent from '@ember/component'; -import * as _EmberComponentHelper from '@ember/component/helper'; -import _TO from '@ember/component/template-only'; -import * as _debug from '@ember/debug'; -import * as _destroyable from '@ember/destroyable'; -import * as _helpers from '@ember/helper'; -import * as _modifier from '@ember/modifier'; -import * as _object from '@ember/object'; -import * as _owner from '@ember/owner'; -import * as _runloop from '@ember/runloop'; -import * as _service from '@ember/service'; -import * as _template from '@ember/template'; -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore -import { createTemplateFactory } from '@ember/template-factory'; -import * as _utils from '@ember/utils'; - -import * as _decoratorsRuntime from 'decorator-transforms/runtime'; - -export const modules = { - '@ember/application': _application, - '@ember/array': _array, - '@ember/component': _EmberComponent, - '@ember/component/helper': _EmberComponentHelper, - '@ember/component/template-only': _TO, - '@ember/debug': _debug, - '@ember/destroyable': _destroyable, - '@ember/helper': _helpers, - '@ember/modifier': _modifier, - '@ember/object': _object, - '@ember/runloop': _runloop, - '@ember/service': _service, - '@ember/template-factory': { createTemplateFactory }, - '@ember/utils': _utils, - '@ember/template': _template, - '@ember/owner': _owner, - - '@glimmer/component': _GlimmerComponent, - '@glimmer/tracking': _tracking, - 'decorator-transforms/runtime': _decoratorsRuntime, -}; diff --git a/packages/ember-repl/addon/src/compile/index.ts b/packages/ember-repl/addon/src/compile/index.ts deleted file mode 100644 index 5f146fea5..000000000 --- a/packages/ember-repl/addon/src/compile/index.ts +++ /dev/null @@ -1,219 +0,0 @@ -import { cell, resource, resourceFactory } from 'ember-resources'; - -import { - compileGJS as processGJS, - compileHBS as processHBS, - compileMD as processMD, -} from './formats.ts'; -import { nameFor } from './utils.ts'; - -import type { CompileResult, UnifiedPlugin } from './types.ts'; -import type { EvalImportMap, ScopeMap } from './types.ts'; -import type { ComponentLike } from '@glint/template'; -type Format = 'glimdown' | 'gjs' | 'hbs'; - -export const CACHE = new Map(); - -interface Events { - onSuccess: (component: ComponentLike) => Promise | unknown; - onError: (error: string) => Promise | unknown; - onCompileStart: () => Promise | unknown; -} - -interface Scope { - importMap?: EvalImportMap; -} - -const SUPPORTED_FORMATS = ['glimdown', 'gjs', 'hbs']; - -interface GlimdownOptions extends Scope, Events { - format: 'glimdown'; - remarkPlugins?: UnifiedPlugin[]; - rehypePlugins?: UnifiedPlugin[]; - CopyComponent?: string; - ShadowComponent?: string; - topLevelScope?: ScopeMap; -} -interface GJSOptions extends Scope, Events { - format: 'gjs'; - - // Make overloads easier? - remarkPlugins?: never; - rehypePlugins?: never; - CopyComponent?: never; - ShadowComponent?: never; -} - -interface HBSOptions extends Scope, Events { - format: 'hbs'; - topLevelScope?: ScopeMap; - - // Make overloads easier? - remarkPlugins?: never; - rehypePlugins?: never; - CopyComponent?: never; - ShadowComponent?: never; -} - -/** - * Compile GitHub-flavored Markdown with GJS support - * and optionally render gjs-snippets via a `live` meta tag - * on the code fences. - */ -export async function compile(text: string, options: GlimdownOptions): Promise; - -/** - * Compile GJS - */ -export async function compile(text: string, options: GJSOptions): Promise; - -/** - * Compile a stateless component using just the template - */ -export async function compile(text: string, options: HBSOptions): Promise; - -/** - * This compileMD is a more robust version of the raw compiling used in "formats". - * This function manages cache, and has events for folks building UIs to hook in to - */ -export async function compile( - text: string, - options: GlimdownOptions | GJSOptions | HBSOptions -): Promise { - let { onSuccess, onError, onCompileStart } = options; - let id = nameFor(`${options.format}:${text}`); - - let existing = CACHE.get(id); - - if (existing) { - onSuccess(existing); - - return; - } - - if (!SUPPORTED_FORMATS.includes(options.format)) { - await onError(`Unsupported format: ${options.format}. Supported formats: ${SUPPORTED_FORMATS}`); - - return; - } - - await onCompileStart(); - - if (!text) { - await onError('No Input Document yet'); - - return; - } - - let result: CompileResult; - - if (options.format === 'glimdown') { - result = await processMD(text, options); - } else if (options.format === 'gjs') { - result = await processGJS(text, options.importMap); - } else if (options.format === 'hbs') { - result = await processHBS(text, { - scope: options.topLevelScope, - }); - } else { - await onError( - `Unsupported format: ${(options as any).format}. Supported formats: ${SUPPORTED_FORMATS}` - ); - - return; - } - - if (result.error) { - await onError(result.error.message || `${result.error}`); - - return; - } - - CACHE.set(id, result.component as ComponentLike); - - await onSuccess(result.component as ComponentLike); -} - -type Input = string | undefined | null; - -type ExtraOptions = - | { - format: 'glimdown'; - remarkPlugins?: UnifiedPlugin[]; - rehypePlugins?: UnifiedPlugin[]; - importMap?: EvalImportMap; - CopyComponent?: string; - ShadowComponent?: string; - topLevelScope?: ScopeMap; - } - | { - format: 'hbs'; - topLevelScope?: ScopeMap; - } - | { - format: 'gjs'; - importMap?: EvalImportMap; - }; - -/** - * @internal - */ -export interface Value { - isReady: boolean; - error: string | null; - component: ComponentLike; -} - -export function Compiled(markdownText: Input | (() => Input)): Value; -export function Compiled(markdownText: Input | (() => Input), options?: Format): Value; -export function Compiled(markdownText: Input | (() => Input), options?: () => Format): Value; -export function Compiled(markdownText: Input | (() => Input), options?: ExtraOptions): Value; -export function Compiled(markdownText: Input | (() => Input), options?: () => ExtraOptions): Value; - -/** - * By default, this compiles to `glimdown`. A Markdown format which - * extracts `live` tagged code snippets and compiles them to components. - */ -export function Compiled( - markdownText: Input | (() => Input), - maybeOptions?: Format | (() => Format) | ExtraOptions | (() => ExtraOptions) -): Value { - return resource(() => { - let maybeObject = typeof maybeOptions === 'function' ? maybeOptions() : maybeOptions; - let format = - (typeof maybeObject === 'string' ? maybeObject : maybeObject?.format) || 'glimdown'; - let options = (typeof maybeObject === 'string' ? {} : maybeObject) || {}; - - let input = typeof markdownText === 'function' ? markdownText() : markdownText; - let ready = cell(false); - let error = cell(); - let result = cell(); - - if (input) { - compile(input, { - // narrowing is hard here, but this is an implementation detail - format: format as any, - onSuccess: async (component) => { - result.current = component; - ready.set(true); - error.set(null); - }, - onError: async (e) => { - error.set(e); - }, - onCompileStart: async () => { - ready.set(false); - }, - ...options, - }); - } - - return () => ({ - isReady: ready.current, - error: error.current, - component: result.current, - }); - }); -} - -resourceFactory(Compiled); diff --git a/packages/ember-repl/addon/src/compile/types.ts b/packages/ember-repl/addon/src/compile/types.ts deleted file mode 100644 index afbccc790..000000000 --- a/packages/ember-repl/addon/src/compile/types.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { ComponentLike } from '@glint/template'; -import type { Pluggable } from 'unified'; - -export interface EvalImportMap { - [moduleName: string]: ScopeMap; -} - -export interface ScopeMap { - [localName: string]: unknown; -} - -export type UnifiedPlugin = Pluggable; - -export interface CompileResult { - component?: ComponentLike; - error?: Error; - name: string; -} - -export type Options = { - /** - * @internal - * @deprecated do not use - not under semver - */ - skypack?: boolean; -}; diff --git a/packages/ember-repl/addon/src/compiler-worker/formats.ts b/packages/ember-repl/addon/src/compiler-worker/formats.ts new file mode 100644 index 000000000..f78c2489c --- /dev/null +++ b/packages/ember-repl/addon/src/compiler-worker/formats.ts @@ -0,0 +1,34 @@ +import type { CompileResult, EvalImportMap } from '../types.ts'; + +export async function compileGJS( + gjsInput: string, + importMap?: EvalImportMap +): Promise { + try { + let { compileJS } = await import('./formats/gjs/index.ts'); + + // let importMap2 = await resolveImportMap(gjsInput, importMap); + // + // console.log(importMap2); + + return await compileJS(gjsInput, importMap); + } catch (error) { + return { error: error as Error, name: 'unknown' }; + } +} + +export async function compileHBS( + hbsInput: string, + options?: { + moduleName?: string; + scope?: Record; + } +): Promise { + try { + let { compileHBS } = await import('./formats/hbs.ts'); + + return compileHBS(hbsInput, options); + } catch (error) { + return { error: error as Error, name: 'unknown' }; + } +} diff --git a/packages/ember-repl/addon/src/compile/formats/gjs/babel.ts b/packages/ember-repl/addon/src/compiler-worker/formats/gjs/babel.ts similarity index 100% rename from packages/ember-repl/addon/src/compile/formats/gjs/babel.ts rename to packages/ember-repl/addon/src/compiler-worker/formats/gjs/babel.ts diff --git a/packages/ember-repl/addon/src/compile/formats/gjs/index.ts b/packages/ember-repl/addon/src/compiler-worker/formats/gjs/index.ts similarity index 84% rename from packages/ember-repl/addon/src/compile/formats/gjs/index.ts rename to packages/ember-repl/addon/src/compiler-worker/formats/gjs/index.ts index 3ef8474bb..36b9124db 100644 --- a/packages/ember-repl/addon/src/compile/formats/gjs/index.ts +++ b/packages/ember-repl/addon/src/compiler-worker/formats/gjs/index.ts @@ -1,9 +1,5 @@ import * as compiler from 'ember-template-compiler'; -import { nameFor } from '../../utils.ts'; -import { evalSnippet } from './eval.ts'; - -import type { CompileResult } from '../../types.ts'; import type { ComponentLike } from '@glint/template'; export interface Info { @@ -28,27 +24,12 @@ export interface Info { * are not provided by extraModules will be searched on npm to see if a package * needs to be downloaded before running the `code` / invoking the component */ -export async function compileJS( - code: string, - extraModules?: Record -): Promise { +export async function compileJS(code: string): Promise { let name = nameFor(code); - let component: undefined | ComponentLike; - let error: undefined | Error; - - try { - let compiled = await transpile({ code: code, name }); - if (!compiled) { - throw new Error(`Compiled output is missing`); - } + let compiled = await transpile({ code: code, name }); - component = evalSnippet(compiled, extraModules).default as unknown as ComponentLike; - } catch (e) { - error = e as Error | undefined; - } - - return { name, component, error }; + return compiled; } async function transpile({ code: input, name }: Info) { @@ -64,6 +45,8 @@ async function transpile({ code: input, name }: Info) { return code; } +import { nameFor } from '../../../browser/utils.ts'; + import type { Babel } from './babel.ts'; let processor: any; diff --git a/packages/ember-repl/addon/src/compile/formats/hbs.ts b/packages/ember-repl/addon/src/compiler-worker/formats/hbs.ts similarity index 96% rename from packages/ember-repl/addon/src/compile/formats/hbs.ts rename to packages/ember-repl/addon/src/compiler-worker/formats/hbs.ts index eb9f6b30d..2712a29fa 100644 --- a/packages/ember-repl/addon/src/compile/formats/hbs.ts +++ b/packages/ember-repl/addon/src/compiler-worker/formats/hbs.ts @@ -14,9 +14,9 @@ import { on } from '@ember/modifier'; // @ts-ignore import { createTemplateFactory } from '@ember/template-factory'; -import { nameFor } from '../utils.ts'; +import { nameFor } from '../../browser/utils.ts'; -import type { CompileResult } from '../types.ts'; +import type { CompileResult } from '../../types'; import type { ComponentLike } from '@glint/template'; /** diff --git a/packages/ember-repl/addon/src/compiler-worker/index.ts b/packages/ember-repl/addon/src/compiler-worker/index.ts new file mode 100644 index 000000000..18b720400 --- /dev/null +++ b/packages/ember-repl/addon/src/compiler-worker/index.ts @@ -0,0 +1,71 @@ +import { PWBWorker } from 'promise-worker-bi'; + +import { compileGJS, compileHBS } from './formats.ts'; +import { debug } from './log.ts'; +import { getString } from './util.ts'; + +const promiseWorker = new PWBWorker(); + +promiseWorker.register(async (message) => { + if (typeof message === 'object') { + if (message !== null) { + if ('command' in message) { + return handleCommand(message); + } + } + } + + debug(`unhandled message: ${JSON.stringify(message)}`); +}); + +async function handleCommand(message: { command: unknown }) { + if (typeof message.command !== 'string') { + debug(`unexpected command type: ${message.command}`); + + return; + } + + switch (message.command) { + case 'compile': { + return compile(message); + } + + default: { + debug(`unexpected command: ${message.command}`); + } + } +} + +/** + * Returns a string of the compiled, browser-native JS + * at this phase, we still need to: + * - swap defined imports for the static values + * - if not one of the static values, replace with the resolver function (requires top-level await) + */ +async function compile(message: NonNullable) { + let format = getString(message, 'format'); + + if (!format) { + debug(`format is required, received: ${format}`); + } + + switch (format) { + case 'gjs': + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + return compileGJS(message); + case 'hbs': + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + return compileHBS(message); + case 'glimdown': + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // TODO: Call out to markdown worker + // @ts-ignore + return compileGDM(message); + default: + debug(`Invalid format. Allowed: gjs, hbs, glimdown`); + + return; + } +} diff --git a/packages/ember-repl/addon/src/compiler-worker/log.ts b/packages/ember-repl/addon/src/compiler-worker/log.ts new file mode 100644 index 000000000..201fb498a --- /dev/null +++ b/packages/ember-repl/addon/src/compiler-worker/log.ts @@ -0,0 +1,3 @@ +export function debug(...msg: Parameters) { + console.debug(`[Worker]`, ...msg); +} diff --git a/packages/ember-repl/addon/src/compiler-worker/util.ts b/packages/ember-repl/addon/src/compiler-worker/util.ts new file mode 100644 index 000000000..5400f1789 --- /dev/null +++ b/packages/ember-repl/addon/src/compiler-worker/util.ts @@ -0,0 +1,11 @@ +export function getString(obj: object, key: string): string | undefined { + if (obj === null) return; + + if (key in obj) { + let value = obj[key as keyof typeof obj]; + + if (typeof value === 'string') { + return value; + } + } +} diff --git a/packages/ember-repl/addon/src/index.ts b/packages/ember-repl/addon/src/index.ts deleted file mode 100644 index bc08b8c58..000000000 --- a/packages/ember-repl/addon/src/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { compile, Compiled } from './compile/index.ts'; -export { invocationName, invocationOf, nameFor } from './compile/utils.ts'; - -// Public Types -export type { CompileResult, EvalImportMap, ScopeMap, UnifiedPlugin } from './compile/types'; diff --git a/packages/ember-repl/addon/src/compile/formats.ts b/packages/ember-repl/addon/src/markdown-worker/index.ts similarity index 77% rename from packages/ember-repl/addon/src/compile/formats.ts rename to packages/ember-repl/addon/src/markdown-worker/index.ts index e148335a8..8237fe34f 100644 --- a/packages/ember-repl/addon/src/compile/formats.ts +++ b/packages/ember-repl/addon/src/markdown-worker/index.ts @@ -1,90 +1,9 @@ -import { invocationName } from './utils.ts'; +import { parseMarkdown } from './markdown.ts'; -import type { ExtractedCode } from './formats/markdown.ts'; -import type { CompileResult, UnifiedPlugin } from './types.ts'; -import type { EvalImportMap, ScopeMap } from './types.ts'; +import type { CompileResult, EvalImportMap, ScopeMap, UnifiedPlugin } from '../types.ts'; +import type { ExtractedCode } from './markdown.ts'; -async function compileGJSArray(js: { code: string }[], importMap?: EvalImportMap) { - let modules = await Promise.all( - js.map(async ({ code }) => { - return await compileGJS(code, importMap); - }) - ); - - return modules; -} - -export async function compileGJS( - gjsInput: string, - importMap?: EvalImportMap -): Promise { - try { - let { compileJS } = await import('./formats/gjs/index.ts'); - - return await compileJS(gjsInput, importMap); - } catch (error) { - return { error: error as Error, name: 'unknown' }; - } -} - -export async function compileHBS( - hbsInput: string, - options?: { - moduleName?: string; - scope?: Record; - } -): Promise { - try { - let { compileHBS } = await import('./formats/hbs.ts'); - - return compileHBS(hbsInput, options); - } catch (error) { - return { error: error as Error, name: 'unknown' }; - } -} - -async function extractScope( - liveCode: ExtractedCode[], - options?: { - importMap?: EvalImportMap; - topLevelScope?: ScopeMap; - } -): Promise { - let scope: CompileResult[] = []; - - let hbs = liveCode.filter((code) => code.lang === 'hbs'); - let js = liveCode.filter((code) => ['js', 'gjs'].includes(code.lang)); - - if (js.length > 0) { - let compiled = await compileGJSArray(js, options?.importMap); - - 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), - // }); - // } - - return scope.push(info); - }) - ); - } - - for (let { code } of hbs) { - let compiled = await compileHBS(code, { scope: options?.topLevelScope }); - - scope.push(compiled); - } - - return scope; -} - -export async function compileMD( +export async function compileGDM( glimdownInput: string, options?: { importMap?: EvalImportMap; @@ -110,7 +29,6 @@ export async function compileMD( * compiled rootTemplate can invoke them */ try { - let { parseMarkdown } = await import('./formats/markdown.ts'); let { templateOnlyGlimdown, blocks } = await parseMarkdown(glimdownInput, { CopyComponent: options?.CopyComponent, ShadowComponent: options?.ShadowComponent, @@ -177,3 +95,54 @@ export async function compileMD( return { error: error as Error, rootTemplate, name: 'unknown' }; } } + +async function compileGJSArray(js: { code: string }[], importMap?: EvalImportMap) { + let modules = await Promise.all( + js.map(async ({ code }) => { + // return await compileGJS(code, importMap); + }) + ); + + return modules; +} + +async function extractScope( + liveCode: ExtractedCode[], + options?: { + importMap?: EvalImportMap; + topLevelScope?: ScopeMap; + } +): Promise { + let scope: CompileResult[] = []; + + let hbs = liveCode.filter((code) => code.lang === 'hbs'); + let js = liveCode.filter((code) => ['js', 'gjs'].includes(code.lang)); + + if (js.length > 0) { + let compiled = await compileGJSArray(js, options?.importMap); + + 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), + // }); + // } + + return scope.push(info); + }) + ); + } + + for (let { code } of hbs) { + let compiled = await compileHBS(code, { scope: options?.topLevelScope }); + + scope.push(compiled); + } + + return scope; +} diff --git a/packages/ember-repl/addon/src/compile/formats/markdown.ts b/packages/ember-repl/addon/src/markdown-worker/markdown.ts similarity index 98% rename from packages/ember-repl/addon/src/compile/formats/markdown.ts rename to packages/ember-repl/addon/src/markdown-worker/markdown.ts index 14c321799..4bc27c071 100644 --- a/packages/ember-repl/addon/src/compile/formats/markdown.ts +++ b/packages/ember-repl/addon/src/markdown-worker/markdown.ts @@ -6,9 +6,9 @@ import remarkRehype from 'remark-rehype'; import { unified } from 'unified'; import { visit } from 'unist-util-visit'; -import { invocationOf, nameFor } from '../utils.ts'; +import { invocationOf, nameFor } from '../browser/utils.ts'; -import type { UnifiedPlugin } from '../types.ts'; +import type { UnifiedPlugin } from '../types'; import type { Node } from 'hast'; import type { Code, Text } from 'mdast'; import type { Parent } from 'unist'; diff --git a/packages/ember-repl/addon/src/service-worker/fetch-handler.ts b/packages/ember-repl/addon/src/service-worker/fetch-handler.ts new file mode 100644 index 000000000..f108e0950 --- /dev/null +++ b/packages/ember-repl/addon/src/service-worker/fetch-handler.ts @@ -0,0 +1,126 @@ +const URLS = ['/compile.sw', '/module.sw']; + +const COMPILE_CACHE = new Map(); + +export async function handleFetch(event: FetchEvent): Promise { + // event.request.url is a string + const url = new URL(event.request.url); + + /** + * We only define two URL handlers, + * - compile.sw - actually does compilation + * - module.sw - loads what we compiled + */ + if (!URLS.some((matcher) => url.pathname.startsWith(matcher))) { + return fetch(event.request); + } + + if (COMPILE_CACHE.has(url.pathname)) { + return moduleResponse(url.pathname); + } + + if (url.pathname === '/compile.sw') { + return maybe(() => compile(event.request)); + } + + return error(`Unhandled URL: ${url.pathname}`); +} + +async function maybe(op: () => Return | Promise) { + try { + return await op(); + } catch (e) { + return error(e); + } +} + +function error(msg: unknown | 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 = JSON.stringify(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(request: Request) { + let body: { code?: string; format?: string } = await request.json(); + + let { code, format } = body; + + if (!code) { + throw new Error(`'code' property missing in body`); + } + + if (!format) { + throw new Error(`'format' property missing in body`); + } + + let name = nameFor(code + format, 'sw:named'); + + let modulePath = `/module.sw/${name}.js`; + + // TODO: all external imports must be changed + // (via babel plugin (because we already have babel)) + // to use https://esm.sh/*thePackage + // + // https://esm.sh/#docs + // + // let compiled = await compileGJS({ name, code }); + // + // TODO: only do this for import paths which have not already + // been declared by the local scope references + let compiled = code.replaceAll( + /from ('|")([^'"]+)('|")/g, + function (_match, quote, moduleName, quote2) { + let replacementModule = `https://esm.sh/*${moduleName}`; + + return `from ${quote}${replacementModule}${quote2}`; + } + ); + + COMPILE_CACHE.set(modulePath, compiled); + + let response = new Response( + JSON.stringify({ + importPath: modulePath, + content: compiled, + }), + { + headers: { + 'Content-Type': 'application/json', + }, + } + ); + + return response; +} diff --git a/packages/ember-repl/addon/src/service-worker/index.ts b/packages/ember-repl/addon/src/service-worker/index.ts new file mode 100644 index 000000000..d7a2b07fa --- /dev/null +++ b/packages/ember-repl/addon/src/service-worker/index.ts @@ -0,0 +1,52 @@ +import { handleFetch } from './fetch-handler.ts'; + +// Silly Workers +export type {}; +declare const self: ServiceWorkerGlobalScope; + +/** + * For a given markdown 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" + * } + * + * + */ +self.addEventListener('install', () => { + // force moving on to activation even if another service worker had control + self.skipWaiting(); +}); + +self.addEventListener('activate', (event) => { + // Claim any clients immediately, so that the page will be under SW control without reloading. + const claim = self.clients.claim(); + + event.waitUntil(claim); + + console.info(`\ + Service Worker installed successfully! + + This service worker is used for compiling JavaScript + and providing modules to the main thread. + `); +}); + +self.addEventListener('fetch', (event) => { + event.respondWith(handleFetch(event)); +}); diff --git a/packages/ember-repl/addon/src/test-support/index.ts b/packages/ember-repl/addon/src/test-support/index.ts index 37a91721e..e69de29bb 100644 --- a/packages/ember-repl/addon/src/test-support/index.ts +++ b/packages/ember-repl/addon/src/test-support/index.ts @@ -1,5 +0,0 @@ -import { CACHE } from '../compile/index.ts'; - -export function clearCompileCache() { - CACHE.clear(); -} diff --git a/packages/ember-repl/addon/src/types.ts b/packages/ember-repl/addon/src/types.ts new file mode 100644 index 000000000..ff8f8742e --- /dev/null +++ b/packages/ember-repl/addon/src/types.ts @@ -0,0 +1,87 @@ +import type { ComponentLike } from '@glint/template'; +import type { Pluggable } from 'unified'; + +export interface EvalImportMap { + /** + * The name of the module to import and the value that will be imported. + * For example: + * ```js + * { + * 'my-library': () => import('my-library'), + * } + * ``` + * + * or, if you want to make a fake module, you may specify its exports + * ```js + * { + * 'my-library': { Foo, Bar } + * } + * ``` + */ + [moduleName: string]: ScopeMap | (() => Promise); +} + +export interface ScopeMap { + /** + * Key-value pairs of values and their export names + */ + [localName: string]: unknown; +} + +export type UnifiedPlugin = Pluggable; + +export interface CompileResult { + component?: ComponentLike; + error?: Error; + name: string; +} + +export type Options = { + /** + * @internal + * @deprecated do not use - not under semver + */ + skypack?: boolean; +}; + +interface Events { + onSuccess: (component: ComponentLike) => Promise | unknown; + onError: (error: string) => Promise | unknown; + onCompileStart: () => Promise | unknown; +} + +interface Scope { + importMap?: EvalImportMap; +} + +export type Format = 'glimdown' | 'gjs' | 'hbs'; +export const SUPPORTED_FORMATS = ['glimdown', 'gjs', 'hbs']; + +export interface GlimdownOptions extends Scope, Events { + format: 'glimdown'; + remarkPlugins?: UnifiedPlugin[]; + rehypePlugins?: UnifiedPlugin[]; + CopyComponent?: string; + ShadowComponent?: string; + topLevelScope?: ScopeMap; +} +export interface GJSOptions extends Scope, Events { + format: 'gjs'; + + // Make overloads easier? + remarkPlugins?: never; + rehypePlugins?: never; + CopyComponent?: never; + ShadowComponent?: never; +} + +export interface HBSOptions extends Scope, Events { + format: 'hbs'; + topLevelScope?: ScopeMap; + + // Make overloads easier? + remarkPlugins?: never; + rehypePlugins?: never; + CopyComponent?: never; + ShadowComponent?: never; +} diff --git a/packages/ember-repl/addon/tsconfig.json b/packages/ember-repl/addon/tsconfig.json index 89ecc5d57..6aac97f50 100644 --- a/packages/ember-repl/addon/tsconfig.json +++ b/packages/ember-repl/addon/tsconfig.json @@ -8,6 +8,7 @@ "skipLibCheck": true, "declaration": true, "allowImportingTsExtensions": true, - "declarationDir": "declarations" + "declarationDir": "declarations", + "lib": ["WebWorker", "DOM", "ESNext"] } } diff --git a/packages/ember-repl/test-app/app/app.ts b/packages/ember-repl/test-app/app/app.ts index a353fd541..819fd8778 100644 --- a/packages/ember-repl/test-app/app/app.ts +++ b/packages/ember-repl/test-app/app/app.ts @@ -1,3 +1,5 @@ +import 'decorator-transforms/globals'; + import Application from '@ember/application'; import loadInitializers from 'ember-load-initializers'; @@ -14,6 +16,15 @@ Object.assign(window, { // Buffer: {}, }); +import { setup } from 'ember-repl'; +import compiler from 'ember-repl/compiler.js?url'; +import sw from 'ember-repl/service-worker.js?url'; + +setup({ + serviceWorker: sw, + compiler: compiler, +}); + export default class App extends Application { modulePrefix = config.modulePrefix; podModulePrefix = config.podModulePrefix; diff --git a/packages/ember-repl/test-app/app/templates/application.gjs b/packages/ember-repl/test-app/app/templates/application.gjs new file mode 100644 index 000000000..e69de29bb diff --git a/packages/ember-repl/test-app/app/templates/application.hbs b/packages/ember-repl/test-app/app/templates/application.hbs deleted file mode 100644 index 5230580f8..000000000 --- a/packages/ember-repl/test-app/app/templates/application.hbs +++ /dev/null @@ -1,3 +0,0 @@ -

Welcome to Ember

- -{{outlet}} \ No newline at end of file diff --git a/packages/ember-repl/test-app/ember-cli-build.js b/packages/ember-repl/test-app/ember-cli-build.js index 9d13ec264..92094971e 100644 --- a/packages/ember-repl/test-app/ember-cli-build.js +++ b/packages/ember-repl/test-app/ember-cli-build.js @@ -9,10 +9,19 @@ module.exports = function (defaults) { const app = new EmberApp(defaults, { // Add options here trees: { - app: sideWatch('app', { watching: [path.join(__dirname, '../addon/dist')] }), + app: sideWatch('app', { + watching: [path.join(__dirname, '../addon/dist')], + }), }, 'ember-cli-babel': { enableTypeScriptTransform: true, + disableDecoratorTransforms: true, + }, + babel: { + plugins: [ + // add the new transform. + require.resolve('decorator-transforms'), + ], }, name: 'test-app', autoImport: { diff --git a/packages/ember-repl/test-app/package.json b/packages/ember-repl/test-app/package.json index d67b6c262..dc3ba9eea 100644 --- a/packages/ember-repl/test-app/package.json +++ b/packages/ember-repl/test-app/package.json @@ -24,12 +24,17 @@ "lint:prettier": "pnpm -w exec lint prettier" }, "dependencies": { + "@babel/standalone": "^7.26.2", "@shikijs/rehype": "^1.21.1", "@types/unist": "^3.0.2", "buffer": "^6.0.3", "common-tags": "^1.8.2", + "content-tag": "^3.0.0", + "decorator-transforms": "^2.3.0", + "ember-modifier": "^4.1.0", "ember-repl": "workspace:*", "ember-resources": "^7.0.3", + "ember-route-template": "^1.0.3", "unist-util-visit": "^5.0.0" }, "devDependencies": { @@ -58,6 +63,7 @@ "@types/rsvp": "^4.0.9", "@typescript-eslint/eslint-plugin": "^8.8.0", "@typescript-eslint/parser": "^8.8.0", + "babel-plugin-ember-template-compilation": "^2.3.0", "concurrently": "^9.0.1", "ember-auto-import": "^2.10.0", "ember-cli": "~5.12.0", diff --git a/packages/ember-repl/test-app/tests/rendering/compiled-test.gts b/packages/ember-repl/test-app/tests/rendering/compiled-test.gts index 33f452f5a..92ce0b288 100644 --- a/packages/ember-repl/test-app/tests/rendering/compiled-test.gts +++ b/packages/ember-repl/test-app/tests/rendering/compiled-test.gts @@ -1,25 +1,24 @@ import { renderSettled } from '@ember/renderer'; import { render, settled } from '@ember/test-helpers'; -import { module, test } from 'qunit'; +import QUnit, { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; import { stripIndent } from 'common-tags'; import { Compiled } from 'ember-repl'; +const { assert } = QUnit; +const { Boolean } = globalThis; + module('Rendering | Compiled()', function (hooks) { setupRenderingTest(hooks); - test('it works', async function (assert) { - let doc = stripIndent` - hello there - `; - + async function renderTest(doc, options = {}) { render(