@git.zone/tstest 3.2.0 → 3.3.1

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,226 @@
1
+ import * as plugins from './tstest.plugins.js';
2
+ import type { DenoOptions, RuntimeOptions } from './tstest.classes.runtime.adapter.js';
3
+ import type { Runtime } from './tstest.classes.runtime.parser.js';
4
+ import { DENO_DEFAULT_PERMISSIONS } from './tstest.classes.runtime.deno.js';
5
+
6
+ type DirectiveScope = Runtime | 'global';
7
+
8
+ export interface ITestFileDirective {
9
+ scope: DirectiveScope;
10
+ key: string;
11
+ value?: string;
12
+ }
13
+
14
+ export interface IParsedDirectives {
15
+ deno: ITestFileDirective[];
16
+ node: ITestFileDirective[];
17
+ bun: ITestFileDirective[];
18
+ chromium: ITestFileDirective[];
19
+ global: ITestFileDirective[];
20
+ }
21
+
22
+ const VALID_SCOPES = new Set<string>(['deno', 'node', 'bun', 'chromium']);
23
+
24
+ const DENO_PERMISSION_MAP: Record<string, string> = {
25
+ allowAll: '--allow-all',
26
+ allowRun: '--allow-run',
27
+ allowFfi: '--allow-ffi',
28
+ allowHrtime: '--allow-hrtime',
29
+ allowRead: '--allow-read',
30
+ allowWrite: '--allow-write',
31
+ allowNet: '--allow-net',
32
+ allowEnv: '--allow-env',
33
+ allowSys: '--allow-sys',
34
+ };
35
+
36
+ function createEmptyDirectives(): IParsedDirectives {
37
+ return { deno: [], node: [], bun: [], chromium: [], global: [] };
38
+ }
39
+
40
+ /**
41
+ * Parse tstest directives from file content.
42
+ * Scans comments at the top of the file (before any code).
43
+ */
44
+ export function parseDirectivesFromContent(content: string): IParsedDirectives {
45
+ const result = createEmptyDirectives();
46
+ const lines = content.split('\n');
47
+ const maxLines = Math.min(lines.length, 30);
48
+
49
+ for (let i = 0; i < maxLines; i++) {
50
+ const line = lines[i].trim();
51
+
52
+ // Skip empty lines
53
+ if (line === '') continue;
54
+
55
+ // Stop at first non-comment line
56
+ if (!line.startsWith('//')) break;
57
+
58
+ // Match tstest directive: // tstest:<rest>
59
+ const match = line.match(/^\/\/\s*tstest:(.+)$/);
60
+ if (!match) continue;
61
+
62
+ const parts = match[1].split(':');
63
+ if (parts.length < 2) {
64
+ console.warn(`Warning: malformed tstest directive: "${line}"`);
65
+ continue;
66
+ }
67
+
68
+ const scopeStr = parts[0].trim();
69
+ const key = parts[1].trim();
70
+ const value = parts.length > 2 ? parts.slice(2).join(':').trim() : undefined;
71
+
72
+ // Handle global directives (env, timeout)
73
+ if (scopeStr === 'env' || scopeStr === 'timeout') {
74
+ result.global.push({
75
+ scope: 'global',
76
+ key: scopeStr,
77
+ value: key + (value !== undefined ? ':' + value : ''),
78
+ });
79
+ continue;
80
+ }
81
+
82
+ if (!VALID_SCOPES.has(scopeStr)) {
83
+ console.warn(`Warning: unknown tstest directive scope "${scopeStr}" in: "${line}"`);
84
+ continue;
85
+ }
86
+
87
+ const scope = scopeStr as Runtime;
88
+ result[scope].push({ scope, key, value });
89
+ }
90
+
91
+ return result;
92
+ }
93
+
94
+ /**
95
+ * Parse directives from a test file on disk.
96
+ */
97
+ export async function parseDirectivesFromFile(filePath: string): Promise<IParsedDirectives> {
98
+ try {
99
+ const content = plugins.fs.readFileSync(filePath, 'utf8');
100
+ return parseDirectivesFromContent(content);
101
+ } catch {
102
+ return createEmptyDirectives();
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Merge directives from 00init.ts and the test file.
108
+ * Test file directives are appended (take effect after init directives).
109
+ */
110
+ export function mergeDirectives(init: IParsedDirectives, testFile: IParsedDirectives): IParsedDirectives {
111
+ return {
112
+ deno: [...init.deno, ...testFile.deno],
113
+ node: [...init.node, ...testFile.node],
114
+ bun: [...init.bun, ...testFile.bun],
115
+ chromium: [...init.chromium, ...testFile.chromium],
116
+ global: [...init.global, ...testFile.global],
117
+ };
118
+ }
119
+
120
+ /**
121
+ * Check if any directives exist for any scope.
122
+ */
123
+ export function hasDirectives(directives: IParsedDirectives): boolean {
124
+ return (
125
+ directives.deno.length > 0 ||
126
+ directives.node.length > 0 ||
127
+ directives.bun.length > 0 ||
128
+ directives.chromium.length > 0 ||
129
+ directives.global.length > 0
130
+ );
131
+ }
132
+
133
+ /**
134
+ * Convert parsed directives into DenoOptions.
135
+ */
136
+ function directivesToDenoOptions(directives: IParsedDirectives): DenoOptions | undefined {
137
+ const denoDirectives = directives.deno;
138
+ if (denoDirectives.length === 0 && directives.global.length === 0) return undefined;
139
+
140
+ const options: DenoOptions = {};
141
+ const extraPermissions: string[] = [];
142
+ const extraArgs: string[] = [];
143
+ const env: Record<string, string> = {};
144
+ let useAllowAll = false;
145
+
146
+ for (const d of denoDirectives) {
147
+ if (d.key === 'allowAll') {
148
+ useAllowAll = true;
149
+ } else if (DENO_PERMISSION_MAP[d.key]) {
150
+ extraPermissions.push(DENO_PERMISSION_MAP[d.key]);
151
+ } else if (d.key === 'flag' && d.value) {
152
+ extraArgs.push(d.value);
153
+ }
154
+ }
155
+
156
+ // Process global directives
157
+ for (const d of directives.global) {
158
+ if (d.key === 'env' && d.value) {
159
+ const eqIndex = d.value.indexOf('=');
160
+ if (eqIndex > 0) {
161
+ env[d.value.substring(0, eqIndex)] = d.value.substring(eqIndex + 1);
162
+ }
163
+ }
164
+ }
165
+
166
+ if (useAllowAll) {
167
+ // --allow-all replaces individual permissions, but keep compatibility flags
168
+ options.permissions = ['--allow-all', '--node-modules-dir', '--sloppy-imports'];
169
+ } else if (extraPermissions.length > 0) {
170
+ // Start with defaults and add extra permissions (deduplicated)
171
+ const allPermissions = [...DENO_DEFAULT_PERMISSIONS];
172
+ for (const p of extraPermissions) {
173
+ if (!allPermissions.includes(p)) {
174
+ allPermissions.push(p);
175
+ }
176
+ }
177
+ options.permissions = allPermissions;
178
+ }
179
+
180
+ if (extraArgs.length > 0) options.extraArgs = extraArgs;
181
+ if (Object.keys(env).length > 0) options.env = env;
182
+
183
+ // Return undefined if nothing was set
184
+ if (!options.permissions && !options.extraArgs && !options.env) return undefined;
185
+ return options;
186
+ }
187
+
188
+ /**
189
+ * Convert parsed directives into RuntimeOptions for Node/Bun (flag directives only).
190
+ */
191
+ function directivesToGenericOptions(directives: ITestFileDirective[], globalDirectives: ITestFileDirective[]): RuntimeOptions | undefined {
192
+ const extraArgs: string[] = [];
193
+ const env: Record<string, string> = {};
194
+
195
+ for (const d of directives) {
196
+ if (d.key === 'flag' && d.value) {
197
+ extraArgs.push(d.value);
198
+ }
199
+ }
200
+
201
+ for (const d of globalDirectives) {
202
+ if (d.key === 'env' && d.value) {
203
+ const eqIndex = d.value.indexOf('=');
204
+ if (eqIndex > 0) {
205
+ env[d.value.substring(0, eqIndex)] = d.value.substring(eqIndex + 1);
206
+ }
207
+ }
208
+ }
209
+
210
+ if (extraArgs.length === 0 && Object.keys(env).length === 0) return undefined;
211
+
212
+ const options: RuntimeOptions = {};
213
+ if (extraArgs.length > 0) options.extraArgs = extraArgs;
214
+ if (Object.keys(env).length > 0) options.env = env;
215
+ return options;
216
+ }
217
+
218
+ /**
219
+ * Convert parsed directives into RuntimeOptions for a specific runtime.
220
+ */
221
+ export function directivesToRuntimeOptions(directives: IParsedDirectives, runtime: Runtime): RuntimeOptions | undefined {
222
+ if (runtime === 'deno') {
223
+ return directivesToDenoOptions(directives);
224
+ }
225
+ return directivesToGenericOptions(directives[runtime] || [], directives.global);
226
+ }
@@ -19,6 +19,14 @@ import { DenoRuntimeAdapter } from './tstest.classes.runtime.deno.js';
19
19
  import { BunRuntimeAdapter } from './tstest.classes.runtime.bun.js';
20
20
  import { DockerRuntimeAdapter } from './tstest.classes.runtime.docker.js';
21
21
 
22
+ // Test file directives
23
+ import {
24
+ parseDirectivesFromFile,
25
+ mergeDirectives,
26
+ directivesToRuntimeOptions,
27
+ hasDirectives,
28
+ } from './tstest.classes.testfile.directives.js';
29
+
22
30
  export class TsTest {
23
31
  public testDir: TestDirectory;
24
32
  public executionMode: TestExecutionMode;
@@ -256,18 +264,32 @@ export class TsTest {
256
264
  return;
257
265
  }
258
266
 
267
+ // Parse directives from the test file (e.g., // tstest:deno:allowAll)
268
+ let directives = await parseDirectivesFromFile(fileNameArg);
269
+
270
+ // Also check for directives in 00init.ts
271
+ const testDir = plugins.path.dirname(fileNameArg);
272
+ const initFile = plugins.path.join(testDir, '00init.ts');
273
+ const initFileExists = await plugins.smartfsInstance.file(initFile).exists();
274
+ if (initFileExists) {
275
+ const initDirectives = await parseDirectivesFromFile(initFile);
276
+ directives = mergeDirectives(initDirectives, directives);
277
+ }
278
+
259
279
  // Execute tests for each runtime
260
280
  if (adapters.length === 1) {
261
281
  // Single runtime - no sections needed
262
282
  const adapter = adapters[0];
263
- const tapParser = await adapter.run(fileNameArg, fileIndex, totalFiles);
283
+ const options = hasDirectives(directives) ? directivesToRuntimeOptions(directives, adapter.id) : undefined;
284
+ const tapParser = await adapter.run(fileNameArg, fileIndex, totalFiles, options);
264
285
  tapCombinator.addTapParser(tapParser);
265
286
  } else {
266
287
  // Multiple runtimes - use sections
267
288
  for (let i = 0; i < adapters.length; i++) {
268
289
  const adapter = adapters[i];
269
290
  this.logger.sectionStart(`Part ${i + 1}: ${adapter.displayName}`);
270
- const tapParser = await adapter.run(fileNameArg, fileIndex, totalFiles);
291
+ const options = hasDirectives(directives) ? directivesToRuntimeOptions(directives, adapter.id) : undefined;
292
+ const tapParser = await adapter.run(fileNameArg, fileIndex, totalFiles, options);
271
293
  tapCombinator.addTapParser(tapParser);
272
294
  this.logger.sectionEnd();
273
295
  }
@@ -454,24 +476,27 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
454
476
  // Find free ports for HTTP and WebSocket
455
477
  const { httpPort, wsPort } = await this.findFreePorts();
456
478
 
457
- // lets create a server
458
- const server = new plugins.typedserver.TypedServer({
459
- cors: true,
460
- port: httpPort,
461
- serveDir: tsbundleCacheDirPath,
462
- });
463
- server.addRoute('/test', 'GET', async () => {
464
- return new Response(`
465
- <html>
466
- <head>
467
- <script>
468
- globalThis.testdom = true;
469
- globalThis.wsPort = ${wsPort};
470
- </script>
471
- </head>
472
- <body></body>
473
- </html>
474
- `, { headers: { 'Content-Type': 'text/html' } });
479
+ // Use SmartServe with setHandler() to bypass global ControllerRegistry
480
+ const fileServer = new plugins.smartserve.FileServer({ root: tsbundleCacheDirPath });
481
+ const server = new plugins.smartserve.SmartServe({ port: httpPort });
482
+ server.setHandler(async (request: Request) => {
483
+ const url = new URL(request.url);
484
+ if (url.pathname === '/test') {
485
+ return new Response(`
486
+ <html>
487
+ <head>
488
+ <script>
489
+ globalThis.testdom = true;
490
+ globalThis.wsPort = ${wsPort};
491
+ </script>
492
+ </head>
493
+ <body></body>
494
+ </html>
495
+ `, { headers: { 'Content-Type': 'text/html' } });
496
+ }
497
+ const staticResponse = await fileServer.serve(request);
498
+ if (staticResponse) return staticResponse;
499
+ return new Response('Not Found', { status: 404 });
475
500
  });
476
501
  await server.start();
477
502
 
@@ -4,16 +4,10 @@ import * as path from 'path';
4
4
 
5
5
  export { fs, path };
6
6
 
7
- // @apiglobal scope
8
- import * as typedserver from '@api.global/typedserver';
9
-
10
- export {
11
- typedserver
12
- }
13
-
14
7
  // @push.rocks scope
15
8
  import * as consolecolor from '@push.rocks/consolecolor';
16
9
  import * as smartbrowser from '@push.rocks/smartbrowser';
10
+ import * as smartserve from '@push.rocks/smartserve';
17
11
  import * as smartdelay from '@push.rocks/smartdelay';
18
12
  import * as smartfile from '@push.rocks/smartfile';
19
13
  import * as smartfs from '@push.rocks/smartfs';
@@ -28,6 +22,7 @@ import * as tapbundle from '../dist_ts_tapbundle/index.js';
28
22
  export {
29
23
  consolecolor,
30
24
  smartbrowser,
25
+ smartserve,
31
26
  smartdelay,
32
27
  smartfile,
33
28
  smartfs,