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.
- package/LICENSE +1 -1
- package/dist/__sw__.js +25 -16
- package/dist/frameworks/next-dev-server.d.ts +61 -0
- package/dist/frameworks/next-dev-server.d.ts.map +1 -1
- package/dist/frameworks/tailwind-config-loader.d.ts +32 -0
- package/dist/frameworks/tailwind-config-loader.d.ts.map +1 -0
- package/dist/index.cjs +869 -24
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +845 -27
- package/dist/index.mjs.map +1 -1
- package/dist/macaly-demo.d.ts +42 -0
- package/dist/macaly-demo.d.ts.map +1 -0
- package/package.json +2 -1
- package/src/frameworks/next-dev-server.ts +913 -34
- package/src/frameworks/tailwind-config-loader.ts +206 -0
- package/src/macaly-demo.ts +172 -0
|
@@ -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 };
|