@travetto/compiler 3.0.0-rc.2 → 3.0.0-rc.21

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,237 @@
1
+ import { Stats, watchFile, unwatchFile, rmSync, mkdirSync, writeFileSync, existsSync } from 'fs';
2
+ import fs from 'fs/promises';
3
+ import { Worker } from 'worker_threads';
4
+ import path from 'path';
5
+
6
+ import type { ManifestContext } from '@travetto/manifest';
7
+
8
+ import { CompilerLogger, LogUtil } from './log';
9
+
10
+ type LockStatus = 'complete' | 'stale';
11
+ type LockDetails = {
12
+ pid: number | undefined;
13
+ file: string;
14
+ };
15
+
16
+ export type LockType = 'build' | 'watch';
17
+ export type LockCompileAction = 'skip' | 'build';
18
+ type LockAction = LockCompileAction | 'retry';
19
+
20
+ const STALE_THRESHOLD = 1000;
21
+
22
+ /**
23
+ * Manager for all lock activity
24
+ */
25
+ export class LockManager {
26
+
27
+ /**
28
+ * Get the lock file name
29
+ */
30
+ static #getFileName(ctx: ManifestContext, type: LockType): string {
31
+ return path.resolve(ctx.workspacePath, ctx.toolFolder, `${type}.pid`);
32
+ }
33
+
34
+ /**
35
+ * Determine if the given stats are stale for modification time
36
+ */
37
+ static #isStale(stat?: Stats): boolean {
38
+ return !!stat && stat.mtimeMs < (Date.now() - STALE_THRESHOLD * 1.1);
39
+ }
40
+
41
+ /**
42
+ * Get the lock file details
43
+ */
44
+ static async #getDetails(ctx: ManifestContext, type: LockType): Promise<LockDetails> {
45
+ const file = this.#getFileName(ctx, type);
46
+ const stat = await fs.stat(file).catch(() => undefined);
47
+ const stale = this.#isStale(stat);
48
+ let pid: number | undefined;
49
+ if (stat) {
50
+ const content = await fs.readFile(file, 'utf8');
51
+ const filePid = parseInt(content, 10);
52
+ if (stale) {
53
+ LogUtil.log('lock', [], 'warn', `${type} file is stale: ${stat.mtimeMs} vs ${Date.now()}`);
54
+ } else {
55
+ pid = filePid;
56
+ }
57
+ }
58
+ return { pid, file };
59
+ }
60
+
61
+ /**
62
+ * Acquire the lock file, and register a cleanup on exit
63
+ */
64
+ static #acquireFile(ctx: ManifestContext, type: LockType): void {
65
+ const file = this.#getFileName(ctx, type);
66
+ mkdirSync(path.dirname(file), { recursive: true });
67
+ LogUtil.log('lock', [], 'debug', `Acquiring ${type}`);
68
+ writeFileSync(file, `${process.pid}`, 'utf8');
69
+ }
70
+
71
+ /**
72
+ * Release the lock file (i.e. deleting)
73
+ */
74
+ static #releaseFile(ctx: ManifestContext, type: LockType): void {
75
+ const file = this.#getFileName(ctx, type);
76
+ if (existsSync(file)) {
77
+ rmSync(file, { force: true });
78
+ LogUtil.log('lock', [], 'debug', `Releasing ${type}`);
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Wait until a lock file is released, or it becomes stale
84
+ */
85
+ static async #waitForRelease(ctx: ManifestContext, type: LockType): Promise<LockStatus> {
86
+ const file = this.#getFileName(ctx, type);
87
+ let remove: (() => void) | undefined = undefined;
88
+
89
+ const prom = new Promise<LockStatus>(resolve => {
90
+ let timer: NodeJS.Timeout | undefined = undefined;
91
+ const handler = async (): Promise<void> => {
92
+ if (timer) {
93
+ clearTimeout(timer);
94
+ }
95
+ const stats = await fs.stat(file).catch(() => undefined);
96
+ if (!stats) {
97
+ resolve('complete');
98
+ } else if (this.#isStale(stats)) {
99
+ resolve('stale');
100
+ } else {
101
+ timer = setTimeout(handler, STALE_THRESHOLD * 1.1);
102
+ }
103
+ };
104
+
105
+ watchFile(file, handler);
106
+ handler();
107
+
108
+ remove = (): void => {
109
+ clearInterval(timer);
110
+ unwatchFile(file, handler);
111
+ };
112
+ });
113
+
114
+ return prom.finally(remove);
115
+ }
116
+
117
+ /**
118
+ * Read the watch lock file and determine its result, communicating with the user as necessary
119
+ */
120
+ static async #getWatchAction(ctx: ManifestContext, log: CompilerLogger, lockType: LockType | undefined, buildState: LockDetails): Promise<LockAction> {
121
+ const level = lockType ? 'info' : 'debug';
122
+ if (lockType === 'watch') {
123
+ log(level, 'Already running');
124
+ return 'skip';
125
+ } else {
126
+ if (buildState.pid) {
127
+ log('info', 'Already running, waiting for build to finish');
128
+ switch (await this.#waitForRelease(ctx, 'build')) {
129
+ case 'complete': {
130
+ log(level, 'Completed build');
131
+ return 'skip';
132
+ }
133
+ case 'stale': {
134
+ log(level, 'Became stale, retrying');
135
+ return 'retry';
136
+ }
137
+ }
138
+ } else {
139
+ log(level, 'Already running, and has built');
140
+ return 'skip';
141
+ }
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Read the build lock file and determine its result, communicating with the user as necessary
147
+ */
148
+ static async #getBuildAction(ctx: ManifestContext, log: CompilerLogger, lockType: LockType | undefined): Promise<LockAction> {
149
+ const level = lockType ? 'info' : 'debug';
150
+ if (lockType === 'watch') {
151
+ log('info', 'Build already running, waiting to begin watch');
152
+ const res = await this.#waitForRelease(ctx, 'build');
153
+ log(level, `Finished with status of ${res}, retrying`);
154
+ return 'retry';
155
+ } else {
156
+ log('info', 'Already running, waiting for completion');
157
+ switch (await this.#waitForRelease(ctx, lockType ?? 'build')) {
158
+ case 'complete': {
159
+ log(level, 'Completed');
160
+ return 'skip';
161
+ }
162
+ case 'stale': {
163
+ log(level, 'Became stale, retrying');
164
+ return 'retry';
165
+ }
166
+ }
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Run code with support for lock acquire and release
172
+ */
173
+ static async withLocks(ctx: ManifestContext, fn: (acquire: (type: LockType) => void, release: (type: LockType) => void) => Promise<unknown>): Promise<void> {
174
+ const activeLockTypes = new Set<LockType>();
175
+
176
+ const pinger = path.resolve(ctx.workspacePath, ctx.compilerFolder, 'node_modules', '@travetto/compiler/support/lock-pinger.js');
177
+ const worker = new Worker(pinger, {
178
+ workerData: {
179
+ interval: STALE_THRESHOLD,
180
+ files: []
181
+ }
182
+ });
183
+
184
+ const notify = (): void => worker.postMessage({ files: [...activeLockTypes].map(t => this.#getFileName(ctx, t)) });
185
+
186
+ const stop = (): void => {
187
+ worker.postMessage('stop');
188
+ for (const type of activeLockTypes) {
189
+ this.#releaseFile(ctx, type);
190
+ }
191
+ worker.terminate().then(() => { });
192
+ };
193
+
194
+ process.on('SIGINT', stop);
195
+ process.on('exit', stop);
196
+
197
+ try {
198
+ await new Promise(r => worker.on('online', r));
199
+ await fn(
200
+ type => {
201
+ if (!activeLockTypes.has(type)) {
202
+ activeLockTypes.add(type);
203
+ this.#acquireFile(ctx, type);
204
+ notify();
205
+ }
206
+ },
207
+ type => {
208
+ if (activeLockTypes.has(type)) {
209
+ activeLockTypes.delete(type);
210
+ this.#releaseFile(ctx, type);
211
+ notify();
212
+ }
213
+ }
214
+ );
215
+ } finally {
216
+ stop();
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Reads the lock file states (build + watch) to determine what action should be taken for the compiler
222
+ */
223
+ static async getCompileAction(ctx: ManifestContext, lockType: LockType | undefined): Promise<LockCompileAction> {
224
+ let result: LockAction;
225
+ do {
226
+ result = 'build';
227
+ const buildState = await this.#getDetails(ctx, 'build');
228
+ const watchState = await this.#getDetails(ctx, 'watch');
229
+ if (watchState.pid) { // Existing watch operation
230
+ result = await LogUtil.withLogger('lock', log => this.#getWatchAction(ctx, log, lockType, buildState), true, ['watch', `pid=${watchState.pid}`]);
231
+ } else if (buildState.pid) { // Existing build operation
232
+ result = await LogUtil.withLogger('lock', log => this.#getBuildAction(ctx, log, lockType), true, ['build', `pid=${buildState.pid}`]);
233
+ }
234
+ } while (result === 'retry');
235
+ return result;
236
+ }
237
+ }
package/support/log.ts ADDED
@@ -0,0 +1,31 @@
1
+ export type CompilerLogEvent = [level: 'info' | 'debug' | 'warn', message: string];
2
+ export type CompilerLogger = (...args: CompilerLogEvent) => void;
3
+ export type WithLogger<T> = (log: CompilerLogger) => Promise<T>;
4
+
5
+ const LEVELS = { warn: true, debug: /^debug$/.test(process.env.TRV_BUILD ?? ''), info: !/^warn$/.test(process.env.TRV_BUILD ?? '') };
6
+ const SCOPE_MAX = 15;
7
+
8
+ export class LogUtil {
9
+
10
+ /**
11
+ * Is object a log event
12
+ */
13
+ static isLogEvent = (o: unknown): o is CompilerLogEvent => o !== null && o !== undefined && Array.isArray(o);
14
+
15
+ /**
16
+ * Log message with filtering by level
17
+ */
18
+ static log(scope: string, args: string[], ...[level, msg]: CompilerLogEvent): void {
19
+ const message = msg.replaceAll(process.cwd(), '.');
20
+ LEVELS[level] && console.debug(new Date().toISOString(), `[${scope.padEnd(SCOPE_MAX, ' ')}]`, ...args, message);
21
+ }
22
+
23
+ /**
24
+ * With logger
25
+ */
26
+ static withLogger<T>(scope: string, op: WithLogger<T>, basic = true, args: string[] = []): Promise<T> {
27
+ const log = this.log.bind(null, scope, args);
28
+ basic && log('debug', 'Started');
29
+ return op(log).finally(() => basic && log('debug', 'Completed'));
30
+ }
31
+ }
@@ -0,0 +1,202 @@
1
+ import path from 'path';
2
+ import fs from 'fs/promises';
3
+ import os from 'os';
4
+ import cp from 'child_process';
5
+ import { createRequire } from 'module';
6
+
7
+ import { DeltaEvent, ManifestContext, ManifestRoot, Package } from '@travetto/manifest';
8
+
9
+ import { LogUtil } from './log';
10
+
11
+ type ModFile = { input: string, output: string, stale: boolean };
12
+ export type CompileResult = 'restart' | 'complete' | 'skipped';
13
+
14
+ const OPT_CACHE: Record<string, import('typescript').CompilerOptions> = {};
15
+ const SRC_REQ = createRequire(path.resolve('node_modules'));
16
+ const RECENT_STAT = (stat: { ctimeMs: number, mtimeMs: number }): number => Math.max(stat.ctimeMs, stat.mtimeMs);
17
+
18
+ /**
19
+ * Transpile utilities for launching
20
+ */
21
+ export class TranspileUtil {
22
+ /**
23
+ * Write text file, and ensure folder exists
24
+ */
25
+ static writeTextFile = (file: string, content: string): Promise<void> =>
26
+ fs.mkdir(path.dirname(file), { recursive: true }).then(() => fs.writeFile(file, content, 'utf8'));
27
+
28
+ /**
29
+ * Returns the compiler options
30
+ */
31
+ static async getCompilerOptions(ctx: ManifestContext): Promise<{}> {
32
+ if (!(ctx.workspacePath in OPT_CACHE)) {
33
+ let tsconfig = path.resolve(ctx.workspacePath, 'tsconfig.json');
34
+
35
+ if (!await fs.stat(tsconfig).then(_ => true, _ => false)) {
36
+ tsconfig = SRC_REQ.resolve('@travetto/compiler/tsconfig.trv.json');
37
+ }
38
+
39
+ const ts = (await import('typescript')).default;
40
+
41
+ const { options } = ts.parseJsonSourceFileConfigFileContent(
42
+ ts.readJsonConfigFile(tsconfig, ts.sys.readFile), ts.sys, ctx.workspacePath
43
+ );
44
+
45
+ OPT_CACHE[ctx.workspacePath] = {
46
+ ...options,
47
+ allowJs: true,
48
+ resolveJsonModule: true,
49
+ sourceRoot: ctx.workspacePath,
50
+ rootDir: ctx.workspacePath,
51
+ outDir: path.resolve(ctx.workspacePath),
52
+ module: ctx.moduleType === 'commonjs' ? ts.ModuleKind.CommonJS : ts.ModuleKind.ESNext,
53
+ };
54
+ }
55
+ return OPT_CACHE[ctx.workspacePath];
56
+ }
57
+
58
+ /**
59
+ * Output a file, support for ts, js, and package.json
60
+ */
61
+ static async transpileFile(ctx: ManifestContext, inputFile: string, outputFile: string): Promise<void> {
62
+ if (inputFile.endsWith('.ts') || inputFile.endsWith('.js')) {
63
+ const compilerOut = path.resolve(ctx.workspacePath, ctx.compilerFolder, 'node_modules');
64
+
65
+ const text = (await fs.readFile(inputFile, 'utf8'))
66
+ .replace(/from '([.][^']+)'/g, (_, i) => `from '${i.replace(/[.]js$/, '')}.js'`)
67
+ .replace(/from '(@travetto\/(.*?))'/g, (_, i, s) => `from '${path.resolve(compilerOut, `${i}${s.includes('/') ? '.js' : '/__index__.js'}`)}'`);
68
+
69
+ const ts = (await import('typescript')).default;
70
+ const content = ts.transpile(text, await this.getCompilerOptions(ctx), inputFile);
71
+ await this.writeTextFile(outputFile, content);
72
+ } else if (inputFile.endsWith('package.json')) {
73
+ const pkg: Package = JSON.parse(await fs.readFile(inputFile, 'utf8'));
74
+ const main = pkg.main?.replace(/[.]ts$/, '.js');
75
+ const files = pkg.files?.map(x => x.replace('.ts', '.js'));
76
+
77
+ const content = JSON.stringify({ ...pkg, main, type: ctx.moduleType, files }, null, 2);
78
+ await this.writeTextFile(outputFile, content);
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Scan directory to find all project sources for comparison
84
+ */
85
+ static async getModuleSources(ctx: ManifestContext, module: string, seed: string[]): Promise<ModFile[]> {
86
+ const inputFolder = (ctx.mainModule === module) ?
87
+ process.cwd() :
88
+ path.dirname(SRC_REQ.resolve(`${module}/package.json`));
89
+
90
+ const folders = seed.filter(x => !/[.]/.test(x)).map(x => path.resolve(inputFolder, x));
91
+ const files = seed.filter(x => /[.]/.test(x)).map(x => path.resolve(inputFolder, x));
92
+
93
+ while (folders.length) {
94
+ const sub = folders.pop();
95
+ if (!sub) {
96
+ continue;
97
+ }
98
+
99
+ for (const file of await fs.readdir(sub).catch(() => [])) {
100
+ if (file.startsWith('.')) {
101
+ continue;
102
+ }
103
+ const resolvedInput = path.resolve(sub, file);
104
+ const stat = await fs.stat(resolvedInput);
105
+
106
+ if (stat.isDirectory()) {
107
+ folders.push(resolvedInput);
108
+ } else if (file.endsWith('.d.ts')) {
109
+ // Do nothing
110
+ } else if (file.endsWith('.ts') || file.endsWith('.js')) {
111
+ files.push(resolvedInput);
112
+ }
113
+ }
114
+ }
115
+
116
+ const outputFolder = path.resolve(ctx.workspacePath, ctx.compilerFolder, 'node_modules', module);
117
+ const out: ModFile[] = [];
118
+ for (const input of files) {
119
+ const output = input.replace(inputFolder, outputFolder).replace(/[.]ts$/, '.js');
120
+ const inputTs = await fs.stat(input).then(RECENT_STAT, () => 0);
121
+ if (inputTs) {
122
+ const outputTs = await fs.stat(output).then(RECENT_STAT, () => 0);
123
+ await fs.mkdir(path.dirname(output), { recursive: true, });
124
+ out.push({ input, output, stale: inputTs > outputTs });
125
+ }
126
+ }
127
+
128
+ return out;
129
+ }
130
+
131
+ /**
132
+ * Recompile folder if stale
133
+ */
134
+ static async compileIfStale(ctx: ManifestContext, scope: string, mod: string, seed: string[]): Promise<string[]> {
135
+ const files = await this.getModuleSources(ctx, mod, seed);
136
+ const changes = files.filter(x => x.stale).map(x => x.input);
137
+ const out: string[] = [];
138
+
139
+ try {
140
+ await LogUtil.withLogger(scope, async log => {
141
+ if (files.some(f => f.stale)) {
142
+ log('debug', 'Starting');
143
+ for (const file of files.filter(x => x.stale)) {
144
+ await this.transpileFile(ctx, file.input, file.output);
145
+ }
146
+ if (changes.length) {
147
+ out.push(...changes.map(x => `${mod}/${x}`));
148
+ log('debug', `Source changed: ${changes.join(', ')}`);
149
+ }
150
+ log('debug', 'Completed');
151
+ } else {
152
+ log('debug', 'Skipped');
153
+ }
154
+ }, false, [mod]);
155
+ } catch (err) {
156
+ console.error(err);
157
+ }
158
+ return out;
159
+ }
160
+
161
+ /**
162
+ * Run compiler
163
+ */
164
+ static async runCompiler(ctx: ManifestContext, manifest: ManifestRoot, changed: DeltaEvent[], watch: boolean, onMessage: (msg: unknown) => void): Promise<CompileResult> {
165
+ const compiler = path.resolve(ctx.workspacePath, ctx.compilerFolder);
166
+ const main = path.resolve(compiler, 'node_modules', '@travetto/compiler/support/compiler-entry.js');
167
+ const deltaFile = path.resolve(os.tmpdir(), `manifest-delta.${Date.now()}.${Math.random()}.json`);
168
+
169
+ const changedFiles = changed[0]?.file === '*' ? ['*'] : changed.map(ev =>
170
+ path.resolve(manifest.workspacePath, manifest.modules[ev.module].sourceFolder, ev.file)
171
+ );
172
+
173
+ let proc: cp.ChildProcess | undefined;
174
+
175
+ try {
176
+ await this.writeTextFile(deltaFile, changedFiles.join('\n'));
177
+
178
+ return await LogUtil.withLogger('compiler-exec', log => new Promise<CompileResult>((res, rej) => {
179
+ proc = cp.spawn(process.argv0, [main, deltaFile, `${watch}`], {
180
+ env: {
181
+ ...process.env,
182
+ TRV_MANIFEST: path.resolve(ctx.workspacePath, ctx.outputFolder, 'node_modules', ctx.mainModule),
183
+ },
184
+ stdio: [0, 1, 2, 'ipc'],
185
+ })
186
+ .on('message', msg => {
187
+ if (LogUtil.isLogEvent(msg)) {
188
+ log(...msg);
189
+ } else if (msg === 'restart') {
190
+ res(msg);
191
+ } else {
192
+ onMessage(msg);
193
+ }
194
+ })
195
+ .on('exit', code => (code !== null && code > 0) ? rej(new Error('Failed during compilation')) : res('complete'));
196
+ }));
197
+ } finally {
198
+ if (proc?.killed === false) { proc.kill('SIGKILL'); }
199
+ await fs.rm(deltaFile, { force: true });
200
+ }
201
+ }
202
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "compilerOptions": {
3
+ "module": "CommonJS",
4
+ "target": "esnext",
5
+ "moduleResolution": "node",
6
+ "lib": [
7
+ "es2022"
8
+ ],
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "strictPropertyInitialization": false,
12
+ "experimentalDecorators": true,
13
+ "noEmitOnError": false,
14
+ "noErrorTruncation": true,
15
+ "resolveJsonModule": true,
16
+ "sourceMap": true,
17
+ "inlineSourceMap": false,
18
+ "removeComments": true,
19
+ "importHelpers": true,
20
+ "noEmitHelpers": true,
21
+ "outDir": "build/"
22
+ },
23
+ }
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2020 ArcSine Technologies
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
package/index.ts DELETED
@@ -1,3 +0,0 @@
1
- export * from './src/host';
2
- export * from './src/compiler';
3
- export * from './src/transformer';
package/src/host.ts DELETED
@@ -1,142 +0,0 @@
1
- import * as ts from 'typescript';
2
- import { readFileSync } from 'fs';
3
- import * as path from 'path';
4
-
5
- import { PathUtil, AppCache } from '@travetto/boot';
6
- import { SourceUtil } from '@travetto/boot/src/internal/source-util';
7
- import { SystemUtil } from '@travetto/boot/src/internal/system';
8
- import { TranspileUtil } from '@travetto/boot/src/internal/transpile-util';
9
- import { SourceIndex } from '@travetto/boot/src/internal/source';
10
- import { AppManifest } from '@travetto/base';
11
-
12
- /**
13
- * Manages the source code and typescript relationship.
14
- */
15
- export class SourceHost implements ts.CompilerHost {
16
-
17
- #rootFiles = new Set<string>();
18
- #hashes = new Map<string, number>();
19
- #sources = new Map<string, ts.SourceFile>();
20
- readonly contents = new Map<string, string>();
21
-
22
- #trackFile(filename: string, content: string): void {
23
- this.contents.set(filename, content);
24
- this.#hashes.set(filename, SystemUtil.naiveHash(readFileSync(filename, 'utf8'))); // Get og content for hashing
25
- }
26
-
27
- getCanonicalFileName: (file: string) => string = (f: string) => f;
28
- getCurrentDirectory: () => string = () => PathUtil.cwd;
29
- getDefaultLibFileName: (opts: ts.CompilerOptions) => string = (opts: ts.CompilerOptions) => ts.getDefaultLibFileName(opts);
30
- getNewLine: () => string = () => ts.sys.newLine;
31
- useCaseSensitiveFileNames: () => boolean = () => ts.sys.useCaseSensitiveFileNames;
32
- getDefaultLibLocation(): string {
33
- return path.dirname(ts.getDefaultLibFilePath(TranspileUtil.compilerOptions));
34
- }
35
-
36
- /**
37
- * Get root files
38
- */
39
- getRootFiles(): Set<string> {
40
- if (!this.#rootFiles.size) {
41
- // Only needed for compilation
42
- this.#rootFiles = new Set(SourceIndex.findByFolders(AppManifest.source, 'required').map(x => x.file));
43
- }
44
- return this.#rootFiles;
45
- }
46
-
47
- /**
48
- * Read file from disk, using the transpile pre-processor on .ts files
49
- */
50
- readFile(filename: string): string {
51
- filename = PathUtil.toUnixTs(filename);
52
- let content = ts.sys.readFile(filename);
53
- if (content === undefined) {
54
- throw new Error(`Unable to read file ${filename}`);
55
- }
56
- if (filename.endsWith(SourceUtil.EXT) && !filename.endsWith('.d.ts')) {
57
- content = SourceUtil.preProcess(filename, content);
58
- }
59
- return content;
60
- }
61
-
62
- /**
63
- * Write file to disk, and set value in cache as well
64
- */
65
- writeFile(filename: string, content: string): void {
66
- filename = PathUtil.toUnixTs(filename);
67
- this.#trackFile(filename, content);
68
- AppCache.writeEntry(filename, content);
69
- }
70
-
71
- /**
72
- * Fetch file
73
- */
74
- fetchFile(filename: string): void {
75
- filename = PathUtil.toUnixTs(filename);
76
- const cached = AppCache.readEntry(filename);
77
- this.#trackFile(filename, cached);
78
- }
79
-
80
- /**
81
- * Get a source file on demand
82
- * @returns
83
- */
84
- getSourceFile(filename: string, __tgt: unknown, __onErr: unknown, force?: boolean): ts.SourceFile {
85
- if (!this.#sources.has(filename) || force) {
86
- const content = this.readFile(filename)!;
87
- this.#sources.set(filename, ts.createSourceFile(filename, content ?? '',
88
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
89
- (TranspileUtil.compilerOptions as ts.CompilerOptions).target!
90
- ));
91
- }
92
- return this.#sources.get(filename)!;
93
- }
94
-
95
- /**
96
- * See if a file exists
97
- */
98
- fileExists(filename: string): boolean {
99
- filename = PathUtil.toUnixTs(filename);
100
- return this.contents.has(filename) || ts.sys.fileExists(filename);
101
- }
102
-
103
- /**
104
- * See if a file's hash code has changed
105
- */
106
- hashChanged(filename: string, content?: string): boolean {
107
- content ??= readFileSync(filename, 'utf8');
108
- // Let's see if they are really different
109
- const hash = SystemUtil.naiveHash(content);
110
- if (hash === this.#hashes.get(filename)) {
111
- console.debug('Contents Unchanged', { filename });
112
- return false;
113
- }
114
- return true;
115
- }
116
-
117
- /**
118
- * Unload a file from the transpiler
119
- */
120
- unload(filename: string, unlink = true): void {
121
- if (this.contents.has(filename)) {
122
- AppCache.removeExpiredEntry(filename, unlink);
123
-
124
- if (unlink && this.#hashes.has(filename)) {
125
- this.#hashes.delete(filename);
126
- }
127
- this.#rootFiles.delete(filename);
128
- this.contents.delete(filename);
129
- this.#sources.delete(filename);
130
- }
131
- }
132
-
133
- /**
134
- * Reset the transpiler
135
- */
136
- reset(): void {
137
- this.contents.clear();
138
- this.#rootFiles.clear();
139
- this.#hashes.clear();
140
- this.#sources.clear();
141
- }
142
- }