diff --git a/babel.config.mjs b/babel.config.mjs index 34b6aa42dee..45b8ed093c4 100644 --- a/babel.config.mjs +++ b/babel.config.mjs @@ -6,17 +6,19 @@ test suite. */ -import { resolve, dirname } from 'node:path'; -import { fileURLToPath } from 'node:url'; +// import { resolve, dirname } from 'node:path'; +// import { fileURLToPath } from 'node:url'; export default { plugins: [ - [ - '@babel/plugin-transform-typescript', - { - allowDeclareFields: true, - }, - ], + // [ + // '@babel/plugin-transform-typescript', + // { + // allowDeclareFields: true, + // allExtensions: true, + // onlyRemoveTypeImports: true, + // }, + // ], [ 'module:decorator-transforms', { @@ -24,14 +26,14 @@ export default { runtime: { import: 'decorator-transforms/runtime' }, }, ], - [ - 'babel-plugin-ember-template-compilation', - { - compilerPath: resolve( - dirname(fileURLToPath(import.meta.url)), - './broccoli/glimmer-template-compiler' - ), - }, - ], + // [ + // 'babel-plugin-ember-template-compilation', + // { + // compilerPath: resolve( + // dirname(fileURLToPath(import.meta.url)), + // './broccoli/glimmer-template-compiler' + // ), + // }, + // ], ], }; diff --git a/package.json b/package.json index 833c32aed10..4edaf74a41d 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "unlink:all": "node bin/unlink-all.mjs" }, "dependencies": { + "@lifeart/gxt": "0.0.53", "@babel/core": "^7.24.4", "@ember/edition-utils": "^1.2.0", "@glimmer/compiler": "0.92.0", diff --git a/packages/@ember/-internals/glimmer/lib/component-managers/curly.ts b/packages/@ember/-internals/glimmer/lib/component-managers/curly.ts index 23d004c28f5..ab88e8ea278 100644 --- a/packages/@ember/-internals/glimmer/lib/component-managers/curly.ts +++ b/packages/@ember/-internals/glimmer/lib/component-managers/curly.ts @@ -28,7 +28,7 @@ import type { import type { Reference } from '@glimmer/reference'; import { childRefFor, createComputeRef, createPrimitiveRef, valueForRef } from '@glimmer/reference'; import { reifyPositional } from '@glimmer/runtime'; -import { EMPTY_ARRAY, unwrapTemplate } from '@glimmer/util'; +import { unwrapTemplate } from '@glimmer/utils'; import { beginTrackFrame, beginUntrackFrame, @@ -54,6 +54,7 @@ import { processComponentArgs } from '../utils/process-args'; export const ARGS = enumerableSymbol('ARGS'); export const HAS_BLOCK = enumerableSymbol('HAS_BLOCK'); +const EMPTY_ARRAY = []; export const DIRTY_TAG = Symbol('DIRTY_TAG'); export const IS_DISPATCHING_ATTRS = Symbol('IS_DISPATCHING_ATTRS'); diff --git a/packages/@ember/-internals/glimmer/lib/component-managers/mount.ts b/packages/@ember/-internals/glimmer/lib/component-managers/mount.ts index 068d30c8fbf..e0d7e9dd9d5 100644 --- a/packages/@ember/-internals/glimmer/lib/component-managers/mount.ts +++ b/packages/@ember/-internals/glimmer/lib/component-managers/mount.ts @@ -21,7 +21,7 @@ import type { Nullable } from '@ember/-internals/utility-types'; import { capabilityFlagsFrom } from '@glimmer/manager'; import type { Reference } from '@glimmer/reference'; import { createConstRef, valueForRef } from '@glimmer/reference'; -import { unwrapTemplate } from '@glimmer/util'; +import { unwrapTemplate } from '@glimmer/utils'; import type RuntimeResolver from '../resolver'; interface EngineState { diff --git a/packages/@ember/-internals/glimmer/lib/component-managers/outlet.ts b/packages/@ember/-internals/glimmer/lib/component-managers/outlet.ts index 4503ffcb43d..21912b6dda7 100644 --- a/packages/@ember/-internals/glimmer/lib/component-managers/outlet.ts +++ b/packages/@ember/-internals/glimmer/lib/component-managers/outlet.ts @@ -21,7 +21,7 @@ import { capabilityFlagsFrom } from '@glimmer/manager'; import type { Reference } from '@glimmer/reference'; import { createConstRef, valueForRef } from '@glimmer/reference'; import { EMPTY_ARGS } from '@glimmer/runtime'; -import { unwrapTemplate } from '@glimmer/util'; +import { unwrapTemplate } from '@glimmer/utils'; import type { DynamicScope } from '../renderer'; import type { OutletState } from '../utils/outlet'; diff --git a/packages/@ember/-internals/glimmer/lib/components/internal.ts b/packages/@ember/-internals/glimmer/lib/components/internal.ts index 9bd6c132b6a..7858d82974f 100644 --- a/packages/@ember/-internals/glimmer/lib/components/internal.ts +++ b/packages/@ember/-internals/glimmer/lib/components/internal.ts @@ -15,9 +15,20 @@ import type { } from '@glimmer/interfaces'; import { setComponentTemplate, setInternalComponentManager } from '@glimmer/manager'; import type { Reference } from '@glimmer/reference'; -import { createConstRef, isConstRef, valueForRef } from '@glimmer/reference'; +import { reference } from '@lifeart/gxt/glimmer-compatibility'; +// import { createConstRef, isConstRef, valueForRef } from '@glimmer/reference'; import { untrack } from '@glimmer/validator'; +const { createConstRef } = reference; +function isConstRef() { + return true; +} +function valueForRef(ref) { + if (!ref) { + debugger; + } + return ref.value; +} function NOOP(): void {} export type EventListener = (event: Event) => void; diff --git a/packages/@ember/-internals/glimmer/lib/renderer.ts b/packages/@ember/-internals/glimmer/lib/renderer.ts index 1c9c0e44469..2f1019176ed 100644 --- a/packages/@ember/-internals/glimmer/lib/renderer.ts +++ b/packages/@ember/-internals/glimmer/lib/renderer.ts @@ -4,6 +4,7 @@ import type { InternalOwner } from '@ember/-internals/owner'; import { getOwner } from '@ember/-internals/owner'; import { guidFor } from '@ember/-internals/utils'; import { getViewElement, getViewId } from '@ember/-internals/views'; +import { destroyElementSync, renderComponent } from '@lifeart/gxt'; import { assert } from '@ember/debug'; import { _backburner, _getCurrentRunLoop } from '@ember/runloop'; import { destroy } from '@glimmer/destroyable'; @@ -31,14 +32,14 @@ import { createConstRef, UNDEFINED_REFERENCE, valueForRef } from '@glimmer/refer import type { CurriedValue } from '@glimmer/runtime'; import { clientBuilder, - curry, + // curry, DOMChanges, DOMTreeConstruction, inTransaction, renderMain, runtimeContext, } from '@glimmer/runtime'; -import { unwrapTemplate } from '@glimmer/util'; +import { unwrapTemplate } from '@glimmer/utils'; import { CURRENT_TAG, validateTag, valueForTag } from '@glimmer/validator'; import type { SimpleDocument, SimpleElement, SimpleNode } from '@simple-dom/interface'; import RSVP from 'rsvp'; @@ -64,6 +65,23 @@ export interface View { [BOUNDS]: Bounds | null; } +function curry( + type: T, + spec: object | string | any, + owner: any, + args: any | null, + resolved = false +) { + console.log('curry'); + return { + type, + spec, + owner, + args, + resolved, + }; +} + export class DynamicScope implements GlimmerDynamicScope { constructor(public view: View | null, public outletState: Reference) {} @@ -129,13 +147,13 @@ class RootState { constructor( public root: Component | OutletView, public runtime: RuntimeContext, - context: CompileTimeCompilationContext, + _context: CompileTimeCompilationContext, owner: InternalOwner, template: Template, self: Reference, parentElement: SimpleElement, - dynamicScope: DynamicScope, - builder: IBuilder + _dynamicScope: DynamicScope, + _builder: IBuilder ) { assert( `You cannot render \`${valueForRef(self)}\` without a template.`, @@ -146,23 +164,16 @@ class RootState { this.result = undefined; this.destroyed = false; - this.render = errorLoopTransaction(() => { - let layout = unwrapTemplate(template).asLayout(); - - let iterator = renderMain( - runtime, - context, - owner, - self, - builder(runtime.env, { element: parentElement, nextSibling: null }), - layout, - dynamicScope - ); - - let result = (this.result = iterator.sync()); + // console.log(layout); - // override .render function after initial render - this.render = errorLoopTransaction(() => result.rerender({ alwaysRevalidate: false })); + this.render = errorLoopTransaction(() => { + let layout = unwrapTemplate(template).asLayout().compile(); + const layoutInstance = new layout(this); + // @ts-expect-error fine + this.result = renderComponent(layoutInstance, parentElement, owner); + this.render = errorLoopTransaction(() => { + // fine + }); }); } @@ -195,7 +206,12 @@ class RootState { */ - inTransaction(env, () => destroy(result!)); + inTransaction(env, () => { + // @ts-expect-error foo-bar + destroyElementSync(result); + // runDestructors(result.ctx); + destroy(result!); + }); } } } @@ -370,6 +386,7 @@ export class Renderer { // renderer HOOKS appendOutletView(view: OutletView, target: SimpleElement): void { + console.log('appendOutletView', view, target); let definition = createRootOutlet(view); this._appendDefinition( view, diff --git a/packages/@ember/-internals/glimmer/lib/templates/empty.ts b/packages/@ember/-internals/glimmer/lib/templates/empty.ts index f5665d5f8b0..38d472ce04e 100644 --- a/packages/@ember/-internals/glimmer/lib/templates/empty.ts +++ b/packages/@ember/-internals/glimmer/lib/templates/empty.ts @@ -1,5 +1,4 @@ -import { precompileTemplate } from '@ember/template-compilation'; -export default precompileTemplate('', { - moduleName: 'packages/@ember/-internals/glimmer/lib/templates/empty.hbs', - strictMode: true, -}); +import { hbs } from '@lifeart/gxt'; +export default function emptyTemplate() { + return hbs``; +}; diff --git a/packages/@ember/-internals/glimmer/lib/templates/input.ts b/packages/@ember/-internals/glimmer/lib/templates/input.ts index 295bb8d2a69..25b42a4de48 100644 --- a/packages/@ember/-internals/glimmer/lib/templates/input.ts +++ b/packages/@ember/-internals/glimmer/lib/templates/input.ts @@ -1,7 +1,6 @@ -import { precompileTemplate } from '@ember/template-compilation'; -import { on } from '@ember/modifier'; -export default precompileTemplate( - ``, - { - moduleName: 'packages/@ember/-internals/glimmer/lib/templates/input.hbs', - strictMode: true, - scope() { - return { on }; - }, - } -); +/>`; +} diff --git a/packages/@ember/-internals/glimmer/lib/templates/link-to.ts b/packages/@ember/-internals/glimmer/lib/templates/link-to.ts index 811ef33d63b..f34d6ad862a 100644 --- a/packages/@ember/-internals/glimmer/lib/templates/link-to.ts +++ b/packages/@ember/-internals/glimmer/lib/templates/link-to.ts @@ -1,30 +1,21 @@ -import { precompileTemplate } from '@ember/template-compilation'; -import { on } from '@ember/modifier'; +import { hbs } from '@lifeart/gxt'; -export default precompileTemplate( - `{{yield}}`, - { - moduleName: 'packages/@ember/-internals/glimmer/lib/templates/link-to.hbs', - strictMode: true, - scope() { - return { on }; - }, - } -); + {{on 'click' this.click}} >{{yield}}`; +} diff --git a/packages/@ember/-internals/glimmer/lib/templates/outlet-helper-component.gts b/packages/@ember/-internals/glimmer/lib/templates/outlet-helper-component.gts new file mode 100644 index 00000000000..3d612863932 --- /dev/null +++ b/packages/@ember/-internals/glimmer/lib/templates/outlet-helper-component.gts @@ -0,0 +1,75 @@ +import { Component, cell } from '@lifeart/gxt'; + +interface State { + outlets: { + main: State | undefined, + }, + render: { + template(): () => unknown, + controller: unknown, + name: string, + } +} + +export default class OutletHelper extends Component { + get state() { + let state = this.args.state(); + if (typeof state === 'function') { + state = state(); + } + return state.outlets.main || state; + } + get nextState() { + return () => { + return this.hasNext; + } + } + get hasNext() { + return this.state.outlets.main; + } + get canRender() { + return !!this?.state?.render; + } + get MyComponent() { + + const state = this.state; + const render = state.render; + const tpl = render.template(); + if (tpl.instance) { + tpl.renderCell.update(render.model); + return tpl.instance.template; + } + const renderCell = cell(render.model); + // console.log('render.model', render.model); + const args = { + get model() { + return renderCell.value; + } + } + + render.controller['args'] = args; + // render.controller.model = render.model; + const tplComponentInstance = new tpl(args); + tplComponentInstance.template = tplComponentInstance.template.bind(render.controller); + // we need to provide stable refs here to avoid re-renders + tpl.instance = tplComponentInstance; + tpl.renderCell = renderCell; + return tplComponentInstance.template; + } + get model() { + const state = this.state; + const render = state.render; + console.log('getModel', render.model); + return render.model; + } + +} diff --git a/packages/@ember/-internals/glimmer/lib/templates/outlet.ts b/packages/@ember/-internals/glimmer/lib/templates/outlet.ts index 803be28628f..f1b198013b1 100644 --- a/packages/@ember/-internals/glimmer/lib/templates/outlet.ts +++ b/packages/@ember/-internals/glimmer/lib/templates/outlet.ts @@ -1,10 +1,11 @@ -import { precompileTemplate } from '@ember/template-compilation'; -import { outletHelper } from '../syntax/outlet'; +import { hbs } from '@lifeart/gxt'; +import Outlet from './outlet-helper-component'; -export default precompileTemplate(`{{component (outletHelper)}}`, { - moduleName: 'packages/@ember/-internals/glimmer/lib/templates/outlet.hbs', - strictMode: true, - scope() { - return { outletHelper }; - }, -}); +export default (owner) => { + globalThis.owner = owner; + return function (args) { + return hbs`{{#let (component Outlet state=(args.state)) as |Outlet|}} + + {{/let}}`; + }; +}; diff --git a/packages/@ember/-internals/glimmer/lib/templates/root.ts b/packages/@ember/-internals/glimmer/lib/templates/root.ts index bb4736d9214..cf70c8d846e 100644 --- a/packages/@ember/-internals/glimmer/lib/templates/root.ts +++ b/packages/@ember/-internals/glimmer/lib/templates/root.ts @@ -1,5 +1,23 @@ -import { precompileTemplate } from '@ember/template-compilation'; -export default precompileTemplate(`{{component this}}`, { - moduleName: 'packages/@ember/-internals/glimmer/lib/templates/root.hbs', - strictMode: true, -}); +import { hbs, $_fin } from '@lifeart/gxt'; +export default function(owner) { + console.log('root-template init', owner); + return function(rootState) { + // console.log('root-template - render', [this], [...arguments]); + // temp1.root.template + // console.log(...arguments); + // return function() { + // console.log(...arguments); + // return $_fin([...rootState.root.template()], this); + // } + // debugger; + const state = rootState.root.ref; + const owner = rootState.render.owner; + console.log('rootState', state); + return hbs` + {{log 'root-template-create' this rootState}} + {{#let (component rootState.root.template state=state owner=owner root=true) as |Layout|}} + + {{/let}} + `; + } +} diff --git a/packages/@ember/-internals/glimmer/lib/templates/textarea.ts b/packages/@ember/-internals/glimmer/lib/templates/textarea.ts index 1b4bdfc2d9b..b51ccb74dfb 100644 --- a/packages/@ember/-internals/glimmer/lib/templates/textarea.ts +++ b/packages/@ember/-internals/glimmer/lib/templates/textarea.ts @@ -1,8 +1,7 @@ -import { precompileTemplate } from '@ember/template-compilation'; -import { on } from '@ember/modifier'; +import { hbs } from '@lifeart/gxt'; -export default precompileTemplate( - ` + + +} diff --git a/packages/demo/src/components/Profile.gts b/packages/demo/src/components/Profile.gts new file mode 100644 index 00000000000..25ed437a609 --- /dev/null +++ b/packages/demo/src/components/Profile.gts @@ -0,0 +1,38 @@ +import { Component } from '@lifeart/gxt'; +import { Input } from '@ember/-internals/glimmer'; + +function formatTimeForReadability(value) { + return new Date(value).toLocaleTimeString(); +} + +export default class ProfileTemplate extends Component { + +} diff --git a/packages/demo/src/config/application.ts b/packages/demo/src/config/application.ts new file mode 100644 index 00000000000..a85dc1dd8d4 --- /dev/null +++ b/packages/demo/src/config/application.ts @@ -0,0 +1,11 @@ +import EmberApplication from '@ember/application'; +import ENV from './env'; +import Resolver from './resolver'; + +export default class App extends EmberApplication { + rootElement = ENV.rootElement; + autoboot = ENV.autoboot; + modulePrefix = ENV.modulePrefix; + podModulePrefix = `${ENV.modulePrefix}/pods`; + Resolver = Resolver; +} diff --git a/packages/demo/src/config/class-factory.ts b/packages/demo/src/config/class-factory.ts new file mode 100644 index 00000000000..4fc4768df25 --- /dev/null +++ b/packages/demo/src/config/class-factory.ts @@ -0,0 +1,11 @@ +export default function classFactory(klass) { + return { + create(injections) { + if (typeof klass.extend === 'function') { + return klass.extend(injections); + } else { + return klass; + } + }, + }; +} diff --git a/packages/demo/src/config/env.ts b/packages/demo/src/config/env.ts new file mode 100644 index 00000000000..fdf5fadc5ad --- /dev/null +++ b/packages/demo/src/config/env.ts @@ -0,0 +1,29 @@ +import packageJSON from '../../package.json'; + +function config(environment: 'production' | 'development') { + const ENV = { + modulePrefix: packageJSON.name, + environment, + rootElement: '#app', + autoboot: false, + rootURL: '/', + locationType: 'history', // here is the change + EmberENV: { + FEATURES: {}, + EXTEND_PROTOTYPES: false, + _JQUERY_INTEGRATION: false, + _APPLICATION_TEMPLATE_WRAPPER: false, + _DEFAULT_ASYNC_OBSERVERS: true, + _TEMPLATE_ONLY_GLIMMER_COMPONENTS: true, + }, + APP: { + version: packageJSON.version, + globalName: 'MyApp', + }, + }; + + return ENV; +} + +const ENV = config(import.meta.env.MODE as 'production' | 'development'); +export default ENV; diff --git a/packages/demo/src/config/helpers.ts b/packages/demo/src/config/helpers.ts new file mode 100644 index 00000000000..4825cd12de3 --- /dev/null +++ b/packages/demo/src/config/helpers.ts @@ -0,0 +1,34 @@ +// import EmberGlimmerComponentManager from 'ember-component-manager'; +import Component from '@glimmer/component'; +import { setOwner, getOwner } from '@ember/owner'; +import { capabilities } from '@ember/component'; +import { setComponentManager } from '@ember/component'; +import { Ember } from '../../types/global'; +import config from './env'; + +class CustomComponentManager { + constructor() { + debugger; + } + capabilities = capabilities('3.13'); + + createComponent( + ...args: Parameters + ) { + const component = super.createComponent(...args); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + setOwner(component, getOwner(this)!); + + return component; + } +} + +export function setupApplicationGlobals(EmberNamespace: Ember) { + setComponentManager((owner) => { + return new CustomComponentManager(owner); + }, Component); + + window.EmberENV = config.EmberENV; + window._Ember = EmberNamespace; + window.Ember = EmberNamespace; +} diff --git a/packages/demo/src/config/initializer.ts b/packages/demo/src/config/initializer.ts new file mode 100644 index 00000000000..db52d10f09c --- /dev/null +++ b/packages/demo/src/config/initializer.ts @@ -0,0 +1,34 @@ +import ENV from './env'; +import registry from './registry'; +import type ApplicationClass from '@ember/application'; +import type RouteClass from './router'; + +export function init( + Application: typeof ApplicationClass, + Router: typeof RouteClass +) { + // Init initializers + // Application.initializer(initializer); + + // Init instance initializers + // Application.instanceInitializer(logger); + // Application.instanceInitializer(modalDialog); + + const app = Application.create({ + name: ENV.modulePrefix, + version: ENV.APP.version, + }); + + const registryObjects = registry(); + console.table(registryObjects); + + Object.keys(registryObjects).forEach((key) => { + const value = registryObjects[key]; + app.register(key, value); + }); + + app.register('config:environment', ENV); + app.register('router:main', Router); + + return app; +} diff --git a/packages/demo/src/config/inspector.ts b/packages/demo/src/config/inspector.ts new file mode 100644 index 00000000000..e98432dfc58 --- /dev/null +++ b/packages/demo/src/config/inspector.ts @@ -0,0 +1,92 @@ +import * as computed from '@ember/object/computed'; +import * as runloop from '@ember/runloop'; +import * as metal from '@ember/-internals/metal'; +import * as inst from '@ember/instrumentation'; +import * as view from '@ember/-internals/views'; +import * as ref from '@glimmer/reference'; +import * as val from '@glimmer/validator'; + +let define = window.define, + requireModule = window.requireModule; +if (typeof define !== 'function' || typeof requireModule !== 'function') { + (function () { + const registry = { + ember: window.Ember, + }, + seen = {}; + + define = function (name, deps, callback) { + if (arguments.length < 3) { + callback = deps; + deps = []; + } + registry[name] = { deps, callback }; + }; + + requireModule = function (name) { + if (name === '@ember/object/computed') { + return computed; + } + if (name === '@ember/runloop') { + return runloop; + } + if (name === '@ember/-internals/metal') { + return metal; + } + if (name === '@ember/instrumentation') { + return inst; + } + if (name === '@ember/-internals/views') { + return view; + } + + if (name === '@glimmer/reference') { + return ref; + } + if (name === '@glimmer/validator') { + return val; + } + + if (name === 'ember') { + return { + default: window.Ember, + }; + } + + if (seen[name]) { + return seen[name]; + } + + const mod = registry[name]; + if (!mod) { + throw new Error(`Module: '${name}' not found.`); + } + + seen[name] = {}; + + const deps = mod.deps; + const callback = mod.callback; + const reified = []; + let exports; + + for (let i = 0, l = deps.length; i < l; i++) { + if (deps[i] === 'exports') { + reified.push((exports = {})); + } else { + reified.push(requireModule(deps[i])); + } + } + + const value = callback.apply(this, reified); + seen[name] = exports || value; + return seen[name]; + }; + + define.registry = registry; + define.seen = seen; + })(); +} +requireModule.entries = define.registry; + +window.define = define; +window.requireModule = requireModule; diff --git a/packages/demo/src/config/registry.ts b/packages/demo/src/config/registry.ts new file mode 100644 index 00000000000..a913771645a --- /dev/null +++ b/packages/demo/src/config/registry.ts @@ -0,0 +1,40 @@ +import type { IRegistry } from './utils'; + +import { ApplicationRoute } from '@/routes/application'; +import ApplicationTemplate from '@/components/Application'; +import ProfileTemplate from '@/components/Profile'; +import MainTemplate from '@/components/Main'; + +import ProfileRoute from '@/routes/profile'; + + +/* imported controllers */ +import { ApplicationController } from '@/controllers/application'; +import { ProfileController } from '@/controllers/profile'; + +function asTemplate(ComponentKlass: any) { + return (_owner: any) => { + // template lookup + return () => { + // template init + return ComponentKlass; + }; + }; +} + +const InitialRegistry = { + 'controller:application': ApplicationController, + 'controller:profile': ProfileController, + 'route:application': ApplicationRoute, + 'route:profile': ProfileRoute, + 'template:main': asTemplate(MainTemplate), + 'template:application': asTemplate(ApplicationTemplate), + 'template:profile': asTemplate(ProfileTemplate), + +}; + +function registry(): IRegistry { + return InitialRegistry; +} + +export default registry; diff --git a/packages/demo/src/config/resolver.ts b/packages/demo/src/config/resolver.ts new file mode 100644 index 00000000000..1d44f71f848 --- /dev/null +++ b/packages/demo/src/config/resolver.ts @@ -0,0 +1,548 @@ +/* globals requirejs, require */ + +import { assert, deprecate, warn } from '@ember/debug'; +import EmberObject from '@ember/object'; +import { dasherize, classify, underscore } from './string'; +import { DEBUG } from '@glimmer/env'; +import classFactory from './class-factory'; + +import { getOwner } from '@ember/owner'; + +// if (typeof requirejs.entries === 'undefined') { +// requirejs.entries = requirejs._eak_seen; +// } + +export class ModuleRegistry { + constructor(entries) { + this._entries = entries || {}; + } + moduleNames() { + return Object.keys(this._entries); + } + has(moduleName) { + return moduleName in this._entries; + } + get(...args) { + return require(...args); + } +} + +/** + * This module defines a subclass of Ember.DefaultResolver that adds two + * important features: + * + * 1) The resolver makes the container aware of es6 modules via the AMD + * output. The loader's _moduleEntries is consulted so that classes can be + * resolved directly via the module loader, without needing a manual + * `import`. + * 2) is able to provide injections to classes that implement `extend` + * (as is typical with Ember). + */ +class Resolver extends EmberObject { + static moduleBasedResolver = true; + moduleBasedResolver = true; + + _deprecatedPodModulePrefix = false; + _normalizeCache = Object.create(null); + + /** + A listing of functions to test for moduleName's based on the provided + `parsedName`. This allows easy customization of additional module based + lookup patterns. + + @property moduleNameLookupPatterns + @returns {Ember.Array} + */ + moduleNameLookupPatterns = [ + this.podBasedModuleName, + this.podBasedComponentsInSubdir, + this.mainModuleName, + this.defaultModuleName, + this.nestedColocationComponentModuleName, + ]; + + constructor() { + super(...arguments); + + if (!this._moduleRegistry) { + this._moduleRegistry = new ModuleRegistry(); + } + + this.pluralizedTypes = this.pluralizedTypes || Object.create(null); + + if (!this.pluralizedTypes.config) { + this.pluralizedTypes.config = 'config'; + } + } + + makeToString(factory, fullName) { + return '' + this.namespace.modulePrefix + '@' + fullName + ':'; + } + + shouldWrapInClassFactory(/* module, parsedName */) { + return false; + } + + parseName(fullName) { + if (fullName.parsedName === true) { + return fullName; + } + + let prefix, type, name; + let fullNameParts = fullName.split('@'); + + if (fullNameParts.length === 3) { + if (fullNameParts[0].length === 0) { + // leading scoped namespace: `@scope/pkg@type:name` + prefix = `@${fullNameParts[1]}`; + let prefixParts = fullNameParts[2].split(':'); + type = prefixParts[0]; + name = prefixParts[1]; + } else { + // interweaved scoped namespace: `type:@scope/pkg@name` + prefix = `@${fullNameParts[1]}`; + type = fullNameParts[0].slice(0, -1); + name = fullNameParts[2]; + } + + if (type === 'template:components') { + name = `components/${name}`; + type = 'template'; + } + } else if (fullNameParts.length === 2) { + let prefixParts = fullNameParts[0].split(':'); + + if (prefixParts.length === 2) { + if (prefixParts[1].length === 0) { + type = prefixParts[0]; + name = `@${fullNameParts[1]}`; + } else { + prefix = prefixParts[1]; + type = prefixParts[0]; + name = fullNameParts[1]; + } + } else { + let nameParts = fullNameParts[1].split(':'); + + prefix = fullNameParts[0]; + type = nameParts[0]; + name = nameParts[1]; + } + + if (type === 'template' && prefix.lastIndexOf('components/', 0) === 0) { + name = `components/${name}`; + prefix = prefix.slice(11); + } + } else { + fullNameParts = fullName.split(':'); + type = fullNameParts[0]; + name = fullNameParts[1]; + } + + let fullNameWithoutType = name; + let namespace = this.namespace; + let root = namespace; + + return { + parsedName: true, + fullName: fullName, + prefix: prefix || this.prefix({ type: type }), + type: type, + fullNameWithoutType: fullNameWithoutType, + name: name, + root: root, + resolveMethodName: 'resolve' + classify(type), + }; + } + + resolveOther(parsedName) { + assert('`modulePrefix` must be defined', this.namespace.modulePrefix); + + let normalizedModuleName = this.findModuleName(parsedName); + + if (normalizedModuleName) { + let defaultExport = this._extractDefaultExport( + normalizedModuleName, + parsedName + ); + + if (defaultExport === undefined) { + throw new Error( + ` Expected to find: '${parsedName.fullName}' within '${normalizedModuleName}' but got 'undefined'. Did you forget to 'export default' within '${normalizedModuleName}'?` + ); + } + + if (this.shouldWrapInClassFactory(defaultExport, parsedName)) { + defaultExport = classFactory(defaultExport); + } + + return defaultExport; + } + } + + normalize(fullName) { + return ( + this._normalizeCache[fullName] || + (this._normalizeCache[fullName] = this._normalize(fullName)) + ); + } + + resolve(fullName) { + let parsedName = this.parseName(fullName); + let resolveMethodName = parsedName.resolveMethodName; + let resolved; + + if (typeof this[resolveMethodName] === 'function') { + resolved = this[resolveMethodName](parsedName); + } + + if (resolved == null) { + resolved = this.resolveOther(parsedName); + } + + return resolved; + } + + _normalize(fullName) { + // A) Convert underscores to dashes + // B) Convert camelCase to dash-case, except for components (their + // templates) and helpers where we want to avoid shadowing camelCase + // expressions + // C) replace `.` with `/` in order to make nested controllers work in the following cases + // 1. `needs: ['posts/post']` + // 2. `{{render "posts/post"}}` + // 3. `this.render('posts/post')` from Route + + let split = fullName.split(':'); + if (split.length > 1) { + let type = split[0]; + + if ( + type === 'component' || + type === 'helper' || + type === 'modifier' || + (type === 'template' && split[1].indexOf('components/') === 0) + ) { + return type + ':' + split[1].replace(/_/g, '-'); + } else { + return type + ':' + dasherize(split[1].replace(/\./g, '/')); + } + } else { + return fullName; + } + } + + pluralize(type) { + return ( + this.pluralizedTypes[type] || (this.pluralizedTypes[type] = type + 's') + ); + } + + podBasedLookupWithPrefix(podPrefix, parsedName) { + let fullNameWithoutType = parsedName.fullNameWithoutType; + + if (parsedName.type === 'template') { + fullNameWithoutType = fullNameWithoutType.replace(/^components\//, ''); + } + + return podPrefix + '/' + fullNameWithoutType + '/' + parsedName.type; + } + + podBasedModuleName(parsedName) { + let podPrefix = + this.namespace.podModulePrefix || this.namespace.modulePrefix; + + return this.podBasedLookupWithPrefix(podPrefix, parsedName); + } + + podBasedComponentsInSubdir(parsedName) { + let podPrefix = + this.namespace.podModulePrefix || this.namespace.modulePrefix; + podPrefix = podPrefix + '/components'; + + if ( + parsedName.type === 'component' || + /^components/.test(parsedName.fullNameWithoutType) + ) { + return this.podBasedLookupWithPrefix(podPrefix, parsedName); + } + } + + resolveEngine(parsedName) { + let engineName = parsedName.fullNameWithoutType; + let engineModule = engineName + '/engine'; + + if (this._moduleRegistry.has(engineModule)) { + return this._extractDefaultExport(engineModule); + } + } + + resolveRouteMap(parsedName) { + let engineName = parsedName.fullNameWithoutType; + let engineRoutesModule = engineName + '/routes'; + + if (this._moduleRegistry.has(engineRoutesModule)) { + let routeMap = this._extractDefaultExport(engineRoutesModule); + + assert( + `The route map for ${engineName} should be wrapped by 'buildRoutes' before exporting.`, + routeMap.isRouteMap + ); + + return routeMap; + } + } + + resolveTemplate(parsedName) { + return this.resolveOther(parsedName); + } + + mainModuleName(parsedName) { + if (parsedName.fullNameWithoutType === 'main') { + // if router:main or adapter:main look for a module with just the type first + return parsedName.prefix + '/' + parsedName.type; + } + } + + defaultModuleName(parsedName) { + return ( + parsedName.prefix + + '/' + + this.pluralize(parsedName.type) + + '/' + + parsedName.fullNameWithoutType + ); + } + + nestedColocationComponentModuleName(parsedName) { + if (parsedName.type === 'component') { + return ( + parsedName.prefix + + '/' + + this.pluralize(parsedName.type) + + '/' + + parsedName.fullNameWithoutType + + '/index' + ); + } + } + + prefix(parsedName) { + let tmpPrefix = this.namespace.modulePrefix; + + if (this.namespace[parsedName.type + 'Prefix']) { + tmpPrefix = this.namespace[parsedName.type + 'Prefix']; + } + + return tmpPrefix; + } + + findModuleName(parsedName, loggingDisabled) { + let moduleNameLookupPatterns = this.moduleNameLookupPatterns; + let moduleName; + + for ( + let index = 0, length = moduleNameLookupPatterns.length; + index < length; + index++ + ) { + let item = moduleNameLookupPatterns[index]; + + let tmpModuleName = item.call(this, parsedName); + + // allow treat all dashed and all underscored as the same thing + // supports components with dashes and other stuff with underscores. + if (tmpModuleName) { + tmpModuleName = this.chooseModuleName(tmpModuleName, parsedName); + } + + if (tmpModuleName && this._moduleRegistry.has(tmpModuleName)) { + moduleName = tmpModuleName; + } + + if (!loggingDisabled) { + this._logLookup(moduleName, parsedName, tmpModuleName); + } + + if (moduleName) { + return moduleName; + } + } + } + + chooseModuleName(moduleName, parsedName) { + let underscoredModuleName = underscore(moduleName); + + if ( + moduleName !== underscoredModuleName && + this._moduleRegistry.has(moduleName) && + this._moduleRegistry.has(underscoredModuleName) + ) { + throw new TypeError( + `Ambiguous module names: '${moduleName}' and '${underscoredModuleName}'` + ); + } + + if (this._moduleRegistry.has(moduleName)) { + return moduleName; + } else if (this._moduleRegistry.has(underscoredModuleName)) { + return underscoredModuleName; + } + // workaround for dasherized partials: + // something/something/-something => something/something/_something + let partializedModuleName = moduleName.replace(/\/-([^/]*)$/, '/_$1'); + + if (this._moduleRegistry.has(partializedModuleName)) { + deprecate( + 'Modules should not contain underscores. ' + + 'Attempted to lookup "' + + moduleName + + '" which ' + + 'was not found. Please rename "' + + partializedModuleName + + '" ' + + 'to "' + + moduleName + + '" instead.', + false, + { + id: 'ember-resolver.underscored-modules', + until: '3.0.0', + for: 'ember-resolver', + since: '0.1.0', + } + ); + + return partializedModuleName; + } + + if (DEBUG) { + let isCamelCaseHelper = + parsedName.type === 'helper' && /[a-z]+[A-Z]+/.test(moduleName); + if (isCamelCaseHelper) { + this._camelCaseHelperWarnedNames = + this._camelCaseHelperWarnedNames || []; + let alreadyWarned = + this._camelCaseHelperWarnedNames.indexOf(parsedName.fullName) > -1; + if (!alreadyWarned && this._moduleRegistry.has(dasherize(moduleName))) { + this._camelCaseHelperWarnedNames.push(parsedName.fullName); + warn( + 'Attempted to lookup "' + + parsedName.fullName + + '" which ' + + 'was not found. In previous versions of ember-resolver, a bug would have ' + + 'caused the module at "' + + dasherize(moduleName) + + '" to be ' + + 'returned for this camel case helper name. This has been fixed. ' + + 'Use the dasherized name to resolve the module that would have been ' + + 'returned in previous versions.', + false, + { id: 'ember-resolver.camelcase-helper-names', until: '3.0.0' } + ); + } + } + } + } + + // used by Ember.DefaultResolver.prototype._logLookup + lookupDescription(fullName) { + let parsedName = this.parseName(fullName); + + let moduleName = this.findModuleName(parsedName, true); + + return moduleName; + } + + // only needed until 1.6.0-beta.2 can be required + _logLookup(found, parsedName, description) { + let owner = getOwner(this); + let env = owner?.resolveRegistration?.('config:environment'); + if (!env?.LOG_MODULE_RESOLVER && !parsedName.root.LOG_RESOLVER) { + return; + } + + let padding; + let symbol = found ? '[✓]' : '[ ]'; + + if (parsedName.fullName.length > 60) { + padding = '.'; + } else { + padding = new Array(60 - parsedName.fullName.length).join('.'); + } + + if (!description) { + description = this.lookupDescription(parsedName); + } + + /* eslint-disable no-console */ + if (console && console.info) { + console.info(symbol, parsedName.fullName, padding, description); + } + } + + knownForType(type) { + let moduleKeys = this._moduleRegistry.moduleNames(); + + let items = Object.create(null); + for (let index = 0, length = moduleKeys.length; index < length; index++) { + let moduleName = moduleKeys[index]; + let fullname = this.translateToContainerFullname(type, moduleName); + + if (fullname) { + items[fullname] = true; + } + } + + return items; + } + + translateToContainerFullname(type, moduleName) { + let prefix = this.prefix({ type }); + + // Note: using string manipulation here rather than regexes for better performance. + // pod modules + // '^' + prefix + '/(.+)/' + type + '$' + let podPrefix = prefix + '/'; + let podSuffix = '/' + type; + let start = moduleName.indexOf(podPrefix); + let end = moduleName.indexOf(podSuffix); + + if ( + start === 0 && + end === moduleName.length - podSuffix.length && + moduleName.length > podPrefix.length + podSuffix.length + ) { + return type + ':' + moduleName.slice(start + podPrefix.length, end); + } + + // non-pod modules + // '^' + prefix + '/' + pluralizedType + '/(.+)$' + let pluralizedType = this.pluralize(type); + let nonPodPrefix = prefix + '/' + pluralizedType + '/'; + + if ( + moduleName.indexOf(nonPodPrefix) === 0 && + moduleName.length > nonPodPrefix.length + ) { + return type + ':' + moduleName.slice(nonPodPrefix.length); + } + } + + _extractDefaultExport(normalizedModuleName) { + let module = this._moduleRegistry.get( + normalizedModuleName, + null, + null, + true /* force sync */ + ); + + if (module && module['default']) { + module = module['default']; + } + + return module; + } +} + +export default Resolver; diff --git a/packages/demo/src/config/router.ts b/packages/demo/src/config/router.ts new file mode 100644 index 00000000000..fc139b8f6cd --- /dev/null +++ b/packages/demo/src/config/router.ts @@ -0,0 +1,104 @@ +import EmberRouter from '@ember/routing/router'; +import config from './env'; +import type Controller from '@ember/controller'; +import Route from '@ember/routing/route'; +import { PrecompiledTemplate } from '@ember/template-compilation'; +import { getOwner } from '@ember/application'; + +/* + Here we use part of lazy-loading logic from https://github.com/embroider-build/embroider/blob/main/packages/router/src/index.ts +*/ + +export type HashReturnType = { + route?: typeof Route | Promise; + controller?: typeof Controller | Promise; + template?: PrecompiledTemplate | Promise; +}; + +class Router extends EmberRouter { + static lazyRoutes: Record HashReturnType> = {}; + location = config.locationType as 'history'; + rootURL = config.rootURL; + loadedRoutes = new Set(); + + // This is necessary in order to prevent the premature loading of lazy routes + // when we are merely trying to render a link-to that points at them. + // Unfortunately the stock query parameter behavior pulls on routes just to + // check what their previous QP values were. + _getQPMeta(handlerInfo: { name: string }, ...rest: unknown[]) { + if ( + handlerInfo.name in Router.lazyRoutes && + !this.loadedRoutes.has(handlerInfo.name) + ) { + return undefined; + } + // @ts-expect-error extending private method + return super._getQPMeta(handlerInfo, ...rest); + } + + // This is the framework method that we're overriding to provide our own + // handlerResolver. + setupRouter(...args: unknown[]) { + // @ts-expect-error extending private method + const isSetup = super.setupRouter(...args); + const microLib = ( + this as unknown as { + // TODO: is there a way don't use the private route? + /* eslint-disable ember/no-private-routing-service */ + _routerMicrolib: { getRoute: (name: string) => unknown }; + } + )._routerMicrolib; + microLib.getRoute = this._handlerResolver(microLib.getRoute.bind(microLib)); + return isSetup; + } + + lazyBundle(name: string) { + if (this.loadedRoutes.has(name)) { + return null; + } + const routeResolver = Router.lazyRoutes[name]; + const owner = getOwner(this); + if (routeResolver) { + return { + load: async () => { + const hash = routeResolver(); + const keys = Object.keys(hash); + const values = await Promise.all(keys.map((key) => hash[key])); + keys.forEach((key, index) => { + // owner.unregister(`${key}:${name}`); + try { + owner.register(`${key}:${name}`, values[index]); + } catch (e) { + // ignore + } + }); + this.loadedRoutes.add(name); + }, + loaded: false, + }; + } + return null; + } + + private _handlerResolver(original: (name: string) => unknown) { + return (name: string) => { + const bundle = this.lazyBundle(name); + + if (!bundle || bundle.loaded) { + return original(name); + } + + return bundle.load().then( + () => { + bundle.loaded = true; + return original(name); + }, + (err: Error) => { + throw err; + } + ); + }; + } +} + +export default Router; diff --git a/packages/demo/src/config/string.ts b/packages/demo/src/config/string.ts new file mode 100644 index 00000000000..f4b591cbea7 --- /dev/null +++ b/packages/demo/src/config/string.ts @@ -0,0 +1,162 @@ +class Cache { + constructor(limit, func, store) { + this.limit = limit; + this.func = func; + this.store = store; + this.size = 0; + this.misses = 0; + this.hits = 0; + this.store = store || new Map(); + } + get(key) { + let value = this.store.get(key); + if (this.store.has(key)) { + this.hits++; + return this.store.get(key); + } else { + this.misses++; + value = this.set(key, this.func(key)); + } + return value; + } + set(key, value) { + if (this.limit > this.size) { + this.size++; + this.store.set(key, value); + } + return value; + } + purge() { + this.store.clear(); + this.size = 0; + this.hits = 0; + this.misses = 0; + } +} +let STRINGS = {}; +export function setStrings(strings) { + STRINGS = strings; +} +export function getStrings() { + return STRINGS; +} +export function getString(name) { + return STRINGS[name]; +} +const STRING_DASHERIZE_REGEXP = /[ _]/g; +const STRING_DASHERIZE_CACHE = new Cache(1000, (key) => + decamelize(key).replace(STRING_DASHERIZE_REGEXP, '-') +); +const STRING_CLASSIFY_REGEXP_1 = /^(\-|_)+(.)?/; +const STRING_CLASSIFY_REGEXP_2 = /(.)(\-|\_|\.|\s)+(.)?/g; +const STRING_CLASSIFY_REGEXP_3 = /(^|\/|\.)([a-z])/g; +const CLASSIFY_CACHE = new Cache(1000, (str) => { + const replace1 = (_match, _separator, chr) => (chr ? `_${chr.toUpperCase()}` : ''); + const replace2 = (_match, initialChar, _separator, chr) => + initialChar + (chr ? chr.toUpperCase() : ''); + const parts = str.split('/'); + for (let i = 0; i < parts.length; i++) { + parts[i] = parts[i] + .replace(STRING_CLASSIFY_REGEXP_1, replace1) + .replace(STRING_CLASSIFY_REGEXP_2, replace2); + } + return parts + .join('/') + .replace(STRING_CLASSIFY_REGEXP_3, (match /*, separator, chr */) => match.toUpperCase()); +}); +const STRING_UNDERSCORE_REGEXP_1 = /([a-z\d])([A-Z]+)/g; +const STRING_UNDERSCORE_REGEXP_2 = /\-|\s+/g; +const UNDERSCORE_CACHE = new Cache(1000, (str) => + str + .replace(STRING_UNDERSCORE_REGEXP_1, '$1_$2') + .replace(STRING_UNDERSCORE_REGEXP_2, '_') + .toLowerCase() +); +const STRING_DECAMELIZE_REGEXP = /([a-z\d])([A-Z])/g; +const DECAMELIZE_CACHE = new Cache(1000, (str) => + str.replace(STRING_DECAMELIZE_REGEXP, '$1_$2').toLowerCase() +); +/** + Converts a camelized string into all lower case separated by underscores. + + ```javascript + import { decamelize } from '@ember/string'; + + decamelize('innerHTML'); // 'inner_html' + decamelize('action_name'); // 'action_name' + decamelize('css-class-name'); // 'css-class-name' + decamelize('my favorite items'); // 'my favorite items' + ``` + + @method decamelize + @param {String} str The string to decamelize. + @return {String} the decamelized string. + @public +*/ +export function decamelize(str) { + return DECAMELIZE_CACHE.get(str); +} +/** + Replaces underscores, spaces, or camelCase with dashes. + + ```javascript + import { dasherize } from '@ember/string'; + + dasherize('innerHTML'); // 'inner-html' + dasherize('action_name'); // 'action-name' + dasherize('css-class-name'); // 'css-class-name' + dasherize('my favorite items'); // 'my-favorite-items' + dasherize('privateDocs/ownerInvoice'; // 'private-docs/owner-invoice' + ``` + + @method dasherize + @param {String} str The string to dasherize. + @return {String} the dasherized string. + @public +*/ +export function dasherize(str) { + return STRING_DASHERIZE_CACHE.get(str); +} +/** + Returns the UpperCamelCase form of a string. + + ```javascript + import { classify } from '@ember/string'; + + classify('innerHTML'); // 'InnerHTML' + classify('action_name'); // 'ActionName' + classify('css-class-name'); // 'CssClassName' + classify('my favorite items'); // 'MyFavoriteItems' + classify('private-docs/owner-invoice'); // 'PrivateDocs/OwnerInvoice' + ``` + + @method classify + @param {String} str the string to classify + @return {String} the classified string + @public +*/ +export function classify(str) { + return CLASSIFY_CACHE.get(str); +} +/** + More general than decamelize. Returns the lower\_case\_and\_underscored + form of a string. + + ```javascript + import { underscore } from '@ember/string'; + + underscore('innerHTML'); // 'inner_html' + underscore('action_name'); // 'action_name' + underscore('css-class-name'); // 'css_class_name' + underscore('my favorite items'); // 'my_favorite_items' + underscore('privateDocs/ownerInvoice'); // 'private_docs/owner_invoice' + ``` + + @method underscore + @param {String} str The string to underscore. + @return {String} the underscored string. + @public +*/ +export function underscore(str) { + return UNDERSCORE_CACHE.get(str); +} diff --git a/packages/demo/src/config/utils.ts b/packages/demo/src/config/utils.ts new file mode 100644 index 00000000000..7a7ba0e7aa9 --- /dev/null +++ b/packages/demo/src/config/utils.ts @@ -0,0 +1,62 @@ +import type Service from '@ember/service'; +import type Controller from '@ember/controller'; +import type Route from '@ember/routing/route'; +import type GlimmerComponent from '@glimmer/component'; +import type Helper from '@ember/component/helper'; +import type Modifier from 'ember-modifier'; +import type { PrecompiledTemplate } from '@ember/template-compilation'; +import { setComponentTemplate } from '@ember/component'; +import env from '@/config/env'; +export type RegisteredComponent = typeof GlimmerComponent & { + template: PrecompiledTemplate; +}; +export type RegistryType = + | 'service' + | 'controller' + | 'route' + | 'template' + | 'component' + | 'helper' + | 'modifier'; +export type RegistryKey = `${RegistryType}:${string}`; +export interface IRegistry { + [key: RegistryKey]: + | typeof Service + | typeof Controller + | typeof Route + | typeof Helper + | Modifier + | RegisteredComponent + | PrecompiledTemplate; +} + +export function registerComponent( + component: T & { template: PrecompiledTemplate } +): RegisteredComponent { + try { + return setComponentTemplate( + component.template, + component as unknown as object + ) as RegisteredComponent; + } catch (e) { + console.error(e); + return component as unknown as RegisteredComponent; + } +} + +export function resoleFromRegistry(key: RegistryKey): T { + // application.__registry__.resolve + return window[env.APP.globalName].resolveRegistration(key) as T; +} + +export function extendRegistry(registry) { + Object.keys(registry).forEach((key) => { + try { + window[env.APP.globalName].register(key, registry[key]); + } catch(e) { + // hot-reload case + window[env.APP.globalName].unregister(key); + window[env.APP.globalName].register(key, registry[key]); + } + }); +} diff --git a/packages/demo/src/controllers/application.ts b/packages/demo/src/controllers/application.ts new file mode 100644 index 00000000000..8c0d478dcd2 --- /dev/null +++ b/packages/demo/src/controllers/application.ts @@ -0,0 +1,9 @@ +import Controller from '@ember/controller'; +import { tracked } from '@glimmer/tracking'; +export class ApplicationController extends Controller { + @tracked showModal = true; + + constructor(...args: ConstructorParameters) { + super(...args); + } +} diff --git a/packages/demo/src/controllers/login.ts b/packages/demo/src/controllers/login.ts new file mode 100644 index 00000000000..d53ff60cac8 --- /dev/null +++ b/packages/demo/src/controllers/login.ts @@ -0,0 +1,15 @@ +import Controller from '@ember/controller'; +import { service } from '@ember/service'; +import { action } from '@ember/object'; +import RouterService from '@ember/routing/router-service'; + +export class LoginController extends Controller { + @service router!: RouterService; + + @action + async authenticate(e: MouseEvent) { + e.preventDefault(); + + await this.session.authenticate('authenticator:custom'); + } +} diff --git a/packages/demo/src/controllers/profile.ts b/packages/demo/src/controllers/profile.ts new file mode 100644 index 00000000000..b49edda9edd --- /dev/null +++ b/packages/demo/src/controllers/profile.ts @@ -0,0 +1,36 @@ +import Controller, { type ControllerQueryParam } from '@ember/controller'; +import { tracked } from '@glimmer/tracking'; +import type RouterService from '@ember/routing/router-service'; +import { service } from '@ember/service'; + + +export class ProfileController extends Controller { + queryParams: readonly ControllerQueryParam[] = ['q']; + @tracked + q = 12; + @tracked now = new Date().toISOString(); + @service router!: RouterService; + + onInputChange = (e) => { + this.q = parseInt(e.target.value) || 0; + }; + + toMain = () => { + this.router.transitionTo('main'); + }; + + incrementQp = () => { + this.q++; + }; + + decrementQp = () => { + this.q--; + }; + + constructor(...args: ConstructorParameters) { + super(...args); + setInterval(() => { + this.now = new Date().toISOString(); + }, 1000); + } +} diff --git a/packages/demo/src/helpers/__mocks__/@ember/component/helper.js b/packages/demo/src/helpers/__mocks__/@ember/component/helper.js new file mode 100644 index 00000000000..e245eb6d266 --- /dev/null +++ b/packages/demo/src/helpers/__mocks__/@ember/component/helper.js @@ -0,0 +1,8 @@ +export default class Helper { + willDestroy() { + // EOL + } + recompute() { + // EOL + } +} diff --git a/packages/demo/src/helpers/is-dev.ts b/packages/demo/src/helpers/is-dev.ts new file mode 100644 index 00000000000..8265b99c49d --- /dev/null +++ b/packages/demo/src/helpers/is-dev.ts @@ -0,0 +1,5 @@ +import env from '@/config/env'; + +export default function isDev(): boolean { + return env.environment === 'development'; +} diff --git a/packages/demo/src/helpers/memory-usage.test.ts b/packages/demo/src/helpers/memory-usage.test.ts new file mode 100644 index 00000000000..f12960745c2 --- /dev/null +++ b/packages/demo/src/helpers/memory-usage.test.ts @@ -0,0 +1,30 @@ +// jest test for memory-usage.ts +import MemoryUsage from './memory-usage'; + +describe('helpers | memory-usage', () => { + it('should retun from computation if no performance property', () => { + const memoryUsage = new MemoryUsage(); + expect(memoryUsage.compute()).toBeNull(); + }); + it('should not call recompute if measureMemory called without performance', async () => { + const memoryUsage = new MemoryUsage(); + const recomputeSpy = jest.spyOn(memoryUsage, 'recompute'); + await memoryUsage.measureMemory(); + expect(recomputeSpy).not.toHaveBeenCalled(); + }); + it('will destroy should clear interval', async () => { + const memoryUsage = new MemoryUsage(); + let a = 1; + const interval = setInterval(() => { + a++; + // NOOP + }, 10); + await new Promise((resolve) => setTimeout(resolve, 100)); + memoryUsage.interval = interval; + expect(memoryUsage.interval).toBe(interval); + const lastIntervalValue = a; + memoryUsage.willDestroy(); + await new Promise((resolve) => setTimeout(resolve, 100)); + expect(a).toBe(lastIntervalValue); + }); +}); diff --git a/packages/demo/src/helpers/memory-usage.ts b/packages/demo/src/helpers/memory-usage.ts new file mode 100644 index 00000000000..f806c72e4b3 --- /dev/null +++ b/packages/demo/src/helpers/memory-usage.ts @@ -0,0 +1,38 @@ +import Helper from '@ember/component/helper'; + +export default class MemoryUsage extends Helper { + interval: number | undefined; + heapSize: number | undefined; + + constructor() { + super(); + + if (!performance.memory) { + return; + } + + this.interval = setInterval(this.measureMemory.bind(this), 1000); + } + + async measureMemory() { + if (!performance.memory) { + return; + } + + const dirtyValue = performance.memory.usedJSHeapSize / 1048576; + + this.heapSize = Math.round(dirtyValue * 100) / 100; + + this.recompute(); + } + + compute() { + return this.heapSize ? `Memory used ${this.heapSize} MB` : null; + } + + willDestroy() { + super.willDestroy(); + + clearInterval(this.interval); + } +} diff --git a/packages/demo/src/instance-initializers/logger.test.ts b/packages/demo/src/instance-initializers/logger.test.ts new file mode 100644 index 00000000000..c7eeb21e852 --- /dev/null +++ b/packages/demo/src/instance-initializers/logger.test.ts @@ -0,0 +1,18 @@ +import logger from './logger'; + +// jest test for instance-initializers/logger.ts +describe('instance-initializers/logger', () => { + it('should log', () => { + const mockLogger = { + log: jest.fn(), + }; + + logger.initialize({ + lookup() { + return mockLogger; + }, + } as any); + + expect(mockLogger.log).toHaveBeenCalledWith('Instance initializer init'); + }); +}); diff --git a/packages/demo/src/instance-initializers/logger.ts b/packages/demo/src/instance-initializers/logger.ts new file mode 100644 index 00000000000..35889c58555 --- /dev/null +++ b/packages/demo/src/instance-initializers/logger.ts @@ -0,0 +1,13 @@ +import type ApplicationInstance from '@ember/application/instance'; +import type { Logger } from '../initializers/logger'; + +export function initialize(applicationInstance: ApplicationInstance) { + const logger = applicationInstance.lookup('logger:main') as Logger; + + logger.log('Instance initializer init'); +} + +export default { + name: 'logger', + initialize, +}; diff --git a/packages/demo/src/main.ts b/packages/demo/src/main.ts new file mode 100644 index 00000000000..a76add68227 --- /dev/null +++ b/packages/demo/src/main.ts @@ -0,0 +1,18 @@ +import './style.css'; + +import Ember from 'ember'; +import App from '@/config/application'; +import { init } from '@/config/initializer'; +import { setupApplicationGlobals } from '@/config/helpers'; +import { extendRegistry } from '@/config/utils'; +import env from '@/config/env'; +import Router from './router'; + +import '@/config/inspector'; +globalThis.EmberFunctionalHelpers = new WeakMap(); +setupApplicationGlobals(Ember); + +const app = init(App, Router); + +window[env.APP.globalName] = app; // for debugging and experiments +app.visit(window.location.href.replace(window.location.origin, '')); diff --git a/packages/demo/src/models/person.ts b/packages/demo/src/models/person.ts new file mode 100644 index 00000000000..f1156f17e3f --- /dev/null +++ b/packages/demo/src/models/person.ts @@ -0,0 +1,7 @@ +import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; + +export default class PersonModel extends Model { + @attr name; + @belongsTo('pet', { inverse: 'owner', async: false }) dog; + @hasMany('person', { inverse: 'friends', async: false }) friends; +} diff --git a/packages/demo/src/models/pet.ts b/packages/demo/src/models/pet.ts new file mode 100644 index 00000000000..ce55c58ff2e --- /dev/null +++ b/packages/demo/src/models/pet.ts @@ -0,0 +1,6 @@ +import Model, { attr, belongsTo } from '@ember-data/model'; + +export default class PetModel extends Model { + @attr name; + @belongsTo('person', { inverse: 'dog', async: false }) owner; +} diff --git a/packages/demo/src/modifiers/click-tracker.ts b/packages/demo/src/modifiers/click-tracker.ts new file mode 100644 index 00000000000..ae8e1b37d2f --- /dev/null +++ b/packages/demo/src/modifiers/click-tracker.ts @@ -0,0 +1,26 @@ +import Modifier from 'ember-modifier'; +import { registerDestructor } from '@ember/destroyable'; + +function cleanup(instance: ClickTracker) { + document.body.removeEventListener('click', instance.handler); +} + +export default class ClickTracker extends Modifier { + element!: Element; + + modify(element: Element) { + this.element = element; + + document.body.addEventListener('click', this.handler); + + registerDestructor(this, cleanup); + } + + handler = (e: Event) => { + const place = this.element.contains(e.target as Element) + ? 'inside' + : 'outside'; + + console.log(`Click ${place} ${this.element}`); + }; +} diff --git a/packages/demo/src/router.ts b/packages/demo/src/router.ts new file mode 100644 index 00000000000..de10d70ccb3 --- /dev/null +++ b/packages/demo/src/router.ts @@ -0,0 +1,46 @@ +import Router from '@/config/router'; +// import type { HashReturnType } from '@/config/router'; +// import MainTemplate from './templates/main'; + +export enum Routes { + Main = 'main', + Profile = 'profile', + Login = 'login', + Logout = 'logout', + About = 'about', + NotFound = 'not-found', + Bootstrap = 'bootstrap', +} + +// Router.lazyRoutes = { +// [Routes.Main]: (): HashReturnType => ({ +// // sample of lazy-loaded route, and statically resolved template +// // have no idea how to fix typings here... +// route: import('./routes/main').then((m) => m.MainRoute), +// template: MainTemplate, +// }), +// [Routes.Profile]: (): HashReturnType => ({ +// route: import('./routes/profile').then((m) => m.default), +// template: import('./templates/profile.hbs').then((m) => m.default), +// }), +// [Routes.NotFound]: (): HashReturnType => ({ +// // sample of lazy-loaded route, and dynamically resolved template +// template: import('./templates/not-found').then((m) => m.default), +// }), +// [Routes.Bootstrap]: (): HashReturnType => ({ +// // sample of lazy-loaded route, and dynamically resolved template +// template: import('./templates/bootstrap').then((m) => m.default), +// }), +// }; + +Router.map(function () { + this.route(Routes.Main, { path: '/' }); + this.route(Routes.Profile, { path: '/profile' }); + this.route(Routes.Login, { path: '/login' }); + this.route(Routes.Logout, { path: '/logout' }); + this.route(Routes.About, { path: '/about' }); + this.route(Routes.Bootstrap, { path: '/bootstrap' }); + this.route(Routes.NotFound, { path: '*wildcard_path' }); +}); + +export default Router; diff --git a/packages/demo/src/routes/application.ts b/packages/demo/src/routes/application.ts new file mode 100644 index 00000000000..9933b6947ce --- /dev/null +++ b/packages/demo/src/routes/application.ts @@ -0,0 +1,12 @@ +import Route from '@ember/routing/route'; + +export class ApplicationRoute extends Route { + + async beforeModel() { + + } + + model() { + return ['red', 'yellow', 'blue']; + } +} diff --git a/packages/demo/src/routes/login.ts b/packages/demo/src/routes/login.ts new file mode 100644 index 00000000000..37a3ee3a977 --- /dev/null +++ b/packages/demo/src/routes/login.ts @@ -0,0 +1,5 @@ +import Route from '@ember/routing/route'; + +export default class LoginRoute extends Route { + +} diff --git a/packages/demo/src/routes/logout.ts b/packages/demo/src/routes/logout.ts new file mode 100644 index 00000000000..81960ad7c39 --- /dev/null +++ b/packages/demo/src/routes/logout.ts @@ -0,0 +1,13 @@ +import Route from '@ember/routing/route'; +import RouterService from '@ember/routing/router-service'; +import Transition from '@ember/routing/transition'; +import { service } from '@ember/service'; + +export default class LogoutRoute extends Route { + @service router!: RouterService; + + async beforeModel(transition: Transition) { + + this.router.transitionTo('main'); + } +} diff --git a/packages/demo/src/routes/main.ts b/packages/demo/src/routes/main.ts new file mode 100644 index 00000000000..1a62e7ca7c4 --- /dev/null +++ b/packages/demo/src/routes/main.ts @@ -0,0 +1,7 @@ +import Route from '@ember/routing/route'; + +export class MainRoute extends Route { + model() { + return ['foo', 'boo', 'blue']; + } +} diff --git a/packages/demo/src/routes/profile.ts b/packages/demo/src/routes/profile.ts new file mode 100644 index 00000000000..bb50c537c47 --- /dev/null +++ b/packages/demo/src/routes/profile.ts @@ -0,0 +1,18 @@ +import Route from '@ember/routing/route'; +import type Transition from '@ember/routing/transition'; +export default class ProfileRoute extends Route { + queryParams: Record = { + q: { + refreshModel: true, + replace: true, + } + } + + beforeModel(transition: Transition) { + console.log('beforeModel:profile', transition); + } + + model() { + return [1, 2, 3, Date.now()]; + } +} diff --git a/packages/demo/src/services/date.ts b/packages/demo/src/services/date.ts new file mode 100644 index 00000000000..57e6e05992c --- /dev/null +++ b/packages/demo/src/services/date.ts @@ -0,0 +1,29 @@ +import Service from '@ember/service'; +import { tracked } from '@glimmer/tracking'; +import { cached } from 'tracked-toolbox'; + +export default class DateService extends Service { + interval: ReturnType<(typeof window)['setInterval']> | null = null; + @tracked _date = new Date(); + + constructor(...args: ConstructorParameters) { + super(...args); + + this.interval = setInterval(() => { + this._date = new Date(); + }, 1000); + } + + willDestroy(...args: Parameters) { + super.willDestroy(...args); + if (this.interval) { + clearInterval(this.interval); + } + } + + @cached + get date() { + console.log('ama cached getter, recalculating only on value change') + return this._date.toLocaleTimeString(); + } +} diff --git a/packages/demo/src/services/store.ts b/packages/demo/src/services/store.ts new file mode 100644 index 00000000000..8ed8aa05539 --- /dev/null +++ b/packages/demo/src/services/store.ts @@ -0,0 +1,23 @@ +import Cache from '@ember-data/json-api'; +import EDataStore, { CacheHandler } from '@ember-data/store'; +import RequestManager from '@ember-data/request'; +import Fetch from '@ember-data/request/fetch'; +import Adapter from '@ember-data/adapter/json-api'; + +export default class Store extends EDataStore { + #adapter = new Adapter(); + + adapterFor() { + return this.#adapter; + } + constructor() { + // eslint-disable-next-line prefer-rest-params + super(...arguments); + this.requestManager = new RequestManager(); + this.requestManager.use([Fetch]); + this.requestManager.useCache(CacheHandler); + } + createCache(storeWrapper) { + return new Cache(storeWrapper); + } +} diff --git a/packages/demo/src/style.css b/packages/demo/src/style.css new file mode 100644 index 00000000000..d8693c21ccb --- /dev/null +++ b/packages/demo/src/style.css @@ -0,0 +1,12 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +a.active { + text-decoration: underline; + text-decoration-color: rgb(218, 118, 118); +} + +code { + color: green; +} \ No newline at end of file diff --git a/packages/demo/src/templates/about.hbs b/packages/demo/src/templates/about.hbs new file mode 100644 index 00000000000..ac4dc94debe --- /dev/null +++ b/packages/demo/src/templates/about.hbs @@ -0,0 +1,14 @@ +{{page-title "Page Title: About"}} +

About

+ +
+
+

Need more bandwidth?

+
+

Ember Application, powered by Vite

+
+
+ Home +
+
+
diff --git a/packages/demo/src/templates/application.hbs b/packages/demo/src/templates/application.hbs new file mode 100644 index 00000000000..2fa0dd57f09 --- /dev/null +++ b/packages/demo/src/templates/application.hbs @@ -0,0 +1,60 @@ +
+ +{{#if this.showModal}} + +
+ Ember Modal Dialog test +
+
+{{/if}} + +
+
+ + {{#if (media 'isDesktop')}} + looks like you on Desktop + {{else if (media 'isTablet')}} + looks like you on Tablet + {{else if (media 'isMobile')}} + looks like you on Mobile + {{else}} + wow + {{/if}} + + {{outlet}} + +
+
+
+ +
+
+

Here is your login status

+

+ {{#if this.session.isAuthenticated}} + You are authenticated as + {{this.session.data.authenticated.name}} + {{else}} + You are not authenticated + {{/if}} +

+
+
+
+ +
+
+ +