create-kai 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.
- package/README.md +311 -0
- package/dist/index.js +4433 -0
- package/dist/templates/angular/_gitignore +7 -0
- package/dist/templates/angular/angular.json +53 -0
- package/dist/templates/angular/package.json +30 -0
- package/dist/templates/angular/src/app/app.config.ts +11 -0
- package/dist/templates/angular/src/app/app.css +36 -0
- package/dist/templates/angular/src/app/app.html +51 -0
- package/dist/templates/angular/src/app/app.ts +78 -0
- package/dist/templates/angular/src/app/components/composer/composer.css +31 -0
- package/dist/templates/angular/src/app/components/composer/composer.html +18 -0
- package/dist/templates/angular/src/app/components/composer/composer.ts +81 -0
- package/dist/templates/angular/src/app/components/icons/moon-icon/moon-icon.css +3 -0
- package/dist/templates/angular/src/app/components/icons/moon-icon/moon-icon.html +12 -0
- package/dist/templates/angular/src/app/components/icons/moon-icon/moon-icon.ts +15 -0
- package/dist/templates/angular/src/app/components/icons/sun-icon/sun-icon.css +3 -0
- package/dist/templates/angular/src/app/components/icons/sun-icon/sun-icon.html +14 -0
- package/dist/templates/angular/src/app/components/icons/sun-icon/sun-icon.ts +13 -0
- package/dist/templates/angular/src/app/components/sidebar/sidebar.css +24 -0
- package/dist/templates/angular/src/app/components/sidebar/sidebar.html +12 -0
- package/dist/templates/angular/src/app/components/sidebar/sidebar.ts +43 -0
- package/dist/templates/angular/src/app/components/theme-toggle/theme-toggle.html +13 -0
- package/dist/templates/angular/src/app/components/theme-toggle/theme-toggle.ts +21 -0
- package/dist/templates/angular/src/app/components/thread-view/thread-view.css +6 -0
- package/dist/templates/angular/src/app/components/thread-view/thread-view.html +6 -0
- package/dist/templates/angular/src/app/components/thread-view/thread-view.ts +45 -0
- package/dist/templates/angular/src/app/state/chat.store.ts +47 -0
- package/dist/templates/angular/src/app/state/conversations.store.ts +42 -0
- package/dist/templates/angular/src/app/state/voice-input.ts +57 -0
- package/dist/templates/angular/src/app/types.ts +2 -0
- package/dist/templates/angular/src/chat-data.ts +106 -0
- package/dist/templates/angular/src/index.html +16 -0
- package/dist/templates/angular/src/main.ts +16 -0
- package/dist/templates/angular/src/styles.css +10 -0
- package/dist/templates/angular/tsconfig.app.json +9 -0
- package/dist/templates/angular/tsconfig.json +22 -0
- package/dist/templates/nextjs/_gitignore +11 -0
- package/dist/templates/nextjs/app/chat-data.ts +125 -0
- package/dist/templates/nextjs/app/components/Composer.tsx +88 -0
- package/dist/templates/nextjs/app/components/HydrationBadge.tsx +68 -0
- package/dist/templates/nextjs/app/components/Sidebar.tsx +48 -0
- package/dist/templates/nextjs/app/components/ThemeToggle.tsx +27 -0
- package/dist/templates/nextjs/app/components/ThreadView.tsx +48 -0
- package/dist/templates/nextjs/app/components/icons/MoonIcon.tsx +25 -0
- package/dist/templates/nextjs/app/components/icons/SunIcon.tsx +25 -0
- package/dist/templates/nextjs/app/components/icons/index.ts +2 -0
- package/dist/templates/nextjs/app/globals.css +153 -0
- package/dist/templates/nextjs/app/hooks/index.ts +1 -0
- package/dist/templates/nextjs/app/hooks/useConversations.ts +42 -0
- package/dist/templates/nextjs/app/layout.tsx +50 -0
- package/dist/templates/nextjs/app/page.tsx +17 -0
- package/dist/templates/nextjs/app/theme.ts +10 -0
- package/dist/templates/nextjs/app/workspace.tsx +148 -0
- package/dist/templates/nextjs/next.config.mjs +22 -0
- package/dist/templates/nextjs/package.json +23 -0
- package/dist/templates/nextjs/postcss.config.mjs +16 -0
- package/dist/templates/nextjs/tsconfig.json +23 -0
- package/dist/templates/react/_gitignore +6 -0
- package/dist/templates/react/index.html +16 -0
- package/dist/templates/react/package.json +25 -0
- package/dist/templates/react/src/App.tsx +108 -0
- package/dist/templates/react/src/chat-data.ts +106 -0
- package/dist/templates/react/src/components/Composer.tsx +87 -0
- package/dist/templates/react/src/components/Sidebar.tsx +38 -0
- package/dist/templates/react/src/components/ThemeToggle.tsx +22 -0
- package/dist/templates/react/src/components/ThreadView.tsx +40 -0
- package/dist/templates/react/src/components/icons/MoonIcon.tsx +25 -0
- package/dist/templates/react/src/components/icons/SunIcon.tsx +25 -0
- package/dist/templates/react/src/components/icons/index.ts +2 -0
- package/dist/templates/react/src/hooks/index.ts +1 -0
- package/dist/templates/react/src/hooks/useConversations.ts +37 -0
- package/dist/templates/react/src/index.css +111 -0
- package/dist/templates/react/src/main.tsx +15 -0
- package/dist/templates/react/src/vite-env.d.ts +1 -0
- package/dist/templates/react/tsconfig.app.json +28 -0
- package/dist/templates/react/tsconfig.json +7 -0
- package/dist/templates/react/tsconfig.node.json +25 -0
- package/dist/templates/react/vite.config.ts +11 -0
- package/dist/templates/solid/_gitignore +6 -0
- package/dist/templates/solid/index.html +13 -0
- package/dist/templates/solid/package.json +23 -0
- package/dist/templates/solid/src/App.tsx +151 -0
- package/dist/templates/solid/src/chat-data.ts +234 -0
- package/dist/templates/solid/src/components/Composer.tsx +90 -0
- package/dist/templates/solid/src/components/Sidebar.tsx +36 -0
- package/dist/templates/solid/src/components/ThemeToggle.tsx +25 -0
- package/dist/templates/solid/src/components/ThreadView.tsx +122 -0
- package/dist/templates/solid/src/components/icons/index.tsx +84 -0
- package/dist/templates/solid/src/index.tsx +5 -0
- package/dist/templates/solid/src/lib/chat.ts +64 -0
- package/dist/templates/solid/src/lib/conversations.ts +54 -0
- package/dist/templates/solid/src/lib/types.ts +2 -0
- package/dist/templates/solid/src/styles.css +11 -0
- package/dist/templates/solid/tsconfig.json +18 -0
- package/dist/templates/solid/vite.config.ts +10 -0
- package/dist/templates/svelte/_gitignore +6 -0
- package/dist/templates/svelte/index.html +16 -0
- package/dist/templates/svelte/package.json +23 -0
- package/dist/templates/svelte/src/App.svelte +116 -0
- package/dist/templates/svelte/src/app.d.ts +20 -0
- package/dist/templates/svelte/src/chat-data.ts +106 -0
- package/dist/templates/svelte/src/components/Composer.svelte +102 -0
- package/dist/templates/svelte/src/components/Sidebar.svelte +61 -0
- package/dist/templates/svelte/src/components/ThemeToggle.svelte +45 -0
- package/dist/templates/svelte/src/components/ThreadView.svelte +49 -0
- package/dist/templates/svelte/src/components/icons/MoonIcon.svelte +24 -0
- package/dist/templates/svelte/src/components/icons/SunIcon.svelte +24 -0
- package/dist/templates/svelte/src/index.css +111 -0
- package/dist/templates/svelte/src/lib/chat.svelte.ts +52 -0
- package/dist/templates/svelte/src/lib/conversations.svelte.ts +46 -0
- package/dist/templates/svelte/src/lib/types.ts +2 -0
- package/dist/templates/svelte/src/lib/voiceInput.ts +57 -0
- package/dist/templates/svelte/src/main.ts +18 -0
- package/dist/templates/svelte/svelte.config.js +8 -0
- package/dist/templates/svelte/tsconfig.app.json +28 -0
- package/dist/templates/svelte/tsconfig.json +7 -0
- package/dist/templates/svelte/tsconfig.node.json +25 -0
- package/dist/templates/svelte/vite.config.ts +15 -0
- package/dist/templates/tanstack-start/_gitignore +14 -0
- package/dist/templates/tanstack-start/package.json +28 -0
- package/dist/templates/tanstack-start/serve.mjs +129 -0
- package/dist/templates/tanstack-start/serve.traversal.test.mjs +445 -0
- package/dist/templates/tanstack-start/src/chat-data.ts +124 -0
- package/dist/templates/tanstack-start/src/components/Composer.tsx +87 -0
- package/dist/templates/tanstack-start/src/components/HydrationBadge.tsx +63 -0
- package/dist/templates/tanstack-start/src/components/Sidebar.tsx +43 -0
- package/dist/templates/tanstack-start/src/components/ThemeToggle.tsx +26 -0
- package/dist/templates/tanstack-start/src/components/ThreadView.tsx +48 -0
- package/dist/templates/tanstack-start/src/components/icons/MoonIcon.tsx +25 -0
- package/dist/templates/tanstack-start/src/components/icons/SunIcon.tsx +25 -0
- package/dist/templates/tanstack-start/src/components/icons/index.ts +2 -0
- package/dist/templates/tanstack-start/src/hooks/index.ts +1 -0
- package/dist/templates/tanstack-start/src/hooks/useConversations.ts +42 -0
- package/dist/templates/tanstack-start/src/router.tsx +16 -0
- package/dist/templates/tanstack-start/src/routes/__root.tsx +62 -0
- package/dist/templates/tanstack-start/src/routes/index.tsx +135 -0
- package/dist/templates/tanstack-start/src/styles.css +153 -0
- package/dist/templates/tanstack-start/src/theme.ts +10 -0
- package/dist/templates/tanstack-start/tsconfig.json +16 -0
- package/dist/templates/tanstack-start/vite.config.ts +28 -0
- package/dist/templates/vanilla/_gitignore +6 -0
- package/dist/templates/vanilla/index.html +16 -0
- package/dist/templates/vanilla/package.json +19 -0
- package/dist/templates/vanilla/src/chat-data.ts +106 -0
- package/dist/templates/vanilla/src/index.css +111 -0
- package/dist/templates/vanilla/src/main.ts +53 -0
- package/dist/templates/vanilla/src/state.ts +119 -0
- package/dist/templates/vanilla/src/view.ts +222 -0
- package/dist/templates/vanilla/src/vite-env.d.ts +1 -0
- package/dist/templates/vanilla/src/voice-input.ts +55 -0
- package/dist/templates/vanilla/tsconfig.json +24 -0
- package/dist/templates/vanilla/vite.config.ts +14 -0
- package/dist/templates/vue/_gitignore +6 -0
- package/dist/templates/vue/index.html +16 -0
- package/dist/templates/vue/package.json +23 -0
- package/dist/templates/vue/src/App.vue +104 -0
- package/dist/templates/vue/src/chat-data.ts +106 -0
- package/dist/templates/vue/src/components/Composer.vue +96 -0
- package/dist/templates/vue/src/components/Sidebar.vue +48 -0
- package/dist/templates/vue/src/components/ThemeToggle.vue +27 -0
- package/dist/templates/vue/src/components/ThreadView.vue +50 -0
- package/dist/templates/vue/src/components/icons/MoonIcon.vue +24 -0
- package/dist/templates/vue/src/components/icons/SunIcon.vue +24 -0
- package/dist/templates/vue/src/composables/index.ts +3 -0
- package/dist/templates/vue/src/composables/useChat.ts +47 -0
- package/dist/templates/vue/src/composables/useConversations.ts +42 -0
- package/dist/templates/vue/src/composables/useVoiceInput.ts +57 -0
- package/dist/templates/vue/src/index.css +111 -0
- package/dist/templates/vue/src/main.ts +18 -0
- package/dist/templates/vue/src/types.ts +4 -0
- package/dist/templates/vue/src/vite-env.d.ts +1 -0
- package/dist/templates/vue/tsconfig.app.json +29 -0
- package/dist/templates/vue/tsconfig.json +7 -0
- package/dist/templates/vue/tsconfig.node.json +25 -0
- package/dist/templates/vue/vite.config.ts +24 -0
- package/package.json +63 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { ChatMessage } from '@kitn.ai/ui';
|
|
2
|
+
import {
|
|
3
|
+
appendMessage,
|
|
4
|
+
createAssistantStream,
|
|
5
|
+
onStreamSettled,
|
|
6
|
+
type SetMessages,
|
|
7
|
+
type AssistantStream,
|
|
8
|
+
} from '@kitn.ai/ui/state';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Svelte port of the kit's `useKaiChat` (a React hook), built on the SAME
|
|
12
|
+
* framework-neutral state core (`@kitn.ai/ui/state`) the React hook uses. It owns
|
|
13
|
+
* the message array + the `loading` flag and exposes ergonomic ops. Swap
|
|
14
|
+
* `mockResponse(text)` for a real `fetch` to ship a real app.
|
|
15
|
+
*
|
|
16
|
+
* `messages` is a `$state` rune (this file is `.svelte.ts` so runes work): every op
|
|
17
|
+
* assigns a BRAND-NEW array reference (`messages = updater(prev)`), which is exactly
|
|
18
|
+
* what re-renders `<kai-thread>` — mutating the array in place does not. Consumers
|
|
19
|
+
* read `chat.messages` / `chat.loading` through the getters so Svelte tracks them
|
|
20
|
+
* across the module boundary; destructuring would break reactivity.
|
|
21
|
+
*/
|
|
22
|
+
export interface ChatController {
|
|
23
|
+
readonly messages: ChatMessage[];
|
|
24
|
+
readonly loading: boolean;
|
|
25
|
+
append: (msg: ChatMessage) => void;
|
|
26
|
+
setMessages: (next: ChatMessage[]) => void;
|
|
27
|
+
streamAssistant: (init?: Partial<ChatMessage>) => AssistantStream;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function createChat(initialMessages: ChatMessage[] = []): ChatController {
|
|
31
|
+
let messages = $state<ChatMessage[]>([...initialMessages]);
|
|
32
|
+
let loading = $state(false);
|
|
33
|
+
|
|
34
|
+
// The one universal contract the state core drives: a functional-updater setter
|
|
35
|
+
// (React setState shape) that assigns a fresh array each time.
|
|
36
|
+
const set: SetMessages = (updater) => { messages = updater(messages); };
|
|
37
|
+
|
|
38
|
+
const append = (msg: ChatMessage) => set((prev) => appendMessage(prev, msg));
|
|
39
|
+
const setMessages = (next: ChatMessage[]) => { messages = [...next]; };
|
|
40
|
+
const streamAssistant = (init?: Partial<ChatMessage>): AssistantStream => {
|
|
41
|
+
loading = true;
|
|
42
|
+
return onStreamSettled(createAssistantStream(set, init), () => { loading = false; });
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
get messages() { return messages; },
|
|
47
|
+
get loading() { return loading; },
|
|
48
|
+
append,
|
|
49
|
+
setMessages,
|
|
50
|
+
streamAssistant,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { THREADS, newId, type Conversation } from '../chat-data';
|
|
2
|
+
import type { ChatController } from './chat.svelte';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Owns the conversation list + which one is active, plus the in-memory `THREADS`
|
|
6
|
+
* stash: on select/new we swap the OPEN thread out to `THREADS` and swap the
|
|
7
|
+
* picked one in. Takes the `createChat` controller so it can read the live messages
|
|
8
|
+
* when stashing and reset them when loading another thread. Mirrors the React /
|
|
9
|
+
* Vue `useConversations`.
|
|
10
|
+
*/
|
|
11
|
+
export interface ConversationsController {
|
|
12
|
+
readonly conversations: Conversation[];
|
|
13
|
+
readonly activeId: string;
|
|
14
|
+
selectConversation: (id: string) => void;
|
|
15
|
+
newChat: () => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function createConversations(chat: ChatController, initial: Conversation[]): ConversationsController {
|
|
19
|
+
let conversations = $state<Conversation[]>([...initial]);
|
|
20
|
+
let activeId = $state(initial[0].id);
|
|
21
|
+
|
|
22
|
+
const selectConversation = (id: string) => {
|
|
23
|
+
THREADS[activeId] = chat.messages; // stash the open thread
|
|
24
|
+
activeId = id;
|
|
25
|
+
chat.setMessages(THREADS[id] ?? []); // load the picked one
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const newChat = () => {
|
|
29
|
+
const id = newId();
|
|
30
|
+
THREADS[activeId] = chat.messages;
|
|
31
|
+
THREADS[id] = [];
|
|
32
|
+
conversations = [
|
|
33
|
+
{ id, title: 'New conversation', scope: { type: 'collection' }, messageCount: 0, lastMessageAt: new Date().toISOString(), updatedAt: new Date().toISOString() },
|
|
34
|
+
...conversations,
|
|
35
|
+
];
|
|
36
|
+
activeId = id;
|
|
37
|
+
chat.setMessages([]);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
get conversations() { return conversations; },
|
|
42
|
+
get activeId() { return activeId; },
|
|
43
|
+
selectConversation,
|
|
44
|
+
newChat,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/** Minimal shape of a Web Speech recognition result event — enough to read the
|
|
2
|
+
* first transcript. The full SpeechRecognition API isn't in TS's lib.dom yet. */
|
|
3
|
+
interface SpeechResultEvent {
|
|
4
|
+
results: ArrayLike<ArrayLike<{ transcript: string }>>;
|
|
5
|
+
}
|
|
6
|
+
interface SpeechRecognitionLike {
|
|
7
|
+
lang: string;
|
|
8
|
+
interimResults: boolean;
|
|
9
|
+
onresult: ((event: SpeechResultEvent) => void) | null;
|
|
10
|
+
start(): void;
|
|
11
|
+
}
|
|
12
|
+
type SpeechRecognitionCtor = new () => SpeechRecognitionLike;
|
|
13
|
+
|
|
14
|
+
/** Resolve the Chromium (`webkitSpeechRecognition`) or standard constructor, if any. */
|
|
15
|
+
function getSpeechRecognition(): SpeechRecognitionCtor | undefined {
|
|
16
|
+
if (typeof window === 'undefined') return undefined;
|
|
17
|
+
const w = window as unknown as {
|
|
18
|
+
SpeechRecognition?: SpeechRecognitionCtor;
|
|
19
|
+
webkitSpeechRecognition?: SpeechRecognitionCtor;
|
|
20
|
+
};
|
|
21
|
+
return w.SpeechRecognition ?? w.webkitSpeechRecognition;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Web Speech mic → one-shot transcript. Chromium-only (`webkitSpeechRecognition`);
|
|
26
|
+
* `supported` is false elsewhere so the caller can degrade gracefully. Svelte port
|
|
27
|
+
* of the kit's `useVoiceInput` (a React hook). It hands the raw transcript to
|
|
28
|
+
* `onTranscript` and lets the caller decide what to do with it (the composer mic
|
|
29
|
+
* appends it to the live text). No composer or DOM knowledge.
|
|
30
|
+
*
|
|
31
|
+
* @param onTranscript called with the final recognized transcript.
|
|
32
|
+
* @returns `supported` (Chromium-only availability) and a `start` that begins one
|
|
33
|
+
* recognition pass (a no-op when unsupported).
|
|
34
|
+
*/
|
|
35
|
+
export function createVoiceInput(onTranscript: (transcript: string) => void): {
|
|
36
|
+
supported: boolean;
|
|
37
|
+
start: () => void;
|
|
38
|
+
} {
|
|
39
|
+
// Latest-callback ref: keeps `start` stable while always calling the current
|
|
40
|
+
// `onTranscript`, mirroring the React hook (no stale closure).
|
|
41
|
+
const cbRef = { current: onTranscript };
|
|
42
|
+
cbRef.current = onTranscript;
|
|
43
|
+
|
|
44
|
+
const supported = getSpeechRecognition() !== undefined;
|
|
45
|
+
|
|
46
|
+
const start = () => {
|
|
47
|
+
const Recognition = getSpeechRecognition();
|
|
48
|
+
if (!Recognition) return;
|
|
49
|
+
const rec = new Recognition();
|
|
50
|
+
rec.lang = 'en-US';
|
|
51
|
+
rec.interimResults = false;
|
|
52
|
+
rec.onresult = (event) => cbRef.current(event.results[0][0].transcript);
|
|
53
|
+
rec.start();
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
return { supported, start };
|
|
57
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { mount } from 'svelte';
|
|
2
|
+
import '@kitn.ai/ui/elements'; // registers the kai-* custom elements (async)
|
|
3
|
+
import '@kitn.ai/ui/theme.tokens.css'; // plain --color-* tokens for the shell (NOT the tailwind source)
|
|
4
|
+
import './index.css';
|
|
5
|
+
import App from './App.svelte';
|
|
6
|
+
|
|
7
|
+
const target = document.getElementById('app');
|
|
8
|
+
if (!target) throw new Error('Root element not found');
|
|
9
|
+
|
|
10
|
+
// The kai-* elements register asynchronously. Wait until the tags we use are
|
|
11
|
+
// defined before mounting, so Svelte's `bind:this` + `$effect` property writes land
|
|
12
|
+
// on UPGRADED elements. Raw web-component consumers have no upgrade-race guard
|
|
13
|
+
// (unlike the React wrappers), so a property set before upgrade is lost and static
|
|
14
|
+
// seed data never re-applies. Attributes survive upgrade; array/object properties do not.
|
|
15
|
+
const TAGS = ['kai-resizable', 'kai-resizable-item', 'kai-conversations', 'kai-thread', 'kai-prompt-input', 'kai-button'];
|
|
16
|
+
Promise.all(TAGS.map((t) => customElements.whenDefined(t))).then(() => {
|
|
17
|
+
mount(App, { target });
|
|
18
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
|
2
|
+
|
|
3
|
+
// Read by both vite-plugin-svelte and svelte-check. `vitePreprocess` lets the
|
|
4
|
+
// `<script lang="ts">` blocks use TypeScript. No SvelteKit here — this is a plain
|
|
5
|
+
// Svelte 5 SPA.
|
|
6
|
+
export default {
|
|
7
|
+
preprocess: vitePreprocess(),
|
|
8
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
|
4
|
+
"target": "ES2020",
|
|
5
|
+
"useDefineForClassFields": true,
|
|
6
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
7
|
+
"module": "ESNext",
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
|
|
10
|
+
/* Bundler mode */
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"allowImportingTsExtensions": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"moduleDetection": "force",
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
|
|
17
|
+
/* Svelte */
|
|
18
|
+
"verbatimModuleSyntax": true,
|
|
19
|
+
|
|
20
|
+
/* Linting */
|
|
21
|
+
"strict": true,
|
|
22
|
+
"noUnusedLocals": true,
|
|
23
|
+
"noUnusedParameters": true,
|
|
24
|
+
"noFallthroughCasesInSwitch": true,
|
|
25
|
+
"noUncheckedSideEffectImports": true
|
|
26
|
+
},
|
|
27
|
+
"include": ["src/**/*.ts", "src/**/*.svelte.ts", "src/**/*.svelte", "src/**/*.d.ts"]
|
|
28
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
|
4
|
+
"target": "ES2022",
|
|
5
|
+
"lib": ["ES2023"],
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
"types": ["node"],
|
|
9
|
+
|
|
10
|
+
/* Bundler mode */
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"allowImportingTsExtensions": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"moduleDetection": "force",
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
|
|
17
|
+
/* Linting */
|
|
18
|
+
"strict": true,
|
|
19
|
+
"noUnusedLocals": true,
|
|
20
|
+
"noUnusedParameters": true,
|
|
21
|
+
"noFallthroughCasesInSwitch": true,
|
|
22
|
+
"noUncheckedSideEffectImports": true
|
|
23
|
+
},
|
|
24
|
+
"include": ["vite.config.ts", "svelte.config.js"]
|
|
25
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
|
3
|
+
|
|
4
|
+
// `@kitn.ai/ui` is linked into this example via `workspace:*`, so it resolves
|
|
5
|
+
// through node_modules + the package's `exports` map exactly like a published
|
|
6
|
+
// consumer would — no aliases, no source stubs. Build the kit first (`nx build ui`).
|
|
7
|
+
//
|
|
8
|
+
// Unlike Vue, Svelte needs no `isCustomElement` config: the compiler already
|
|
9
|
+
// treats any hyphenated tag (`kai-*`) as a native custom element and passes
|
|
10
|
+
// props/events straight through to the DOM.
|
|
11
|
+
// https://vite.dev/config/
|
|
12
|
+
export default defineConfig({
|
|
13
|
+
plugins: [svelte()],
|
|
14
|
+
server: { port: 5175 },
|
|
15
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kitn.ai/ui-example-tanstack-start",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite dev",
|
|
8
|
+
"build": "vite build",
|
|
9
|
+
"start": "node serve.mjs",
|
|
10
|
+
"test": "node --test",
|
|
11
|
+
"typecheck": "tsc --noEmit"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@kitn.ai/ui": "file:../../../packages/ui",
|
|
15
|
+
"@tanstack/react-router": "^1.170.16",
|
|
16
|
+
"@tanstack/react-start": "^1.168.26",
|
|
17
|
+
"react": "^19.0.0",
|
|
18
|
+
"react-dom": "^19.0.0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/node": "^22.0.0",
|
|
22
|
+
"@types/react": "^19.0.0",
|
|
23
|
+
"@types/react-dom": "^19.0.0",
|
|
24
|
+
"@vitejs/plugin-react": "^4.3.4",
|
|
25
|
+
"typescript": "~5.7.2",
|
|
26
|
+
"vite": "^7.0.0"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// Minimal production server for the TanStack Start build.
|
|
2
|
+
//
|
|
3
|
+
// `vite build` emits a portable Web-`fetch` handler at dist/server/server.js
|
|
4
|
+
// (not a Node http listener) plus the static client assets in dist/client. A
|
|
5
|
+
// real deploy would pick a target (Node, Bun, a CDN/worker, …); for this example
|
|
6
|
+
// we bridge Node's http to the fetch handler and serve the static assets, so
|
|
7
|
+
// `npm start` runs the SSR build locally. Node 18+ provides the Web globals
|
|
8
|
+
// (Request/Response/ReadableStream) this needs.
|
|
9
|
+
import { createServer } from 'node:http';
|
|
10
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
11
|
+
import { join, extname, sep } from 'node:path';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
|
|
14
|
+
const root = fileURLToPath(new URL('.', import.meta.url));
|
|
15
|
+
const clientDir = join(root, 'dist', 'client');
|
|
16
|
+
// Trailing separator, on purpose. A bare `startsWith(clientDir)` also matches
|
|
17
|
+
// SIBLINGS whose name merely begins with it (dist/client-secret/), so the boundary
|
|
18
|
+
// has to be the separator, not the string.
|
|
19
|
+
const clientPrefix = clientDir.endsWith(sep) ? clientDir : clientDir + sep;
|
|
20
|
+
const { default: handler } = await import('./dist/server/server.js');
|
|
21
|
+
|
|
22
|
+
const PORT = Number(process.env.PORT) || 3000;
|
|
23
|
+
const TYPES = {
|
|
24
|
+
'.js': 'text/javascript',
|
|
25
|
+
'.css': 'text/css',
|
|
26
|
+
'.json': 'application/json',
|
|
27
|
+
'.svg': 'image/svg+xml',
|
|
28
|
+
'.ico': 'image/x-icon',
|
|
29
|
+
'.woff2': 'font/woff2',
|
|
30
|
+
'.map': 'application/json',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const badRequest = () => new Response('Bad Request', { status: 400 });
|
|
34
|
+
|
|
35
|
+
// Returns a Response to send, or null to hand the request to the SSR handler.
|
|
36
|
+
//
|
|
37
|
+
// THE ORDER HERE IS THE WHOLE POINT: decode, then resolve, then check containment,
|
|
38
|
+
// then touch the disk. `url.pathname` is still percent-encoded, so the string we
|
|
39
|
+
// would validate and the string the filesystem actually opens are different
|
|
40
|
+
// strings. Validate the encoded one and `/..%2f..%2fsecret` sails past a guard that
|
|
41
|
+
// has already pronounced the path safe, because `..%2f` only becomes `../` after
|
|
42
|
+
// the decode. `serve.traversal.test.mjs` pins this down by running the same probes
|
|
43
|
+
// against a build with the check before the decode, and asserting it LEAKS.
|
|
44
|
+
//
|
|
45
|
+
// `new URL` in the request handler does part of the job already: it resolves
|
|
46
|
+
// dot-segments and counts `%2e` as a dot, so plain `..` and `%2e%2e/` never arrive
|
|
47
|
+
// here. It does NOT touch `%2f`, which is exactly the vector this function has to
|
|
48
|
+
// stop. Don't lean on that upstream pass; it is one refactor away from disappearing.
|
|
49
|
+
async function serveStatic(pathname) {
|
|
50
|
+
if (pathname === '/' || pathname.endsWith('/')) return null;
|
|
51
|
+
|
|
52
|
+
let decoded;
|
|
53
|
+
try {
|
|
54
|
+
decoded = decodeURIComponent(pathname);
|
|
55
|
+
} catch {
|
|
56
|
+
return badRequest(); // decodeURIComponent('%') throws URIError. A 400, not a crash.
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Exactly one decode pass, never a loop-until-stable: re-decoding would turn
|
|
60
|
+
// `%252e%252e` back into `..` and reopen the hole from the other side. After one
|
|
61
|
+
// pass `%252e%252e` is a literal filename, which is the correct reading of it.
|
|
62
|
+
|
|
63
|
+
// A NUL truncates the path at the syscall boundary; a backslash is a separator on
|
|
64
|
+
// Windows and a legal filename character on POSIX, so the same request would mean
|
|
65
|
+
// two different files. Neither appears in a real Vite asset name.
|
|
66
|
+
if (decoded.includes('\0') || decoded.includes('\\')) return badRequest();
|
|
67
|
+
|
|
68
|
+
// join(), not resolve(): `decoded` is attacker-controlled and may be absolute, and
|
|
69
|
+
// resolve() would honour that and walk out of clientDir entirely. join() re-roots
|
|
70
|
+
// it and normalises away the `..` the decode just produced.
|
|
71
|
+
const filePath = join(clientDir, decoded);
|
|
72
|
+
if (filePath === clientDir) return null;
|
|
73
|
+
if (!filePath.startsWith(clientPrefix)) return badRequest();
|
|
74
|
+
|
|
75
|
+
let body;
|
|
76
|
+
try {
|
|
77
|
+
if (!(await stat(filePath)).isFile()) return null;
|
|
78
|
+
body = await readFile(filePath);
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
return new Response(body, {
|
|
83
|
+
headers: { 'content-type': TYPES[extname(filePath)] ?? 'application/octet-stream' },
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const server = createServer(async (req, res) => {
|
|
88
|
+
try {
|
|
89
|
+
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
90
|
+
|
|
91
|
+
// Hashed client assets are static files; serve them straight from disk.
|
|
92
|
+
const staticRes = await serveStatic(url.pathname);
|
|
93
|
+
const webRes = staticRes ?? (await handler.fetch(toRequest(req, url)));
|
|
94
|
+
|
|
95
|
+
res.statusCode = webRes.status;
|
|
96
|
+
webRes.headers.forEach((v, k) => res.setHeader(k, v));
|
|
97
|
+
if (webRes.body) {
|
|
98
|
+
const reader = webRes.body.getReader();
|
|
99
|
+
for (;;) {
|
|
100
|
+
const { done, value } = await reader.read();
|
|
101
|
+
if (done) break;
|
|
102
|
+
res.write(value);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
res.end();
|
|
106
|
+
} catch (err) {
|
|
107
|
+
console.error(err);
|
|
108
|
+
res.statusCode = 500;
|
|
109
|
+
res.end('Internal Server Error');
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
function toRequest(req, url) {
|
|
114
|
+
const headers = new Headers();
|
|
115
|
+
for (const [k, v] of Object.entries(req.headers)) {
|
|
116
|
+
if (v !== undefined) headers.set(k, Array.isArray(v) ? v.join(', ') : v);
|
|
117
|
+
}
|
|
118
|
+
const hasBody = req.method !== 'GET' && req.method !== 'HEAD';
|
|
119
|
+
return new Request(url, {
|
|
120
|
+
method: req.method,
|
|
121
|
+
headers,
|
|
122
|
+
body: hasBody ? req : undefined,
|
|
123
|
+
duplex: hasBody ? 'half' : undefined,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
server.listen(PORT, () => {
|
|
128
|
+
console.log(`▶ TanStack Start (production) on http://localhost:${PORT}`);
|
|
129
|
+
});
|