@openzim/libzim 2.4.4 → 3.0.0

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/.env CHANGED
@@ -1 +1 @@
1
- LIBZIM_VERSION=6.3.2
1
+ LIBZIM_VERSION=8.2.0
package/Changelog CHANGED
@@ -1,3 +1,10 @@
1
+ 3.0.0:
2
+ * NEW: Use libzim v8.2.0
3
+ * NEW: Calling API (to follow libzim changes)
4
+ * NEW: Add support of ARM(64) for both Linux and macOS
5
+ * UPDATE: Most of the dependencies
6
+ * UPDATE: Many improvements around the CI
7
+
1
8
  2.4.4:
2
9
  * NEW: Use libzim v6.3.2
3
10
 
package/README.md CHANGED
@@ -6,8 +6,8 @@ This is the Node.js binding to the
6
6
  [ZIM](https://openzim.org) files easily in Javascript.
7
7
 
8
8
  [![npm](https://img.shields.io/npm/v/@openzim/libzim.svg)](https://www.npmjs.com/package/@openzim/libzim)
9
- [![Build Status](https://github.com/openzim/node-libzim/workflows/CI/badge.svg?branch=master)](https://github.com/openzim/node-libzim/actions?query=branch%3Amaster)
10
- [![codecov](https://codecov.io/gh/openzim/node-libzim/branch/master/graph/badge.svg)](https://codecov.io/gh/openzim/node-libzim)
9
+ [![Build Status](https://github.com/openzim/node-libzim/workflows/CI/badge.svg?branch=main)](https://github.com/openzim/node-libzim/actions?query=branch%3Amain)
10
+ [![codecov](https://codecov.io/gh/openzim/node-libzim/branch/main/graph/badge.svg)](https://codecov.io/gh/openzim/node-libzim)
11
11
  [![CodeFactor](https://www.codefactor.io/repository/github/openzim/node-libzim/badge)](https://www.codefactor.io/repository/github/openzim/node-libzim)
12
12
  [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
13
13
 
@@ -25,52 +25,74 @@ here](https://github.com/openzim/libzim/)).
25
25
  npm i openzim/libzim
26
26
  ```
27
27
 
28
- ### Writing a Zim file
28
+ ### Writing a ZIM file
29
29
  ```javascript
30
30
  // write.js
31
- const { ZimArticle, ZimCreator } = require("@openzim/libzim");
31
+ import { Creator, StringItem } from "@openzim/libzim";
32
32
 
33
33
  (async () => {
34
-
35
34
  console.info('Starting');
36
- const creator = new ZimCreator({ fileName: 'test.zim' }, { welcome: 'index.html' });
37
-
38
- for (let i = 100; i > 0; i--) {
39
- const a = new ZimArticle({ url: `file${i}`, data: `Content ${i}` });
40
- await creator.addArticle(a);
35
+ const outFile = "./test.zim";
36
+ const creator = new Creator()
37
+ .configNbWorkers(1)
38
+ .configIndexing(true, "en")
39
+ .configClusterSize(2048)
40
+ .startZimCreation(outFile);
41
+
42
+ for (let i = 0; i < 100; i++) {
43
+ const item = new StringItem(
44
+ `file${i}`, // path url
45
+ "text/plain", // content-type
46
+ `Title ${i}`, // title
47
+ {FRONT_ARTICLE: 1, COMPRESS: 1}, // hint option flags
48
+ `<h1>Content / Data ${i}</h1>` // content
49
+ );
50
+ await creator.addItem(item);
41
51
  }
42
52
 
43
- const welcome = new ZimArticle({ url: `index.html`, data: `<h1>Welcome!</h1>` });
44
- await creator.addArticle(welcome);
45
-
46
- await creator.finalise();
53
+ creator.setMainPath("file0");
54
+ await creator.finishZimCreation();
47
55
 
48
56
  console.log('Done Writing');
49
-
50
57
  })();
51
58
  ```
52
59
 
53
- ### Reading a Zim file
60
+ ### Reading a ZIM file
54
61
  ```javascript
55
62
  // read.js
56
-
57
- const { ZimArticle, ZimReader } = require("@openzim/libzim");
63
+ import { Archive, SuggestionSearcher, Searcher } from "@openzim/libzim";
58
64
 
59
65
  (async () => {
66
+ const outFile = "./test.zim";
67
+ const archive = new Archive(outFile);
68
+ console.log(`Archive opened: main entry path - ${archive.mainEntry.path}`);
60
69
 
61
- const zimFile = new ZimReader(path.join(__dirname, '../test.zim'));
62
-
63
- const suggestResults = await zimFile.suggest('laborum');
64
- console.info(`Suggest Results:`, suggestResults);
70
+ for (const entry of archive.iterByPath()) {
71
+ console.log(`entry: ${entry.path} - ${entry.title}`);
72
+ }
65
73
 
66
- const searchResults = await zimFile.search('rem');
67
- console.info(`Search Results:`, searchResults);
74
+ const suggestionSearcher = new SuggestionSearcher(archive);
75
+ const suggestion = suggestionSearcher.suggest('laborum');
76
+ let results = suggestion.getResults(0, 10);
77
+ console.log("Suggestion results:");
78
+ for(const entry of results) {
79
+ console.log(`\t- ${entry.path} - ${entry.title}`);
80
+ }
68
81
 
69
- const readArticleContent = await zimFile.getArticleByUrl('A/laborum');
70
- console.info(`Article by url (laborum):`, readArticleContent);
82
+ const searcher = new Searcher(archive);
83
+ const search = searcher.search(new Query('rem'));
84
+ results = search.getResults(0, 10);
85
+ console.log("Search results:");
86
+ for(const entry of results) {
87
+ console.log(`\t- ${entry.path} - ${entry.title}`);
88
+ }
71
89
 
72
- await zimFile.destroy();
73
90
 
91
+ const entry = await archive.getEntryByPath("A/laborum");
92
+ const item = entry.item;
93
+ const blob = item.data;
94
+ console.info(`Entry by url (laborum):`, blob.data);
95
+ delete archive;
74
96
  })();
75
97
 
76
98
  ```
package/binding.gyp CHANGED
@@ -24,7 +24,7 @@
24
24
  "libraries": [
25
25
  "-Wl,-rpath,'$$ORIGIN'",
26
26
  "-L<(libzim_dir)/lib/x86_64-linux-gnu",
27
- "<(libzim_dir)/lib/x86_64-linux-gnu/libzim.so.6",
27
+ "<(libzim_dir)/lib/x86_64-linux-gnu/libzim.so.8",
28
28
  ],
29
29
  }],
30
30
  ["libzim_local!='true' and OS=='mac'", {
@@ -33,7 +33,8 @@
33
33
  "xcode_settings": {
34
34
  "GCC_SYMBOLS_PRIVATE_EXTERN": "YES", # -fvisibility=hidden
35
35
  "GCC_ENABLE_CPP_EXCEPTIONS": "YES",
36
- "LD_RUNPATH_SEARCH_PATHS": "@loader_path/"
36
+ "LD_RUNPATH_SEARCH_PATHS": "@loader_path/",
37
+ "OTHER_CFLAGS": [ "-std=c++17", "-fexceptions" ],
37
38
  },
38
39
  "libraries": ["-Wl,-rpath,@loader_path/", "-L<(libzim_dir)/lib", "-lzim"],
39
40
  }],
@@ -45,9 +46,6 @@
45
46
  "cflags_cc": [ "-std=c++17", "-fexceptions" ],
46
47
  "sources": [
47
48
  "src/module.cc",
48
- "src/article.cc",
49
- "src/reader.cc",
50
- "src/writer.cc",
51
49
  ],
52
50
  "include_dirs": [
53
51
  "<!@(node -p \"require('node-addon-api').include\")",
package/bundle-libzim.js CHANGED
@@ -13,14 +13,14 @@ if (!isMacOS && !isLinux) {
13
13
  }
14
14
 
15
15
  if (isLinux) {
16
- console.info(`Copying libzim.so.6 to build folder`)
17
- exec(`cp download/lib/x86_64-linux-gnu/libzim.so.6 build/Release/libzim.so.6`)
18
- exec(`ln -sf build/Release/libzim.so.6 build/Release/libzim.so`) // convienience only, not required
16
+ console.info(`Copying libzim.so.8 to build folder`)
17
+ exec(`cp download/lib/x86_64-linux-gnu/libzim.so.8 build/Release/libzim.so.8`)
18
+ exec(`ln -sf build/Release/libzim.so.8 build/Release/libzim.so`) // convienience only, not required
19
19
  }
20
20
  if (isMacOS) {
21
- console.info(`Copying libzim.6.dylib to build folder`);
22
- exec(`cp download/lib/libzim.6.dylib build/Release/libzim.6.dylib`)
23
- exec(`ln -sf build/Release/libzim.6.dylib build/Release/libzim.dylib`) // convienience only, not required
21
+ console.info(`Copying libzim.8.dylib to build folder`);
22
+ exec(`cp download/lib/libzim.8.dylib build/Release/libzim.8.dylib`)
23
+ exec(`ln -sf build/Release/libzim.8.dylib build/Release/libzim.dylib`) // convienience only, not required
24
24
  console.info(`Fixing rpath`)
25
- exec(`install_name_tool -change libzim.6.dylib @loader_path/libzim.6.dylib build/Release/zim_binding.node`)
25
+ exec(`install_name_tool -change libzim.8.dylib @loader_path/libzim.8.dylib build/Release/zim_binding.node`)
26
26
  }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,234 @@
1
- import { ZimArticle } from './zim';
2
- import { ZimReader } from './ZimReader';
3
- import { ZimCreator } from './ZimCreator';
4
- export { ZimArticle, ZimCreator, ZimReader, };
1
+
2
+ export class IntegrityCheck {
3
+ static CHECKSUM: symbol;
4
+ static DIRENT_PTRS: symbol;
5
+ static DIRENT_ORDER: symbol;
6
+ static TITLE_INDEX: symbol;
7
+ static CLUSTER_PTRS: symbol;
8
+ static DIRENT_MIMETYPES: symbol;
9
+ static COUNT: symbol; // DO NOT USE THIS. See libzim docs.
10
+ }
11
+
12
+ export class Compression {
13
+ static None: symbol;
14
+ static Zstd: symbol;
15
+ }
16
+
17
+ export class Blob {
18
+ constructor(buf?: ArrayBuffer | Buffer | string);
19
+ get data(): Buffer;
20
+ get size(): number | bigint;
21
+ toString(): string;
22
+ }
23
+
24
+ export type ContentProvider = {
25
+ size: number | bigint;
26
+ feed() : Blob;
27
+ }
28
+
29
+ export class StringProvider {
30
+ constructor(content: string);
31
+ get size(): number;
32
+ feed() : Blob;
33
+ }
34
+
35
+ export class FileProvider {
36
+ constructor(filepath: string);
37
+ get size(): number | bigint;
38
+ feed() : Blob;
39
+ }
40
+
41
+ export type Hint = {
42
+ COMPRESS?: number;
43
+ FRONT_ARTICLE?: number;
44
+ }
45
+
46
+ export interface IndexData {
47
+ hasIndexData?: boolean;
48
+ title?: string;
49
+ content?: string;
50
+ keywords?: string;
51
+ wordcount?: number;
52
+ position?: [boolean, number, number];
53
+ }
54
+
55
+ export interface WriterItem {
56
+ path: string;
57
+ title: string;
58
+ mimeType: string;
59
+ getContentProvider() : ContentProvider;
60
+ hints: Hint;
61
+ getIndexData?: () => IndexData;
62
+ }
63
+
64
+ export class StringItem {
65
+ constructor(path: string, mimeType: string, title: string, hint: Hint, content: string);
66
+ readonly path: string;
67
+ readonly title: string;
68
+ readonly mimeType: string;
69
+ getContentProvider() : StringProvider;
70
+ readonly hints: Hint;
71
+ }
72
+
73
+ export class FileItem {
74
+ constructor(path: string, mimeType: string, title: string, hints: Hint, filePath: string);
75
+ readonly path: string;
76
+ readonly title: string;
77
+ readonly mimeType: string;
78
+ getContentProvider() : StringProvider;
79
+ readonly hints: Hint;
80
+ }
81
+
82
+ export class Creator {
83
+ constructor();
84
+ configVerbose(verbose: boolean) : this;
85
+ configCompression(value: Compression) : this;
86
+ configClusterSize(size: number) : this;
87
+ configIndexing(indexing: boolean, language: string) : this;
88
+ configNbWorkers(num: number) : this;
89
+ startZimCreation(filepath: string): this;
90
+ addItem(item: WriterItem): Promise<void>;
91
+ addMetadata(name: string, content: string | ContentProvider, mimetype?: string): void;
92
+ addIllustration(size: number, content: string | ContentProvider): void;
93
+ addRedirection(path: string, title: string, targetPath: string, hints?: Hint): void;
94
+ setMainPath(mainPath: string): void;
95
+ setUuid(uuid: string): void;
96
+ finishZimCreation() : Promise<void>;
97
+ }
98
+
99
+ export class Item {
100
+ get title(): string;
101
+ get path(): string;
102
+ get mimetype(): string;
103
+ get data(): Blob;
104
+ getData(offset?: number | bigint, limit?: number | bigint) : Blob;
105
+ get size(): number | bigint;
106
+ get directAccessInformation(): {
107
+ filename: string,
108
+ offset: number,
109
+ };
110
+ get index(): number | bigint;
111
+ }
112
+
113
+ export class Entry {
114
+ get isRedirect(): boolean;
115
+ get title(): string;
116
+ get path(): string;
117
+ get item(): Item;
118
+ getItem(followRedirect?: boolean) : Item;
119
+ get redirect(): Item;
120
+ get redirectEntry(): Entry;
121
+ get index(): number;
122
+ }
123
+
124
+ export interface EntryRange extends Iterable<Entry> {
125
+ size: number;
126
+ offset(start: number, maxResults: number) : EntryRange;
127
+ }
128
+
129
+ export class Archive {
130
+ constructor(filepath: string);
131
+ get filename(): string;
132
+ get filesize(): number | bigint;
133
+ get allEntryCount(): number;
134
+ get entryCount(): number;
135
+ get articleCount(): number;
136
+ get uuid(): string;
137
+ getMetadata(name: string) : string;
138
+ getMetadataItem(name: string) : Item;
139
+ get metadataKeys(): string[];
140
+ getIllustrationItem(size: number) : Item;
141
+ get illustrationSizes() : Set<number>;
142
+ getEntryByPath(path_or_idx: string | number) : Entry;
143
+ getEntryByTitle(title_or_idx: string | number) : Entry;
144
+ getEntryByClusterOrder(idx: number) : Entry;
145
+ get mainEntry(): Entry;
146
+ get randomEntry(): Entry;
147
+ hasEntryByPath(path: string) : boolean;
148
+ hasEntryByTitle(title: string) : boolean;
149
+ hasMainEntry(): boolean;
150
+ hasIllustration(size: number) : boolean;
151
+ hasFulltextIndex() : boolean;
152
+ hasTitleIndex(): boolean;
153
+ iterByPath() : EntryRange;
154
+ iterByTitle() : EntryRange;
155
+ iterEfficient() : EntryRange;
156
+ findByPath(path: string) : EntryRange;
157
+ findByTitle(title: string) : EntryRange;
158
+ get hasChecksum(): boolean;
159
+ get checksum(): string;
160
+ check() : boolean;
161
+ checkIntegrity(checkType: symbol) : boolean; // one of IntegrityCheck
162
+ get isMultiPart(): boolean;
163
+ get hasNewNamespaceScheme(): boolean;
164
+
165
+ static validate(zimPath: string, checksToRun: symbol[]) : boolean; // list of IntegrityCheck
166
+ }
167
+
168
+ interface Georange {
169
+ latitude: number;
170
+ longitude: number;
171
+ distance: number;
172
+ }
173
+
174
+ export class Query {
175
+ constructor(query: string);
176
+ setQuery(query: string): this;
177
+ setGeorange(latitude: number, longitude: number, distance: number): this;
178
+ get query() : string;
179
+ set query(query: string);
180
+ toString(): string;
181
+ get georange() : Georange;
182
+ set georange(range: Georange);
183
+ }
184
+
185
+ export class SearchIterator {
186
+ get path(): string;
187
+ get title(): string;
188
+ get score(): number;
189
+ get snippet(): string;
190
+ get wordCount(): number;
191
+ get size() : number;
192
+ get fileIndex(): number;
193
+ get zimId(): string;
194
+ get entry(): Entry;
195
+ }
196
+
197
+ export interface SearchResultSet extends Iterable<SearchIterator> {
198
+ readonly size: number;
199
+ }
200
+
201
+ export class Search {
202
+ getResults(start: number, maxResults: number) : SearchResultSet;
203
+ get estimatedMatches(): number;
204
+ }
205
+
206
+ export class Searcher {
207
+ constructor(archives: Archive | Archive[]);
208
+ addArchive(archive: Archive): this;
209
+ search(query: string | Query) : Search;
210
+ setVerbose(verbose: boolean) : this;
211
+ }
212
+
213
+ export class SuggestionIterator {
214
+ get entry(): Entry;
215
+ get title(): string;
216
+ get path(): string;
217
+ get snippet(): string;
218
+ get hasSnippet(): boolean;
219
+ }
220
+
221
+ export interface SuggestionResultSet extends Iterable<SuggestionIterator> {
222
+ readonly size: number;
223
+ }
224
+
225
+ export class SuggestionSearch {
226
+ getResults(start: number, maxResults: number) : SuggestionResultSet;
227
+ get estimatedMatches(): number;
228
+ }
229
+
230
+ export class SuggestionSearcher {
231
+ constructor(archives: Archive);
232
+ suggest(query: string): SuggestionSearch;
233
+ setVerbose(verbose: boolean) : this;
234
+ }
package/dist/index.js CHANGED
@@ -1,9 +1,34 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ZimReader = exports.ZimCreator = exports.ZimArticle = void 0;
4
- var zim_1 = require("./zim");
5
- Object.defineProperty(exports, "ZimArticle", { enumerable: true, get: function () { return zim_1.ZimArticle; } });
6
- var ZimReader_1 = require("./ZimReader");
7
- Object.defineProperty(exports, "ZimReader", { enumerable: true, get: function () { return ZimReader_1.ZimReader; } });
8
- var ZimCreator_1 = require("./ZimCreator");
9
- Object.defineProperty(exports, "ZimCreator", { enumerable: true, get: function () { return ZimCreator_1.ZimCreator; } });
1
+
2
+ const bindings = require('bindings');
3
+
4
+ const {
5
+ Archive,
6
+ Entry,
7
+ IntegrityCheck,
8
+ Compression,
9
+ Blob,
10
+ Searcher,
11
+ Query,
12
+ SuggestionSearcher,
13
+ Creator,
14
+ StringProvider,
15
+ FileProvider,
16
+ StringItem,
17
+ FileItem,
18
+ } = bindings('zim_binding');
19
+
20
+ module.exports = {
21
+ Archive,
22
+ Entry,
23
+ IntegrityCheck,
24
+ Compression,
25
+ Blob,
26
+ Searcher,
27
+ Query,
28
+ SuggestionSearcher,
29
+ Creator,
30
+ StringProvider,
31
+ FileProvider,
32
+ StringItem,
33
+ FileItem,
34
+ }
@@ -10,14 +10,29 @@ mkdirp.sync('./download');
10
10
 
11
11
  const isMacOS = os.type() === 'Darwin'
12
12
  const isLinux = os.type() === 'Linux'
13
+ const rawArch = os.arch()
14
+ const isAvailableArch = rawArch === 'x64' || rawArch == 'arm' || rawArch == 'arm64'
13
15
 
14
16
  if (!isMacOS && !isLinux) {
15
17
  console.warn(`\x1b[41m\n================================ README \n\nPre-built binaries only available on Linux and MacOS for now...\nPlease ensure you have libzim installed globally on this machine:\n\n\thttps://github.com/openzim/libzim/\n\n================================\x1b[0m\n`);
16
18
  }
19
+ if (!isAvailableArch) {
20
+ console.warn(`\x1b[41m\n================================ README \n\nPre-built binaries only available on x86_64, arm and arm64 for now...\nPlease ensure you have libzim installed globally on this machine:\n\n\thttps://github.com/openzim/libzim/\n\n================================\x1b[0m\n`);
21
+ }
17
22
 
18
23
  let osPrefix = (isMacOS) ? 'macos' : 'linux';
24
+ let osArch = (isLinux) ? 'x86_64-bionic' : 'x86_64';
25
+
26
+ if (rawArch != 'x64'){
27
+ if (isLinux) {
28
+ osArch = rawArch == 'arm64' ? 'aarch64-bionic' : 'armhf'
29
+ } else {
30
+ osArch = rawArch
31
+ }
32
+ }
33
+
19
34
  const urls = [
20
- `http://download.openzim.org/release/libzim/libzim_${osPrefix}-x86_64-${process.env.LIBZIM_VERSION}.tar.gz`,
35
+ `https://download.openzim.org/release/libzim/libzim_${osPrefix}-${osArch}-${process.env.LIBZIM_VERSION}.tar.gz`,
21
36
  ].filter(a => a);
22
37
 
23
38
  for (let url of urls) {
package/package.json CHANGED
@@ -1,13 +1,16 @@
1
1
  {
2
2
  "name": "@openzim/libzim",
3
- "version": "2.4.4",
3
+ "main": "dist/index.js",
4
+ "types": "dist/index.d.js",
5
+ "version": "3.0.0",
4
6
  "description": "Libzim bindings for NodeJS",
5
7
  "scripts": {
6
8
  "clean": "rm -rf dist build/native/build",
7
9
  "tsc": "tsc",
8
- "prepare": "npm run tsc",
10
+ "prepare": "mkdir -p dist/ && cp -v src/index.js src/index.d.ts dist/",
9
11
  "codecov": "nyc --reporter=lcov npm t",
10
12
  "install": "npm run download && node-gyp rebuild -v && npm run bundle",
13
+ "build": "node-gyp rebuild -v && npm run bundle",
11
14
  "download": "node ./download-libzim.js",
12
15
  "bundle": "node ./bundle-libzim.js",
13
16
  "test": "jest",
@@ -22,7 +25,6 @@
22
25
  "node_modules"
23
26
  ]
24
27
  },
25
- "main": "dist/index.js",
26
28
  "author": "Joseph Reeve",
27
29
  "license": "GPL-3.0",
28
30
  "repository": {
@@ -35,32 +37,26 @@
35
37
  "homepage": "https://github.com/openzim/node-libzim#readme",
36
38
  "gypfile": true,
37
39
  "dependencies": {
38
- "@types/bindings": "^1.5.0",
39
- "@types/faker": "^4.1.12",
40
- "@types/jest": "^25.2.3",
41
- "@types/mime": "^2.0.3",
42
- "@types/node": "^13.13.52",
43
- "@types/rimraf": "^3.0.0",
44
- "axios": "^0.21.1",
40
+ "@types/bindings": "^1.5.1",
41
+ "@types/jest": "^28.1.6",
42
+ "@types/node": "^18.0.6",
43
+ "axios": "^0.27.2",
45
44
  "bindings": "^1.5.0",
46
- "dotenv": "^8.6.0",
45
+ "dotenv": "^16.0.1",
47
46
  "exec-then": "^1.3.1",
48
- "faker": "^4.1.0",
49
- "jest": "^25.5.4",
50
- "mime": "^2.5.2",
51
- "minimist": "^1.2.5",
52
47
  "mkdirp": "^1.0.4",
53
- "node-addon-api": "^2.0.2",
54
- "node-gyp": "^6.1.0",
55
- "rimraf": "^3.0.2",
56
- "tar": "^6.1.0",
57
- "ts-jest": "^25.4.0",
58
- "ts-node": "^8.10.2",
59
- "tsconfig-paths": "^3.9.0"
48
+ "node-addon-api": "^5.0.0",
49
+ "node-gyp": "^9.3.1",
50
+ "tqdm": "^2.0.3",
51
+ "ts-node": "^10.9.1",
52
+ "tsconfig-paths": "^4.0.0"
60
53
  },
61
54
  "devDependencies": {
55
+ "@faker-js/faker": "^7.6.0",
56
+ "jest": "^28.1.3",
62
57
  "nyc": "^15.1.0",
63
- "typescript": "^3.9.9"
58
+ "ts-jest": "^28.0.7",
59
+ "typescript": "^4.7.4"
64
60
  },
65
61
  "jest": {
66
62
  "preset": "ts-jest/presets/js-with-ts"