humanjs-core 1.0.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,129 @@
1
+ import { html, component, layout, createRouter, buildUrl, getParams } from '../../src/index.js';
2
+
3
+ const root = document.getElementById('app');
4
+ const session = {
5
+ loggedIn: false
6
+ };
7
+
8
+ const PillLink = component(({ path, label }) => html`
9
+ <a
10
+ href="#${path}"
11
+ style="padding: 10px 14px; border-radius: 999px; text-decoration: none; background: white; color: #0f172a; border: 1px solid #cbd5e1; font-weight: 600;"
12
+ >
13
+ ${label}
14
+ </a>
15
+ `);
16
+
17
+ const InfoCard = component(({ title, body }) => html`
18
+ <section style="padding: 18px; border: 1px solid #e2e8f0; border-radius: 20px; background: white;">
19
+ <h2 style="margin: 0 0 10px; font-size: 20px; color: #0f172a;">${title}</h2>
20
+ <div style="color: #475569; line-height: 1.6;">${body}</div>
21
+ </section>
22
+ `);
23
+
24
+ const AppLayout = layout(({ title = 'HumanJS Router', children }) => html`
25
+ <div style="min-height: 100vh; background: linear-gradient(180deg, #f8fafc 0%, #dbeafe 100%); padding: 24px;">
26
+ <div style="max-width: 920px; margin: 0 auto; background: rgba(255,255,255,0.95); border-radius: 28px; box-shadow: 0 24px 80px rgba(15, 23, 42, 0.12); overflow: hidden;">
27
+ <header style="padding: 24px 28px; background: #0f172a; color: white;">
28
+ <p style="margin: 0; font-size: 12px; letter-spacing: 0.18em; text-transform: uppercase; color: rgba(255,255,255,0.65);">Routing + Layouts</p>
29
+ <h1 style="margin: 10px 0 6px; font-size: 32px;">${title}</h1>
30
+ <p style="margin: 0; color: rgba(255,255,255,0.72);">Components, layouts, params, redirects, and protected routes.</p>
31
+ </header>
32
+ <nav style="display: flex; flex-wrap: wrap; gap: 10px; padding: 20px 28px; border-bottom: 1px solid #e2e8f0; background: #f8fafc;">
33
+ ${PillLink({ path: '/', label: 'Home' })}
34
+ ${PillLink({ path: '/user/42', label: 'User 42' })}
35
+ ${PillLink({ path: '/user/7?tab=settings', label: 'User 7 Settings' })}
36
+ ${PillLink({ path: '/dashboard', label: 'Dashboard' })}
37
+ ${PillLink({ path: '/go-home', label: 'Redirect Route' })}
38
+ ${PillLink({ path: '/missing-page', label: '404' })}
39
+ </nav>
40
+ <main style="padding: 28px; display: grid; gap: 16px;">
41
+ ${children}
42
+ </main>
43
+ </div>
44
+ </div>
45
+ `);
46
+
47
+ const router = createRouter(
48
+ {
49
+ '/': {
50
+ layout: AppLayout,
51
+ render: () => html`
52
+ ${InfoCard({ title: 'Start here', body: 'Use the links above to move around without reloading the page.' })}
53
+ ${InfoCard({ title: 'Protected route', body: 'Open Dashboard. If you are not logged in, the router redirects you to Login first.' })}
54
+ ${InfoCard({ title: 'Params and query', body: `Open <code>#${buildUrl('/user/7', { tab: 'settings' })}</code> to see route params and query params together.` })}
55
+ `
56
+ },
57
+
58
+ '/login': {
59
+ layout: ({ children }) => AppLayout({ title: 'Login', children }),
60
+ state: () => ({
61
+ next: getParams().next || '/dashboard'
62
+ }),
63
+ actions: {
64
+ login({ state }) {
65
+ session.loggedIn = true;
66
+ router.navigate(state.next, true);
67
+ }
68
+ },
69
+ render: (params, { state }) => html`
70
+ ${InfoCard({ title: 'Redirected to login', body: `The router sent you here because <code>${state.next}</code> is protected.` })}
71
+ <button data-click="login" style="padding: 16px 18px; border: none; border-radius: 18px; background: #2563eb; color: white; font-weight: 700; width: 220px;">
72
+ Login and continue
73
+ </button>
74
+ `
75
+ },
76
+
77
+ '/dashboard': {
78
+ layout: ({ children }) => AppLayout({ title: 'Dashboard', children }),
79
+ actions: {
80
+ logout() {
81
+ session.loggedIn = false;
82
+ router.navigate('/', true);
83
+ }
84
+ },
85
+ render: () => html`
86
+ ${InfoCard({ title: 'Protected page', body: 'You are inside the dashboard because the route guard allowed you through.' })}
87
+ ${InfoCard({ title: 'Current state', body: `loggedIn = <strong>${String(session.loggedIn)}</strong>` })}
88
+ <button data-click="logout" style="padding: 16px 18px; border: none; border-radius: 18px; background: #e11d48; color: white; font-weight: 700; width: 180px;">
89
+ Logout
90
+ </button>
91
+ `
92
+ },
93
+
94
+ '/user/:id': {
95
+ layout: ({ children, params }) => AppLayout({ title: `User ${params.id}`, children }),
96
+ render: (params, { query }) => html`
97
+ ${InfoCard({ title: 'Route param', body: `User id from path: <strong>${params.id}</strong>` })}
98
+ ${InfoCard({ title: 'Query params', body: `tab = <strong>${query.tab || 'overview'}</strong>` })}
99
+ ${InfoCard({ title: 'Built URL', body: `<code>#${buildUrl(`/user/${params.id}`, { tab: 'activity' })}</code>` })}
100
+ `
101
+ },
102
+
103
+ '/go-home': {
104
+ layout: ({ children }) => AppLayout({ title: 'Redirecting', children }),
105
+ render: () => html`${InfoCard({ title: 'Redirect route', body: 'You should not stay here. The router sends you back home.' })}`
106
+ },
107
+
108
+ '*': {
109
+ layout: ({ children }) => AppLayout({ title: 'Not Found', children }),
110
+ render: () => html`
111
+ ${InfoCard({ title: '404', body: 'That route does not exist.' })}
112
+ ${InfoCard({ title: 'Try this', body: 'Go back home or open one of the predefined links in the header.' })}
113
+ `
114
+ }
115
+ },
116
+ {
117
+ root,
118
+ beforeEach(to) {
119
+ if (to === '/dashboard' && !session.loggedIn) {
120
+ return buildUrl('/login', { next: '/dashboard' });
121
+ }
122
+
123
+ if (to === '/go-home') {
124
+ return '/';
125
+ }
126
+ }
127
+ }
128
+ );
129
+ window.humanRouter = router;
@@ -0,0 +1,30 @@
1
+ import { app } from '../../src/index.js';
2
+
3
+ app.simple({
4
+ state: {
5
+ count: 0,
6
+ step: 1
7
+ },
8
+ template: `
9
+ <main style="min-height: 100vh; display: grid; place-items: center; padding: 24px; background: linear-gradient(180deg, #f8fafc 0%, #e0f2fe 100%);">
10
+ <section style="width: min(100%, 560px); background: white; border-radius: 28px; padding: 28px; box-shadow: 0 24px 80px rgba(15, 23, 42, 0.12); display: grid; gap: 18px;">
11
+ <p style="margin: 0; font-size: 12px; text-transform: uppercase; letter-spacing: 0.18em; color: #64748b;">simple js app</p>
12
+ <h1 style="margin: 0; font-size: clamp(40px, 10vw, 72px); line-height: 1; color: #0f172a;">{count}</h1>
13
+ <p style="margin: 0; color: #475569;">Step: {step}</p>
14
+
15
+ <div style="display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px;">
16
+ <button @click="count -= step" style="padding: 16px; border: none; border-radius: 18px; background: #e11d48; color: white; font-weight: 700;">-</button>
17
+ <button @click="count = 0" style="padding: 16px; border: none; border-radius: 18px; background: #64748b; color: white; font-weight: 700;">reset</button>
18
+ <button @click="count += step" style="padding: 16px; border: none; border-radius: 18px; background: #2563eb; color: white; font-weight: 700;">+</button>
19
+ </div>
20
+
21
+ <div style="display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px;">
22
+ <button @click="step = 1" style="padding: 12px; border-radius: 16px; border: 1px solid #cbd5e1; background: {step === 1 ? '#0f172a' : 'white'}; color: {step === 1 ? 'white' : '#334155'};">1</button>
23
+ <button @click="step = 5" style="padding: 12px; border-radius: 16px; border: 1px solid #cbd5e1; background: {step === 5 ? '#0f172a' : 'white'}; color: {step === 5 ? 'white' : '#334155'};">5</button>
24
+ <button @click="step = 10" style="padding: 12px; border-radius: 16px; border: 1px solid #cbd5e1; background: {step === 10 ? '#0f172a' : 'white'}; color: {step === 10 ? 'white' : '#334155'};">10</button>
25
+ <button @click="step = 100" style="padding: 12px; border-radius: 16px; border: 1px solid #cbd5e1; background: {step === 100 ? '#0f172a' : 'white'}; color: {step === 100 ? 'white' : '#334155'};">100</button>
26
+ </div>
27
+ </section>
28
+ </main>
29
+ `
30
+ });
@@ -0,0 +1,378 @@
1
+ /**
2
+ * TODO APP - Complete Example
3
+ *
4
+ * Features:
5
+ * - Add, edit, delete todos
6
+ * - Mark as complete
7
+ * - Filter (all, active, completed)
8
+ * - LocalStorage persistence
9
+ * - Form validation
10
+ */
11
+
12
+ import { app, html, each, when } from '../../src/index.js';
13
+ import { local } from '../../src/plugins/storage.js';
14
+ import { createValidator, rules, getFormData, displayErrors } from '../../src/plugins/validator.js';
15
+ import { uid } from '../../src/utils/helpers.js';
16
+
17
+ // ============================================
18
+ // COMPONENTS
19
+ // ============================================
20
+
21
+ function TodoItem(todo, onToggle, onDelete, onEdit) {
22
+ return html`
23
+ <div
24
+ class="todo-item ${todo.completed ? 'completed' : ''}"
25
+ style="
26
+ display: flex;
27
+ align-items: center;
28
+ padding: 15px;
29
+ border-bottom: 1px solid #eee;
30
+ gap: 10px;
31
+ "
32
+ >
33
+ <input
34
+ type="checkbox"
35
+ ${todo.completed ? 'checked' : ''}
36
+ data-id="${todo.id}"
37
+ class="todo-checkbox"
38
+ style="width: 20px; height: 20px; cursor: pointer;"
39
+ />
40
+ <span
41
+ style="
42
+ flex: 1;
43
+ text-decoration: ${todo.completed ? 'line-through' : 'none'};
44
+ color: ${todo.completed ? '#999' : '#333'};
45
+ "
46
+ >
47
+ ${todo.text}
48
+ </span>
49
+ <button
50
+ data-id="${todo.id}"
51
+ class="edit-btn"
52
+ style="
53
+ padding: 5px 10px;
54
+ background: #4CAF50;
55
+ color: white;
56
+ border: none;
57
+ border-radius: 4px;
58
+ cursor: pointer;
59
+ "
60
+ >
61
+ Edit
62
+ </button>
63
+ <button
64
+ data-id="${todo.id}"
65
+ class="delete-btn"
66
+ style="
67
+ padding: 5px 10px;
68
+ background: #f44336;
69
+ color: white;
70
+ border: none;
71
+ border-radius: 4px;
72
+ cursor: pointer;
73
+ "
74
+ >
75
+ Delete
76
+ </button>
77
+ </div>
78
+ `;
79
+ }
80
+
81
+ function TodoStats(todos) {
82
+ const total = todos.length;
83
+ const completed = todos.filter(t => t.completed).length;
84
+ const active = total - completed;
85
+
86
+ return html`
87
+ <div style="
88
+ display: flex;
89
+ justify-content: space-around;
90
+ padding: 20px;
91
+ background: #f5f5f5;
92
+ border-radius: 8px;
93
+ margin: 20px 0;
94
+ ">
95
+ <div style="text-align: center;">
96
+ <div style="font-size: 24px; font-weight: bold; color: #2196F3;">
97
+ ${total}
98
+ </div>
99
+ <div style="color: #666;">Total</div>
100
+ </div>
101
+ <div style="text-align: center;">
102
+ <div style="font-size: 24px; font-weight: bold; color: #FF9800;">
103
+ ${active}
104
+ </div>
105
+ <div style="color: #666;">Active</div>
106
+ </div>
107
+ <div style="text-align: center;">
108
+ <div style="font-size: 24px; font-weight: bold; color: #4CAF50;">
109
+ ${completed}
110
+ </div>
111
+ <div style="color: #666;">Completed</div>
112
+ </div>
113
+ </div>
114
+ `;
115
+ }
116
+
117
+ // ============================================
118
+ // VALIDATION
119
+ // ============================================
120
+
121
+ const todoValidator = createValidator({
122
+ todoText: [
123
+ rules.required,
124
+ rules.minLength(3),
125
+ rules.maxLength(100)
126
+ ]
127
+ });
128
+
129
+ // ============================================
130
+ // MAIN APP
131
+ // ============================================
132
+
133
+ const todoApp = app.create({
134
+ state: {
135
+ todos: local.get('todos', []),
136
+ filter: 'all', // all, active, completed
137
+ editingId: null
138
+ },
139
+
140
+ render: (state) => {
141
+ // Filter todos based on current filter
142
+ const filteredTodos = state.todos.filter(todo => {
143
+ if (state.filter === 'active') return !todo.completed;
144
+ if (state.filter === 'completed') return todo.completed;
145
+ return true;
146
+ });
147
+
148
+ const element = html`
149
+ <div style="
150
+ max-width: 600px;
151
+ margin: 50px auto;
152
+ padding: 20px;
153
+ font-family: system-ui, sans-serif;
154
+ ">
155
+ <h1 style="text-align: center; color: #2196F3;">
156
+ 📝 Todo App
157
+ </h1>
158
+
159
+ ${TodoStats(state.todos)}
160
+
161
+ <!-- Add Todo Form -->
162
+ <form id="todo-form" style="margin: 20px 0;">
163
+ <div style="display: flex; gap: 10px;">
164
+ <input
165
+ type="text"
166
+ name="todoText"
167
+ placeholder="What needs to be done?"
168
+ style="
169
+ flex: 1;
170
+ padding: 12px;
171
+ border: 2px solid #ddd;
172
+ border-radius: 4px;
173
+ font-size: 16px;
174
+ "
175
+ />
176
+ <button
177
+ type="submit"
178
+ style="
179
+ padding: 12px 24px;
180
+ background: #2196F3;
181
+ color: white;
182
+ border: none;
183
+ border-radius: 4px;
184
+ cursor: pointer;
185
+ font-size: 16px;
186
+ font-weight: bold;
187
+ "
188
+ >
189
+ Add
190
+ </button>
191
+ </div>
192
+ </form>
193
+
194
+ <!-- Filter Buttons -->
195
+ <div style="
196
+ display: flex;
197
+ gap: 10px;
198
+ margin: 20px 0;
199
+ justify-content: center;
200
+ ">
201
+ <button
202
+ class="filter-btn"
203
+ data-filter="all"
204
+ style="
205
+ padding: 8px 16px;
206
+ border: 2px solid ${state.filter === 'all' ? '#2196F3' : '#ddd'};
207
+ background: ${state.filter === 'all' ? '#2196F3' : 'white'};
208
+ color: ${state.filter === 'all' ? 'white' : '#666'};
209
+ border-radius: 4px;
210
+ cursor: pointer;
211
+ "
212
+ >
213
+ All
214
+ </button>
215
+ <button
216
+ class="filter-btn"
217
+ data-filter="active"
218
+ style="
219
+ padding: 8px 16px;
220
+ border: 2px solid ${state.filter === 'active' ? '#FF9800' : '#ddd'};
221
+ background: ${state.filter === 'active' ? '#FF9800' : 'white'};
222
+ color: ${state.filter === 'active' ? 'white' : '#666'};
223
+ border-radius: 4px;
224
+ cursor: pointer;
225
+ "
226
+ >
227
+ Active
228
+ </button>
229
+ <button
230
+ class="filter-btn"
231
+ data-filter="completed"
232
+ style="
233
+ padding: 8px 16px;
234
+ border: 2px solid ${state.filter === 'completed' ? '#4CAF50' : '#ddd'};
235
+ background: ${state.filter === 'completed' ? '#4CAF50' : 'white'};
236
+ color: ${state.filter === 'completed' ? 'white' : '#666'};
237
+ border-radius: 4px;
238
+ cursor: pointer;
239
+ "
240
+ >
241
+ Completed
242
+ </button>
243
+ </div>
244
+
245
+ <!-- Todo List -->
246
+ <div style="
247
+ border: 2px solid #ddd;
248
+ border-radius: 8px;
249
+ overflow: hidden;
250
+ ">
251
+ ${when(
252
+ filteredTodos.length > 0,
253
+ () => html`
254
+ <div>
255
+ ${each(filteredTodos, (todo) => TodoItem(todo))}
256
+ </div>
257
+ `,
258
+ () => html`
259
+ <div style="
260
+ padding: 40px;
261
+ text-align: center;
262
+ color: #999;
263
+ ">
264
+ ${state.filter === 'all' ? 'No todos yet! Add one above.' : `No ${state.filter} todos.`}
265
+ </div>
266
+ `
267
+ )}
268
+ </div>
269
+
270
+ <!-- Clear Completed -->
271
+ ${when(
272
+ state.todos.some(t => t.completed),
273
+ () => html`
274
+ <button
275
+ id="clear-completed"
276
+ style="
277
+ margin-top: 20px;
278
+ padding: 10px 20px;
279
+ background: #f44336;
280
+ color: white;
281
+ border: none;
282
+ border-radius: 4px;
283
+ cursor: pointer;
284
+ width: 100%;
285
+ "
286
+ >
287
+ Clear Completed
288
+ </button>
289
+ `
290
+ )}
291
+ </div>
292
+ `;
293
+
294
+ const events = {
295
+ '#todo-form': {
296
+ submit: (e) => {
297
+ e.preventDefault();
298
+
299
+ const formData = getFormData(e.target);
300
+ const validation = todoValidator.validate(formData);
301
+
302
+ if (!validation.isValid) {
303
+ displayErrors(e.target, validation.errors);
304
+ return;
305
+ }
306
+
307
+ // Add todo
308
+ const newTodo = {
309
+ id: uid('todo'),
310
+ text: formData.todoText,
311
+ completed: false,
312
+ createdAt: Date.now()
313
+ };
314
+
315
+ state.todos = [...state.todos, newTodo];
316
+ local.set('todos', state.todos);
317
+
318
+ e.target.reset();
319
+ }
320
+ },
321
+ '.todo-checkbox': {
322
+ change: (e) => {
323
+ const id = e.target.dataset.id;
324
+ state.todos = state.todos.map(todo =>
325
+ todo.id === id ? { ...todo, completed: !todo.completed } : todo
326
+ );
327
+ local.set('todos', state.todos);
328
+ }
329
+ },
330
+ '.delete-btn': {
331
+ click: (e) => {
332
+ const id = e.target.dataset.id;
333
+ if (confirm('Delete this todo?')) {
334
+ state.todos = state.todos.filter(todo => todo.id !== id);
335
+ local.set('todos', state.todos);
336
+ }
337
+ }
338
+ },
339
+ '.edit-btn': {
340
+ click: (e) => {
341
+ const id = e.target.dataset.id;
342
+ const todo = state.todos.find(t => t.id === id);
343
+ const newText = prompt('Edit todo:', todo.text);
344
+
345
+ if (newText && newText.trim()) {
346
+ state.todos = state.todos.map(t =>
347
+ t.id === id ? { ...t, text: newText.trim() } : t
348
+ );
349
+ local.set('todos', state.todos);
350
+ }
351
+ }
352
+ },
353
+ '.filter-btn': {
354
+ click: (e) => {
355
+ state.filter = e.target.dataset.filter;
356
+ }
357
+ },
358
+ '#clear-completed': {
359
+ click: () => {
360
+ if (confirm('Clear all completed todos?')) {
361
+ state.todos = state.todos.filter(todo => !todo.completed);
362
+ local.set('todos', state.todos);
363
+ }
364
+ }
365
+ }
366
+ };
367
+
368
+ return { element, events };
369
+ },
370
+
371
+ onMount: () => {
372
+ console.log('✅ Todo App mounted!');
373
+ },
374
+
375
+ onUpdate: (state) => {
376
+ console.log('🔄 Todos updated:', state.todos.length);
377
+ }
378
+ });
File without changes
package/package.json ADDED
@@ -0,0 +1,78 @@
1
+ {
2
+ "name": "humanjs-core",
3
+ "version": "1.0.0",
4
+ "description": "A framework built for humans, not machines. Zero dependencies, zero build tools, 100% readable.",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "module": "src/index.js",
8
+ "exports": {
9
+ ".": "./src/index.js",
10
+ "./compiler": "./src/compiler/human.js",
11
+ "./package.json": "./package.json"
12
+ },
13
+ "bin": {
14
+ "humanjs": "./scripts/humanjs.js"
15
+ },
16
+ "keywords": [
17
+ "human-js",
18
+ "humanjs",
19
+ "framework",
20
+ "frontend",
21
+ "javascript",
22
+ "reactive",
23
+ "spa",
24
+ "human-first",
25
+ "simple",
26
+ "minimalist",
27
+ "zero-build",
28
+ "no-dependencies",
29
+ "vanilla-js",
30
+ "lightweight",
31
+ "web-framework",
32
+ "ui-framework",
33
+ "state-management",
34
+ "router",
35
+ "components",
36
+ "template-literals",
37
+ "human-readable"
38
+ ],
39
+ "author": {
40
+ "name": "Abderrazzak Elouazghi",
41
+ "email": "abderrazzak.elouazghi@gmail.com",
42
+ "url": "https://github.com/kaiserofthenight"
43
+ },
44
+ "license": "MIT",
45
+ "repository": {
46
+ "type": "git",
47
+ "url": "git+https://github.com/kaiserofthenight/human-js.git"
48
+ },
49
+ "bugs": {
50
+ "url": "https://github.com/kaiserofthenight/human-js/issues",
51
+ "email": "abderrazzak.elouazghi@gmail.com"
52
+ },
53
+ "homepage": "https://github.com/kaiserofthenight/human-js#readme",
54
+ "files": [
55
+ "src/",
56
+ "scripts/",
57
+ "examples/",
58
+ "README.md",
59
+ "LICENSE",
60
+ "CHANGELOG.md"
61
+ ],
62
+ "scripts": {
63
+ "start": "python -m http.server 8000 || python3 -m http.server 8000 || npx serve .",
64
+ "dev": "python -m http.server 8000 || python3 -m http.server 8000 || npx serve .",
65
+ "demo": "open index.html || start index.html || xdg-open index.html",
66
+ "build:human": "node scripts/human-compile.js",
67
+ "check": "node --check src/index.js && node --check src/compiler/human.js && node --check scripts/human-compile.js && node --check scripts/humanjs.js",
68
+ "test": "echo \"Tests coming soon! Contributions welcome.\" && exit 0",
69
+ "prepublishOnly": "npm run check && echo \"Publishing humanjs-core v1.0.0...\""
70
+ },
71
+ "engines": {
72
+ "node": ">=18.0.0"
73
+ },
74
+ "funding": {
75
+ "type": "github",
76
+ "url": "https://github.com/sponsors/kaiserofthenight"
77
+ }
78
+ }
@@ -0,0 +1,43 @@
1
+ import { readFile, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { compileHuman } from '../src/compiler/human.js';
4
+
5
+ const [, , inputPath, outputPath] = process.argv;
6
+
7
+ if (!inputPath || !outputPath) {
8
+ console.error('Usage: node scripts/human-compile.js <input.human> <output.js>');
9
+ process.exit(1);
10
+ }
11
+
12
+ const cwd = process.cwd();
13
+ const absoluteInput = path.resolve(cwd, inputPath);
14
+ const absoluteOutput = path.resolve(cwd, outputPath);
15
+ const source = await readFile(absoluteInput, 'utf8');
16
+ const appImportPath = await resolveAppImportPath(cwd, absoluteOutput);
17
+
18
+ const compiled = compileHuman(source, {
19
+ appImportPath
20
+ });
21
+
22
+ await writeFile(absoluteOutput, compiled, 'utf8');
23
+ console.log(`Compiled ${path.relative(cwd, absoluteInput)} -> ${path.relative(cwd, absoluteOutput)}`);
24
+
25
+ async function resolveAppImportPath(cwd, absoluteOutput) {
26
+ try {
27
+ const packageJson = JSON.parse(await readFile(path.resolve(cwd, 'package.json'), 'utf8'));
28
+ const localEntry = path.resolve(cwd, 'src/index.js');
29
+
30
+ if (packageJson.name === 'human-js') {
31
+ const relativeImport = path
32
+ .relative(path.dirname(absoluteOutput), localEntry)
33
+ .split(path.sep)
34
+ .join('/');
35
+
36
+ return relativeImport.startsWith('.') ? relativeImport : `./${relativeImport}`;
37
+ }
38
+ } catch {
39
+ // Fall back to package import when compiling from another project.
40
+ }
41
+
42
+ return 'human-js';
43
+ }