almostnode 0.2.5 → 0.2.6

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,206 @@
1
+ /**
2
+ * Tailwind Config Loader
3
+ *
4
+ * Parses tailwind.config.ts files and generates JavaScript to configure
5
+ * the Tailwind CDN at runtime via window.tailwind.config.
6
+ */
7
+
8
+ import type { VirtualFS } from '../virtual-fs';
9
+
10
+ export interface TailwindConfigResult {
11
+ /** JavaScript code to set window.tailwind.config (empty string if no config) */
12
+ configScript: string;
13
+ /** Whether config was successfully loaded */
14
+ success: boolean;
15
+ /** Error message if loading failed */
16
+ error?: string;
17
+ }
18
+
19
+ /** Config file names to search for, in priority order */
20
+ const CONFIG_FILE_NAMES = [
21
+ '/tailwind.config.ts',
22
+ '/tailwind.config.js',
23
+ '/tailwind.config.mjs',
24
+ ];
25
+
26
+ /**
27
+ * Load and parse a Tailwind config file from VirtualFS
28
+ */
29
+ export async function loadTailwindConfig(
30
+ vfs: VirtualFS,
31
+ root: string = '/'
32
+ ): Promise<TailwindConfigResult> {
33
+ // Find config file
34
+ let configPath: string | null = null;
35
+ let configContent: string | null = null;
36
+
37
+ for (const fileName of CONFIG_FILE_NAMES) {
38
+ const fullPath = root === '/' ? fileName : `${root}${fileName}`;
39
+ try {
40
+ const content = vfs.readFileSync(fullPath);
41
+ configContent =
42
+ typeof content === 'string'
43
+ ? content
44
+ : content instanceof Uint8Array
45
+ ? new TextDecoder('utf-8').decode(content)
46
+ : Buffer.from(content).toString('utf-8');
47
+ configPath = fullPath;
48
+ break;
49
+ } catch {
50
+ // File not found, try next
51
+ continue;
52
+ }
53
+ }
54
+
55
+ if (!configPath || configContent === null) {
56
+ return {
57
+ configScript: '',
58
+ success: true, // Not an error, just no config
59
+ };
60
+ }
61
+
62
+ try {
63
+ // Strip TypeScript syntax and extract config object
64
+ const jsConfig = stripTypescriptSyntax(configContent);
65
+ const configObject = extractConfigObject(jsConfig);
66
+
67
+ if (!configObject) {
68
+ return {
69
+ configScript: '',
70
+ success: false,
71
+ error: 'Could not extract config object from tailwind.config',
72
+ };
73
+ }
74
+
75
+ // Generate the script to inject
76
+ const configScript = generateConfigScript(configObject);
77
+
78
+ return {
79
+ configScript,
80
+ success: true,
81
+ };
82
+ } catch (error) {
83
+ return {
84
+ configScript: '',
85
+ success: false,
86
+ error: `Failed to parse tailwind.config: ${error instanceof Error ? error.message : String(error)}`,
87
+ };
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Strip TypeScript-specific syntax from config content
93
+ */
94
+ export function stripTypescriptSyntax(content: string): string {
95
+ let result = content;
96
+
97
+ // Remove import type statements
98
+ // e.g., import type { Config } from "tailwindcss"
99
+ result = result.replace(/import\s+type\s+\{[^}]*\}\s+from\s+['"][^'"]*['"]\s*;?\s*/g, '');
100
+
101
+ // Remove regular import statements (Config type, etc.)
102
+ // e.g., import { Config } from "tailwindcss"
103
+ result = result.replace(/import\s+\{[^}]*\}\s+from\s+['"][^'"]*['"]\s*;?\s*/g, '');
104
+
105
+ // Remove satisfies Type assertions
106
+ // e.g., } satisfies Config
107
+ result = result.replace(/\s+satisfies\s+\w+\s*$/gm, '');
108
+ result = result.replace(/\s+satisfies\s+\w+\s*;?\s*$/gm, '');
109
+
110
+ // Remove type annotations on variables
111
+ // e.g., const config: Config = { ... }
112
+ result = result.replace(/:\s*Config\s*=/g, ' =');
113
+
114
+ // Remove 'as const' assertions
115
+ result = result.replace(/\s+as\s+const\s*/g, ' ');
116
+
117
+ return result;
118
+ }
119
+
120
+ /**
121
+ * Extract the config object from the processed content
122
+ */
123
+ export function extractConfigObject(content: string): string | null {
124
+ // Look for export default { ... }
125
+ // We need to find the opening brace and match it to the closing brace
126
+
127
+ // First, find "export default"
128
+ const exportDefaultMatch = content.match(/export\s+default\s*/);
129
+ if (!exportDefaultMatch || exportDefaultMatch.index === undefined) {
130
+ return null;
131
+ }
132
+
133
+ const startIndex = exportDefaultMatch.index + exportDefaultMatch[0].length;
134
+ const remaining = content.substring(startIndex);
135
+
136
+ // Check if it starts with an object literal
137
+ const trimmedRemaining = remaining.trimStart();
138
+ if (!trimmedRemaining.startsWith('{')) {
139
+ return null;
140
+ }
141
+
142
+ // Find the matching closing brace
143
+ const objectStart = startIndex + (remaining.length - trimmedRemaining.length);
144
+ const objectContent = content.substring(objectStart);
145
+
146
+ let braceCount = 0;
147
+ let inString = false;
148
+ let stringChar = '';
149
+ let escaped = false;
150
+ let endIndex = -1;
151
+
152
+ for (let i = 0; i < objectContent.length; i++) {
153
+ const char = objectContent[i];
154
+
155
+ if (escaped) {
156
+ escaped = false;
157
+ continue;
158
+ }
159
+
160
+ if (char === '\\') {
161
+ escaped = true;
162
+ continue;
163
+ }
164
+
165
+ if (inString) {
166
+ if (char === stringChar) {
167
+ inString = false;
168
+ }
169
+ continue;
170
+ }
171
+
172
+ if (char === '"' || char === "'" || char === '`') {
173
+ inString = true;
174
+ stringChar = char;
175
+ continue;
176
+ }
177
+
178
+ if (char === '{') {
179
+ braceCount++;
180
+ } else if (char === '}') {
181
+ braceCount--;
182
+ if (braceCount === 0) {
183
+ endIndex = i + 1;
184
+ break;
185
+ }
186
+ }
187
+ }
188
+
189
+ if (endIndex === -1) {
190
+ return null;
191
+ }
192
+
193
+ return objectContent.substring(0, endIndex);
194
+ }
195
+
196
+ /**
197
+ * Generate the script to inject the Tailwind config
198
+ */
199
+ export function generateConfigScript(configObject: string): string {
200
+ // Wrap in a script that sets tailwind.config
201
+ // This must run AFTER the Tailwind CDN script loads
202
+ // The CDN creates the global `tailwind` object, then we configure it
203
+ return `<script>
204
+ tailwind.config = ${configObject};
205
+ </script>`;
206
+ }
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Macaly Demo - Load the REAL macaly-web repository into almostnode
3
+ * This tests almostnode's ability to run a real-world Next.js app
4
+ */
5
+
6
+ import { VirtualFS } from './virtual-fs';
7
+ import { createRuntime } from './create-runtime';
8
+ import type { IRuntime } from './runtime-interface';
9
+ import { NextDevServer } from './frameworks/next-dev-server';
10
+ import { getServerBridge } from './server-bridge';
11
+
12
+ /**
13
+ * Files to load from the real macaly-web repository
14
+ * We'll populate this dynamically, but here's the structure we need
15
+ */
16
+ export interface MacalyFiles {
17
+ [path: string]: string;
18
+ }
19
+
20
+ /**
21
+ * Load the real macaly-web project into VirtualFS
22
+ */
23
+ export function loadMacalyProject(vfs: VirtualFS, files: MacalyFiles): void {
24
+ for (const [path, content] of Object.entries(files)) {
25
+ // Ensure directory exists
26
+ const dir = path.substring(0, path.lastIndexOf('/'));
27
+ if (dir) {
28
+ vfs.mkdirSync(dir, { recursive: true });
29
+ }
30
+
31
+ // Handle base64-encoded binary files
32
+ if (content.startsWith('base64:')) {
33
+ const base64Data = content.slice(7); // Remove 'base64:' prefix
34
+ const binaryData = atob(base64Data);
35
+ const bytes = new Uint8Array(binaryData.length);
36
+ for (let i = 0; i < binaryData.length; i++) {
37
+ bytes[i] = binaryData.charCodeAt(i);
38
+ }
39
+ vfs.writeFileSync(path, Buffer.from(bytes));
40
+ } else {
41
+ vfs.writeFileSync(path, content);
42
+ }
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Initialize the Macaly demo with real files
48
+ */
49
+ export async function initMacalyDemo(
50
+ outputElement: HTMLElement,
51
+ files: MacalyFiles,
52
+ options: { useWorker?: boolean } = {}
53
+ ): Promise<{ vfs: VirtualFS; runtime: IRuntime }> {
54
+ const { useWorker = false } = options;
55
+
56
+ const log = (message: string) => {
57
+ const line = document.createElement('div');
58
+ line.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
59
+ outputElement.appendChild(line);
60
+ outputElement.scrollTop = outputElement.scrollHeight;
61
+ };
62
+
63
+ log('Creating virtual file system...');
64
+ const vfs = new VirtualFS();
65
+
66
+ log(`Loading ${Object.keys(files).length} files from macaly-web...`);
67
+ loadMacalyProject(vfs, files);
68
+
69
+ log(`Initializing runtime (${useWorker ? 'Web Worker mode' : 'main thread'})...`);
70
+ const runtime = await createRuntime(vfs, {
71
+ dangerouslyAllowSameOrigin: true,
72
+ useWorker,
73
+ cwd: '/',
74
+ env: {
75
+ NODE_ENV: 'development',
76
+ },
77
+ onConsole: (method, args) => {
78
+ const prefix = method === 'error' ? '[ERROR]' : method === 'warn' ? '[WARN]' : '';
79
+ log(`${prefix} ${args.map((a) => String(a)).join(' ')}`);
80
+ },
81
+ });
82
+
83
+ if (useWorker) {
84
+ log('Runtime is running in a Web Worker for better UI responsiveness');
85
+ }
86
+
87
+ log('Setting up file watcher...');
88
+ vfs.watch('/app', { recursive: true }, (eventType, filename) => {
89
+ log(`File ${eventType}: ${filename}`);
90
+ });
91
+
92
+ log('Macaly demo initialized!');
93
+
94
+ return { vfs, runtime };
95
+ }
96
+
97
+ /**
98
+ * Start the Macaly dev server
99
+ */
100
+ export async function startMacalyDevServer(
101
+ vfs: VirtualFS,
102
+ options: {
103
+ port?: number;
104
+ log?: (message: string) => void;
105
+ } = {}
106
+ ): Promise<{
107
+ server: NextDevServer;
108
+ url: string;
109
+ stop: () => void;
110
+ }> {
111
+ const port = options.port || 3001;
112
+ const log = options.log || console.log;
113
+
114
+ log('Starting Macaly dev server...');
115
+
116
+ const server = new NextDevServer(vfs, { port, root: '/' });
117
+ const bridge = getServerBridge();
118
+
119
+ try {
120
+ log('Initializing Service Worker...');
121
+ await bridge.initServiceWorker();
122
+ log('Service Worker ready');
123
+ } catch (error) {
124
+ log(`Warning: Service Worker failed to initialize: ${error}`);
125
+ }
126
+
127
+ bridge.on('server-ready', (p: unknown, u: unknown) => {
128
+ log(`Server ready at ${u}`);
129
+ });
130
+
131
+ const httpServer = {
132
+ listening: true,
133
+ address: () => ({ port: server.getPort(), address: '0.0.0.0', family: 'IPv4' }),
134
+ async handleRequest(
135
+ method: string,
136
+ url: string,
137
+ headers: Record<string, string>,
138
+ body?: string | ArrayBuffer
139
+ ) {
140
+ const bodyBuffer = body
141
+ ? typeof body === 'string'
142
+ ? Buffer.from(body)
143
+ : Buffer.from(body)
144
+ : undefined;
145
+ return server.handleRequest(method, url, headers, bodyBuffer);
146
+ },
147
+ };
148
+
149
+ bridge.registerServer(httpServer as any, port);
150
+ server.start();
151
+ log('File watcher started');
152
+
153
+ server.on('hmr-update', (update: unknown) => {
154
+ log(`HMR update: ${JSON.stringify(update)}`);
155
+ });
156
+
157
+ const url = bridge.getServerUrl(port);
158
+ log(`Macaly dev server running at: ${url}/`);
159
+
160
+ return {
161
+ server,
162
+ url: url + '/',
163
+ stop: () => {
164
+ server.stop();
165
+ bridge.unregisterServer(port);
166
+ },
167
+ };
168
+ }
169
+
170
+ // Export for use in the demo page
171
+ export { VirtualFS, NextDevServer, createRuntime };
172
+ export type { IRuntime };