eclipsa 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.
@@ -0,0 +1,7 @@
1
+ import type { JSX } from '../jsx/types.ts'
2
+
3
+ export type Component<T = unknown> = (props: T) => JSX.Element
4
+
5
+ export const component$ = <T = unknown>(component: Component<T>): Component<T> => {
6
+ return component
7
+ }
@@ -0,0 +1,17 @@
1
+ import { hydrate } from './renderer.ts'
2
+ import type { DevClientInfo } from './types.ts'
3
+
4
+ const getDevInfo = (): DevClientInfo => {
5
+ const elem = document.getElementById('eclipsa-devinfo')
6
+
7
+ if (!elem) {
8
+ throw new Error('devinfo element is falsy.')
9
+ }
10
+
11
+ return JSON.parse(elem.innerHTML)
12
+ }
13
+
14
+ export const initDevClient = async () => {
15
+ const Component = (await import(/* @vite-ignore */getDevInfo().filePath)).default
16
+ hydrate(Component, document.body)
17
+ }
@@ -0,0 +1,5 @@
1
+ import type { Component } from '../component.ts'
2
+
3
+ export const hydrate = (Component: Component, target: HTMLElement) => {
4
+ console.log(Component)
5
+ }
@@ -0,0 +1,3 @@
1
+ export interface DevClientInfo {
2
+ filePath: string
3
+ }
package/core/mod.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * from './component.ts'
2
+ export * from './signal.ts'
3
+ export * from './types.ts'
4
+ export * from './dev-client/mod.ts'
package/core/signal.ts ADDED
@@ -0,0 +1,20 @@
1
+ import { signal } from 'alien-signals'
2
+
3
+ interface Signal<T> {
4
+ value: T
5
+ }
6
+ interface UseSignal {
7
+ <T>(v: T): Signal<T>
8
+ <T>(v?: T | undefined): Signal<T | undefined>
9
+ }
10
+ export const useSignal: UseSignal = (value) => {
11
+ const sig = signal(value)
12
+ return {
13
+ get value() {
14
+ return sig.get()
15
+ },
16
+ set value(value) {
17
+ sig.set(value)
18
+ },
19
+ }
20
+ }
package/core/types.ts ADDED
@@ -0,0 +1,6 @@
1
+ import type { JSX } from '../jsx/jsx-runtime.ts'
2
+
3
+ export interface SSRRootProps {
4
+ children: JSX.Element[] | JSX.Element
5
+ head: JSX.Element
6
+ }
package/deno.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@xely/eclipsa",
3
+ "exports": {
4
+ ".": "./mod.ts",
5
+ "./vite": "./vite/mod.ts",
6
+ "./jsx-runtime": "./jsx/jsx-runtime.ts",
7
+ "./jsx-dev-runtime": "./jsx/jsx-dev-runtime.ts",
8
+ "./jsx": "./jsx/mod.ts"
9
+ },
10
+ "imports": {
11
+ "@babel/core": "npm:@babel/core@^7.25.8",
12
+ "@babel/plugin-syntax-jsx": "npm:@babel/plugin-syntax-jsx@^7.25.7",
13
+ "@babel/plugin-transform-react-jsx": "npm:@babel/plugin-transform-react-jsx@^7.25.7",
14
+ "@babel/traverse": "npm:@babel/traverse@^7.25.7",
15
+ "@babel/types": "npm:@babel/types@^7.25.8",
16
+ "@types/babel__core": "npm:@types/babel__core@^7.20.5",
17
+ "@types/babel__traverse": "npm:@types/babel__traverse@^7.20.6",
18
+ "alien-signals": "npm:alien-signals@^0.0.6",
19
+ "babel-plugin-jsx-dom-expressions": "npm:babel-plugin-jsx-dom-expressions@^0.39.2",
20
+ "hono": "npm:hono@^4.6.4",
21
+ "vite": "npm:vite@^6.0.0-beta.2"
22
+ }
23
+ }
@@ -0,0 +1,19 @@
1
+ import { FRAGMENT } from './shared.ts'
2
+ import type { JSX } from './types.ts'
3
+
4
+ interface Source {
5
+ fileName: string
6
+ }
7
+ export const jsxDEV = (
8
+ type: JSX.Type,
9
+ props: Record<string, unknown>,
10
+ key: string | number | symbol,
11
+ isStatic: boolean,
12
+ _source: Source,
13
+ ): JSX.Element => ({
14
+ type,
15
+ props,
16
+ key,
17
+ isStatic,
18
+ })
19
+ export const Fragment = FRAGMENT
@@ -0,0 +1 @@
1
+ export type { JSX } from './types.ts'
package/jsx/mod.ts ADDED
@@ -0,0 +1,34 @@
1
+ import type { JSX } from './jsx-runtime.ts'
2
+ import { FRAGMENT } from './shared.ts'
3
+
4
+ export const renderToString = (elem: JSX.Element): string => {
5
+ if (!elem) {
6
+ return ''
7
+ }
8
+ if (typeof elem === 'string' || typeof elem === 'boolean' || typeof elem === 'number') {
9
+ return elem.toString()
10
+ }
11
+ if (typeof elem.type === 'function') {
12
+ return renderToString(elem.type(elem.props))
13
+ }
14
+ let attrText = ''
15
+ for (const [k, v] of Object.entries(elem.props)) {
16
+ switch (k) {
17
+ case 'children':
18
+ break
19
+ default: {
20
+ attrText += `${k}="${v}"`
21
+ }
22
+ }
23
+ }
24
+ let childrenText = ''
25
+ if (Array.isArray(elem.props.children)) {
26
+ for (const child of elem.props.children) {
27
+ childrenText += renderToString(child)
28
+ }
29
+ } else {
30
+ childrenText += renderToString(elem.props.children as JSX.Element)
31
+ }
32
+ const result = elem.type === FRAGMENT ? childrenText : `<${elem.type} ${attrText}>${childrenText}</${elem.type}>`
33
+ return result
34
+ }
package/jsx/shared.ts ADDED
@@ -0,0 +1 @@
1
+ export const FRAGMENT = '__ECLIPSA_FRAGMENT'
package/jsx/types.ts ADDED
@@ -0,0 +1,23 @@
1
+ // deno-lint-ignore no-namespace
2
+ export namespace JSX {
3
+ export type Type = string | ((props: unknown) => Element)
4
+ export type Childable = Element
5
+ export type Element = {
6
+ type: Type
7
+ props: Record<string, unknown>
8
+ key?: string | number | symbol
9
+ isStatic: boolean
10
+ } | string | number | undefined | null | boolean
11
+
12
+ export interface IntrinsicAttributes {
13
+ key?: any
14
+ }
15
+
16
+ export interface IntrinsicElements {
17
+ [name: string]: any
18
+ }
19
+
20
+ export interface ElementChildrenAttribute {
21
+ children?: any
22
+ }
23
+ }
package/mod.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from './core/mod.ts'
2
+ export * from './jsx/types.ts'
package/package.json ADDED
@@ -0,0 +1,4 @@
1
+ {
2
+ "name": "eclipsa",
3
+ "version": "0.1.0"
4
+ }
@@ -0,0 +1,39 @@
1
+ // @ts-types="@types/babel__core"
2
+ import { transform} from '@babel/core'
3
+ // @ts-types="@types/babel__traverse"
4
+ import type { Visitor } from '@babel/traverse'
5
+ import SyntaxJSX from '@babel/plugin-syntax-jsx'
6
+ import { getJSXType, transformChildren, transformProps } from '../utils/jsx.ts'
7
+ import * as t from '@babel/types'
8
+
9
+ const pluginClientDevJSX = () => {
10
+ return {
11
+ inherits: SyntaxJSX.default,
12
+ visitor: {
13
+ JSXElement(path) {
14
+ const openingElement = path.node.openingElement
15
+ const type = getJSXType(openingElement)
16
+ const { props } = transformProps(openingElement)
17
+ const children = transformChildren(path.node)
18
+ props.properties.push(t.objectProperty(t.stringLiteral('children'), children))
19
+
20
+ const fn = t.arrowFunctionExpression([], t.objectExpression([
21
+ t.objectProperty(t.stringLiteral('type'), type),
22
+ t.objectProperty(t.stringLiteral('props'), props),
23
+ ]))
24
+ path.replaceWith(fn)
25
+ }
26
+ } satisfies Visitor
27
+ }
28
+ }
29
+
30
+ export const transformClientDevJSX = (input: string) => {
31
+ const resultCode = transform(input, {
32
+ plugins: [pluginClientDevJSX()],
33
+ sourceMaps: 'inline'
34
+ })?.code
35
+ if (!resultCode) {
36
+ throw new Error('Compiling JSX was failed.')
37
+ }
38
+ return resultCode
39
+ }
@@ -0,0 +1,52 @@
1
+ // @ts-types="@types/babel__core"
2
+ import { transform, types as t } from '@babel/core'
3
+ // @ts-types="@types/babel__traverse"
4
+ import type { Visitor } from '@babel/traverse'
5
+ import SyntaxJSX from '@babel/plugin-syntax-jsx'
6
+ import { getJSXType, transformChildren, transformProps } from '../utils/jsx.ts'
7
+
8
+ const pluginJSX = () => {
9
+ return {
10
+ inherits: SyntaxJSX.default,
11
+ visitor: {
12
+ Program: {
13
+ enter(path) {
14
+ const jsxDEV = t.identifier('jsxDEV')
15
+ const importDeclaration = t.importDeclaration([
16
+ t.importSpecifier(jsxDEV, jsxDEV)
17
+ ], t.stringLiteral('@xely/eclipsa/jsx-dev-runtime'))
18
+
19
+ path.unshiftContainer('body', importDeclaration)
20
+ }
21
+ },
22
+ JSXElement(path) {
23
+ const openingElement = path.node.openingElement
24
+
25
+ const type = getJSXType(openingElement)
26
+ const { props, key } = transformProps(openingElement)
27
+ const children = transformChildren(path.node)
28
+ props.properties.push(t.objectProperty(t.stringLiteral('children'), children))
29
+
30
+ const fn = t.callExpression(t.identifier('jsxDEV'), [
31
+ type,
32
+ props,
33
+ key ?? t.nullLiteral(),
34
+ t.booleanLiteral(false)
35
+ ])
36
+ path.replaceWith(fn)
37
+ },
38
+ } satisfies Visitor,
39
+ }
40
+ }
41
+
42
+ export const transformJSX = (code: string): string => {
43
+ const resultCode = transform(code, {
44
+ plugins: [pluginJSX()],
45
+ sourceMaps: 'inline'
46
+ })?.code
47
+
48
+ if (!resultCode) {
49
+ throw new Error('Compiling JSX was failed.')
50
+ }
51
+ return resultCode
52
+ }
@@ -0,0 +1,77 @@
1
+ // @ts-types="@types/babel__core"
2
+ import { types as t } from '@babel/core'
3
+
4
+ export const transformProps = (elem: t.JSXOpeningElement) => {
5
+ const propArr: (t.ObjectProperty | t.SpreadElement)[] = []
6
+ let key: t.Expression | undefined
7
+ for (const attr of elem.attributes) {
8
+ if (t.isJSXSpreadAttribute(attr)) {
9
+ propArr.push(t.spreadElement(attr.argument))
10
+ continue
11
+ }
12
+ if (t.isJSXNamespacedName(attr.name)) {
13
+ throw new Error('JSXNamespacedName is not supported.')
14
+ }
15
+ const isKey = attr.name.name === 'key'
16
+ const name = t.stringLiteral(attr.name.name)
17
+ if (attr.value === null) {
18
+ propArr.push(t.objectProperty(name, t.booleanLiteral(true)))
19
+ break
20
+ }
21
+ if (t.isStringLiteral(attr.value)) {
22
+ if (isKey) {
23
+ key = attr.value
24
+ }
25
+ propArr.push(t.objectProperty(name, attr.value))
26
+ continue
27
+ }
28
+ if (t.isJSXExpressionContainer(attr.value)) {
29
+ if (t.isJSXEmptyExpression(attr.value.expression)) {
30
+ continue
31
+ }
32
+ if (isKey) {
33
+ key = attr.value.expression
34
+ }
35
+ propArr.push(t.objectProperty(name, attr.value.expression))
36
+ continue
37
+ }
38
+ }
39
+ return {
40
+ props: t.objectExpression(propArr),
41
+ key,
42
+ }
43
+ }
44
+
45
+ const UPPER_CASE_REGEX = /[A-Z]/
46
+ export const getJSXType = (elem: t.JSXOpeningElement) => {
47
+ if (elem.name.type !== 'JSXIdentifier') {
48
+ throw new TypeError('expected JSXIdentifier')
49
+ }
50
+ const name = elem.name.name
51
+ if (UPPER_CASE_REGEX.test(name[0])) {
52
+ return t.identifier(name)
53
+ }
54
+ return t.stringLiteral(name)
55
+ }
56
+
57
+ export const transformChildren = (elem: t.JSXElement) => t.arrayExpression(
58
+ elem.children.map((child) => {
59
+ if (t.isJSXText(child)) {
60
+ const str = child.value.trim()
61
+ if (str === '') {
62
+ return null
63
+ }
64
+ return t.stringLiteral(str)
65
+ }
66
+ if (t.isJSXExpressionContainer(child)) {
67
+ if (t.isJSXEmptyExpression(child.expression)) {
68
+ return null
69
+ }
70
+ return child.expression
71
+ }
72
+ if (t.isJSXElement(child)) {
73
+ return child as unknown as t.Expression
74
+ }
75
+ return null
76
+ }).filter(Boolean)
77
+ )
@@ -0,0 +1,55 @@
1
+ import type {
2
+ IncomingMessage,
3
+ ServerResponse,
4
+ } from 'node:http'
5
+
6
+ export const incomingMessageToRequest = (
7
+ incomingMessage: IncomingMessage,
8
+ ): Request => {
9
+ const body =
10
+ (incomingMessage.method !== 'GET' && incomingMessage.method !== 'HEAD')
11
+ ? new ReadableStream<Uint8Array>({
12
+ start(controller) {
13
+ incomingMessage.on('data', (chunk) => {
14
+ controller.enqueue(new Uint8Array(chunk))
15
+ })
16
+ incomingMessage.on('end', () => {
17
+ controller.close()
18
+ })
19
+ },
20
+ })
21
+ : null
22
+ const headers = new Headers()
23
+ for (const [k, v] of Object.entries(incomingMessage.headers)) {
24
+ if (Array.isArray(v)) {
25
+ for (const value of v) {
26
+ headers.append(k, value)
27
+ }
28
+ } else if (v) {
29
+ headers.append(k, v)
30
+ }
31
+ }
32
+ return new Request(new URL(incomingMessage.url ?? '', 'http://localhost'), {
33
+ method: incomingMessage.method,
34
+ body,
35
+ headers,
36
+ })
37
+ }
38
+
39
+ export const responseForServerResponse = async (
40
+ res: Response,
41
+ serverRes: ServerResponse,
42
+ ) => {
43
+ for (const [k, v] of res.headers) {
44
+ serverRes.setHeader(k, v)
45
+ }
46
+ serverRes.statusCode = res.status
47
+ serverRes.statusMessage = res.statusText
48
+
49
+ if (res.body) {
50
+ for await (const chunk of res.body) {
51
+ serverRes.write(chunk)
52
+ }
53
+ }
54
+ serverRes.end()
55
+ }
@@ -0,0 +1,88 @@
1
+ import { Hono, type Context } from 'hono'
2
+ import type { DevEnvironment, ResolvedConfig, ViteDevServer } from 'vite'
3
+ import type { ModuleRunner } from 'vite/module-runner'
4
+ import { renderToString } from '../../jsx/mod.ts'
5
+ import type { SSRRootProps } from '../../core/types.ts'
6
+ import { Fragment } from '../../jsx/jsx-dev-runtime.ts'
7
+ import { createRoutes, type RouteEntry } from '../utils/routing.ts'
8
+ import type { DevClientInfo } from '../../core/dev-client/types.ts'
9
+
10
+ interface DevAppInit {
11
+ resolvedConfig: ResolvedConfig
12
+ devServer: ViteDevServer
13
+ runner: ModuleRunner
14
+ ssrEnv: DevEnvironment
15
+ }
16
+
17
+ const createDevApp = async (init: DevAppInit) => {
18
+ const app = new Hono()
19
+
20
+ const createHandler = (entry: RouteEntry) => async (c: Context) => {
21
+ const [
22
+ { default: Page },
23
+ { default: SSRRoot }
24
+ ] = await Promise.all([
25
+ await init.runner.import(entry.filePath),
26
+ await init.runner.import('/app/+ssr-root.tsx')
27
+ ])
28
+
29
+ const page = Page()
30
+ const parent = SSRRoot({
31
+ children: page,
32
+ head: {
33
+ type: Fragment,
34
+ isStatic: true,
35
+ props: {
36
+ children: [
37
+ {
38
+ type: 'script',
39
+ isStatic: true,
40
+ props: {
41
+ children: 'import("/@vite/client")'
42
+ }
43
+ },
44
+ {
45
+ type: 'script',
46
+ props: {
47
+ type: 'module',
48
+ src: '/app/+client.dev.tsx'
49
+ }
50
+ },
51
+ {
52
+ type: 'script',
53
+ isStatic: true,
54
+ props: {
55
+ type: 'text/eclipsa+devinfo',
56
+ id: 'eclipsa-devinfo',
57
+ children: JSON.stringify({
58
+ filePath: entry.filePath
59
+ } satisfies DevClientInfo)
60
+ }
61
+ }
62
+ ]
63
+ }
64
+ }
65
+ } satisfies SSRRootProps)
66
+
67
+ return c.html(renderToString(parent))
68
+ }
69
+
70
+ for (const entry of await createRoutes(init.resolvedConfig.root)) {
71
+ app.get(entry.honoPath, createHandler(entry))
72
+ }
73
+
74
+ return app
75
+ }
76
+ export const createDevFetch = (
77
+ init: DevAppInit,
78
+ ): (req: Request) => Promise<Response | undefined> => {
79
+ let app = createDevApp(init)
80
+
81
+ return async (req) => {
82
+ const fetched = await (await app).fetch(req)
83
+ if (fetched.status === 404) {
84
+ return
85
+ }
86
+ return fetched
87
+ }
88
+ }
package/vite/mod.ts ADDED
@@ -0,0 +1,76 @@
1
+ import {
2
+ createServerModuleRunner,
3
+ DevEnvironment,
4
+ type Plugin,
5
+ type ResolvedConfig,
6
+ } from 'vite'
7
+ import { createDevFetch } from './dev-app/mod.ts'
8
+ import {
9
+ incomingMessageToRequest,
10
+ responseForServerResponse,
11
+ } from '../utils/node-connect.ts'
12
+ import { transformJSX } from '../transformers/dev-ssr/mod.ts'
13
+ import { transformClientDevJSX } from '../transformers/dev-client/mod.ts'
14
+
15
+ export const eclipsa = (): Plugin => {
16
+ let config: ResolvedConfig
17
+ return {
18
+ name: 'vite-plugin-eclipsa',
19
+ config() {
20
+ return {
21
+ esbuild: {
22
+ jsxFactory: 'jsx',
23
+ jsxImportSource: '@xely/eclipsa',
24
+ jsx: 'preserve',
25
+ },
26
+ //environments: {
27
+ /* ssr: {
28
+ dev: {
29
+ createEnvironment(name, config, _context) {
30
+ return new DevEnvironment(name, config, {
31
+ hot: false,
32
+ })
33
+ },
34
+ },
35
+ },*/
36
+ // },
37
+ }
38
+ },
39
+ configResolved(resolvedConfig) {
40
+ config = resolvedConfig
41
+ },
42
+ configureServer(server) {
43
+ const ssrEnv = server.environments.ssr
44
+ const runner = createServerModuleRunner(ssrEnv, {
45
+ hmr: false,
46
+ })
47
+ const devFetch = createDevFetch({
48
+ resolvedConfig: config,
49
+ devServer: server,
50
+ runner,
51
+ ssrEnv,
52
+ })
53
+ server.middlewares.use(async (req, res, next) => {
54
+ const webReq = incomingMessageToRequest(req)
55
+ const webRes = await devFetch(webReq)
56
+ if (webRes) {
57
+ responseForServerResponse(webRes, res)
58
+ return
59
+ }
60
+ next()
61
+ })
62
+ },
63
+ hotUpdate(options) {
64
+ options.server.hot.send({ type: 'full-reload' })
65
+ },
66
+ transform(code, id) {
67
+ if (id.endsWith('.tsx')) {
68
+ const result = (this.environment.name === 'ssr' ? transformJSX : transformClientDevJSX)(code)
69
+ return {
70
+ code: result
71
+ }
72
+ }
73
+ return
74
+ },
75
+ }
76
+ }
@@ -0,0 +1,26 @@
1
+ import fg from 'fast-glob'
2
+ import path from 'node:path'
3
+
4
+ // WIP
5
+ const filePathToHonoPath = (filePath: string) => {
6
+ const segments = filePath.split('/').slice(0, -1)
7
+
8
+ return segments.join('/') || '/'
9
+ }
10
+
11
+ export interface RouteEntry {
12
+ filePath: string
13
+ honoPath: string
14
+ }
15
+ export const createRoutes = async (root: string): Promise<RouteEntry[]> => {
16
+ const appDir = path.join(root, 'app')
17
+ const result = []
18
+ for await (const entry of fg.stream(path.join(root, '/**/+page.tsx'))) {
19
+ const relativePath = path.relative(appDir, entry.toString())
20
+ result.push({
21
+ filePath: entry.toString(),
22
+ honoPath: filePathToHonoPath(relativePath)
23
+ })
24
+ }
25
+ return result
26
+ }