@vicinae/api 0.16.8 → 0.16.9
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/dist/api/alert.js +1 -1
- package/dist/api/bus.d.ts +4 -4
- package/dist/api/bus.js +12 -12
- package/dist/api/clipboard.js +4 -4
- package/dist/api/command.js +1 -1
- package/dist/api/components/action-pannel.d.ts +5 -6
- package/dist/api/components/action-pannel.js +15 -2
- package/dist/api/components/actions.d.ts +2 -2
- package/dist/api/components/actions.js +21 -7
- package/dist/api/context/navigation-provider.d.ts +1 -1
- package/dist/api/context/navigation-provider.js +20 -3
- package/dist/api/controls.js +5 -5
- package/dist/api/file-search.js +1 -1
- package/dist/api/local-storage.js +5 -5
- package/dist/api/oauth.js +5 -5
- package/dist/api/toast.js +3 -3
- package/dist/api/utils.js +4 -4
- package/dist/api/window-management.js +7 -7
- package/dist/commands/develop/index.js +37 -50
- package/dist/utils/logger.d.ts +2 -0
- package/dist/utils/logger.js +12 -9
- package/dist/utils/tail.d.ts +66 -0
- package/dist/utils/tail.js +352 -0
- package/dist/utils/utils.d.ts +2 -0
- package/dist/utils/utils.js +45 -9
- package/dist/utils/yoctocolors.d.ts +42 -0
- package/dist/utils/yoctocolors.js +83 -0
- package/package.json +1 -2
- package/types/jsx.d.ts +14 -5
|
@@ -40,14 +40,15 @@ const core_1 = require("@oclif/core");
|
|
|
40
40
|
const chokidar = __importStar(require("chokidar"));
|
|
41
41
|
const esbuild = __importStar(require("esbuild"));
|
|
42
42
|
const node_child_process_1 = require("node:child_process");
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
const
|
|
43
|
+
const fs = __importStar(require("node:fs"));
|
|
44
|
+
const fsp = __importStar(require("node:fs/promises"));
|
|
45
|
+
const path = __importStar(require("node:path"));
|
|
46
46
|
const logger_js_1 = require("../../utils/logger.js");
|
|
47
47
|
const utils_js_1 = require("../../utils/utils.js");
|
|
48
48
|
const extension_types_js_1 = require("../../utils/extension-types.js");
|
|
49
49
|
const vicinae_js_1 = require("../../utils/vicinae.js");
|
|
50
50
|
const manifest_js_1 = __importDefault(require("../../schemas/manifest.js"));
|
|
51
|
+
const tail_js_1 = require("../../utils/tail.js");
|
|
51
52
|
class Develop extends core_1.Command {
|
|
52
53
|
static args = {};
|
|
53
54
|
static description = "Start an extension development session";
|
|
@@ -67,13 +68,13 @@ class Develop extends core_1.Command {
|
|
|
67
68
|
async run() {
|
|
68
69
|
const { flags } = await this.parse(Develop);
|
|
69
70
|
const logger = new logger_js_1.Logger();
|
|
70
|
-
const pkgPath =
|
|
71
|
+
const pkgPath = path.join(flags.target, "package.json");
|
|
71
72
|
const parseManifest = () => {
|
|
72
|
-
if (!
|
|
73
|
+
if (!fs.existsSync(pkgPath)) {
|
|
73
74
|
logger.logError(`No package.json found at ${pkgPath}. Does this location point to a valid extension repository?`);
|
|
74
75
|
process.exit(1);
|
|
75
76
|
}
|
|
76
|
-
const json = JSON.parse(
|
|
77
|
+
const json = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
77
78
|
const e = manifest_js_1.default.safeParse(json);
|
|
78
79
|
if (e.error) {
|
|
79
80
|
logger.logError(`${pkgPath} is not a valid extension manifest: ${e.error}`);
|
|
@@ -107,22 +108,22 @@ class Develop extends core_1.Command {
|
|
|
107
108
|
});
|
|
108
109
|
*/
|
|
109
110
|
const entryPoints = manifest.commands
|
|
110
|
-
.map((cmd) =>
|
|
111
|
-
.filter(
|
|
111
|
+
.map((cmd) => path.join("src", `${cmd.name}.tsx`))
|
|
112
|
+
.filter(fs.existsSync);
|
|
112
113
|
logger.logInfo(`entrypoints [${entryPoints.join(", ")}]`);
|
|
113
114
|
const promises = manifest.commands.map((cmd) => {
|
|
114
|
-
const base =
|
|
115
|
+
const base = path.join(process.cwd(), "src", `${cmd.name}`);
|
|
115
116
|
const tsxSource = `${base}.tsx`;
|
|
116
117
|
const tsSource = `${base}.ts`;
|
|
117
118
|
let source = tsxSource;
|
|
118
|
-
if (cmd.mode === "view" && !
|
|
119
|
+
if (cmd.mode === "view" && !fs.existsSync(tsxSource)) {
|
|
119
120
|
throw new Error(`could not find entrypoint src/${cmd.name}.tsx for command ${cmd.name}.`);
|
|
120
121
|
}
|
|
121
122
|
// we allow .ts or .tsx for no-view
|
|
122
123
|
if (cmd.mode === "no-view") {
|
|
123
|
-
if (!
|
|
124
|
+
if (!fs.existsSync(tsxSource)) {
|
|
124
125
|
source = tsSource;
|
|
125
|
-
if (!
|
|
126
|
+
if (!fs.existsSync(tsSource)) {
|
|
126
127
|
throw new Error(`could not find entrypoint src/${cmd.name}.{ts,tsx} for command ${cmd.name}.`);
|
|
127
128
|
}
|
|
128
129
|
}
|
|
@@ -132,19 +133,19 @@ class Develop extends core_1.Command {
|
|
|
132
133
|
entryPoints: [source],
|
|
133
134
|
external: ["react", "@vicinae/api", "@raycast/api"],
|
|
134
135
|
format: "cjs",
|
|
135
|
-
outfile:
|
|
136
|
+
outfile: path.join(outDir, `${cmd.name}.js`),
|
|
136
137
|
platform: "node",
|
|
137
138
|
});
|
|
138
139
|
});
|
|
139
140
|
await Promise.all(promises);
|
|
140
|
-
const targetPkg =
|
|
141
|
-
const targetAssets =
|
|
142
|
-
|
|
143
|
-
if (
|
|
144
|
-
|
|
141
|
+
const targetPkg = path.join(outDir, "package.json");
|
|
142
|
+
const targetAssets = path.join(outDir, "assets");
|
|
143
|
+
fs.cpSync("package.json", targetPkg, { force: true });
|
|
144
|
+
if (fs.existsSync("assets")) {
|
|
145
|
+
fs.cpSync("assets", targetAssets, { force: true, recursive: true });
|
|
145
146
|
}
|
|
146
147
|
else {
|
|
147
|
-
|
|
148
|
+
fs.mkdirSync(targetAssets, { recursive: true });
|
|
148
149
|
}
|
|
149
150
|
};
|
|
150
151
|
const pingError = vicinae.ping();
|
|
@@ -172,12 +173,23 @@ class Develop extends core_1.Command {
|
|
|
172
173
|
process.chdir(flags.target);
|
|
173
174
|
const dataDir = (0, utils_js_1.extensionDataDir)();
|
|
174
175
|
const id = `${manifest.name}`;
|
|
175
|
-
const extensionDir =
|
|
176
|
-
const
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
(
|
|
176
|
+
const extensionDir = path.join(dataDir, id);
|
|
177
|
+
const internalSupportDir = (0, utils_js_1.extensionInternalSupportDir)(id);
|
|
178
|
+
const stdoutPath = path.join(internalSupportDir, "stdout.txt");
|
|
179
|
+
const stderrPath = path.join(internalSupportDir, "stderr.txt");
|
|
180
|
+
const pidFile = path.join(internalSupportDir, "cli.pid");
|
|
181
|
+
await Promise.all([extensionDir, internalSupportDir].map((dir) => fsp.mkdir(dir, { recursive: true })));
|
|
182
|
+
await fsp.writeFile(pidFile, `${process.pid}`);
|
|
183
|
+
const outStream = new tail_js_1.Tail(stdoutPath, { forceCreate: true, nLines: 0 });
|
|
184
|
+
const errStream = new tail_js_1.Tail(stderrPath, { forceCreate: true, nLines: 0 });
|
|
185
|
+
outStream.on("line", (data) => {
|
|
186
|
+
logger.logExtensionOut(data.toString());
|
|
187
|
+
});
|
|
188
|
+
outStream.on("error", () => { });
|
|
189
|
+
errStream.on("line", (data) => {
|
|
190
|
+
logger.logExtensionError(data.toString());
|
|
191
|
+
});
|
|
192
|
+
errStream.on("error", () => { });
|
|
181
193
|
await safeBuild(extensionDir);
|
|
182
194
|
process.on("SIGINT", () => {
|
|
183
195
|
logger.logInfo("Shutting down...");
|
|
@@ -203,31 +215,6 @@ class Develop extends core_1.Command {
|
|
|
203
215
|
logger.logEvent(`changed file ${path}`);
|
|
204
216
|
await safeBuild(extensionDir);
|
|
205
217
|
});
|
|
206
|
-
const logFiles = new Map();
|
|
207
|
-
chokidar.watch(logFile).on("all", async (_, path) => {
|
|
208
|
-
const stats = await (0, promises_1.stat)(path);
|
|
209
|
-
if (!stats.isFile())
|
|
210
|
-
return;
|
|
211
|
-
if (!logFiles.has(path)) {
|
|
212
|
-
//logger.logInfo(`Monitoring new log file at ${path}`);
|
|
213
|
-
logFiles.set(path, { cursor: 0, path });
|
|
214
|
-
}
|
|
215
|
-
const info = logFiles.get(path);
|
|
216
|
-
if (info.cursor > stats.size) {
|
|
217
|
-
info.cursor = 0;
|
|
218
|
-
}
|
|
219
|
-
if (stats.size === info.cursor)
|
|
220
|
-
return;
|
|
221
|
-
const handle = await (0, promises_1.open)(path, "r");
|
|
222
|
-
const buffer = Buffer.alloc(stats.size - info.cursor);
|
|
223
|
-
(0, node_fs_1.read)(handle.fd, buffer, 0, buffer.length, info.cursor, (error, nRead) => {
|
|
224
|
-
if (error)
|
|
225
|
-
return;
|
|
226
|
-
info.cursor += nRead;
|
|
227
|
-
logger.logTimestamp(buffer.toString());
|
|
228
|
-
handle.close();
|
|
229
|
-
});
|
|
230
|
-
});
|
|
231
218
|
}
|
|
232
219
|
}
|
|
233
220
|
exports.default = Develop;
|
package/dist/utils/logger.d.ts
CHANGED
package/dist/utils/logger.js
CHANGED
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
3
|
exports.Logger = void 0;
|
|
7
|
-
const
|
|
4
|
+
const yoctocolors_1 = require("./yoctocolors");
|
|
8
5
|
class Logger {
|
|
9
6
|
prefixes = {
|
|
10
|
-
error: `${
|
|
11
|
-
event: `${
|
|
12
|
-
info: `${
|
|
13
|
-
ready: `${
|
|
7
|
+
error: `${(0, yoctocolors_1.red)("error")}${(0, yoctocolors_1.reset)()}`,
|
|
8
|
+
event: `${(0, yoctocolors_1.magenta)("event")}${(0, yoctocolors_1.reset)()}`,
|
|
9
|
+
info: `${(0, yoctocolors_1.blue)("info")}${(0, yoctocolors_1.reset)()}`,
|
|
10
|
+
ready: `${(0, yoctocolors_1.green)("ready")}${(0, yoctocolors_1.reset)()}`,
|
|
14
11
|
};
|
|
15
12
|
logError(message) {
|
|
16
13
|
console.log(`${this.prefixes.error.padEnd(15)} - ${message}`);
|
|
@@ -24,6 +21,12 @@ class Logger {
|
|
|
24
21
|
logReady(message) {
|
|
25
22
|
console.log(`${this.prefixes.ready.padEnd(15)} - ${message}`);
|
|
26
23
|
}
|
|
24
|
+
logExtensionOut(s) {
|
|
25
|
+
this.logTimestamp(s);
|
|
26
|
+
}
|
|
27
|
+
logExtensionError(s) {
|
|
28
|
+
this.logTimestamp(`${(0, yoctocolors_1.red)(s)}${(0, yoctocolors_1.reset)()}`);
|
|
29
|
+
}
|
|
27
30
|
logTimestamp(s) {
|
|
28
31
|
const ts = new Date().toJSON();
|
|
29
32
|
const lines = s.split("\n");
|
|
@@ -31,7 +34,7 @@ class Logger {
|
|
|
31
34
|
const line = lines[i];
|
|
32
35
|
if (i === lines.length - 1 && line.length === 0)
|
|
33
36
|
continue;
|
|
34
|
-
console.log(`${
|
|
37
|
+
console.log(`${(0, yoctocolors_1.gray)(ts.padEnd(20))}${(0, yoctocolors_1.reset)()} - ${line}`);
|
|
35
38
|
}
|
|
36
39
|
}
|
|
37
40
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import events from "node:events";
|
|
2
|
+
interface FSWatchOptions {
|
|
3
|
+
interval: number;
|
|
4
|
+
}
|
|
5
|
+
export interface TailOptions {
|
|
6
|
+
separator?: string | RegExp | null;
|
|
7
|
+
fsWatchOptions?: FSWatchOptions;
|
|
8
|
+
follow?: boolean;
|
|
9
|
+
logger?: DevNull;
|
|
10
|
+
useWatchFile?: boolean;
|
|
11
|
+
flushAtEOF?: boolean;
|
|
12
|
+
encoding?: BufferEncoding;
|
|
13
|
+
fromBeginning?: boolean;
|
|
14
|
+
nLines?: number;
|
|
15
|
+
/**
|
|
16
|
+
* Create an empty file if it does not exist.
|
|
17
|
+
*/
|
|
18
|
+
forceCreate?: boolean;
|
|
19
|
+
}
|
|
20
|
+
declare class DevNull {
|
|
21
|
+
info(...args: any): void;
|
|
22
|
+
error(...args: any): void;
|
|
23
|
+
}
|
|
24
|
+
export declare class Tail extends events.EventEmitter {
|
|
25
|
+
private filename;
|
|
26
|
+
private absPath;
|
|
27
|
+
private separator?;
|
|
28
|
+
private fsWatchOptions;
|
|
29
|
+
private follow;
|
|
30
|
+
private logger;
|
|
31
|
+
private useWatchFile;
|
|
32
|
+
private flushAtEOF;
|
|
33
|
+
private encoding;
|
|
34
|
+
private nLines?;
|
|
35
|
+
private rewatchId;
|
|
36
|
+
private isWatching;
|
|
37
|
+
private queue;
|
|
38
|
+
private buffer;
|
|
39
|
+
private watcher;
|
|
40
|
+
private internalDispatcher;
|
|
41
|
+
private currentCursorPos;
|
|
42
|
+
constructor(filename: string, options?: TailOptions);
|
|
43
|
+
/**
|
|
44
|
+
* Grabs the index of the last line of text in the format /.*(\n)?/.
|
|
45
|
+
* Returns null if a full line can not be found.
|
|
46
|
+
* @param {string} text
|
|
47
|
+
* @returns {number | null}
|
|
48
|
+
*/
|
|
49
|
+
private getIndexOfLastLine;
|
|
50
|
+
/**
|
|
51
|
+
* Returns the position of the start of the `nLines`th line from the bottom.
|
|
52
|
+
* Returns 0 if `nLines` is greater than the total number of lines in the file.
|
|
53
|
+
* @param {number} nLines
|
|
54
|
+
* @returns {number}
|
|
55
|
+
*/
|
|
56
|
+
private getPositionAtNthLine;
|
|
57
|
+
private latestPosition;
|
|
58
|
+
private readBlock;
|
|
59
|
+
private change;
|
|
60
|
+
watch(startingCursor: number, flush?: boolean): void;
|
|
61
|
+
private rename;
|
|
62
|
+
private watchEvent;
|
|
63
|
+
private watchFileEvent;
|
|
64
|
+
unwatch(): void;
|
|
65
|
+
}
|
|
66
|
+
export {};
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Based on https://github.com/WandersonAlves/node-tail/blob/master/src/tail.ts
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.Tail = void 0;
|
|
8
|
+
const node_events_1 = __importDefault(require("node:events"));
|
|
9
|
+
const node_fs_1 = require("node:fs");
|
|
10
|
+
const node_path_1 = require("node:path");
|
|
11
|
+
class DevNull {
|
|
12
|
+
info(...args) { }
|
|
13
|
+
error(...args) { }
|
|
14
|
+
}
|
|
15
|
+
class Tail extends node_events_1.default.EventEmitter {
|
|
16
|
+
filename;
|
|
17
|
+
absPath;
|
|
18
|
+
separator;
|
|
19
|
+
fsWatchOptions;
|
|
20
|
+
follow;
|
|
21
|
+
logger;
|
|
22
|
+
useWatchFile;
|
|
23
|
+
flushAtEOF;
|
|
24
|
+
encoding;
|
|
25
|
+
nLines;
|
|
26
|
+
rewatchId;
|
|
27
|
+
isWatching;
|
|
28
|
+
queue = [];
|
|
29
|
+
// NOTE Should we rename that as it is a string instead of a Buffer?
|
|
30
|
+
buffer;
|
|
31
|
+
watcher;
|
|
32
|
+
internalDispatcher;
|
|
33
|
+
currentCursorPos = 0;
|
|
34
|
+
constructor(filename, options = {}) {
|
|
35
|
+
super();
|
|
36
|
+
this.filename = filename;
|
|
37
|
+
this.absPath = (0, node_path_1.dirname)(this.filename);
|
|
38
|
+
this.separator =
|
|
39
|
+
options.separator !== undefined ? options.separator : /[\r]{0,1}\n/; // null is a valid param
|
|
40
|
+
this.fsWatchOptions = options.fsWatchOptions || {};
|
|
41
|
+
this.follow = options.follow ?? true;
|
|
42
|
+
this.logger = options.logger || new DevNull();
|
|
43
|
+
this.useWatchFile = options.useWatchFile || false;
|
|
44
|
+
this.flushAtEOF = options.flushAtEOF || false;
|
|
45
|
+
this.encoding = options.encoding || "utf-8";
|
|
46
|
+
const fromBeginning = options.fromBeginning || false;
|
|
47
|
+
this.nLines = options.nLines ?? 0;
|
|
48
|
+
this.logger.info(`Tail starting...`);
|
|
49
|
+
this.logger.info(`filename: ${this.filename}`);
|
|
50
|
+
this.logger.info(`encoding: ${this.encoding}`);
|
|
51
|
+
try {
|
|
52
|
+
(0, node_fs_1.accessSync)(this.filename, node_fs_1.constants.F_OK);
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
if (err.code === "ENOENT") {
|
|
56
|
+
if (!options.forceCreate)
|
|
57
|
+
throw err;
|
|
58
|
+
(0, node_fs_1.writeFileSync)(this.filename, "");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
this.buffer = "";
|
|
62
|
+
this.internalDispatcher = new node_events_1.default.EventEmitter();
|
|
63
|
+
this.isWatching = false;
|
|
64
|
+
// this.internalDispatcher.on('next',this.readBlock);
|
|
65
|
+
this.internalDispatcher.on("next", () => {
|
|
66
|
+
this.readBlock();
|
|
67
|
+
});
|
|
68
|
+
let cursor = 0;
|
|
69
|
+
this.logger.info(`fromBeginning: ${fromBeginning}`);
|
|
70
|
+
if (fromBeginning) {
|
|
71
|
+
cursor = 0;
|
|
72
|
+
}
|
|
73
|
+
else if (this.nLines <= 0) {
|
|
74
|
+
cursor = 0;
|
|
75
|
+
}
|
|
76
|
+
else if (this.nLines !== undefined) {
|
|
77
|
+
cursor = this.getPositionAtNthLine(this.nLines);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
cursor = this.latestPosition();
|
|
81
|
+
}
|
|
82
|
+
if (cursor === undefined)
|
|
83
|
+
throw new Error("Tail can't initialize.");
|
|
84
|
+
const flush = fromBeginning || this.nLines !== undefined;
|
|
85
|
+
try {
|
|
86
|
+
this.watch(cursor, flush);
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
this.logger.error(`watch for ${this.filename} failed: ${err}`);
|
|
90
|
+
this.emit("error", `watch for ${this.filename} failed: ${err}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Grabs the index of the last line of text in the format /.*(\n)?/.
|
|
95
|
+
* Returns null if a full line can not be found.
|
|
96
|
+
* @param {string} text
|
|
97
|
+
* @returns {number | null}
|
|
98
|
+
*/
|
|
99
|
+
getIndexOfLastLine(text) {
|
|
100
|
+
/**
|
|
101
|
+
* Helper function get the last match as string
|
|
102
|
+
* @param {string} haystack
|
|
103
|
+
* @param {string | RegExp} needle
|
|
104
|
+
* @returns {string | undefined}
|
|
105
|
+
*/
|
|
106
|
+
const getLastMatch = (haystack, needle) => {
|
|
107
|
+
// NOTE `as string` was used to cast the needle to string, but it can be null as well. Just making TS compiler happy
|
|
108
|
+
const matches = haystack.match(needle);
|
|
109
|
+
if (matches === null) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
return matches[matches.length - 1];
|
|
113
|
+
};
|
|
114
|
+
// NOTE `as string` was used to cast the needle to string, but it can be null as well. Just making TS compiler happy
|
|
115
|
+
const endSep = getLastMatch(text, this.separator);
|
|
116
|
+
if (!endSep)
|
|
117
|
+
return null;
|
|
118
|
+
const endSepIndex = text.lastIndexOf(endSep);
|
|
119
|
+
let lastLine = "";
|
|
120
|
+
if (text.endsWith(endSep)) {
|
|
121
|
+
// If the text ends with a separator, look back further to find the next
|
|
122
|
+
// separator to complete the line
|
|
123
|
+
const trimmed = text.substring(0, endSepIndex);
|
|
124
|
+
// NOTE `as string` was used to cast the needle to string, but it can be null as well. Just making TS compiler happy
|
|
125
|
+
const startSep = getLastMatch(trimmed, this.separator);
|
|
126
|
+
// If there isn't another separator, the line isn't complete so
|
|
127
|
+
// so return null to get more data
|
|
128
|
+
if (!startSep) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
const startSepIndex = trimmed.lastIndexOf(startSep);
|
|
132
|
+
// Exclude the starting separator, include the ending separator
|
|
133
|
+
lastLine = text.substring(startSepIndex + startSep.length, endSepIndex + endSep.length);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
// If the text does not end with a separator, grab everything after
|
|
137
|
+
// the last separator
|
|
138
|
+
lastLine = text.substring(endSepIndex + endSep.length);
|
|
139
|
+
}
|
|
140
|
+
return text.lastIndexOf(lastLine);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Returns the position of the start of the `nLines`th line from the bottom.
|
|
144
|
+
* Returns 0 if `nLines` is greater than the total number of lines in the file.
|
|
145
|
+
* @param {number} nLines
|
|
146
|
+
* @returns {number}
|
|
147
|
+
*/
|
|
148
|
+
getPositionAtNthLine(nLines) {
|
|
149
|
+
const { size } = (0, node_fs_1.statSync)(this.filename);
|
|
150
|
+
if (size === 0) {
|
|
151
|
+
return 0;
|
|
152
|
+
}
|
|
153
|
+
const fd = (0, node_fs_1.openSync)(this.filename, "r");
|
|
154
|
+
// Start from the end of the file and work backwards in specific chunks
|
|
155
|
+
let currentReadPosition = size;
|
|
156
|
+
const chunkSizeBytes = Math.min(1024, size);
|
|
157
|
+
const lineBytes = [];
|
|
158
|
+
let remaining = "";
|
|
159
|
+
while (lineBytes.length < nLines) {
|
|
160
|
+
// Shift the current read position backward to the amount we're about to read
|
|
161
|
+
currentReadPosition -= chunkSizeBytes;
|
|
162
|
+
// If negative, we've reached the beginning of the file and we should stop and return 0, starting the
|
|
163
|
+
// stream at the beginning.
|
|
164
|
+
if (currentReadPosition < 0) {
|
|
165
|
+
return 0;
|
|
166
|
+
}
|
|
167
|
+
// Read a chunk of the file and prepend it to the working buffer
|
|
168
|
+
const buffer = Buffer.alloc(chunkSizeBytes);
|
|
169
|
+
const bytesRead = (0, node_fs_1.readSync)(fd, buffer, 0, // position in buffer to write to
|
|
170
|
+
chunkSizeBytes, // number of bytes to read
|
|
171
|
+
currentReadPosition);
|
|
172
|
+
// .subarray returns Uint8Array in node versions < 16.x and Buffer
|
|
173
|
+
// in versions >= 16.x. To support both, allocate a new buffer with
|
|
174
|
+
// Buffer.from which accepts both types
|
|
175
|
+
const readArray = buffer.subarray(0, bytesRead);
|
|
176
|
+
remaining = Buffer.from(readArray).toString(this.encoding) + remaining;
|
|
177
|
+
let index = this.getIndexOfLastLine(remaining);
|
|
178
|
+
while (index !== null && lineBytes.length < nLines) {
|
|
179
|
+
const line = remaining.substring(index);
|
|
180
|
+
lineBytes.push(Buffer.byteLength(line));
|
|
181
|
+
remaining = remaining.substring(0, index);
|
|
182
|
+
index = this.getIndexOfLastLine(remaining);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
(0, node_fs_1.closeSync)(fd);
|
|
186
|
+
return size - lineBytes.reduce((acc, cur) => acc + cur, 0);
|
|
187
|
+
}
|
|
188
|
+
latestPosition() {
|
|
189
|
+
try {
|
|
190
|
+
return (0, node_fs_1.statSync)(this.filename).size;
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
this.logger.error(`size check for ${this.filename} failed: ${err}`);
|
|
194
|
+
this.emit("error", `size check for ${this.filename} failed: ${err}`);
|
|
195
|
+
throw err;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
readBlock() {
|
|
199
|
+
if (this.queue.length >= 1) {
|
|
200
|
+
const block = this.queue[0];
|
|
201
|
+
if (block.end > block.start) {
|
|
202
|
+
const stream = (0, node_fs_1.createReadStream)(this.filename, {
|
|
203
|
+
start: block.start,
|
|
204
|
+
end: block.end - 1,
|
|
205
|
+
encoding: this.encoding,
|
|
206
|
+
});
|
|
207
|
+
stream.on("error", (error) => {
|
|
208
|
+
this.logger.error(`Tail error: ${error}`);
|
|
209
|
+
this.emit("error", error);
|
|
210
|
+
});
|
|
211
|
+
stream.on("end", () => {
|
|
212
|
+
this.queue.shift();
|
|
213
|
+
if (this.queue.length > 0) {
|
|
214
|
+
this.internalDispatcher.emit("next");
|
|
215
|
+
}
|
|
216
|
+
if (this.flushAtEOF && this.buffer.length > 0) {
|
|
217
|
+
this.emit("line", this.buffer);
|
|
218
|
+
this.buffer = "";
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
stream.on("data", (d) => {
|
|
222
|
+
if (this.separator === null) {
|
|
223
|
+
this.emit("line", d);
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
this.buffer += d;
|
|
227
|
+
// NOTE `as string` was used to cast the needle to string, but it can be null as well. Just making TS compiler happy
|
|
228
|
+
let parts = this.buffer.split(this.separator);
|
|
229
|
+
// NOTE Since parts.pop could return undefined, i'm returning a empty string when that happens
|
|
230
|
+
this.buffer = parts.pop() ?? "";
|
|
231
|
+
for (const chunk of parts) {
|
|
232
|
+
this.emit("line", chunk);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
change() {
|
|
240
|
+
const p = this.latestPosition();
|
|
241
|
+
if (p < this.currentCursorPos) {
|
|
242
|
+
//scenario where text is not appended but it's actually a w+
|
|
243
|
+
this.currentCursorPos = p;
|
|
244
|
+
}
|
|
245
|
+
else if (p > this.currentCursorPos) {
|
|
246
|
+
this.queue.push({ start: this.currentCursorPos, end: p });
|
|
247
|
+
this.currentCursorPos = p;
|
|
248
|
+
if (this.queue.length === 1) {
|
|
249
|
+
this.internalDispatcher.emit("next");
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
watch(startingCursor, flush) {
|
|
254
|
+
if (this.isWatching)
|
|
255
|
+
return;
|
|
256
|
+
this.logger.info(`filesystem.watch present? ${node_fs_1.watch !== undefined}`);
|
|
257
|
+
this.logger.info(`useWatchFile: ${this.useWatchFile}`);
|
|
258
|
+
this.isWatching = true;
|
|
259
|
+
this.currentCursorPos = startingCursor;
|
|
260
|
+
//force a file flush is either fromBegining or nLines flags were passed.
|
|
261
|
+
if (flush)
|
|
262
|
+
this.change();
|
|
263
|
+
if (!this.useWatchFile) {
|
|
264
|
+
this.logger.info(`watch strategy: watch`);
|
|
265
|
+
this.watcher = (0, node_fs_1.watch)(this.filename, this.fsWatchOptions, (e, filename) => {
|
|
266
|
+
// NOTE Filename here is a `Buffer`, how it's used as a string?
|
|
267
|
+
// NOTE Test if filename.toString changes the behavior
|
|
268
|
+
if (filename)
|
|
269
|
+
this.watchEvent(e, filename.toString());
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
this.logger.info(`watch strategy: watchFile`);
|
|
274
|
+
(0, node_fs_1.watchFile)(this.filename, this.fsWatchOptions, (curr, prev) => {
|
|
275
|
+
this.watchFileEvent(curr, prev);
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
rename(filename) {
|
|
280
|
+
//TODO
|
|
281
|
+
//MacOS sometimes throws a rename event for no reason.
|
|
282
|
+
//Different platforms might behave differently.
|
|
283
|
+
//see https://nodejs.org/api/fs.html#fs_fs_watch_filename_options_listener
|
|
284
|
+
//filename might not be present.
|
|
285
|
+
//https://nodejs.org/api/fs.html#fs_filename_argument
|
|
286
|
+
//Better solution would be check inode but it will require a timeout and
|
|
287
|
+
// a sync file read.
|
|
288
|
+
if (filename === undefined || filename !== this.filename) {
|
|
289
|
+
this.unwatch();
|
|
290
|
+
if (this.follow) {
|
|
291
|
+
this.filename = (0, node_path_1.join)(this.absPath, filename);
|
|
292
|
+
this.rewatchId = setTimeout(() => {
|
|
293
|
+
try {
|
|
294
|
+
this.watch(this.currentCursorPos);
|
|
295
|
+
}
|
|
296
|
+
catch (ex) {
|
|
297
|
+
this.logger.error(`'rename' event for ${this.filename}. File not available anymore.`);
|
|
298
|
+
this.emit("error", ex);
|
|
299
|
+
}
|
|
300
|
+
}, 1000);
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
this.logger.error(`'rename' event for ${this.filename}. File not available anymore.`);
|
|
304
|
+
this.emit("error", `'rename' event for ${this.filename}. File not available anymore.`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
// this.logger.info("rename event but same filename")
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
watchEvent(evtName, evtFilename) {
|
|
312
|
+
try {
|
|
313
|
+
if (evtName === "change") {
|
|
314
|
+
this.change();
|
|
315
|
+
}
|
|
316
|
+
else if (evtName === "rename") {
|
|
317
|
+
this.rename(evtFilename);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
catch (err) {
|
|
321
|
+
this.logger.error(`watchEvent for ${this.filename} failed: ${err}`);
|
|
322
|
+
this.emit("error", `watchEvent for ${this.filename} failed: ${err}`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
watchFileEvent(curr, prev) {
|
|
326
|
+
if (curr.size > prev.size) {
|
|
327
|
+
this.currentCursorPos = curr.size; //Update this.currentCursorPos so that a consumer can determine if entire file has been handled
|
|
328
|
+
this.queue.push({ start: prev.size, end: curr.size });
|
|
329
|
+
if (this.queue.length === 1) {
|
|
330
|
+
this.internalDispatcher.emit("next");
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
unwatch() {
|
|
335
|
+
if (this.watcher) {
|
|
336
|
+
this.watcher.close();
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
(0, node_fs_1.unwatchFile)(this.filename);
|
|
340
|
+
}
|
|
341
|
+
if (this.rewatchId) {
|
|
342
|
+
clearTimeout(this.rewatchId);
|
|
343
|
+
this.rewatchId = undefined;
|
|
344
|
+
}
|
|
345
|
+
this.isWatching = false;
|
|
346
|
+
this.queue = []; // TODO: is this correct behaviour?
|
|
347
|
+
if (this.logger) {
|
|
348
|
+
this.logger.info(`Unwatch ${this.filename}`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
exports.Tail = Tail;
|
package/dist/utils/utils.d.ts
CHANGED