From ea4a581fe61dd641a922e0773229d3d88eefa1d2 Mon Sep 17 00:00:00 2001 From: Alex Kanunnikov Date: Mon, 26 Jan 2026 11:22:50 +0000 Subject: [PATCH 001/456] feat: Add glimmer-next integration foundation - Added demo package with glimmer-next (@lifeart/gxt) integration - Created compatibility layers for @glimmer/* APIs: - manager.ts: Component/helper/modifier managers - validator.ts: Tracking and tag system - reference.ts: Reference system - destroyable.ts: Destroyable utilities - Updated vite.config.mjs to support GXT_MODE for glimmer-next - Added @lifeart/gxt as a dependency - Cherry-picked and resolved conflicts from demo-app branch This provides the foundation for testing glimmer-next reactivity alongside the standard Ember/Glimmer rendering pipeline. Co-Authored-By: Claude Opus 4.5 --- babel.config.mjs | 37 +- package.json | 1 + .../glimmer/lib/components/internal.ts | 13 +- .../@ember/-internals/glimmer/lib/renderer.ts | 13 +- .../-internals/glimmer/lib/templates/input.ts | 7 +- .../lib/templates/outlet-helper-component.gts | 75 +++ .../glimmer/lib/templates/outlet.ts | 19 +- .../-internals/glimmer/lib/templates/root.ts | 28 +- .../-internals/glimmer/lib/views/outlet.ts | 26 +- .../application/helper-registration-test.js | 26 +- packages/@ember/-internals/metal/lib/alias.ts | 47 +- .../@ember/-internals/metal/lib/decorator.ts | 10 +- .../@ember/-internals/metal/lib/tracked.ts | 9 +- packages/@ember/-internals/package.json | 3 +- packages/@ember/object/core.ts | 2 +- packages/@ember/object/package.json | 3 +- packages/@ember/routing/package.json | 3 +- packages/@ember/routing/router.ts | 1 + packages/demo/compat/compile.ts | 12 + packages/demo/compat/deprecate.ts | 15 + packages/demo/compat/destroyable.ts | 21 + packages/demo/compat/glimmer-application.ts | 4 + packages/demo/compat/glimmer-util.ts | 14 + packages/demo/compat/helper-manager/index.ts | 63 ++ packages/demo/compat/manager.ts | 193 ++++++ packages/demo/compat/reference.ts | 59 ++ packages/demo/compat/validator.ts | 86 +++ packages/demo/index.html | 25 + packages/demo/package.json | 58 ++ packages/demo/postcss.config.cjs | 6 + packages/demo/src/components/Application.gts | 21 + packages/demo/src/components/Main.gts | 20 + packages/demo/src/components/Profile.gts | 38 ++ packages/demo/src/config/application.ts | 11 + packages/demo/src/config/class-factory.ts | 11 + packages/demo/src/config/env.ts | 29 + packages/demo/src/config/helpers.ts | 34 ++ packages/demo/src/config/initializer.ts | 34 ++ packages/demo/src/config/inspector.ts | 92 +++ packages/demo/src/config/registry.ts | 40 ++ packages/demo/src/config/resolver.ts | 548 ++++++++++++++++++ packages/demo/src/config/router.ts | 104 ++++ packages/demo/src/config/string.ts | 162 ++++++ packages/demo/src/config/utils.ts | 62 ++ packages/demo/src/controllers/application.ts | 9 + packages/demo/src/controllers/login.ts | 15 + packages/demo/src/controllers/profile.ts | 36 ++ .../__mocks__/@ember/component/helper.js | 8 + packages/demo/src/helpers/is-dev.ts | 5 + .../demo/src/helpers/memory-usage.test.ts | 30 + packages/demo/src/helpers/memory-usage.ts | 38 ++ .../src/instance-initializers/logger.test.ts | 18 + .../demo/src/instance-initializers/logger.ts | 13 + packages/demo/src/main.ts | 18 + packages/demo/src/models/person.ts | 7 + packages/demo/src/models/pet.ts | 6 + packages/demo/src/modifiers/click-tracker.ts | 26 + packages/demo/src/router.ts | 46 ++ packages/demo/src/routes/application.ts | 12 + packages/demo/src/routes/login.ts | 5 + packages/demo/src/routes/logout.ts | 13 + packages/demo/src/routes/main.ts | 7 + packages/demo/src/routes/profile.ts | 18 + packages/demo/src/services/date.ts | 29 + packages/demo/src/services/store.ts | 23 + packages/demo/src/style.css | 12 + packages/demo/src/templates/about.hbs | 14 + packages/demo/src/templates/application.hbs | 60 ++ packages/demo/src/templates/bootstrap.ts | 20 + packages/demo/src/templates/login.hbs | 4 + packages/demo/src/templates/not-found.ts | 33 ++ packages/demo/src/templates/profile.hbs | 15 + packages/demo/src/vite-env.d.ts | 1 + packages/demo/tailwind.config.js | 8 + packages/demo/tsconfig.json | 21 + packages/demo/vite.config.mts | 60 ++ .../test-cases/test-resolver-application.ts | 97 ++++ tsconfig.json | 3 +- tsconfig/compiler-options.json | 2 + vite.config.mjs | 88 ++- 80 files changed, 2807 insertions(+), 98 deletions(-) create mode 100644 packages/@ember/-internals/glimmer/lib/templates/outlet-helper-component.gts create mode 100644 packages/demo/compat/compile.ts create mode 100644 packages/demo/compat/deprecate.ts create mode 100644 packages/demo/compat/destroyable.ts create mode 100644 packages/demo/compat/glimmer-application.ts create mode 100644 packages/demo/compat/glimmer-util.ts create mode 100644 packages/demo/compat/helper-manager/index.ts create mode 100644 packages/demo/compat/manager.ts create mode 100644 packages/demo/compat/reference.ts create mode 100644 packages/demo/compat/validator.ts create mode 100644 packages/demo/index.html create mode 100644 packages/demo/package.json create mode 100644 packages/demo/postcss.config.cjs create mode 100644 packages/demo/src/components/Application.gts create mode 100644 packages/demo/src/components/Main.gts create mode 100644 packages/demo/src/components/Profile.gts create mode 100644 packages/demo/src/config/application.ts create mode 100644 packages/demo/src/config/class-factory.ts create mode 100644 packages/demo/src/config/env.ts create mode 100644 packages/demo/src/config/helpers.ts create mode 100644 packages/demo/src/config/initializer.ts create mode 100644 packages/demo/src/config/inspector.ts create mode 100644 packages/demo/src/config/registry.ts create mode 100644 packages/demo/src/config/resolver.ts create mode 100644 packages/demo/src/config/router.ts create mode 100644 packages/demo/src/config/string.ts create mode 100644 packages/demo/src/config/utils.ts create mode 100644 packages/demo/src/controllers/application.ts create mode 100644 packages/demo/src/controllers/login.ts create mode 100644 packages/demo/src/controllers/profile.ts create mode 100644 packages/demo/src/helpers/__mocks__/@ember/component/helper.js create mode 100644 packages/demo/src/helpers/is-dev.ts create mode 100644 packages/demo/src/helpers/memory-usage.test.ts create mode 100644 packages/demo/src/helpers/memory-usage.ts create mode 100644 packages/demo/src/instance-initializers/logger.test.ts create mode 100644 packages/demo/src/instance-initializers/logger.ts create mode 100644 packages/demo/src/main.ts create mode 100644 packages/demo/src/models/person.ts create mode 100644 packages/demo/src/models/pet.ts create mode 100644 packages/demo/src/modifiers/click-tracker.ts create mode 100644 packages/demo/src/router.ts create mode 100644 packages/demo/src/routes/application.ts create mode 100644 packages/demo/src/routes/login.ts create mode 100644 packages/demo/src/routes/logout.ts create mode 100644 packages/demo/src/routes/main.ts create mode 100644 packages/demo/src/routes/profile.ts create mode 100644 packages/demo/src/services/date.ts create mode 100644 packages/demo/src/services/store.ts create mode 100644 packages/demo/src/style.css create mode 100644 packages/demo/src/templates/about.hbs create mode 100644 packages/demo/src/templates/application.hbs create mode 100644 packages/demo/src/templates/bootstrap.ts create mode 100644 packages/demo/src/templates/login.hbs create mode 100644 packages/demo/src/templates/not-found.ts create mode 100644 packages/demo/src/templates/profile.hbs create mode 100644 packages/demo/src/vite-env.d.ts create mode 100644 packages/demo/tailwind.config.js create mode 100644 packages/demo/tsconfig.json create mode 100644 packages/demo/vite.config.mts diff --git a/babel.config.mjs b/babel.config.mjs index 839aa46ec94..84b2af897e5 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,15 @@ export default { runtime: { import: 'decorator-transforms/runtime' }, }, ], - [ - 'babel-plugin-ember-template-compilation', - { - compilerPath: resolve( - dirname(fileURLToPath(import.meta.url)), - './broccoli/glimmer-template-compiler.mjs' - ), - }, - ], + // Commented out for glimmer-next integration - using gxt compiler instead + // [ + // 'babel-plugin-ember-template-compilation', + // { + // compilerPath: resolve( + // dirname(fileURLToPath(import.meta.url)), + // './broccoli/glimmer-template-compiler.mjs' + // ), + // }, + // ], ], }; diff --git a/package.json b/package.json index 008724da9c5..522ec1386ab 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "type-check": "npm-run-all type-check:*" }, "dependencies": { + "@lifeart/gxt": "0.0.53", "@babel/core": "^7.24.4", "@embroider/addon-shim": "^1.10.2", "@simple-dom/interface": "^1.4.0", 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 cfc74e0e423..884fe9f2c4e 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 { @@ -37,7 +38,7 @@ import type { CurriedValue } from '@glimmer/runtime'; import { clientBuilder, createCapturedArgs, - curry, + curry as glimmerCurry, EMPTY_POSITIONAL, inTransaction, renderComponent as glimmerRenderComponent, @@ -74,6 +75,9 @@ export interface View { [BOUNDS]: Bounds | null; } +// Use glimmerCurry imported from @glimmer/runtime +const curry = glimmerCurry; + export class DynamicScope implements GlimmerDynamicScope { constructor( public view: View | null, @@ -269,7 +273,12 @@ class ClassicRootState { */ - inTransaction(env, () => destroy(result!)); + inTransaction(env, () => { + // @ts-expect-error foo-bar + destroyElementSync(result); + // runDestructors(result.ctx); + destroy(result!); + }); } } } diff --git a/packages/@ember/-internals/glimmer/lib/templates/input.ts b/packages/@ember/-internals/glimmer/lib/templates/input.ts index 1d15a85275c..230e6e117b0 100644 --- a/packages/@ember/-internals/glimmer/lib/templates/input.ts +++ b/packages/@ember/-internals/glimmer/lib/templates/input.ts @@ -19,10 +19,9 @@ export default precompileTemplate( {{on "cut" this.valueDidChange}} />`, { - moduleName: 'packages/@ember/-internals/glimmer/lib/templates/input.hbs', strictMode: true, - scope() { - return { on }; - }, + scope: () => ({ + on, + }), } ); 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/views/outlet.ts b/packages/@ember/-internals/glimmer/lib/views/outlet.ts index 7cc9be26e73..69422efc5f0 100644 --- a/packages/@ember/-internals/glimmer/lib/views/outlet.ts +++ b/packages/@ember/-internals/glimmer/lib/views/outlet.ts @@ -8,12 +8,12 @@ import { assert } from '@ember/debug'; import { schedule } from '@ember/runloop'; import type { Template, TemplateFactory } from '@glimmer/interfaces'; import type { Reference } from '@glimmer/reference'; -import { createComputeRef, updateRef } from '@glimmer/reference'; -import { consumeTag, createTag, dirtyTag } from '@glimmer/validator'; +import { cellFor } from '@lifeart/gxt'; import type { SimpleElement } from '@simple-dom/interface'; import type { OutletDefinitionState } from '../component-managers/outlet'; import type { Renderer } from '../renderer'; import type { OutletState } from '../utils/outlet'; +// const { createComputeRef, updateRef } = reference; export interface BootEnvironment { hasDOM: boolean; @@ -46,6 +46,7 @@ export default class OutletView { application: InternalOwner; template: TemplateFactory; }): OutletView { + console.log('outlet-view create', options); let { environment: _environment, application: namespace, template: templateFactory } = options; let owner = getOwner(options); assert('OutletView is unexpectedly missing an owner', owner); @@ -62,7 +63,6 @@ export default class OutletView { public template: Template, public namespace: any ) { - let outletStateTag = createTag(); let outletState: OutletState = { outlets: { main: undefined }, render: { @@ -74,16 +74,11 @@ export default class OutletView { }, }; - let ref = (this.ref = createComputeRef( - () => { - consumeTag(outletStateTag); - return outletState; - }, - (state: OutletState) => { - dirtyTag(outletStateTag); - outletState.outlets['main'] = state; - } - )); + cellFor(outletState.outlets, 'main'); + + let ref = (this.ref = outletState); + + // ref.compute(); this.state = { ref, @@ -114,7 +109,10 @@ export default class OutletView { } setOutletState(state: OutletState): void { - updateRef(this.ref, state); + // debugger; + // @todo - fix re-renders + this.ref.outlets['main'] = state; + // updateRef(this.ref, state); } destroy(): void { diff --git a/packages/@ember/-internals/glimmer/tests/integration/application/helper-registration-test.js b/packages/@ember/-internals/glimmer/tests/integration/application/helper-registration-test.js index cb68e3e07d9..a55ed855193 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/application/helper-registration-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/application/helper-registration-test.js @@ -2,15 +2,15 @@ import { moduleFor, ApplicationTestCase } from 'internal-test-helpers'; import Controller from '@ember/controller'; import Service, { service } from '@ember/service'; import { Helper, helper } from '@ember/-internals/glimmer'; -import { precompileTemplate } from '@ember/template-compilation'; +import { hbs } from '@lifeart/gxt'; moduleFor( 'Application Lifecycle - Helper Registration', class extends ApplicationTestCase { ['@test Unbound dashed helpers registered on the container can be late-invoked'](assert) { - this.add( - 'template:application', - precompileTemplate(`
{{x-borf}} {{x-borf 'YES'}}
`) + this.addTemplate( + 'application', + () => hbs`
{{x-borf}} {{x-borf 'YES'}}
` ); let myHelper = helper((params) => params[0] || 'BORF'); @@ -26,10 +26,9 @@ moduleFor( } ['@test Bound helpers registered on the container can be late-invoked'](assert) { - this.add( - 'template:application', - precompileTemplate(`
{{x-reverse}} {{x-reverse this.foo}}
`) - ); + this.addTemplate('application', function () { + return hbs`
{{x-reverse}} {{x-reverse this.foo}}
`; + }); this.add( 'controller:application', @@ -55,10 +54,9 @@ moduleFor( } ['@test Undashed helpers registered on the container can be invoked'](assert) { - this.add( - 'template:application', - precompileTemplate(`
{{omg}}|{{yorp 'boo'}}|{{yorp 'ya'}}
`) - ); + this.addTemplate('application', function () { + return hbs`
{{omg}}|{{yorp 'boo'}}|{{yorp 'ya'}}
`; + }); this.application.register( 'helper:omg', @@ -80,7 +78,9 @@ moduleFor( } ['@test Helpers can receive injections'](assert) { - this.add('template:application', precompileTemplate(`
{{full-name}}
`)); + this.addTemplate('application', function () { + return hbs`
{{full-name}}
`; + }); let serviceCalled = false; diff --git a/packages/@ember/-internals/metal/lib/alias.ts b/packages/@ember/-internals/metal/lib/alias.ts index 66ebf468f31..679b315ac98 100644 --- a/packages/@ember/-internals/metal/lib/alias.ts +++ b/packages/@ember/-internals/metal/lib/alias.ts @@ -2,15 +2,6 @@ import type { Meta } from '@ember/-internals/meta'; import { meta as metaFor } from '@ember/-internals/meta'; import { assert, inspect } from '@ember/debug'; import type { UpdatableTag } from '@glimmer/validator'; -import { - consumeTag, - tagFor, - tagMetaFor, - untrack, - updateTag, - validateTag, - valueForTag, -} from '@glimmer/validator'; import { CHAIN_PASS_THROUGH, finishLazyChains, getChainTagsForKey } from './chain-tags'; import type { ExtendedMethodDecorator } from './decorator'; import { @@ -23,6 +14,27 @@ import { defineProperty } from './properties'; import { get } from './property_get'; import { set } from './property_set'; +import { validator } from '@lifeart/gxt/glimmer-compatibility'; + +const { + consumeTag, + tagFor, + tagMetaFor, + // updateTag, + validateTag, + valueForTag, +} = validator; + +function updateTag(tag, args) { + console.log('updateTag', tag, args); + debugger; + // tag.update(tag.value); +} + +function untrack(cb) { + cb(); +} + export type AliasDecorator = ExtendedMethodDecorator & PropertyDecorator & AliasDecoratorImpl; export default function alias(altKey: string): AliasDecorator { @@ -76,24 +88,27 @@ class AliasedProperty extends ComputedDescriptor { get(obj: object, keyName: string): any { let ret: any; - let meta = metaFor(obj); + // let meta = metaFor(obj); let tagMeta = tagMetaFor(obj); let propertyTag = tagFor(obj, keyName, tagMeta) as UpdatableTag; // We don't use the tag since CPs are not automatic, we just want to avoid // anything tracking while we get the altKey + // debugger; untrack(() => { ret = get(obj, this.altKey); }); - let lastRevision = meta.revisionFor(keyName); + propertyTag.update(ret); - if (lastRevision === undefined || !validateTag(propertyTag, lastRevision)) { - updateTag(propertyTag, getChainTagsForKey(obj, this.altKey, tagMeta, meta)); - meta.setRevisionFor(keyName, valueForTag(propertyTag)); - finishLazyChains(meta, keyName, ret); - } + // let lastRevision = meta.revisionFor(keyName); + // if (lastRevision === undefined || !validateTag(propertyTag, lastRevision)) { + // updateTag(propertyTag, getChainTagsForKey(obj, this.altKey, tagMeta, meta)); + // meta.setRevisionFor(keyName, valueForTag(propertyTag)); + // finishLazyChains(meta, keyName, ret); + // } + // finishLazyChains(meta, keyName, ret); consumeTag(propertyTag); return ret; diff --git a/packages/@ember/-internals/metal/lib/decorator.ts b/packages/@ember/-internals/metal/lib/decorator.ts index 5c71ad98338..7ca68a3e67c 100644 --- a/packages/@ember/-internals/metal/lib/decorator.ts +++ b/packages/@ember/-internals/metal/lib/decorator.ts @@ -76,20 +76,14 @@ export abstract class ComputedDescriptor { abstract set(obj: object, keyName: string, value: any | null | undefined): any | null | undefined; } -let COMPUTED_GETTERS: WeakSet<() => unknown>; - -if (DEBUG) { - COMPUTED_GETTERS = new WeakSet(); -} +export let COMPUTED_GETTERS: WeakSet<() => unknown> = new WeakSet(); function DESCRIPTOR_GETTER_FUNCTION(name: string, descriptor: ComputedDescriptor): () => unknown { function getter(this: object): unknown { return descriptor.get(this, name); } - if (DEBUG) { - COMPUTED_GETTERS.add(getter); - } + COMPUTED_GETTERS.add(getter); return getter; } diff --git a/packages/@ember/-internals/metal/lib/tracked.ts b/packages/@ember/-internals/metal/lib/tracked.ts index 226b632c2e3..ff7dee3c049 100644 --- a/packages/@ember/-internals/metal/lib/tracked.ts +++ b/packages/@ember/-internals/metal/lib/tracked.ts @@ -2,13 +2,20 @@ import { meta as metaFor } from '@ember/-internals/meta'; import { isEmberArray } from '@ember/array/-internals'; import { assert } from '@ember/debug'; import { DEBUG } from '@glimmer/env'; -import { consumeTag, dirtyTagFor, tagFor, trackedData } from '@glimmer/validator'; +// import { consumeTag, dirtyTagFor, tagFor, trackedData } from '@glimmer/validator'; +import { validator } from '@lifeart/gxt/glimmer-compatibility'; + import type { ElementDescriptor } from '..'; import { CHAIN_PASS_THROUGH } from './chain-tags'; import type { ExtendedMethodDecorator, DecoratorPropertyDescriptor } from './decorator'; import { COMPUTED_SETTERS, isElementDescriptor, setClassicDecorator } from './decorator'; import { SELF_TAG } from './tags'; +const { + consumeTag, dirtyTagFor, tagFor, trackedData +} = validator; + + /** @decorator @private diff --git a/packages/@ember/-internals/package.json b/packages/@ember/-internals/package.json index cc947bbb6b5..e40c2918031 100644 --- a/packages/@ember/-internals/package.json +++ b/packages/@ember/-internals/package.json @@ -70,7 +70,8 @@ "expect-type": "^0.15.0", "internal-test-helpers": "workspace:*", "router_js": "workspace:*", - "rsvp": "^4.8.5" + "rsvp": "^4.8.5", + "@lifeart/gxt": "0.0.53" }, "devDependencies": { "@ember/reactive": "workspace:*", diff --git a/packages/@ember/object/core.ts b/packages/@ember/object/core.ts index a8ed8d112e1..07e2fd9e7a3 100644 --- a/packages/@ember/object/core.ts +++ b/packages/@ember/object/core.ts @@ -298,7 +298,7 @@ class CoreObject { } const destroyable = self; - registerDestructor(self, ensureDestroyCalled, true); + registerDestructor(self, () => ensureDestroyCalled(self), true); registerDestructor(self, () => destroyable.willDestroy()); // disable chains diff --git a/packages/@ember/object/package.json b/packages/@ember/object/package.json index d6e7c7eaeb4..379ed2cd936 100644 --- a/packages/@ember/object/package.json +++ b/packages/@ember/object/package.json @@ -33,7 +33,8 @@ "@glimmer/util": "workspace:*", "@glimmer/validator": "workspace:*", "expect-type": "^0.15.0", - "internal-test-helpers": "workspace:*" + "internal-test-helpers": "workspace:*", + "@lifeart/gxt": "0.0.53" }, "devDependencies": { "@ember/owner": "workspace:*" diff --git a/packages/@ember/routing/package.json b/packages/@ember/routing/package.json index b75af126221..4f387126265 100644 --- a/packages/@ember/routing/package.json +++ b/packages/@ember/routing/package.json @@ -37,6 +37,7 @@ "dag-map": "^2.0.2", "expect-type": "^0.15.0", "internal-test-helpers": "workspace:*", - "router_js": "workspace:*" + "router_js": "workspace:*", + "@lifeart/gxt": "0.0.53" } } diff --git a/packages/@ember/routing/router.ts b/packages/@ember/routing/router.ts index 114c398476e..dfca71fc993 100644 --- a/packages/@ember/routing/router.ts +++ b/packages/@ember/routing/router.ts @@ -673,6 +673,7 @@ class EmberRouter extends EmberObject.extend(Evented) implements Evented { // not great on multiple fronts! instance.didCreateRootView(this._toplevelView as any); } else { + // here we need to figure out how to provide atomic reactivity per outlet level this._toplevelView.setOutletState(root); } } diff --git a/packages/demo/compat/compile.ts b/packages/demo/compat/compile.ts new file mode 100644 index 00000000000..2cf4c08d3df --- /dev/null +++ b/packages/demo/compat/compile.ts @@ -0,0 +1,12 @@ +export function precompileTemplate() { + console.log('precompile template', ...arguments); + return {}; +} +export default function templateCompilation() { + console.log('templateCompilation', ...arguments); + return {}; +} +export function __registerTemplateCompiler() { + console.log('__registerTemplateCompiler', ...arguments); + return {}; +} diff --git a/packages/demo/compat/deprecate.ts b/packages/demo/compat/deprecate.ts new file mode 100644 index 00000000000..1466598a2fb --- /dev/null +++ b/packages/demo/compat/deprecate.ts @@ -0,0 +1,15 @@ +export function deprecateUntil() {} +export const DEPRECATIONS = { + DEPRECATE_TEMPLATE_ACTION: { + isRemoved: true, + }, + DEPRECATE_IMPLICIT_ROUTE_MODEL: { + isRemoved: true, + }, + DEPRECATE_IMPORT_EMBER() { + return { isRemoved: true }; + }, + DEPRECATE_COMPONENT_TEMPLATE_RESOLVING: { + isEnabled: true, + }, +}; diff --git a/packages/demo/compat/destroyable.ts b/packages/demo/compat/destroyable.ts new file mode 100644 index 00000000000..418768f0793 --- /dev/null +++ b/packages/demo/compat/destroyable.ts @@ -0,0 +1,21 @@ +import { destroyable } from '@lifeart/gxt/glimmer-compatibility'; +// console.log('destroyable', destroyable); +export const { + registerDestructor, + isDestroyed, + destroy, + destroyChildren, + associateDestroyableChild, + unregisterDestructor, + _hasDestroyableChildren, +} = destroyable; + +export function assertDestroyablesDestroyed() { + console.log('assertDestroyablesDestroyed', ...arguments); +} +export function enableDestroyableTracking() { + console.log('enableDestroyableTracking', ...arguments); +} +export function isDestroying() { + return false; +} diff --git a/packages/demo/compat/glimmer-application.ts b/packages/demo/compat/glimmer-application.ts new file mode 100644 index 00000000000..ef1e4b6e6bb --- /dev/null +++ b/packages/demo/compat/glimmer-application.ts @@ -0,0 +1,4 @@ +export { setComponentManager } from '@glimmer/manager'; +export function capabilities() { + return {}; +} diff --git a/packages/demo/compat/glimmer-util.ts b/packages/demo/compat/glimmer-util.ts new file mode 100644 index 00000000000..fde865d533a --- /dev/null +++ b/packages/demo/compat/glimmer-util.ts @@ -0,0 +1,14 @@ +export function unwrapTemplate(tpl) { + return { + asLayout(){ + return { + compile() { + // debugger; + console.log('as-layout compile', ...arguments); + return tpl; + } + }; + } + }; +}; + diff --git a/packages/demo/compat/helper-manager/index.ts b/packages/demo/compat/helper-manager/index.ts new file mode 100644 index 00000000000..fbac78d15ee --- /dev/null +++ b/packages/demo/compat/helper-manager/index.ts @@ -0,0 +1,63 @@ +function argsProxyFor(capturedArgs: any, type: string) { + return new Proxy(capturedArgs, { + get(target, prop) { + if (prop === 'named') { + return {}; + } else if (prop === 'positional') { + return target; + } else { + throw new Error(`Cannot get ${prop} from ${type} args`); + } + }, + }); +} + +export class CustomHelperManager { + factory: (owner: unknown) => any; + constructor(factory: (owner: unknown) => any) { + this.factory = factory; + } + private helperManagerDelegates = new WeakMap(); + private undefinedDelegate: any | null = null; + + private getDelegateForOwner(owner: any) { + let delegate = this.helperManagerDelegates.get(owner); + + if (delegate === undefined) { + let { factory } = this; + delegate = factory(owner); + + this.helperManagerDelegates.set(owner, delegate); + } + + return delegate; + } + + getDelegateFor(owner: any | undefined) { + if (owner === undefined) { + let { undefinedDelegate } = this; + + if (undefinedDelegate === null) { + let { factory } = this; + this.undefinedDelegate = undefinedDelegate = factory(undefined); + } + + return undefinedDelegate; + } else { + return this.getDelegateForOwner(owner); + } + } + + getHelper(definition: any): any { + return (capturedArgs, owner) => { + let manager = this.getDelegateFor(owner as any | undefined); + + const args = argsProxyFor(capturedArgs, 'helper'); + const bucket = manager.createHelper(definition, args); + + return () => { + return manager.getValue(bucket); + }; + }; + } +} diff --git a/packages/demo/compat/manager.ts b/packages/demo/compat/manager.ts new file mode 100644 index 00000000000..6abcaff510b --- /dev/null +++ b/packages/demo/compat/manager.ts @@ -0,0 +1,193 @@ +import { $_MANAGERS, $PROPS_SYMBOL, formula } from '@lifeart/gxt'; + +import { CustomHelperManager } from './helper-manager'; + +globalThis.EmberFunctionalHelpers = new Set(); +globalThis.COMPONENT_TEMPLATES = globalThis.COMPONENT_TEMPLATES || new WeakMap(); +globalThis.COMPONENT_MANAGERS = globalThis.COMPONENT_MANAGERS || new WeakMap(); +globalThis.INTERNAL_MANAGERS = globalThis.INTERNAL_MANAGERS || new WeakMap(); +globalThis.INTERNAL_HELPER_MANAGERS = globalThis.INTERNAL_HELPER_MANAGERS || new WeakMap(); +globalThis.INTERNAL_MODIFIER_MANAGERS = globalThis.INTERNAL_MODIFIER_MANAGERS || new WeakMap(); + +$_MANAGERS.component.canHandle = function (komp) { + if (globalThis.INTERNAL_MANAGERS.has(komp)) { + return true; + } else if (globalThis.COMPONENT_MANAGERS.has(komp)) { + return true; + } + if (komp.create) { + debugger; + } + return false; + // console.log('canHandle', komp); + // debugger; +}; +$_MANAGERS.helper.canHandle = function (helper: unknown) { + if (typeof helper === 'string') { + return true; + } + return false; +}; +$_MANAGERS.helper.handle = function (helper: any, params: any, hash: any) { + if (typeof helper === 'string') { + const argScope = hash['$_scope']?.() || null; + if (!argScope) { + const owner = globalThis.owner; + const maybeHelper = owner.lookup(`helper:${helper}`); + const manager = getInternalHelperManager(maybeHelper); + if (manager) { + return manager.getHelper(maybeHelper)(params, owner); + } else { + debugger; + } + } + } +}; + +function argsForInternalManager(args, fw) { + const named = {}; + Object.keys(args).forEach((arg) => { + named[arg] = formula(() => args[arg], 'argsForInternalManager'); + }); + + return { + capture() { + return { + positional: [], + named, // args + }; + }, + }; +} + +$_MANAGERS.component.handle = function (komp, args, fw, ctx) { + const manager = globalThis.INTERNAL_MANAGERS.get(komp) || globalThis.COMPONENT_MANAGERS.get(komp); + // debugger; + + const instance = manager.create( + globalThis.owner, + komp, + argsForInternalManager(args, fw), + {}, + {}, + formula(() => ctx, 'internalManager:caller') + ); + const tpl = + getComponentTemplate(instance) || + getComponentTemplate(instance.prototype) || + getComponentTemplate(komp); + // debugger; + + return () => { + args[$PROPS_SYMBOL] = fw || [[], [], []]; + return tpl.bind(instance)(args); + }; +}; +// console.log('$_MANAGERS', $_MANAGERS); + +export function capabilityFlagsFrom(capabilities) { + console.log('capabilityFlagsFrom', ...arguments); + return {}; +} + +export function setInternalComponentManager(manager: any, handle: any) { + globalThis.INTERNAL_MANAGERS.set(handle, manager); + return handle; +} + +export function getInternalHelperManager(helper: any) { + return ( + globalThis.INTERNAL_HELPER_MANAGERS.get(helper) || + globalThis.INTERNAL_HELPER_MANAGERS.get(Object.getPrototypeOf(helper)) + ); +} +export function helperCapabilities(v: string, value: any) { + return value; +} +export function modifierCapabilities() { + console.log('modifierCapabilities', ...arguments); +} +export function componentCapabilities() { + console.log('componentCapabilities', ...arguments); +} +export function setHelperManager(factory: any, helper: any) { + return setInternalHelperManager(new CustomHelperManager(factory), helper); + // console.log('setHelperManager', ...arguments); + // debugger; + // globalThis.HELPER_MANAGERS.set(helper, manager); + // return helper; +} +export function getHelperManager(helper: any) { + return getInternalHelperManager(helper); +} +export function getInternalComponentManager(handle: any) { + return globalThis.INTERNAL_MANAGERS.get(handle); +} +export function getComponentTemplate(comp: any) { + return globalThis.COMPONENT_TEMPLATES.get(comp); +} +export function setComponentTemplate(tpl: any, comp: any) { + globalThis.COMPONENT_TEMPLATES.set(comp, tpl); + return comp; +} +export function setInternalModifierManager(manager: any, modifier: any) { + globalThis.INTERNAL_MODIFIER_MANAGERS.set(modifier, manager); + return modifier; +} + +export function setComponentManager(manager: any, component: any) { + return globalThis.COMPONENT_MANAGERS.set(component, manager); +} + +export function getComponentManager(component: any) { + return globalThis.COMPONENT_MANAGERS.get(component); +} + +export function setModifierManager() { + console.log('setModifierManager', ...arguments); +} +export function getCustomTagFor(obj: any) { + console.log('getCustomTagFor', ...arguments); + return undefined; + // return function (obj, key) { + // console.log('getCustomTagFor usage', obj, key); + // return obj[key]; + // }; + // console.log('getCustomTagFor', ...arguments); +} +export function setCustomTagFor() { + console.log('setCustomTagFor', ...arguments); +} + +export function setInternalHelperManager(manager: any, helper: any) { + globalThis.INTERNAL_HELPER_MANAGERS.set(helper, manager); + return helper; +} + +export function hasInternalHelperManager(helper: any) { + return globalThis.INTERNAL_HELPER_MANAGERS.has(helper); +} + +export function hasCapability() { + console.log('hasCapability', ...arguments); +} + +export function getInternalModifierManager(modifier: any) { + return globalThis.INTERNAL_MODIFIER_MANAGERS.get(modifier); +} + +export function managerHasCapability() { + console.log('managerHasCapability', ...arguments); +} + +export function hasInternalComponentManager() { + console.log('hasInternalComponentManager', ...arguments); +} + +export function hasValue() { + console.log('hasValue', ...arguments); +} + +export function hasDestroyable() { + console.log('hasDestroyable', ...arguments); +} diff --git a/packages/demo/compat/reference.ts b/packages/demo/compat/reference.ts new file mode 100644 index 00000000000..b147c55c7a8 --- /dev/null +++ b/packages/demo/compat/reference.ts @@ -0,0 +1,59 @@ +import { cell } from '@lifeart/gxt'; +import { reference } from '@lifeart/gxt/glimmer-compatibility'; + +export const { + createComputeRef, + createConstRef, + createUnboundRef, + createPrimitiveRef, + childRefFor, + valueForRef, +} = reference; + +export function isConstRef(ref) { + console.log('isConstRef', ...arguments); + // if ('fn' in ref) { + // return true; + // } + return false; +} + +export function isUpdatableRef(ref) { + console.log('isUpdatableRef', ...arguments); + if ('fn' in ref) { + return false; + } + return true; + +} + +export function updateRef() { + console.log('updateRef', ...arguments); + +} +export function childRefFromParts() { + console.log('childRefFromParts', ...arguments); +} +export function isInvokableRef() { + console.log('isInvokableRef', ...arguments); +} +export function createInvokableRef() { + console.log('createInvokableRef', ...arguments); +} +export function createReadOnlyRef() { + console.log('createReadOnlyRef', ...arguments); +} +export function createDebugAliasRef() { + console.log('createDebugAliasRef' , ...arguments); +} +export function createIteratorRef() { + console.log('createIteratorRef', ...arguments); +} +export function createIteratorItemRef() { + console.log('createIteratorItemRef', ...arguments); +} +export const REFERENCE = Symbol("REFERENCE"); +export const FALSE_REFERENCE = cell(false, 'FALSE_REFERENCE'); +export const UNDEFINED_REFERENCE = cell(undefined, 'UNDEFINED_REFERENCE'); +export const NULL_REFERENCE = cell(null, 'NULL_REFERENCE'); +export const TRUE_REFERENCE = cell(true, 'TRUE_REFERENCE'); diff --git a/packages/demo/compat/validator.ts b/packages/demo/compat/validator.ts new file mode 100644 index 00000000000..e1619c2de4c --- /dev/null +++ b/packages/demo/compat/validator.ts @@ -0,0 +1,86 @@ +import { formula } from '@lifeart/gxt'; +import { validator, caching } from '@lifeart/gxt/glimmer-compatibility'; + +export const { dirtyTagFor, tagFor, isTracking, tagMetaFor, track, trackedData } = validator; +export const { getValue, createCache } = caching; // createCache, + +export function consumeTag(tag) { + if (!tag) { + console.log('consumeEmptyTag'); + return; + } + return validator.consumeTag(tag); +} + +export const CURRENT_TAG = formula(() => { + return Date.now() + Math.random(); +}); +export const CONSTANT_TAG = 11; +export const ALLOW_CYCLES = true; +export function combine(tags) { + if (tags.some((t => typeof t !== 'object'))) { + debugger; + } + return formula(() => { + return tags.map((t) => t.value); + }, 'combine'); +} +const validated = new WeakSet(); +export function validateTag(tag) { + if (!tag) { + debugger; + } + if ('fn' in tag) { + return true; + } + if (!validated.has(tag)) { + validated.add(tag); + return false; + } + return true; +} +export function resetTracking() { + console.log('resetTracking', ...arguments); +} +export const COMPUTE = 13; +export const INITIAL = 31; +export function valueForTag(tag) { + return Date.now() + Math.random(); +} +export function createUpdatableTag() { + console.log('createUpdatableTag'); +} +export function updateTag() { + console.log('updateTag'); +} +// TODO: untrack is breaking reactivity here +export function untrack(cb) { + // console.log('untrack', cb); + return cb(); + // console.log('untrack', cb); +} +export function isConst() { + console.log('isConst'); +} +export function beginUntrackFrame() { + console.log('beginUntrackFrame'); +} +export function endUntrackFrame() { + console.log('endUntrackFrame'); +} + +export function beginTrackFrame() { + console.log('beginTrackFrame'); +} +export function endTrackFrame() { + console.log('endTrackFrame'); +} +export function createTag() { + console.log('createTag'); +} +export function dirtyTag() { + console.log('dirtyTag'); +} +export function debug() { + console.log('debug'); +} diff --git a/packages/demo/index.html b/packages/demo/index.html new file mode 100644 index 00000000000..458e7f7f465 --- /dev/null +++ b/packages/demo/index.html @@ -0,0 +1,25 @@ + + + + + + + Vite + TS + + + +
+
+ + + diff --git a/packages/demo/package.json b/packages/demo/package.json new file mode 100644 index 00000000000..e2ca5d7ca84 --- /dev/null +++ b/packages/demo/package.json @@ -0,0 +1,58 @@ +{ + "name": "demo", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "clean": "rm -rf ./node_modules/.vite", + "start": "pnpm run clean && pnpm dev" + }, + "dependencies": { + "@ember/-internals": "workspace:*", + "@ember/application": "workspace:*", + "@ember/array": "workspace:*", + "@ember/canary-features": "workspace:*", + "@ember/component": "workspace:*", + "@ember/controller": "workspace:*", + "@ember/debug": "workspace:*", + "@ember/destroyable": "workspace:*", + "@ember/engine": "workspace:*", + "@ember/enumerable": "workspace:*", + "@ember/helper": "workspace:*", + "@ember/instrumentation": "workspace:*", + "@ember/modifier": "workspace:*", + "@ember/object": "workspace:*", + "@ember/owner": "workspace:*", + "@ember/routing": "workspace:*", + "@ember/runloop": "workspace:*", + "@ember/service": "workspace:*", + "@ember/template": "workspace:*", + "@ember/template-compilation": "workspace:*", + "@ember/template-factory": "workspace:*", + "@ember/test": "workspace:*", + "@ember/utils": "workspace:*", + "@ember/version": "workspace:*", + "@glimmer/destroyable": "0.92.0", + "@glimmer/env": "^0.1.7", + "@glimmer/manager": "0.92.0", + "@glimmer/owner": "0.92.0", + "@glimmer/runtime": "0.92.0", + "@glimmer/tracking": "workspace:*", + "@glimmer/util": "0.92.0", + "@glimmer/validator": "0.92.0", + "@lifeart/gxt": "0.0.53", + "autoprefixer": "^10.4.19", + "backburner.js": "^2.7.0", + "dag-map": "^2.0.2", + "ember": "workspace:*", + "ember-template-compiler": "workspace:*", + "ember-testing": "workspace:*", + "expect-type": "^0.15.0", + "internal-test-helpers": "workspace:*", + "postcss": "^8.4.39", + "router_js": "^8.0.5", + "rsvp": "^4.8.5", + "tailwindcss": "^3.4.4", + "vite": "^5.0.10" + } +} diff --git a/packages/demo/postcss.config.cjs b/packages/demo/postcss.config.cjs new file mode 100644 index 00000000000..12a703d900d --- /dev/null +++ b/packages/demo/postcss.config.cjs @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/packages/demo/src/components/Application.gts b/packages/demo/src/components/Application.gts new file mode 100644 index 00000000000..b2da10bd57f --- /dev/null +++ b/packages/demo/src/components/Application.gts @@ -0,0 +1,21 @@ +import { Component } from '@lifeart/gxt'; +import { LinkTo } from '@ember/routing'; + +export default class ApplicationTemplate extends Component { + +} diff --git a/packages/demo/src/components/Main.gts b/packages/demo/src/components/Main.gts new file mode 100644 index 00000000000..c4e0ef5fd6d --- /dev/null +++ b/packages/demo/src/components/Main.gts @@ -0,0 +1,20 @@ +import { Component } from '@lifeart/gxt'; +import { LinkTo } from '@ember/routing'; +import { Textarea } from '@ember/-internals/glimmer'; + +export default class MainTemplate extends Component { + +} 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}} +

+
+
+
+ +
+
+ +