Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: Deploy to GitHub Pages

on:
push:
branches:
- easy
workflow_dispatch:

permissions:
contents: read
pages: write
id-token: write

concurrency:
group: "pages"
cancel-in-progress: true

jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: latest

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "pnpm"

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Build
run: pnpm run build

- name: Create 404.html for SPA routing
run: |
cp ./dist/index.html ./dist/404.html

- name: Setup Pages
uses: actions/configure-pages@v4

- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: "./dist"

- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
26 changes: 26 additions & 0 deletions 404.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>상품 쇼핑몰</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="/src/styles.css">
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: "#3b82f6",
secondary: "#6b7280"
}
}
}
};
</script>
</head>
<body class="bg-gray-50">
<div id="root"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
129 changes: 127 additions & 2 deletions src/lib/createElement.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,130 @@
import { addEvent } from "./eventManager";

export function createElement(vNode) {}
const isEventProp = (prop) => /^on[A-Z]/.test(prop);

function updateAttributes($el, props) {}
const setStyle = (element, style = {}) => {
Object.entries(style || {}).forEach(([key, value]) => {
if (value == null) return;
element.style[key] = value;
});
};

const BOOLEAN_NO_ATTR = new Set(["checked", "selected"]);

const setAttribute = (element, name, value) => {
if (name === "className") {
element.setAttribute("class", value ?? "");
return;
}

if (name === "htmlFor") {
element.setAttribute("for", value ?? "");
return;
}

if (name === "style" && value && typeof value === "object") {
setStyle(element, value);
return;
}

if (typeof value === "boolean") {
element[name] = value;
if (BOOLEAN_NO_ATTR.has(name)) {
if (!value) {
element.removeAttribute(name);
} else {
element.removeAttribute(name);
}
return;
}
if (value) {
element.setAttribute(name, "");
} else {
element.removeAttribute(name);
}
return;
}

if (value == null) {
element.removeAttribute(name);
return;
}

if (name in element) {
try {
element[name] = value;
return;
} catch {
// fallthrough to setAttribute
}
}

element.setAttribute(name, value);
};

const applyProps = (element, props = {}) => {
if (!props) return;

Object.entries(props).forEach(([key, value]) => {
if (key === "children" || key === "key") {
return;
}

if (isEventProp(key) && typeof value === "function") {
const eventName = key.slice(2).toLowerCase();
addEvent(element, eventName, value);
return;
}

setAttribute(element, key, value);
});
};

const createFragment = (children) => {
const fragment = document.createDocumentFragment();
children.flat().forEach((child) => {
const node = createElement(child);
if (node) fragment.appendChild(node);
});
return fragment;
};

export function createElement(node) {
if (Array.isArray(node)) {
return createFragment(node);
}

if (node == null || typeof node === "boolean") {
return document.createTextNode("");
}

if (typeof node === "string" || typeof node === "number") {
return document.createTextNode(String(node));
}

if (typeof node !== "object") {
return document.createTextNode("");
}

const { type, props = null, children = [] } = node;

if (typeof type === "function") {
throw new Error("Functional components must be normalized before rendering.");
}

if (typeof type !== "string") {
return document.createTextNode("");
}

const element = document.createElement(type);
applyProps(element, props);

(children || []).forEach((child) => {
const childNode = createElement(child);
if (childNode) {
element.appendChild(childNode);
}
});

return element;
}
20 changes: 18 additions & 2 deletions src/lib/createVNode.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
export function createVNode(type, props, ...children) {
return {};
export function createVNode(type, originProps = null, ...rawChildren) {
const props = originProps == null ? null : { ...originProps };
const children = rawChildren
.flat(Infinity)
.reduce((acc, child) => {
if (child == null || typeof child === "boolean") {
return acc;
}

acc.push(child);
return acc;
}, []);

return {
type,
props,
children,
};
}
127 changes: 124 additions & 3 deletions src/lib/eventManager.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,126 @@
export function setupEventListeners(root) {}
const elementEvents = new WeakMap();
const rootContainers = new Set();
const rootListeners = new WeakMap();
const eventTypeCounts = new Map();

export function addEvent(element, eventType, handler) {}
const delegateEvent = (event, root, eventType) => {
let current = event.target;

export function removeEvent(element, eventType, handler) {}
while (current && root.contains(current)) {
const eventsMap = elementEvents.get(current);
if (eventsMap) {
const handlers = eventsMap.get(eventType);
if (handlers) {
handlers.forEach((handler) => handler.call(current, event));
}
}

if (current === root || event.cancelBubble) {
break;
}
current = current.parentNode;
}
};

const attachToRoot = (root, eventType) => {
let listeners = rootListeners.get(root);
if (!listeners) {
listeners = new Map();
rootListeners.set(root, listeners);
}
if (listeners.has(eventType)) return;

const listener = (event) => delegateEvent(event, root, eventType);
root.addEventListener(eventType, listener);
listeners.set(eventType, listener);
};

const detachFromRoot = (root, eventType) => {
const listeners = rootListeners.get(root);
if (!listeners) return;

const listener = listeners.get(eventType);
if (!listener) return;

root.removeEventListener(eventType, listener);
listeners.delete(eventType);
};

const incrementEventType = (eventType) => {
const prev = eventTypeCounts.get(eventType) ?? 0;
eventTypeCounts.set(eventType, prev + 1);
if (prev === 0) {
rootContainers.forEach((root) => attachToRoot(root, eventType));
}
};

const decrementEventType = (eventType) => {
const prev = eventTypeCounts.get(eventType);
if (prev == null) return;

const next = prev - 1;
if (next <= 0) {
eventTypeCounts.delete(eventType);
rootContainers.forEach((root) => detachFromRoot(root, eventType));
} else {
eventTypeCounts.set(eventType, next);
}
};

export function setupEventListeners(root) {
if (!root) return;
if (!rootListeners.has(root)) {
rootListeners.set(root, new Map());
}
rootContainers.add(root);

for (const eventType of eventTypeCounts.keys()) {
attachToRoot(root, eventType);
}
}

export function addEvent(element, eventType, handler) {
if (!element || typeof handler !== "function" || !eventType) return;

let eventsMap = elementEvents.get(element);
if (!eventsMap) {
eventsMap = new Map();
elementEvents.set(element, eventsMap);
}

let handlers = eventsMap.get(eventType);
if (!handlers) {
handlers = new Set();
eventsMap.set(eventType, handlers);
}

if (handlers.has(handler)) {
return;
}

handlers.add(handler);
incrementEventType(eventType);
}

export function removeEvent(element, eventType, handler) {
if (!element || !eventType || typeof handler !== "function") return;

const eventsMap = elementEvents.get(element);
if (!eventsMap) return;

const handlers = eventsMap.get(eventType);
if (!handlers) return;

const removed = handlers.delete(handler);
if (!removed) return;

if (handlers.size === 0) {
eventsMap.delete(eventType);
}

if (eventsMap.size === 0) {
elementEvents.delete(element);
}

decrementEventType(eventType);
}
Loading