@potok-web-framework/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/bun.lock +25 -0
  3. package/package.json +39 -0
  4. package/src/block.ts +102 -0
  5. package/src/bootstrap-app.ts +115 -0
  6. package/src/client-only.ts +17 -0
  7. package/src/constants.ts +27 -0
  8. package/src/context.ts +85 -0
  9. package/src/detect-child.ts +21 -0
  10. package/src/error-boundary.ts +51 -0
  11. package/src/exports/client.ts +1 -0
  12. package/src/exports/hmr.ts +1 -0
  13. package/src/exports/index.ts +21 -0
  14. package/src/exports/jsx-runtime.ts +4 -0
  15. package/src/exports/server.ts +1 -0
  16. package/src/fragment.ts +28 -0
  17. package/src/global.dev.d.ts +12 -0
  18. package/src/hmr/hmr-dev.ts +10 -0
  19. package/src/hmr/register-component.ts +109 -0
  20. package/src/hmr/registered-component.ts +59 -0
  21. package/src/hmr/registry.ts +78 -0
  22. package/src/hmr/types.ts +6 -0
  23. package/src/hmr/utils.ts +20 -0
  24. package/src/html-element.ts +95 -0
  25. package/src/jsx-types.ts +13 -0
  26. package/src/lazy.ts +44 -0
  27. package/src/lib-context-reader.ts +33 -0
  28. package/src/lib-scripts.ts +8 -0
  29. package/src/lifecycle.ts +44 -0
  30. package/src/list.ts +175 -0
  31. package/src/portal.ts +101 -0
  32. package/src/prop-types.ts +1165 -0
  33. package/src/ref.ts +11 -0
  34. package/src/render-to-dom.ts +325 -0
  35. package/src/render-to-string.ts +65 -0
  36. package/src/server-node.ts +98 -0
  37. package/src/show.ts +46 -0
  38. package/src/signals.ts +323 -0
  39. package/src/store.ts +68 -0
  40. package/src/text.ts +35 -0
  41. package/src/types.ts +69 -0
  42. package/src/utils.ts +118 -0
  43. package/tests/signals.test.ts +403 -0
  44. package/tsconfig.json +17 -0
  45. package/vite.config.ts +21 -0
@@ -0,0 +1,109 @@
1
+ import { fragment } from '../fragment'
2
+ import { Lifecycle } from '../lifecycle'
3
+ import { LibContextReader } from '../lib-context-reader'
4
+ import { Show } from '../show'
5
+ import { createSignal } from '../signals'
6
+ import { Component, ComponentProps, LibContext, PotokElement } from '../types'
7
+ import { getComponentInstanceId, isWindowDefined } from './utils'
8
+ import { HMR_DEV } from './hmr-dev'
9
+
10
+ export function registerComponent<Props extends ComponentProps>(
11
+ component: (props: Props) => PotokElement,
12
+ moduleId: string,
13
+ componentId: string,
14
+ ): Component<Props> {
15
+ if (!isWindowDefined()) {
16
+ return component
17
+ }
18
+
19
+ const registeredComponent = HMR_DEV.registry.addComponent(
20
+ moduleId,
21
+ componentId,
22
+ component,
23
+ )
24
+ HMR_DEV.registry.notifyComponentUpdate(moduleId, componentId)
25
+
26
+ function renderComponent(
27
+ props: Props,
28
+ context: LibContext,
29
+ ignoreCache = false,
30
+ ) {
31
+ const instanceId = getComponentInstanceId(context)
32
+ const cachedInstance = registeredComponent.getCachedInstance(instanceId)
33
+
34
+ if (!ignoreCache && cachedInstance) {
35
+ return cachedInstance.element
36
+ }
37
+
38
+ HMR_DEV.currentInstance = {
39
+ states: cachedInstance ? cachedInstance.states : [],
40
+ stateIndex: 0,
41
+ }
42
+
43
+ const element = registeredComponent.component(props)
44
+ registeredComponent.addCachedInstance(
45
+ instanceId,
46
+ element,
47
+ HMR_DEV.currentInstance.states,
48
+ )
49
+ HMR_DEV.currentInstance = null
50
+
51
+ return element
52
+ }
53
+
54
+ return (props: Props) => {
55
+ return LibContextReader({
56
+ children(context) {
57
+ const value = createSignal({
58
+ element: renderComponent(props, context) as PotokElement | null,
59
+ })
60
+
61
+ const instanceId = getComponentInstanceId(context)
62
+
63
+ return Lifecycle({
64
+ onMounted() {
65
+ const unsubscribe = HMR_DEV.registry.subscribeToComponent(
66
+ moduleId,
67
+ componentId,
68
+ () => {
69
+ value.element = null
70
+ value.element = renderComponent(props, context, true)
71
+ },
72
+ )
73
+
74
+ registeredComponent.unmarkInstanceAsRemovable(instanceId)
75
+
76
+ return () => {
77
+ unsubscribe()
78
+
79
+ registeredComponent.markInstanceAsRemovable(instanceId)
80
+
81
+ setTimeout(() => {
82
+ const cachedInstance =
83
+ registeredComponent.getCachedInstance(instanceId)
84
+
85
+ if (cachedInstance?.isRemovable) {
86
+ registeredComponent.removeCachedInstance(instanceId)
87
+ }
88
+ })
89
+ }
90
+ },
91
+ children: [
92
+ Show({
93
+ get when() {
94
+ return value.element !== null
95
+ },
96
+ children: [
97
+ fragment({
98
+ get children() {
99
+ return [value.element!]
100
+ },
101
+ }),
102
+ ],
103
+ }),
104
+ ],
105
+ })
106
+ },
107
+ })
108
+ }
109
+ }
@@ -0,0 +1,59 @@
1
+ import { Component, PotokElement } from '../types'
2
+ import { HMRSubscriber } from './types'
3
+
4
+ export type CachedInstanceState = {
5
+ current: unknown
6
+ initial: unknown
7
+ }
8
+
9
+ type CachedInstance = {
10
+ isRemovable: boolean
11
+ element: PotokElement
12
+ states: CachedInstanceState[]
13
+ }
14
+
15
+ export class RegisteredComponent {
16
+ component: Component<any>
17
+ subscribers: Set<HMRSubscriber>
18
+ private cachedInstances: Map<string, CachedInstance>
19
+
20
+ constructor(component: Component<any>) {
21
+ this.component = component
22
+ this.subscribers = new Set()
23
+ this.cachedInstances = new Map()
24
+ }
25
+
26
+ getCachedInstance(instanceId: string): CachedInstance | undefined {
27
+ return this.cachedInstances.get(instanceId)
28
+ }
29
+
30
+ addCachedInstance(
31
+ instanceId: string,
32
+ element: PotokElement,
33
+ states: CachedInstanceState[],
34
+ ) {
35
+ this.cachedInstances.set(instanceId, {
36
+ isRemovable: false,
37
+ element,
38
+ states,
39
+ })
40
+ }
41
+
42
+ markInstanceAsRemovable(instanceId: string) {
43
+ const cachedInstance = this.cachedInstances.get(instanceId)
44
+ if (cachedInstance) {
45
+ cachedInstance.isRemovable = true
46
+ }
47
+ }
48
+
49
+ unmarkInstanceAsRemovable(instanceId: string) {
50
+ const cachedInstance = this.cachedInstances.get(instanceId)
51
+ if (cachedInstance) {
52
+ cachedInstance.isRemovable = false
53
+ }
54
+ }
55
+
56
+ removeCachedInstance(instanceId: string) {
57
+ this.cachedInstances.delete(instanceId)
58
+ }
59
+ }
@@ -0,0 +1,78 @@
1
+ import { Component } from '../types'
2
+ import { ComponentId, HMRSubscriber, ModudeId } from './types'
3
+ import { RegisteredComponent } from './registered-component'
4
+
5
+ export class HMRRegistry {
6
+ private components: Map<ModudeId, Map<ComponentId, RegisteredComponent>> =
7
+ new Map()
8
+
9
+ private getModuleComponentsMap(moduleId: ModudeId) {
10
+ if (!this.components.has(moduleId)) {
11
+ this.components.set(moduleId, new Map())
12
+ }
13
+
14
+ return this.components.get(moduleId)!
15
+ }
16
+
17
+ addComponent(
18
+ moduleId: ModudeId,
19
+ componentId: ComponentId,
20
+ component: Component<any>,
21
+ ): RegisteredComponent {
22
+ const moduleComponentsMap = this.getModuleComponentsMap(moduleId)
23
+
24
+ if (!moduleComponentsMap.has(componentId)) {
25
+ moduleComponentsMap.set(componentId, new RegisteredComponent(component))
26
+ }
27
+
28
+ const registeredComponent = moduleComponentsMap.get(componentId)!
29
+
30
+ registeredComponent.component = component
31
+
32
+ return registeredComponent
33
+ }
34
+
35
+ getRegisteredComponent(
36
+ moduleId: ModudeId,
37
+ componentId: ComponentId,
38
+ ): RegisteredComponent | undefined {
39
+ return this.getModuleComponentsMap(moduleId).get(componentId)
40
+ }
41
+
42
+ subscribeToComponent(
43
+ moduleId: ModudeId,
44
+ componentId: ComponentId,
45
+ subscriber: HMRSubscriber,
46
+ ) {
47
+ this.getRegisteredComponent(moduleId, componentId)?.subscribers.add(
48
+ subscriber,
49
+ )
50
+
51
+ return () => {
52
+ this.unsubscribeFromComponent(moduleId, componentId, subscriber)
53
+ }
54
+ }
55
+
56
+ unsubscribeFromComponent(
57
+ moduleId: ModudeId,
58
+ componentId: ComponentId,
59
+ subscriber: HMRSubscriber,
60
+ ) {
61
+ this.getRegisteredComponent(moduleId, componentId)?.subscribers.delete(
62
+ subscriber,
63
+ )
64
+ }
65
+
66
+ notifyComponentUpdate(moduleId: ModudeId, componentId: ComponentId) {
67
+ const registeredComponent = this.getRegisteredComponent(
68
+ moduleId,
69
+ componentId,
70
+ )
71
+
72
+ if (!registeredComponent) return
73
+
74
+ for (const subscriber of registeredComponent.subscribers) {
75
+ subscriber(registeredComponent)
76
+ }
77
+ }
78
+ }
@@ -0,0 +1,6 @@
1
+ import { RegisteredComponent } from './registered-component'
2
+
3
+ export type HMRSubscriber = (component: RegisteredComponent | undefined) => void
4
+
5
+ export type ModudeId = string
6
+ export type ComponentId = string
@@ -0,0 +1,20 @@
1
+ import { LibContext } from '../types'
2
+
3
+ export function isWindowDefined(): boolean {
4
+ return typeof window !== 'undefined'
5
+ }
6
+
7
+ export function getComponentInstanceId(context: LibContext): string {
8
+ const indexes: number[] = []
9
+
10
+ let currentContext: LibContext | null = context
11
+
12
+ while (currentContext) {
13
+ indexes.push(currentContext.index)
14
+ currentContext = currentContext.parentBlock
15
+ ? currentContext.parentBlock.context
16
+ : null
17
+ }
18
+
19
+ return indexes.reverse().join('_')
20
+ }
@@ -0,0 +1,95 @@
1
+ import { LibBlock } from './block'
2
+ import type { LibHTMLElementTagNameMap } from './prop-types'
3
+ import { createEffect } from './signals'
4
+ import type { LibContext, LibHtmlElementNode, PotokElement } from './types'
5
+ import {
6
+ extractListenersFromProps,
7
+ mergeContext,
8
+ normalizeChildren,
9
+ objectKeys,
10
+ } from './utils'
11
+
12
+ export type LibHTMLElementProps<Tag extends keyof LibHTMLElementTagNameMap> = {
13
+ tag: Tag
14
+ } & LibHTMLElementTagNameMap[Tag]
15
+
16
+ class LibHTMLElement<
17
+ Tag extends keyof LibHTMLElementTagNameMap
18
+ > extends LibBlock {
19
+ constructor(
20
+ readonly props: LibHTMLElementProps<Tag>,
21
+ readonly context: LibContext
22
+ ) {
23
+ super()
24
+
25
+ const element: LibHtmlElementNode<Tag> = {
26
+ type: 'html-element',
27
+ context,
28
+ props,
29
+ }
30
+
31
+ this.node = element
32
+
33
+ const descriptors = Object.getOwnPropertyDescriptors(props)
34
+
35
+ for (const [key, descriptor] of Object.entries(descriptors)) {
36
+ if (['tag', 'children', 'ref'].includes(key)) {
37
+ continue
38
+ }
39
+
40
+ if (descriptor.value) {
41
+ context.updateHtmlElementNodeProp(element, key, descriptor.value)
42
+ }
43
+
44
+ const getter = descriptor.get
45
+ if (getter) {
46
+ this.addEffect(
47
+ createEffect(() => {
48
+ context.updateHtmlElementNodeProp(element, key, getter())
49
+ }, true)
50
+ )
51
+ }
52
+ }
53
+
54
+ if (context.isHydrating) {
55
+ this.insertNode()
56
+ }
57
+
58
+ normalizeChildren(props.children).forEach((childCreator, index) => {
59
+ this.children.push(
60
+ childCreator(
61
+ mergeContext(context, {
62
+ parentBlock: this,
63
+ index,
64
+ })
65
+ )
66
+ )
67
+ })
68
+
69
+ if (!context.isHydrating) {
70
+ this.insertNode()
71
+ }
72
+ }
73
+
74
+ override unmount() {
75
+ if (this.node) {
76
+ objectKeys(extractListenersFromProps(this.props)).forEach((key) => {
77
+ this.context.listeners[key]?.delete(this.node as unknown as HTMLElement)
78
+ })
79
+ }
80
+
81
+ if (this.props.ref) {
82
+ this.props.ref.element = null
83
+ }
84
+
85
+ super.unmount()
86
+ }
87
+ }
88
+
89
+ export function htmlElement<Tag extends keyof LibHTMLElementTagNameMap>(
90
+ props: LibHTMLElementProps<Tag>
91
+ ): PotokElement {
92
+ return function (context: LibContext) {
93
+ return new LibHTMLElement(props, context)
94
+ }
95
+ }
@@ -0,0 +1,13 @@
1
+ import { LibHTMLElementTagNameMap } from './prop-types'
2
+
3
+ declare global {
4
+ namespace JSX {
5
+ interface IntrinsicElements extends LibHTMLElementTagNameMap {}
6
+
7
+ interface ElementChildrenAttribute {
8
+ children: {}
9
+ }
10
+ }
11
+ }
12
+
13
+ export {}
package/src/lazy.ts ADDED
@@ -0,0 +1,44 @@
1
+ import { fragment } from './fragment'
2
+ import { LibContextReader } from './lib-context-reader'
3
+ import { Show } from './show'
4
+ import { createSignal } from './signals'
5
+ import { PotokElement } from './types'
6
+
7
+ export type LazyProps<Module> = {
8
+ import: () => Promise<Module>
9
+ resolve: (module: Module) => PotokElement
10
+ fallback?: PotokElement
11
+ }
12
+
13
+ export function Lazy<Module>(props: LazyProps<Module>): PotokElement {
14
+ return LibContextReader({
15
+ children: (context) => {
16
+ const signal = createSignal({
17
+ block: null as PotokElement | null,
18
+ })
19
+
20
+ context.promises.push(
21
+ props.import().then((module) => {
22
+ signal.block = props.resolve(module)
23
+ }),
24
+ )
25
+
26
+ return fragment({
27
+ children: [
28
+ Show({
29
+ get when() {
30
+ return signal.block !== null
31
+ },
32
+ children: [signal.block!],
33
+ }),
34
+ Show({
35
+ get when() {
36
+ return signal.block === null
37
+ },
38
+ children: [props.fallback],
39
+ }),
40
+ ],
41
+ })
42
+ },
43
+ })
44
+ }
@@ -0,0 +1,33 @@
1
+ import { LibBlock } from './block'
2
+ import type { LibContext, MaybeArray, PotokElement } from './types'
3
+ import { mergeContext, normalizeArray } from './utils'
4
+
5
+ export type LibContextReaderProps = {
6
+ children: MaybeArray<(context: LibContext) => PotokElement>
7
+ }
8
+
9
+ class LibContextReaderClass extends LibBlock {
10
+ constructor(
11
+ readonly props: LibContextReaderProps,
12
+ readonly context: LibContext
13
+ ) {
14
+ super()
15
+
16
+ normalizeArray(props.children).forEach((childCreator, index) => {
17
+ this.children.push(
18
+ childCreator(context)(
19
+ mergeContext(context, {
20
+ parentBlock: this,
21
+ index,
22
+ })
23
+ )
24
+ )
25
+ })
26
+ }
27
+ }
28
+
29
+ export function LibContextReader(props: LibContextReaderProps): PotokElement {
30
+ return function (context: LibContext) {
31
+ return new LibContextReaderClass(props, context)
32
+ }
33
+ }
@@ -0,0 +1,8 @@
1
+ import { PortalOut } from './portal'
2
+ import { PotokElement } from './types'
3
+
4
+ export function LibScripts(): PotokElement {
5
+ return PortalOut({
6
+ name: 'lib-scripts',
7
+ })
8
+ }
@@ -0,0 +1,44 @@
1
+ import { LibBlock } from './block'
2
+ import type { LibContext, PotokElement, WithChildren } from './types'
3
+ import { mergeContext, normalizeChildren } from './utils'
4
+
5
+ type OnMoutedReturnCallback = () => void
6
+
7
+ export type LifecycleProps = WithChildren<{
8
+ onMounted?: () => OnMoutedReturnCallback | void
9
+ onUnmounted?: () => void
10
+ }>
11
+
12
+ class LifecycleClass extends LibBlock {
13
+ private onMountedReturn?: OnMoutedReturnCallback | void
14
+
15
+ constructor(readonly props: LifecycleProps, readonly context: LibContext) {
16
+ super()
17
+
18
+ normalizeChildren(props.children).forEach((childCreator, index) => {
19
+ this.children.push(
20
+ childCreator(
21
+ mergeContext(context, {
22
+ parentBlock: this,
23
+ index,
24
+ })
25
+ )
26
+ )
27
+ })
28
+
29
+ this.onMountedReturn = props.onMounted?.()
30
+ }
31
+
32
+ override unmount() {
33
+ super.unmount()
34
+
35
+ this.onMountedReturn?.()
36
+ this.props.onUnmounted?.()
37
+ }
38
+ }
39
+
40
+ export function Lifecycle(props: LifecycleProps): PotokElement {
41
+ return function (context: LibContext) {
42
+ return new LifecycleClass(props, context)
43
+ }
44
+ }
package/src/list.ts ADDED
@@ -0,0 +1,175 @@
1
+ import { LibBlock } from './block'
2
+ import {
3
+ createEffect,
4
+ createSignal,
5
+ deepTrack,
6
+ getSignal,
7
+ isSignal,
8
+ untrack,
9
+ type Signal,
10
+ } from './signals'
11
+ import type { LibContext, PotokElement } from './types'
12
+ import { mergeContext } from './utils'
13
+
14
+ export type ListProps<Item> = {
15
+ items: Item[]
16
+ render: (each: { item: Item; index: number }) => PotokElement
17
+ }
18
+
19
+ type ArrayDiff<Item> =
20
+ | {
21
+ type: 'delete'
22
+ index: number
23
+ }
24
+ | {
25
+ type: 'add'
26
+ index: number
27
+ item: Item
28
+ }
29
+ | {
30
+ type: 'replace'
31
+ index: number
32
+ oldItem: Item
33
+ newItem: Item
34
+ }
35
+
36
+ class ListClass<Item> extends LibBlock {
37
+ signals: Signal<{
38
+ item: Item
39
+ index: number
40
+ }>[] = []
41
+
42
+ constructor(readonly props: ListProps<Item>, readonly context: LibContext) {
43
+ super()
44
+
45
+ let prevItems: Item[] = []
46
+
47
+ this.addEffect(
48
+ createEffect(() => {
49
+ const items = props.items
50
+
51
+ if (isSignal(items)) {
52
+ deepTrack(items, 1)
53
+ }
54
+
55
+ untrack(() => {
56
+ const changes = this.diff(prevItems, items)
57
+
58
+ changes.forEach((change) => {
59
+ if (change.type === 'add') {
60
+ const signal = createSignal({
61
+ item: change.item,
62
+ index: change.index,
63
+ })
64
+
65
+ this.children.splice(change.index, 0, null)
66
+ this.signals.splice(change.index, 0, getSignal(signal)!)
67
+
68
+ const creator = props.render(signal)
69
+
70
+ const block = creator(
71
+ mergeContext(context, {
72
+ parentBlock: this,
73
+ get index() {
74
+ return signal.index
75
+ },
76
+ })
77
+ )
78
+
79
+ this.children[change.index] = block
80
+ } else if (change.type === 'replace') {
81
+ this.signals[change.index]!.proxy.item = change.newItem
82
+ } else if (change.type === 'delete') {
83
+ const child = this.children[change.index]
84
+ this.children.splice(change.index, 1)
85
+ this.signals.splice(change.index, 1)
86
+ child?.unmount()
87
+ }
88
+ })
89
+
90
+ this.signals.forEach((signal, index) => {
91
+ if (signal.value.index !== index) {
92
+ signal.proxy.index = index
93
+ }
94
+ })
95
+
96
+ prevItems = [...items]
97
+ })
98
+ }, true)
99
+ )
100
+ }
101
+
102
+ diff(firstArray: Item[], secondArray: Item[]): ArrayDiff<Item>[] {
103
+ const paths: number[][] = []
104
+
105
+ for (let y = 0; y <= firstArray.length; y++) {
106
+ paths.push([y])
107
+
108
+ for (let x = 1; x <= secondArray.length; x++) {
109
+ if (y === 0) {
110
+ paths[y]!.push(x)
111
+ } else {
112
+ const top = paths[y - 1]![x]! + 1
113
+ const left = paths[y]![x - 1]! + 1
114
+ const topLeft =
115
+ paths[y - 1]![x - 1]! +
116
+ (firstArray[y - 1] !== secondArray[x - 1] ? 1 : 0)
117
+
118
+ paths[y]!.push(Math.min(top, left, topLeft))
119
+ }
120
+ }
121
+ }
122
+
123
+ const diffs: ArrayDiff<Item>[] = []
124
+
125
+ let x = secondArray.length
126
+ let y = firstArray.length
127
+
128
+ while (x > 0 || y > 0) {
129
+ const top = y > 0 ? paths[y - 1]![x]! : Infinity
130
+ const left = x > 0 ? paths[y]![x - 1]! : Infinity
131
+ const topLeft = y > 0 && x > 0 ? paths[y - 1]![x - 1]! : Infinity
132
+
133
+ const min = Math.min(top, left, topLeft)
134
+
135
+ if (min === topLeft) {
136
+ if (firstArray[y - 1] !== secondArray[x - 1]) {
137
+ diffs.push({
138
+ type: 'replace',
139
+ index: y - 1,
140
+ oldItem: firstArray[y - 1]!,
141
+ newItem: secondArray[x - 1]!,
142
+ })
143
+ }
144
+ x--
145
+ y--
146
+ } else if (min === top) {
147
+ diffs.push({
148
+ type: 'delete',
149
+ index: y - 1,
150
+ })
151
+ y--
152
+ } else {
153
+ diffs.push({
154
+ type: 'add',
155
+ index: y,
156
+ item: secondArray[x - 1]!,
157
+ })
158
+ x--
159
+ }
160
+ }
161
+
162
+ return diffs
163
+ }
164
+
165
+ override unmount() {
166
+ super.unmount()
167
+ this.signals = []
168
+ }
169
+ }
170
+
171
+ export function List<Item>(props: ListProps<Item>): PotokElement {
172
+ return function (context: LibContext) {
173
+ return new ListClass(props, context)
174
+ }
175
+ }