@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
package/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ # @potok-web-framework/core
2
+
3
+ ## 0.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 1bd95c1: Релиз фреймворка
package/bun.lock ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "lockfileVersion": 1,
3
+ "workspaces": {
4
+ "": {
5
+ "name": "web-framework",
6
+ "devDependencies": {
7
+ "@types/bun": "latest",
8
+ },
9
+ "peerDependencies": {
10
+ "typescript": "^5",
11
+ },
12
+ },
13
+ },
14
+ "packages": {
15
+ "@types/bun": ["@types/bun@1.2.17", "", { "dependencies": { "bun-types": "1.2.17" } }, "sha512-l/BYs/JYt+cXA/0+wUhulYJB6a6p//GTPiJ7nV+QHa8iiId4HZmnu/3J/SowP5g0rTiERY2kfGKXEK5Ehltx4Q=="],
16
+
17
+ "@types/node": ["@types/node@24.0.3", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-R4I/kzCYAdRLzfiCabn9hxWfbuHS573x+r0dJMkkzThEa7pbrcDWK+9zu3e7aBOouf+rQAciqPFMnxwr0aWgKg=="],
18
+
19
+ "bun-types": ["bun-types@1.2.17", "", { "dependencies": { "@types/node": "*" } }, "sha512-ElC7ItwT3SCQwYZDYoAH+q6KT4Fxjl8DtZ6qDulUFBmXA8YB4xo+l54J9ZJN+k2pphfn9vk7kfubeSd5QfTVJQ=="],
20
+
21
+ "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
22
+
23
+ "undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
24
+ }
25
+ }
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@potok-web-framework/core",
3
+ "version": "0.1.0",
4
+ "exports": {
5
+ ".": {
6
+ "import": "./dist/index.mjs",
7
+ "types": "./dist/exports/index.d.ts"
8
+ },
9
+ "./server": {
10
+ "import": "./dist/server.mjs",
11
+ "types": "./dist/exports/server.d.ts"
12
+ },
13
+ "./client": {
14
+ "import": "./dist/client.mjs",
15
+ "types": "./dist/exports/client.d.ts"
16
+ },
17
+ "./jsx-runtime": {
18
+ "import": "./dist/jsx-runtime.mjs"
19
+ },
20
+ "./hmr": {
21
+ "import": "./dist/hmr.mjs"
22
+ }
23
+ },
24
+ "scripts": {
25
+ "build": "vite build",
26
+ "build:watch": "vite build --watch"
27
+ },
28
+ "devDependencies": {
29
+ "@types/bun": "catalog:",
30
+ "fast-deep-equal": "^3.1.3",
31
+ "fetch-to-node": "^2.1.0",
32
+ "klona": "^2.0.6",
33
+ "vite": "catalog:",
34
+ "vite-plugin-dts": "catalog:"
35
+ },
36
+ "peerDependencies": {
37
+ "typescript": "catalog:"
38
+ }
39
+ }
package/src/block.ts ADDED
@@ -0,0 +1,102 @@
1
+ import type { Effect } from './signals'
2
+ import type { LibContext, LibHtmlElementNode, LibNode, LibProps } from './types'
3
+
4
+ export abstract class LibBlock {
5
+ node: LibNode | null = null
6
+ abstract context: LibContext
7
+ abstract props: LibProps
8
+ effects: Effect[] = []
9
+ children: (LibBlock | null)[] = []
10
+
11
+ get nearestParentElement(): LibHtmlElementNode | null {
12
+ return (
13
+ (this.node as LibHtmlElementNode) ??
14
+ this.context.parentBlock?.nearestParentElement ??
15
+ null
16
+ )
17
+ }
18
+
19
+ get previousBlock(): LibBlock | null {
20
+ const parentBlock = this.context.parentBlock
21
+
22
+ if (!parentBlock) {
23
+ return null
24
+ }
25
+
26
+ const previousBlock = parentBlock.children[this.context.index - 1]
27
+
28
+ if (previousBlock) {
29
+ return previousBlock
30
+ }
31
+
32
+ if (!parentBlock.node) {
33
+ return parentBlock.previousBlock
34
+ }
35
+
36
+ return null
37
+ }
38
+
39
+ get nearestPreviousNode(): LibNode | null {
40
+ if (this.node) {
41
+ return this.node
42
+ }
43
+
44
+ const lastChild = this.children[this.children.length - 1]
45
+ if (lastChild) {
46
+ return lastChild.nearestPreviousNode
47
+ }
48
+
49
+ const previousBlock = this.previousBlock
50
+ if (previousBlock) {
51
+ return previousBlock.nearestPreviousNode
52
+ }
53
+
54
+ return null
55
+ }
56
+
57
+ get previousNode(): LibNode | null {
58
+ const previousBlock = this.previousBlock
59
+
60
+ if (previousBlock) {
61
+ return previousBlock.nearestPreviousNode
62
+ }
63
+
64
+ return null
65
+ }
66
+
67
+ addEffect(effect: Effect) {
68
+ this.effects.push(effect)
69
+ }
70
+
71
+ unmountChildren() {
72
+ this.children.forEach((childBlock) => {
73
+ childBlock?.unmount()
74
+ })
75
+ this.children = []
76
+ }
77
+
78
+ insertNode() {
79
+ if (!this.node) {
80
+ return
81
+ }
82
+
83
+ this.context.insertNode(
84
+ this.context.parentBlock?.nearestParentElement ?? null,
85
+ this.node,
86
+ this.previousNode,
87
+ )
88
+ }
89
+
90
+ unmount() {
91
+ if (this.node) {
92
+ this.context.removeNode(this.node)
93
+ }
94
+
95
+ this.unmountChildren()
96
+
97
+ this.effects.forEach((effect) => {
98
+ effect.dispose()
99
+ })
100
+ this.effects = []
101
+ }
102
+ }
@@ -0,0 +1,115 @@
1
+ import { CLIENT_ENTRY_FILE_NAME } from './constants'
2
+ import { PortalIn } from './portal'
3
+ import { renderToString } from './render-to-string'
4
+ import path from 'path'
5
+ import type { PotokElement } from './types'
6
+ import { toFetchResponse, toReqRes } from 'fetch-to-node'
7
+ import { fragment } from './fragment'
8
+ import { htmlElement } from './html-element'
9
+
10
+ type App = (request: Request) => PotokElement
11
+ type BootstrapAppOptions = {
12
+ app: App
13
+ port?: number
14
+ }
15
+
16
+ let serverStarted = false
17
+ let setApp: (app: App) => void = () => {}
18
+
19
+ export function bootstrapApp(options: BootstrapAppOptions) {
20
+ const port = options.port ?? 3000
21
+
22
+ if (!serverStarted) {
23
+ let currentApp = options.app
24
+
25
+ setApp = (app: App) => {
26
+ currentApp = app
27
+ }
28
+
29
+ Bun.serve({
30
+ async fetch(request) {
31
+ const url = new URL(request.url)
32
+ const pathname = decodeURIComponent(url.pathname)
33
+
34
+ if (process.env.NODE_ENV === 'development') {
35
+ const viteServer = __POTOK_DEV__?.vite
36
+
37
+ if (viteServer) {
38
+ const response = await new Promise<Response | undefined>(
39
+ (resolve) => {
40
+ const { req, res } = toReqRes(request)
41
+
42
+ const originalEnd = res.end.bind(res)
43
+
44
+ res.end = (...args: any[]) => {
45
+ const response = originalEnd(...args)
46
+ resolve(toFetchResponse(response))
47
+ return response
48
+ }
49
+
50
+ viteServer.middlewares(req, res, () => {
51
+ resolve(undefined)
52
+ })
53
+ },
54
+ )
55
+
56
+ if (response) {
57
+ return response
58
+ }
59
+ }
60
+ }
61
+
62
+ if (/\.[a-z0-9]+$/i.test(pathname)) {
63
+ const file = Bun.file(path.join(process.cwd(), 'dist', pathname))
64
+ if (await file.exists()) {
65
+ return new Response(file, {
66
+ headers: { 'Content-Type': file.type },
67
+ })
68
+ }
69
+ }
70
+
71
+ const html = await renderToString(
72
+ fragment({
73
+ children: [
74
+ currentApp(request),
75
+ PortalIn({
76
+ name: 'lib-scripts',
77
+ children: __POTOK_DEV__
78
+ ? fragment({
79
+ children: [
80
+ htmlElement({
81
+ tag: 'script',
82
+ type: 'module',
83
+ src: '/@vite/client',
84
+ }),
85
+ htmlElement({
86
+ tag: 'script',
87
+ type: 'module',
88
+ src: '/src/client.ts',
89
+ }),
90
+ ],
91
+ })
92
+ : htmlElement({
93
+ tag: 'script',
94
+ type: 'module',
95
+ src: `/${CLIENT_ENTRY_FILE_NAME}.js`,
96
+ }),
97
+ }),
98
+ ],
99
+ }),
100
+ )
101
+
102
+ return new Response(html, {
103
+ headers: { 'Content-Type': 'text/html; charset=utf-8' },
104
+ })
105
+ },
106
+ port,
107
+ })
108
+
109
+ serverStarted = true
110
+
111
+ console.log(`Приложение запущено на порте ${port}`)
112
+ } else {
113
+ setApp(options.app)
114
+ }
115
+ }
@@ -0,0 +1,17 @@
1
+ import { LibContextReader } from './lib-context-reader'
2
+ import { Show } from './show'
3
+ import { PotokElement, WithChildren } from './types'
4
+
5
+ export type ClientOnlyProps = WithChildren<{}>
6
+
7
+ export function ClientOnly(props: ClientOnlyProps): PotokElement {
8
+ return LibContextReader({
9
+ children: (context) =>
10
+ Show({
11
+ get when() {
12
+ return !context.isServer && !context.isHydrating
13
+ },
14
+ children: props.children,
15
+ }),
16
+ })
17
+ }
@@ -0,0 +1,27 @@
1
+ export const HTML_ELEMENT_STYLE_PX_PROPERTIES = new Set([
2
+ 'width',
3
+ 'height',
4
+ 'top',
5
+ 'left',
6
+ 'right',
7
+ 'bottom',
8
+ 'margin',
9
+ 'marginTop',
10
+ 'marginBottom',
11
+ 'marginLeft',
12
+ 'marginRight',
13
+ 'padding',
14
+ 'paddingTop',
15
+ 'paddingBottom',
16
+ 'paddingLeft',
17
+ 'paddingRight',
18
+ 'fontSize',
19
+ 'borderWidth',
20
+ 'borderRadius',
21
+ 'maxWidth',
22
+ 'minWidth',
23
+ 'maxHeight',
24
+ 'minHeight',
25
+ ])
26
+
27
+ export const CLIENT_ENTRY_FILE_NAME = 'client'
package/src/context.ts ADDED
@@ -0,0 +1,85 @@
1
+ import { LibBlock } from './block'
2
+ import type {
3
+ LibContext,
4
+ MaybeArray,
5
+ PotokElement,
6
+ WithChildren,
7
+ } from './types'
8
+ import { mergeContext, normalizeArray, normalizeChildren } from './utils'
9
+
10
+ type ContextProviderProps<Value extends Record<string, unknown>> =
11
+ WithChildren<{
12
+ value: Value
13
+ }>
14
+
15
+ class ContextProvider<Value extends Record<string, unknown>> extends LibBlock {
16
+ constructor(
17
+ readonly props: ContextProviderProps<Value>,
18
+ key: symbol,
19
+ readonly context: LibContext
20
+ ) {
21
+ super()
22
+
23
+ normalizeChildren(props.children).forEach((childCreator, index) => {
24
+ this.children.push(
25
+ childCreator(
26
+ mergeContext(context, {
27
+ parentBlock: this,
28
+ index,
29
+ contexts: {
30
+ [key]: props.value,
31
+ },
32
+ })
33
+ )
34
+ )
35
+ })
36
+ }
37
+ }
38
+
39
+ type ContextConsumerProps<Value extends Record<string, unknown>> = {
40
+ children: MaybeArray<(value: Value) => PotokElement>
41
+ }
42
+
43
+ class ContextConsumer<Value extends Record<string, unknown>> extends LibBlock {
44
+ constructor(
45
+ readonly props: ContextConsumerProps<Value>,
46
+ key: symbol,
47
+ readonly context: LibContext
48
+ ) {
49
+ super()
50
+
51
+ const value = context.contexts[key]
52
+
53
+ if (!value) {
54
+ throw new Error(`Контекст не найден`)
55
+ }
56
+
57
+ normalizeArray(props.children).forEach((childCreator, index) => {
58
+ this.children.push(
59
+ childCreator(value as Value)(
60
+ mergeContext(context, {
61
+ parentBlock: this,
62
+ index,
63
+ })
64
+ )
65
+ )
66
+ })
67
+ }
68
+ }
69
+
70
+ export function createContext<Value extends Record<string, unknown>>() {
71
+ const key = Symbol('context')
72
+
73
+ return {
74
+ provider(props: ContextProviderProps<Value>): PotokElement {
75
+ return function (context: LibContext) {
76
+ return new ContextProvider(props, key, context)
77
+ }
78
+ },
79
+ reader(props: ContextConsumerProps<Value>): PotokElement {
80
+ return function (context: LibContext) {
81
+ return new ContextConsumer(props, key, context)
82
+ }
83
+ },
84
+ }
85
+ }
@@ -0,0 +1,21 @@
1
+ import { text } from './text'
2
+ import { Child } from './types'
3
+
4
+ type DetectChildProps = {
5
+ readonly value: Child
6
+ }
7
+
8
+ export function detectChild(props: DetectChildProps): Child {
9
+ const value = props.value
10
+ const type = typeof value
11
+
12
+ if (type === 'string' || type === 'number' || type === 'boolean') {
13
+ return text({
14
+ get text() {
15
+ return props.value
16
+ },
17
+ })
18
+ }
19
+
20
+ return value
21
+ }
@@ -0,0 +1,51 @@
1
+ import { LibBlock } from './block'
2
+ import type { LibContext, PotokElement, WithChildren } from './types'
3
+ import { mergeContext, normalizeChildren } from './utils'
4
+
5
+ export type ErrorBoundaryProps = WithChildren<{
6
+ onError?: (error: Error) => void
7
+ }>
8
+
9
+ class ErrorBoundaryClass extends LibBlock {
10
+ constructor(
11
+ readonly props: ErrorBoundaryProps,
12
+ readonly context: LibContext
13
+ ) {
14
+ super()
15
+
16
+ try {
17
+ normalizeChildren(props.children).forEach((childCreator, index) => {
18
+ this.children.push(
19
+ childCreator(
20
+ mergeContext(context, {
21
+ parentBlock: this,
22
+ index,
23
+ })
24
+ )
25
+ )
26
+ })
27
+ } catch (error) {
28
+ props.onError?.(this.buildError(error))
29
+ }
30
+ }
31
+
32
+ override unmount() {
33
+ try {
34
+ super.unmount()
35
+ } catch (error) {
36
+ this.props.onError?.(this.buildError(error))
37
+ }
38
+ }
39
+
40
+ private buildError(error: unknown) {
41
+ return error instanceof Error
42
+ ? error
43
+ : new Error(typeof error === 'string' ? error : 'Unknown error')
44
+ }
45
+ }
46
+
47
+ export function ErrorBoundary(props: ErrorBoundaryProps): PotokElement {
48
+ return function (context: LibContext) {
49
+ return new ErrorBoundaryClass(props, context)
50
+ }
51
+ }
@@ -0,0 +1 @@
1
+ export { renderToDOM } from '../render-to-dom'
@@ -0,0 +1 @@
1
+ export { registerComponent } from '../hmr/register-component'
@@ -0,0 +1,21 @@
1
+ export { ClientOnly } from '../client-only'
2
+ export { ErrorBoundary } from '../error-boundary'
3
+ export { Lazy } from '../lazy'
4
+ export { LibContextReader } from '../lib-context-reader'
5
+ export { LibScripts } from '../lib-scripts'
6
+ export { Lifecycle } from '../lifecycle'
7
+ export { List } from '../list'
8
+ export { PortalIn, PortalOut } from '../portal'
9
+ export { createElementReference } from '../ref'
10
+ export { Show } from '../show'
11
+ export {
12
+ createSignal,
13
+ createEffect,
14
+ batch,
15
+ untrack,
16
+ deepTrack,
17
+ getSignal,
18
+ } from '../signals'
19
+ export { createStore } from '../store'
20
+ export type { PotokElement, WithChildren } from '../types'
21
+ export type * from '../jsx-types.ts'
@@ -0,0 +1,4 @@
1
+ export { fragment } from '../fragment'
2
+ export { htmlElement } from '../html-element'
3
+ export { text } from '../text'
4
+ export { detectChild } from '../detect-child'
@@ -0,0 +1 @@
1
+ export { bootstrapApp } from '../bootstrap-app'
@@ -0,0 +1,28 @@
1
+ import { LibBlock } from './block'
2
+ import type { LibContext, PotokElement, WithChildren } from './types'
3
+ import { mergeContext, normalizeChildren } from './utils'
4
+
5
+ type FragmentProps = WithChildren<{}>
6
+
7
+ class FragmentClass extends LibBlock {
8
+ constructor(readonly props: FragmentProps, readonly context: LibContext) {
9
+ super()
10
+
11
+ normalizeChildren(props.children).forEach((childCreator, index) => {
12
+ this.children.push(
13
+ childCreator(
14
+ mergeContext(context, {
15
+ parentBlock: this,
16
+ index,
17
+ })
18
+ )
19
+ )
20
+ })
21
+ }
22
+ }
23
+
24
+ export function fragment(props: FragmentProps): PotokElement {
25
+ return function (context: LibContext) {
26
+ return new FragmentClass(props, context)
27
+ }
28
+ }
@@ -0,0 +1,12 @@
1
+ import type { ViteDevServer } from 'vite'
2
+ import { PotokElement } from './types'
3
+
4
+ declare global {
5
+ const __POTOK_DEV__:
6
+ | {
7
+ vite: ViteDevServer
8
+ }
9
+ | undefined
10
+ }
11
+
12
+ export {}
@@ -0,0 +1,10 @@
1
+ import { CachedInstanceState } from './registered-component'
2
+ import { HMRRegistry } from './registry'
3
+
4
+ export const HMR_DEV = {
5
+ registry: new HMRRegistry(),
6
+ currentInstance: null as {
7
+ states: CachedInstanceState[]
8
+ stateIndex: number
9
+ } | null,
10
+ }