Back to home page

EIC code displayed by LXR

 
 

    


Warning, /firebird/packages/firebird-core/src/no-injector.spec.ts is written in an unsupported language. File is not indexed.

0001 /**
0002  * firebird-core must work with no Angular injector and no bootstrap - the
0003  * geometry-loader web worker runs this code, and workers have no DI (event
0004  * parsing is expected to move off the main thread the same way). This spec
0005  * is the enforced form of that constraint, in two parts:
0006  * - it parses a DEX file and paints it into a bare three.js Scene using only
0007  *   core classes, so a dependency that needs an injector at run time fails;
0008  * - it scans every non-spec source file for Angular DI and component APIs
0009  *   (InjectionToken, Injectable, inject(), HttpClient, component decorators),
0010  *   so a statically present token fails even when no code path runs it.
0011  * Angular signals (`signal`, `Signal`, `computed`) stay allowed: they work
0012  * without an injector.
0013  */
0014 import { readdirSync, readFileSync } from 'node:fs';
0015 import { join, relative } from 'node:path';
0016 import { Scene } from 'three';
0017 import { DataExchange, DataModelPainter, initPieceFactories, registerDefaultPainters } from './index';
0018 
0019 const DEX_SAMPLE = {
0020   type: 'firebird-dex-json',
0021   version: '1.0',
0022   origin: { by: 'no-injector.spec' },
0023   events: [
0024     {
0025       id: 'event_0',
0026       pieces: [
0027         {
0028           name: 'TestHits',
0029           type: 'BoxHit',
0030           version: '1.0',
0031           origin: { by: 'spec' },
0032           count: 2,
0033           columns: {
0034             pos: [10, 20, 30, 40, 50, 60],
0035             dim: [1, 1, 1, 2, 2, 2],
0036             time: [0, 5],
0037             edep: [0.001, 0.002],
0038           },
0039         },
0040         {
0041           name: 'TestTracks',
0042           type: 'PointTrajectory',
0043           version: '1.0',
0044           origin: { by: 'spec' },
0045           count: 1,
0046           columns: { theta: [0.5] },
0047           pointColumns: ['x', 'y', 'z', 't'],
0048           points: [
0049             [[0, 0, 0, 0], [10, 10, 10, 1], [20, 15, 30, 2]],
0050           ],
0051         },
0052       ],
0053     },
0054   ],
0055 };
0056 
0057 describe('firebird-core without an injector', () => {
0058   it('parses DEX and paints an event into a bare Scene', () => {
0059     // Registration is explicit — no import side effects, no DI (this is how
0060     // the workers wire core; the Angular app wires the same classes via tokens).
0061     initPieceFactories();
0062 
0063     const dex = DataExchange.fromDexObj(DEX_SAMPLE);
0064     expect(dex.events.length).toBe(1);
0065     expect(dex.events[0].pieces.length).toBe(2);
0066 
0067     const scene = new Scene();
0068     const painter = new DataModelPainter();
0069     registerDefaultPainters(painter);
0070     painter.setThreeSceneParent(scene);
0071     painter.setEntry(dex.events[0]);
0072     painter.paint(null);
0073 
0074     // The painter must have created three.js objects under the scene.
0075     let meshCount = 0;
0076     scene.traverse(() => meshCount++);
0077     expect(meshCount).toBeGreaterThan(2);
0078 
0079     // Time filtering runs without any framework machinery either.
0080     painter.paint(1.5);
0081   });
0082 });
0083 
0084 // Angular APIs that need an injector or a compiled component, with the name
0085 // the scan reports for each.
0086 const FORBIDDEN_APIS: ReadonlyArray<readonly [string, RegExp]> = [
0087   ['InjectionToken', /\bInjectionToken\b/],
0088   ['Injectable', /\bInjectable\b/],
0089   ['inject()', /\binject\s*\(/],
0090   ['HttpClient', /\bHttpClient\b/],
0091   ['component decorator', /@(?:Component|Directive|Pipe|NgModule)\s*\(/],
0092 ];
0093 
0094 /** Removes comments, so prose that names a forbidden API does not count. Keeps `://` in URLs. */
0095 function stripComments(source: string): string {
0096   return source.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|[^:])\/\/.*$/gm, '$1');
0097 }
0098 
0099 /** Names of the forbidden APIs that a TypeScript source uses outside comments. */
0100 function findForbiddenApis(source: string): string[] {
0101   const code = stripComments(source);
0102   return FORBIDDEN_APIS.filter(([, pattern]) => pattern.test(code)).map(([name]) => name);
0103 }
0104 
0105 /** Every non-spec .ts file under `dir`, recursively. */
0106 function sourceFiles(dir: string): string[] {
0107   return readdirSync(dir, { withFileTypes: true }).flatMap(entry => {
0108     const path = join(dir, entry.name);
0109     if (entry.isDirectory()) {
0110       return sourceFiles(path);
0111     }
0112     return entry.name.endsWith('.ts') && !entry.name.endsWith('.spec.ts') ? [path] : [];
0113   });
0114 }
0115 
0116 describe('firebird-core source scan', () => {
0117   // vitest defines __dirname for spec modules; the jsdom environment's
0118   // import.meta.url is not a file: URL.
0119   const srcDir = __dirname;
0120   const files = sourceFiles(srcDir);
0121 
0122   it('finds the package sources', () => {
0123     // Guards against a vacuous pass when the directory walk finds nothing.
0124     expect(files.length).toBeGreaterThan(10);
0125     expect(files.map(file => relative(srcDir, file))).toContain('index.ts');
0126   });
0127 
0128   it('uses no Angular DI, HttpClient or component API', () => {
0129     const offenders = files.flatMap(file =>
0130       findForbiddenApis(readFileSync(file, 'utf8')).map(api => `${relative(srcDir, file)}: ${api}`));
0131     expect(offenders).toEqual([]);
0132   });
0133 
0134   it('detects each forbidden API and allows signals and comments', () => {
0135     expect(findForbiddenApis("import { InjectionToken } from '@angular/core';")).toEqual(['InjectionToken']);
0136     expect(findForbiddenApis("@Injectable({ providedIn: 'root' }) export class S {}")).toEqual(['Injectable']);
0137     expect(findForbiddenApis('const http = inject(HttpClient);')).toEqual(['inject()', 'HttpClient']);
0138     expect(findForbiddenApis('@Component({ template: "" }) export class C {}')).toEqual(['component decorator']);
0139     expect(findForbiddenApis("import { Signal, computed, signal } from '@angular/core';")).toEqual([]);
0140     expect(findForbiddenApis('// @Injectable, inject(), HttpClient\n/* InjectionToken */')).toEqual([]);
0141     expect(findForbiddenApis("const url = 'https://example.org'; const t = new InjectionToken('t');"))
0142       .toEqual(['InjectionToken']);
0143   });
0144 });