@kapeta/local-cluster-service 0.15.3 → 0.16.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.
Files changed (40) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/cjs/src/RepositoryWatcher.d.ts +23 -0
  3. package/dist/cjs/src/RepositoryWatcher.js +269 -0
  4. package/dist/cjs/src/assetManager.d.ts +4 -6
  5. package/dist/cjs/src/assetManager.js +34 -44
  6. package/dist/cjs/src/assets/routes.js +2 -2
  7. package/dist/cjs/src/cacheManager.d.ts +16 -0
  8. package/dist/cjs/src/cacheManager.js +38 -0
  9. package/dist/cjs/src/definitionsManager.d.ts +2 -4
  10. package/dist/cjs/src/definitionsManager.js +7 -21
  11. package/dist/cjs/src/instanceManager.js +4 -1
  12. package/dist/cjs/src/repositoryManager.d.ts +9 -4
  13. package/dist/cjs/src/repositoryManager.js +18 -91
  14. package/dist/cjs/src/types.d.ts +2 -0
  15. package/dist/cjs/src/utils/utils.js +1 -1
  16. package/dist/esm/src/RepositoryWatcher.d.ts +23 -0
  17. package/dist/esm/src/RepositoryWatcher.js +262 -0
  18. package/dist/esm/src/assetManager.d.ts +4 -6
  19. package/dist/esm/src/assetManager.js +35 -45
  20. package/dist/esm/src/assets/routes.js +2 -2
  21. package/dist/esm/src/cacheManager.d.ts +16 -0
  22. package/dist/esm/src/cacheManager.js +31 -0
  23. package/dist/esm/src/definitionsManager.d.ts +2 -4
  24. package/dist/esm/src/definitionsManager.js +7 -21
  25. package/dist/esm/src/instanceManager.js +4 -1
  26. package/dist/esm/src/repositoryManager.d.ts +9 -4
  27. package/dist/esm/src/repositoryManager.js +18 -91
  28. package/dist/esm/src/types.d.ts +2 -0
  29. package/dist/esm/src/utils/utils.js +1 -1
  30. package/package.json +2 -2
  31. package/src/RepositoryWatcher.ts +302 -0
  32. package/src/assetManager.ts +51 -55
  33. package/src/assets/routes.ts +2 -2
  34. package/src/cacheManager.ts +45 -0
  35. package/src/definitionsManager.ts +9 -33
  36. package/src/identities/routes.ts +0 -1
  37. package/src/instanceManager.ts +4 -1
  38. package/src/repositoryManager.ts +20 -95
  39. package/src/types.ts +2 -2
  40. package/src/utils/utils.ts +1 -1
@@ -0,0 +1,31 @@
1
+ import NodeCache from 'node-cache';
2
+ const DEFAULT_CACHE_TTL = 60 * 1000; // 1 min
3
+ class CacheManager {
4
+ cache = new NodeCache();
5
+ flush() {
6
+ this.cache.flushAll();
7
+ }
8
+ doCached(key, getter, ttl = DEFAULT_CACHE_TTL) {
9
+ const data = this.cache.get(key);
10
+ if (data !== undefined) {
11
+ return data;
12
+ }
13
+ const result = getter();
14
+ this.cache.set(key, result, ttl);
15
+ return result;
16
+ }
17
+ get(key) {
18
+ return this.cache.get(key);
19
+ }
20
+ set(key, data, ttl = DEFAULT_CACHE_TTL) {
21
+ this.cache.set(key, data, ttl);
22
+ }
23
+ has(key) {
24
+ return this.cache.has(key);
25
+ }
26
+ remove(key) {
27
+ return this.cache.del(key);
28
+ }
29
+ }
30
+ export const cacheManager = new CacheManager();
31
+ export const doCached = (key, getter, ttl = DEFAULT_CACHE_TTL) => cacheManager.doCached(key, getter, ttl);
@@ -1,9 +1,7 @@
1
1
  import { DefinitionInfo } from '@kapeta/local-cluster-config';
2
2
  declare class DefinitionsManager {
3
- private cache;
4
- private getKey;
5
- clearCache(): void;
6
- private doCached;
3
+ private getHash;
4
+ private getFullKey;
7
5
  getDefinitions(kindFilter?: string | string[]): DefinitionInfo[];
8
6
  exists(ref: string): boolean;
9
7
  getProviderDefinitions(): DefinitionInfo[];
@@ -1,9 +1,8 @@
1
1
  import ClusterConfiguration from '@kapeta/local-cluster-config';
2
2
  import { parseKapetaUri } from '@kapeta/nodejs-utils';
3
- const CACHE_TTL = 60 * 1000; // 1 min
3
+ import { doCached } from './cacheManager';
4
4
  class DefinitionsManager {
5
- cache = {};
6
- getKey(kindFilter) {
5
+ getHash(kindFilter) {
7
6
  if (kindFilter) {
8
7
  if (Array.isArray(kindFilter)) {
9
8
  return kindFilter.join(',');
@@ -12,31 +11,18 @@ class DefinitionsManager {
12
11
  }
13
12
  return 'none';
14
13
  }
15
- clearCache() {
16
- this.cache = {};
17
- }
18
- doCached(key, getter) {
19
- if (this.cache[key]) {
20
- if (this.cache[key].expires > Date.now()) {
21
- return this.cache[key].definitions;
22
- }
23
- delete this.cache[key];
24
- }
25
- this.cache[key] = {
26
- expires: Date.now() + CACHE_TTL,
27
- definitions: getter(),
28
- };
29
- return this.cache[key].definitions;
14
+ getFullKey(kindFilter) {
15
+ return `DefinitionsManager:${this.getHash(kindFilter)}`;
30
16
  }
31
17
  getDefinitions(kindFilter) {
32
- const key = this.getKey(kindFilter);
33
- return this.doCached(key, () => ClusterConfiguration.getDefinitions(kindFilter));
18
+ const key = this.getFullKey(kindFilter);
19
+ return doCached(key, () => ClusterConfiguration.getDefinitions(kindFilter));
34
20
  }
35
21
  exists(ref) {
36
22
  return !!this.getDefinition(ref);
37
23
  }
38
24
  getProviderDefinitions() {
39
- return this.doCached('providers', () => ClusterConfiguration.getProviderDefinitions());
25
+ return doCached('providers', () => ClusterConfiguration.getProviderDefinitions());
40
26
  }
41
27
  getDefinition(ref) {
42
28
  const uri = parseKapetaUri(ref);
@@ -51,7 +51,10 @@ export class InstanceManager {
51
51
  return [];
52
52
  }
53
53
  const plan = planInfo.definition;
54
- const instanceIds = plan.spec.blocks.map((block) => block.id);
54
+ if (!plan?.spec?.blocks) {
55
+ return [];
56
+ }
57
+ const instanceIds = plan.spec?.blocks?.map((block) => block.id) || [];
55
58
  return this._instances.filter((instance) => instance.systemId === systemId && instanceIds.includes(instance.instanceId));
56
59
  }
57
60
  getInstance(systemId, instanceId) {
@@ -1,13 +1,18 @@
1
1
  import { Task } from './taskManager';
2
+ import { SourceOfChange } from './types';
2
3
  declare class RepositoryManager {
3
- private changeEventsEnabled;
4
4
  private _registryService;
5
5
  private _cache;
6
- private watcher?;
6
+ private watcher;
7
7
  constructor();
8
- setChangeEventsEnabled(enabled: boolean): void;
9
8
  listenForChanges(): void;
10
- stopListening(): void;
9
+ stopListening(): Promise<void>;
10
+ /**
11
+ * Setting the source of change helps us know
12
+ * how to react to changes in the UI.
13
+ */
14
+ setSourceOfChangeFor(file: string, source: SourceOfChange): Promise<void>;
15
+ clearSourceOfChangeFor(file: string): Promise<void>;
11
16
  ensureDefaultProviders(): void;
12
17
  private _install;
13
18
  ensureAsset(handle: string, name: string, version: string, wait?: boolean): Promise<undefined | Task[]>;
@@ -1,17 +1,12 @@
1
- import FS from 'node:fs';
2
1
  import os from 'node:os';
3
- import Path from 'node:path';
4
- import watch from 'recursive-watch';
5
- import FSExtra from 'fs-extra';
6
- import ClusterConfiguration from '@kapeta/local-cluster-config';
7
- import { parseKapetaUri } from '@kapeta/nodejs-utils';
8
2
  import { socketManager } from './socketManager';
9
3
  import { Actions, Config, RegistryService } from '@kapeta/nodejs-registry-utils';
10
4
  import { definitionsManager } from './definitionsManager';
11
5
  import { taskManager } from './taskManager';
12
6
  import { normalizeKapetaUri } from './utils/utils';
13
- import { assetManager } from './assetManager';
14
7
  import { ProgressListener } from './progressListener';
8
+ import { RepositoryWatcher } from './RepositoryWatcher';
9
+ import { cacheManager } from './cacheManager';
15
10
  const EVENT_DEFAULT_PROVIDERS_START = 'default-providers-start';
16
11
  const EVENT_DEFAULT_PROVIDERS_END = 'default-providers-end';
17
12
  const DEFAULT_PROVIDERS = [
@@ -30,92 +25,30 @@ const DEFAULT_PROVIDERS = [
30
25
  ];
31
26
  const INSTALL_ATTEMPTED = {};
32
27
  class RepositoryManager {
33
- changeEventsEnabled;
34
28
  _registryService;
35
29
  _cache;
36
30
  watcher;
37
31
  constructor() {
38
- this.changeEventsEnabled = true;
39
- this.listenForChanges();
40
32
  this._registryService = new RegistryService(Config.data.registry.url);
41
33
  this._cache = {};
42
- }
43
- setChangeEventsEnabled(enabled) {
44
- this.changeEventsEnabled = enabled;
34
+ this.watcher = new RepositoryWatcher();
35
+ this.listenForChanges();
45
36
  }
46
37
  listenForChanges() {
47
- const baseDir = ClusterConfiguration.getRepositoryBasedir();
48
- if (!FS.existsSync(baseDir)) {
49
- FSExtra.mkdirpSync(baseDir);
50
- }
51
- let allDefinitions = ClusterConfiguration.getDefinitions();
52
- console.log('Watching local repository for provider changes: %s', baseDir);
53
- try {
54
- this.watcher = watch(baseDir, (filename) => {
55
- if (!filename) {
56
- return;
57
- }
58
- const [handle, name, version] = filename.toString().split(/\//g);
59
- if (!name || !version) {
60
- return;
61
- }
62
- if (!this.changeEventsEnabled) {
63
- return;
64
- }
65
- const ymlPath = Path.join(baseDir, handle, name, version, 'kapeta.yml');
66
- const newDefinitions = ClusterConfiguration.getDefinitions();
67
- const newDefinition = newDefinitions.find((d) => d.ymlPath === ymlPath);
68
- let currentDefinition = allDefinitions.find((d) => d.ymlPath === ymlPath);
69
- const ymlExists = FS.existsSync(ymlPath);
70
- let type;
71
- if (ymlExists) {
72
- if (currentDefinition) {
73
- type = 'updated';
74
- }
75
- else if (newDefinition) {
76
- type = 'added';
77
- currentDefinition = newDefinition;
78
- }
79
- else {
80
- //Other definition was added / updated - ignore
81
- return;
82
- }
83
- }
84
- else {
85
- if (currentDefinition) {
86
- const ref = parseKapetaUri(`${currentDefinition.definition.metadata.name}:${currentDefinition.version}`).id;
87
- delete INSTALL_ATTEMPTED[ref];
88
- //Something was removed
89
- type = 'removed';
90
- }
91
- else {
92
- //Other definition was removed - ignore
93
- return;
94
- }
95
- }
96
- const payload = {
97
- type,
98
- definition: currentDefinition?.definition,
99
- asset: { handle, name, version },
100
- };
101
- allDefinitions = newDefinitions;
102
- socketManager.emit(`assets`, 'changed', payload);
103
- definitionsManager.clearCache();
104
- });
105
- }
106
- catch (e) {
107
- // Fallback to run without watch mode due to potential platform issues.
108
- // https://nodejs.org/docs/latest/api/fs.html#caveats
109
- console.log('Unable to watch for changes. Changes to assets will not update automatically.', e);
110
- return;
111
- }
38
+ this.watcher.watch();
112
39
  }
113
- stopListening() {
114
- if (!this.watcher) {
115
- return;
116
- }
117
- this.watcher();
118
- this.watcher = undefined;
40
+ async stopListening() {
41
+ return this.watcher.unwatch();
42
+ }
43
+ /**
44
+ * Setting the source of change helps us know
45
+ * how to react to changes in the UI.
46
+ */
47
+ setSourceOfChangeFor(file, source) {
48
+ return this.watcher.setSourceOfChangeFor(file, source);
49
+ }
50
+ clearSourceOfChangeFor(file) {
51
+ return this.watcher.clearSourceOfChangeFor(file);
119
52
  }
120
53
  ensureDefaultProviders() {
121
54
  socketManager.emitGlobal(EVENT_DEFAULT_PROVIDERS_START, { providers: DEFAULT_PROVIDERS });
@@ -140,19 +73,13 @@ class RepositoryManager {
140
73
  try {
141
74
  //We change to a temp dir to avoid issues with the current working directory
142
75
  process.chdir(os.tmpdir());
143
- //Disable change events while installing
144
- this.setChangeEventsEnabled(false);
145
76
  await Actions.install(new ProgressListener(), [ref], {});
146
77
  }
147
78
  catch (e) {
148
79
  console.error(`Failed to install asset: ${ref}`, e);
149
80
  throw e;
150
81
  }
151
- finally {
152
- this.setChangeEventsEnabled(true);
153
- }
154
- definitionsManager.clearCache();
155
- assetManager.clearCache();
82
+ cacheManager.flush();
156
83
  //console.log(`Asset installed: ${ref}`);
157
84
  };
158
85
  };
@@ -8,6 +8,8 @@ export type StringMap = {
8
8
  export type AnyMap = {
9
9
  [key: string]: any;
10
10
  };
11
+ export type SourceOfChange = 'user' | 'filesystem';
12
+ export type WatchEventName = 'add' | 'addDir' | 'change' | 'unlink' | 'unlinkDir';
11
13
  export type LogLevel = 'ERROR' | 'WARN' | 'INFO' | 'DEBUG' | 'TRACE' | 'FATAL';
12
14
  export type LogSource = 'stdout' | 'stderr';
13
15
  export type EnvironmentType = 'docker' | 'process';
@@ -21,7 +21,7 @@ export function readYML(path) {
21
21
  return YAML.parse(rawYaml.toString());
22
22
  }
23
23
  catch (err) {
24
- throw new Error('Failed to parse plan YAML: ' + err);
24
+ throw new Error(`Failed to parse plan YAML: ${err}`);
25
25
  }
26
26
  }
27
27
  export function isWindows() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kapeta/local-cluster-service",
3
- "version": "0.15.3",
3
+ "version": "0.16.1",
4
4
  "description": "Manages configuration, ports and service discovery for locally running Kapeta systems",
5
5
  "type": "commonjs",
6
6
  "exports": {
@@ -53,6 +53,7 @@
53
53
  "@kapeta/sdk-config": "<2",
54
54
  "@kapeta/web-microfrontend": "^0.2.1",
55
55
  "async-lock": "^1.4.0",
56
+ "chokidar": "^3.5.3",
56
57
  "express": "4.17.1",
57
58
  "express-promise-router": "^4.1.1",
58
59
  "fs-extra": "^11.1.0",
@@ -62,7 +63,6 @@
62
63
  "node-cache": "^5.1.2",
63
64
  "node-docker-api": "1.1.22",
64
65
  "node-uuid": "^1.4.8",
65
- "recursive-watch": "^1.1.4",
66
66
  "request": "2.88.2",
67
67
  "request-promise": "4.2.6",
68
68
  "socket.io": "^4.5.2",
@@ -0,0 +1,302 @@
1
+ import chokidar, { FSWatcher } from 'chokidar';
2
+ import ClusterConfiguration, { Definition, DefinitionInfo } from '@kapeta/local-cluster-config';
3
+ import FS from 'fs-extra';
4
+ import Path from 'node:path';
5
+ import YAML from 'yaml';
6
+ import { parseKapetaUri } from '@kapeta/nodejs-utils';
7
+ import _ from 'lodash';
8
+ import { socketManager } from './socketManager';
9
+ import { definitionsManager } from './definitionsManager';
10
+ import { assetManager } from './assetManager';
11
+ import { SourceOfChange, WatchEventName } from './types';
12
+ import { cacheManager } from './cacheManager';
13
+
14
+ interface AssetIdentity {
15
+ handle: string;
16
+ name: string;
17
+ version: string;
18
+ }
19
+ const KAPETA_YML_RX = /^kapeta.ya?ml$/;
20
+ export class RepositoryWatcher {
21
+ private watcher?: FSWatcher;
22
+ private disabled: boolean = false;
23
+ private readonly baseDir: string;
24
+ private allDefinitions: DefinitionInfo[] = [];
25
+ private symbolicLinks: { [link: string]: string } = {};
26
+ private sourceOfChange: Map<string, SourceOfChange> = new Map();
27
+ constructor() {
28
+ this.baseDir = ClusterConfiguration.getRepositoryBasedir();
29
+ }
30
+
31
+ setDisabled(disabled: boolean) {
32
+ this.disabled = disabled;
33
+ }
34
+ public watch() {
35
+ if (!FS.existsSync(this.baseDir)) {
36
+ FS.mkdirpSync(this.baseDir);
37
+ }
38
+
39
+ this.allDefinitions = ClusterConfiguration.getDefinitions();
40
+
41
+ try {
42
+ this.watcher = chokidar.watch(this.baseDir, {
43
+ followSymlinks: false,
44
+ ignorePermissionErrors: true,
45
+ disableGlobbing: true,
46
+ persistent: true,
47
+ depth: 2,
48
+ ignored: (path) => this.ignoreFile(path),
49
+ });
50
+ this.watcher.on('all', this.handleFileChange.bind(this));
51
+ this.watcher.on('error', (error) => {
52
+ console.log('Error watching repository', error);
53
+ });
54
+ this.watcher.on('ready', () => {
55
+ console.log('Watching local repository for provider changes: %s', this.baseDir);
56
+ });
57
+ } catch (e) {
58
+ // Fallback to run without watch mode due to potential platform issues.
59
+ // https://nodejs.org/docs/latest/api/fs.html#caveats
60
+ console.log('Unable to watch for changes. Changes to assets will not update automatically.', e);
61
+ return;
62
+ }
63
+ }
64
+
65
+ async setSourceOfChangeFor(file: string, source: SourceOfChange) {
66
+ this.sourceOfChange.set(file, source);
67
+ const realPath = await FS.realpath(file);
68
+ if (realPath !== file) {
69
+ this.sourceOfChange.set(realPath, source);
70
+ }
71
+ }
72
+
73
+ async clearSourceOfChangeFor(file: string) {
74
+ this.sourceOfChange.delete(file);
75
+ const realPath = await FS.realpath(file);
76
+ if (realPath !== file) {
77
+ this.sourceOfChange.delete(realPath);
78
+ }
79
+ }
80
+
81
+ public async unwatch() {
82
+ if (!this.watcher) {
83
+ return;
84
+ }
85
+ this.symbolicLinks = {};
86
+ await this.watcher.close();
87
+ this.watcher = undefined;
88
+ }
89
+
90
+ private async getAssetIdentity(path: string): Promise<AssetIdentity | undefined> {
91
+ const baseName = Path.basename(path);
92
+ let handle, name, version;
93
+ if (path.startsWith(this.baseDir)) {
94
+ const relativePath = Path.relative(this.baseDir, path);
95
+ // Inside the repo we can use the path to determine the handle, name and version
96
+ [handle, name, version] = relativePath.split(/\//g);
97
+ if (!handle || !name || !version) {
98
+ // Do nothing with this
99
+ return;
100
+ }
101
+
102
+ return {
103
+ handle,
104
+ name,
105
+ version,
106
+ };
107
+ }
108
+
109
+ if (!KAPETA_YML_RX.test(baseName)) {
110
+ // Do nothing with this
111
+ return;
112
+ }
113
+ // Outside the repo we need to use the file content to determine the handle, name
114
+ // Version is always 'local'
115
+ version = 'local';
116
+
117
+ try {
118
+ const definition: Definition = YAML.parse((await FS.readFile(path)).toString());
119
+ const uri = parseKapetaUri(definition.metadata.name);
120
+ handle = uri.handle;
121
+ name = uri.name;
122
+ return {
123
+ handle,
124
+ name,
125
+ version,
126
+ };
127
+ } catch (e) {
128
+ // Ignore issues in the YML file
129
+ return;
130
+ }
131
+ }
132
+
133
+ private async handleFileChange(eventName: WatchEventName, path: string) {
134
+ if (!path) {
135
+ return;
136
+ }
137
+
138
+ //console.log('File changed', eventName, path);
139
+
140
+ const assetIdentity = await this.getAssetIdentity(path);
141
+ if (!assetIdentity) {
142
+ return;
143
+ }
144
+
145
+ if (this.disabled) {
146
+ return;
147
+ }
148
+
149
+ // If this is false it's because we're watching a symlink target
150
+ const withinRepo = path.startsWith(this.baseDir);
151
+ if (withinRepo && assetIdentity.version === 'local' && path.endsWith('/local')) {
152
+ // This is likely a symlink target
153
+ if (eventName === 'add') {
154
+ //console.log('Checking if we should add symlink target', handle, name, version, path);
155
+ await this.addSymlinkTarget(path);
156
+ }
157
+
158
+ if (eventName === 'unlink') {
159
+ await this.removeSymlinkTarget(path);
160
+ }
161
+
162
+ if (eventName === 'change') {
163
+ await this.updateSymlinkTarget(path);
164
+ }
165
+ }
166
+
167
+ const sourceOfChange = this.sourceOfChange.get(path) ?? 'filesystem';
168
+ await this.checkForChange(assetIdentity, sourceOfChange);
169
+
170
+ // We consume the sourceOfChange when the file is changed
171
+ this.sourceOfChange.delete(path);
172
+ }
173
+
174
+ private async checkForChange(assetIdentity: AssetIdentity, sourceOfChange: SourceOfChange) {
175
+ const ymlPath = Path.join(
176
+ this.baseDir,
177
+ assetIdentity.handle,
178
+ assetIdentity.name,
179
+ assetIdentity.version,
180
+ 'kapeta.yml'
181
+ );
182
+ const newDefinitions = ClusterConfiguration.getDefinitions();
183
+ const newDefinition = newDefinitions.find((d) => d.ymlPath === ymlPath);
184
+ let currentDefinition = this.allDefinitions.find((d) => d.ymlPath === ymlPath);
185
+ const ymlExists = await this.exists(ymlPath);
186
+ let type;
187
+ if (ymlExists) {
188
+ if (currentDefinition) {
189
+ if (newDefinition && _.isEqual(currentDefinition, newDefinition)) {
190
+ //Definition was not changed
191
+ return;
192
+ }
193
+ type = 'updated';
194
+ } else if (newDefinition) {
195
+ type = 'added';
196
+ currentDefinition = newDefinition;
197
+ } else {
198
+ //Other definition was added / updated - ignore
199
+ return;
200
+ }
201
+ } else {
202
+ if (currentDefinition) {
203
+ const ref = parseKapetaUri(
204
+ `${currentDefinition.definition.metadata.name}:${currentDefinition.version}`
205
+ ).id;
206
+ //Something was removed
207
+ type = 'removed';
208
+ } else {
209
+ //Other definition was removed - ignore
210
+ return;
211
+ }
212
+ }
213
+
214
+ const payload = {
215
+ type,
216
+ definition: newDefinition?.definition ?? currentDefinition?.definition,
217
+ asset: assetIdentity,
218
+ sourceOfChange,
219
+ };
220
+
221
+ this.allDefinitions = newDefinitions;
222
+
223
+ //console.log('Asset changed', payload);
224
+ socketManager.emitGlobal('asset-change', payload);
225
+
226
+ cacheManager.flush();
227
+ }
228
+
229
+ private async exists(path: string): Promise<boolean> {
230
+ try {
231
+ await FS.access(path);
232
+ return true;
233
+ } catch (e) {
234
+ return false;
235
+ }
236
+ }
237
+ private async removeSymlinkTarget(path: string) {
238
+ if (this.symbolicLinks[path]) {
239
+ //console.log('Unwatching symlink target %s => %s', path, this.symbolicLinks[path]);
240
+ this.watcher?.unwatch(this.symbolicLinks[path]);
241
+ delete this.symbolicLinks[path];
242
+ }
243
+ }
244
+
245
+ private async updateSymlinkTarget(path: string) {
246
+ if (this.symbolicLinks[path]) {
247
+ //console.log('Updating symlink target %s => %s', path, this.symbolicLinks[path]);
248
+ this.watcher?.unwatch(this.symbolicLinks[path]);
249
+ delete this.symbolicLinks[path];
250
+ await this.addSymlinkTarget(path);
251
+ }
252
+ }
253
+
254
+ private async addSymlinkTarget(path: string) {
255
+ try {
256
+ // Make sure we're not watching the symlink target
257
+ await this.removeSymlinkTarget(path);
258
+ const stat = await FS.lstat(path);
259
+ if (stat.isSymbolicLink()) {
260
+ const realPath = `${await FS.realpath(path)}/kapeta.yml`;
261
+ if (await this.exists(realPath)) {
262
+ //console.log('Watching symlink target %s => %s', path, realPath);
263
+ this.watcher?.add(realPath);
264
+ this.symbolicLinks[path] = realPath;
265
+ }
266
+ }
267
+ } catch (e) {
268
+ // Ignore
269
+ console.warn('Failed to check local symlink target', e);
270
+ }
271
+ }
272
+
273
+ private ignoreFile(path: string) {
274
+ if (!path.startsWith(this.baseDir)) {
275
+ return false;
276
+ }
277
+ if (path.includes('/node_modules/')) {
278
+ return true;
279
+ }
280
+
281
+ const filename = Path.basename(path);
282
+ if (filename.startsWith('.')) {
283
+ return true;
284
+ }
285
+
286
+ const relativePath = Path.relative(this.baseDir, path).split(Path.sep);
287
+
288
+ try {
289
+ if (FS.statSync(path).isDirectory()) {
290
+ if (relativePath.length > 3) {
291
+ return true;
292
+ }
293
+ return false;
294
+ }
295
+ } catch (e) {
296
+ // Didn't exist - dont ignore
297
+ return false;
298
+ }
299
+
300
+ return !/^kapeta\.ya?ml$/.test(filename);
301
+ }
302
+ }