connectbase-client 6.7.0 → 6.9.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/CHANGELOG.md +56 -0
- package/README.md +14 -3
- package/dist/cli.js +44 -10
- package/dist/connect-base.umd.js +2 -2
- package/dist/index.d.mts +52 -6
- package/dist/index.d.ts +52 -6
- package/dist/index.js +58 -18
- package/dist/index.mjs +58 -18
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,62 @@
|
|
|
3
3
|
본 SDK 의 모든 주요 변경사항을 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/) 형식으로 기록합니다.
|
|
4
4
|
버전은 [Semantic Versioning](https://semver.org/lang/ko/) 을 따릅니다.
|
|
5
5
|
|
|
6
|
+
## [6.9.0] - 2026-10-30
|
|
7
|
+
|
|
8
|
+
### Added — 확장자 없는 파일 배포: `deploy --include-extensionless`
|
|
9
|
+
|
|
10
|
+
무작위 이름의 데이터 파일처럼 확장자 없이 서빙해야 하는 파일을 웹 스토리지에 배포할 수 있습니다. 켜면 확장자 없는
|
|
11
|
+
파일을 바이너리로 올리고(한도를 넘는 큰 파일은 presigned 직접 업로드), 사이트는 `application/octet-stream` 으로
|
|
12
|
+
바이트 그대로 서빙합니다(추적 스크립트 주입, gzip 없음, Range 지원, 캐시 1시간).
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npx connectbase-client deploy ./dist --include-extensionless
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`.connectbaserc` 에 `"includeExtensionless": true` 를 넣어도 같습니다. 기본값은 지금처럼 제외입니다 — `deploy` 의
|
|
19
|
+
기본 디렉터리가 `.` 이라 프로젝트 루트를 배포하면 Dockerfile, LICENSE, 키 파일(id_rsa, kubeconfig 등) 같은 확장자
|
|
20
|
+
없는 파일이 공개될 수 있어서입니다. 켜도 숨김 파일과 폴더(`.env`, `.ssh/` 등)는 여전히 제외되고, `_redirects`,
|
|
21
|
+
`_headers` 는 지금처럼 텍스트 사이트 설정 파일로 올라갑니다. 확장자 없는 파일이 제외되면 제외 목록 경고에 켜는 방법이
|
|
22
|
+
함께 나옵니다.
|
|
23
|
+
|
|
24
|
+
서빙은 2026-10-30 이후 서버(manifest 항목의 `binary` 표시)가 필요합니다. 그 전 배포에 들어 있던 확장자 없는 경로는
|
|
25
|
+
지금처럼 HTML(SPA 라우트)로 다룹니다.
|
|
26
|
+
|
|
27
|
+
### Added — `.mvx` 허용
|
|
28
|
+
|
|
29
|
+
`.mvx`(사이트 자체 형식의 3D/자료 파일)를 바이너리 허용 확장자로 추가했습니다. `application/octet-stream`, 캐시
|
|
30
|
+
1시간으로 서빙되며, 큰 파일은 presigned 경로로 자동 오프로드됩니다.
|
|
31
|
+
|
|
32
|
+
## [6.8.0] - 2026-10-23
|
|
33
|
+
|
|
34
|
+
### Added — 서버 코드에서 토픽 만들기: `cb.push.createTopic(appId, name, options?)`
|
|
35
|
+
|
|
36
|
+
방마다 쓰는 토픽(`cc_<room>` 같은)을 서버 코드가 만들어 두고, 기기는 그 뒤에 `subscribeTopic` 으로 구독합니다. 같은
|
|
37
|
+
이름의 토픽이 이미 있으면 오류 없이 그 토픽을 돌려줍니다(설정은 바꾸지 않습니다) — 트리거가 다시 실행돼도 안전합니다.
|
|
38
|
+
공개 키(cb_pk_) 단독 인스턴스로는 호출할 수 없고, 함수의 `ctx.cbAdmin` 은 management_scopes 에 `push:write` 가 있어야
|
|
39
|
+
합니다(platform-issue 01a167dd). 함수는 배포할 때 이 SDK 의 최신 판을 설치하므로, 쓰려면 함수를 다시 배포하세요.
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
// concerts 트리거 함수
|
|
43
|
+
await ctx.cbAdmin.push.createTopic(appId, `cc_${row.room}`, { displayName: row.title })
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Fixed — 웹 푸시 기기의 토픽 구독과 등록 해제
|
|
47
|
+
|
|
48
|
+
`registerWebPush` 로 등록한 브라우저 기기를 `subscribeTopic` / `unsubscribeTopic` 으로 구독시키지 못했고,
|
|
49
|
+
`unregisterWebPush` 도 실패했습니다. 웹 푸시 기기의 `device_token` 은 endpoint URL(`https://…/send/…`, 슬래시 포함)인데
|
|
50
|
+
SDK 가 그 값을 요청 경로에 넣어 서버 라우트가 맞지 않았습니다(인코딩해도 서버 라우팅 전에 풀립니다). 이제 기기 토큰은
|
|
51
|
+
요청 본문으로 보냅니다(`POST /v1/public/push/topics/subscribe`, `/topics/unsubscribe`, `/devices/unregister`) — 네이티브
|
|
52
|
+
(FCM, APNs) 기기도 같은 방법으로 동작하며, 호출 방법은 그대로입니다(platform-issue 01a167dd).
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
const device = await cb.push.registerWebPush(subscription)
|
|
56
|
+
await cb.push.subscribeTopic(device.device_token, 'announcements') // 토픽은 콘솔에서 미리 만든다
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
이 판은 위 엔드포인트가 있는 서버(2026-10-23 배포)가 필요합니다. 예전 판(경로에 토큰)도 네이티브 기기에서는 계속
|
|
60
|
+
동작합니다.
|
|
61
|
+
|
|
6
62
|
## [6.7.0] - 2026-10-22
|
|
7
63
|
|
|
8
64
|
### Added — 서버가 검증한 보낸 사람: `message.fromMember`
|
package/README.md
CHANGED
|
@@ -294,7 +294,8 @@ In CI, read the Secret Key from a repository secret:
|
|
|
294
294
|
### Requirements
|
|
295
295
|
|
|
296
296
|
- `index.html` must exist in the root of the deploy directory
|
|
297
|
-
- Supported file types: `.html`, `.css`, `.js`, `.json`, `.svg`, `.png`, `.jpg`, `.gif`, `.webp`, `.woff`, `.woff2`, `.ttf`, `.mp3`, `.mp4`, `.pdf`, `.wasm`, 3D assets (`.glb`, `.gltf`, `.bin`), browser ML models (`.onnx`, `.tflite`, `.task`, `.safetensors`), etc.
|
|
297
|
+
- Supported file types: `.html`, `.css`, `.js`, `.json`, `.svg`, `.png`, `.jpg`, `.gif`, `.webp`, `.woff`, `.woff2`, `.ttf`, `.mp3`, `.mp4`, `.pdf`, `.wasm`, 3D/data assets (`.glb`, `.gltf`, `.bin`, `.mvx`), browser ML models (`.onnx`, `.tflite`, `.task`, `.safetensors`), etc.
|
|
298
|
+
- Files without an extension (e.g. randomly named data files) are skipped by default so that a project-root deploy does not publish files like `Dockerfile` or `id_rsa`. Pass `--include-extensionless` (or set `"includeExtensionless": true` in `.connectbaserc`) to deploy them as binary; they are served as `application/octet-stream` byte for byte (requires 6.9.0+).
|
|
298
299
|
- Files with other extensions are skipped, and `deploy` prints the skipped list as a warning. Hidden files and folders (`.env`, `.git/`) are skipped silently.
|
|
299
300
|
- Binary files over the per-file limit (~6 MB) are uploaded directly to object storage via presigned URLs.
|
|
300
301
|
|
|
@@ -1108,7 +1109,7 @@ try {
|
|
|
1108
1109
|
```
|
|
1109
1110
|
|
|
1110
1111
|
방송인 여부는 구독할 때 정해집니다. 멤버 역할이나 카테고리 설정을 바꾸면 다시 구독(또는 재연결)할 때 반영되고,
|
|
1111
|
-
카테고리 설정은
|
|
1112
|
+
카테고리 설정은 저장하면 몇 초 안에 서버 전체에 퍼집니다. room 채널(v6.7.0)은 room 인가 함수가 `publisher: true` 로
|
|
1112
1113
|
room 마다 방송인을 정할 수도 있습니다(위 "Rooms").
|
|
1113
1114
|
|
|
1114
1115
|
#### Presence / Typing
|
|
@@ -1431,7 +1432,17 @@ const subscription = await registration.pushManager.subscribe({
|
|
|
1431
1432
|
userVisibleOnly: true,
|
|
1432
1433
|
applicationServerKey: vapidKey.public_key
|
|
1433
1434
|
})
|
|
1434
|
-
await cb.push.registerWebPush(subscription)
|
|
1435
|
+
const webDevice = await cb.push.registerWebPush(subscription)
|
|
1436
|
+
// Browser devices subscribe the same way (v6.8.0+: device_token is the endpoint URL)
|
|
1437
|
+
await cb.push.subscribeTopic(webDevice.device_token, 'news')
|
|
1438
|
+
```
|
|
1439
|
+
|
|
1440
|
+
Topics must exist before devices subscribe; subscribing to an unknown topic fails with 404. Create them in the console,
|
|
1441
|
+
or from server code (v6.8.0+, not available to public-key-only instances):
|
|
1442
|
+
|
|
1443
|
+
```typescript
|
|
1444
|
+
// e.g. in a database trigger function: one topic per room, safe to call again (returns the existing topic)
|
|
1445
|
+
await ctx.cbAdmin.push.createTopic(ctx.appId, `cc_${room}`, { displayName: title })
|
|
1435
1446
|
```
|
|
1436
1447
|
|
|
1437
1448
|
### WebRTC
|
package/dist/cli.js
CHANGED
|
@@ -43,6 +43,7 @@ __export(cli_exports, {
|
|
|
43
43
|
guessMimeType: () => guessMimeType,
|
|
44
44
|
isAllowedDeployExt: () => isAllowedDeployExt,
|
|
45
45
|
isBinaryDeployExt: () => isBinaryDeployExt,
|
|
46
|
+
isExtensionlessDataFile: () => isExtensionlessDataFile,
|
|
46
47
|
makeRequest: () => makeRequest,
|
|
47
48
|
normalizeRelativePath: () => normalizeRelativePath,
|
|
48
49
|
packageManagerCommands: () => packageManagerCommands,
|
|
@@ -156,7 +157,7 @@ function handleTunnelError(msg, appId, localPort) {
|
|
|
156
157
|
}
|
|
157
158
|
|
|
158
159
|
// src/cli.ts
|
|
159
|
-
var VERSION = "6.
|
|
160
|
+
var VERSION = "6.9.0" ? "6.9.0" : getPackageVersion();
|
|
160
161
|
var DEFAULT_BASE_URL = "https://api.connectbase.world";
|
|
161
162
|
var colors = {
|
|
162
163
|
reset: "\x1B[0m",
|
|
@@ -218,6 +219,8 @@ var ALLOWED_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
|
218
219
|
".glb",
|
|
219
220
|
".gltf",
|
|
220
221
|
".bin",
|
|
222
|
+
// 사이트 자체 형식의 3D/자료 파일(바이너리, 2026-10-30). 브라우저는 fetch 로만 읽는다.
|
|
223
|
+
".mvx",
|
|
221
224
|
// 브라우저 추론용 ML 모델 (platform-issue 01a0f660). 전부 바이너리다.
|
|
222
225
|
// .onnx — onnxruntime-web
|
|
223
226
|
// .tflite — TensorFlow Lite (tfjs-tflite 등)
|
|
@@ -253,6 +256,7 @@ var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
|
253
256
|
".wasm",
|
|
254
257
|
".glb",
|
|
255
258
|
".bin",
|
|
259
|
+
".mvx",
|
|
256
260
|
".onnx",
|
|
257
261
|
".tflite",
|
|
258
262
|
".task",
|
|
@@ -283,6 +287,7 @@ var MIME_BY_EXT = {
|
|
|
283
287
|
".glb": "model/gltf-binary",
|
|
284
288
|
".gltf": "model/gltf+json",
|
|
285
289
|
".bin": "application/octet-stream",
|
|
290
|
+
".mvx": "application/octet-stream",
|
|
286
291
|
// ML 모델은 등록된 표준 MIME 이 없다. 로더(onnxruntime-web/MediaPipe/transformers.js)는
|
|
287
292
|
// fetch → ArrayBuffer 로 읽으므로 octet-stream 이면 충분하다.
|
|
288
293
|
".onnx": "application/octet-stream",
|
|
@@ -305,6 +310,10 @@ function isAllowedDeployExt(filePath) {
|
|
|
305
310
|
function isBinaryDeployExt(filePath) {
|
|
306
311
|
return BINARY_EXTENSIONS.has(path2.extname(filePath).toLowerCase());
|
|
307
312
|
}
|
|
313
|
+
function isExtensionlessDataFile(filePath) {
|
|
314
|
+
const base = path2.basename(filePath);
|
|
315
|
+
return path2.extname(base) === "" && !ALLOWED_EXTENSIONLESS_FILES.has(base);
|
|
316
|
+
}
|
|
308
317
|
var WEB_FILE_MAX_STORED_BYTES = 6 * 1024 * 1024 - 64 * 1024;
|
|
309
318
|
function sha256Hex(value) {
|
|
310
319
|
return crypto.createHash("sha256").update(value, "utf8").digest("hex");
|
|
@@ -365,19 +374,21 @@ function loadConfig() {
|
|
|
365
374
|
if (rcContent.storageId) config.storageId = rcContent.storageId;
|
|
366
375
|
if (rcContent.baseUrl) config.baseUrl = rcContent.baseUrl;
|
|
367
376
|
if (rcContent.deployDir) config.deployDir = rcContent.deployDir;
|
|
377
|
+
if (rcContent.includeExtensionless === true)
|
|
378
|
+
config.includeExtensionless = true;
|
|
368
379
|
} catch {
|
|
369
380
|
warn(".connectbaserc \uD30C\uC77C\uC744 \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
|
|
370
381
|
}
|
|
371
382
|
}
|
|
372
383
|
return config;
|
|
373
384
|
}
|
|
374
|
-
function collectDeployFiles(dir) {
|
|
385
|
+
function collectDeployFiles(dir, options = {}) {
|
|
375
386
|
const skipped = [];
|
|
376
|
-
const files = collectFiles(dir, dir, skipped);
|
|
387
|
+
const files = collectFiles(dir, dir, skipped, options);
|
|
377
388
|
skipped.sort();
|
|
378
389
|
return { files, skipped };
|
|
379
390
|
}
|
|
380
|
-
function collectFiles(dir, baseDir, skipped) {
|
|
391
|
+
function collectFiles(dir, baseDir, skipped, options) {
|
|
381
392
|
const files = [];
|
|
382
393
|
const entries = fs2.readdirSync(dir, { withFileTypes: true });
|
|
383
394
|
for (const entry of entries) {
|
|
@@ -385,18 +396,19 @@ function collectFiles(dir, baseDir, skipped) {
|
|
|
385
396
|
const relativePath = path2.relative(baseDir, fullPath);
|
|
386
397
|
if (entry.isDirectory()) {
|
|
387
398
|
if (!entry.name.startsWith(".")) {
|
|
388
|
-
files.push(...collectFiles(fullPath, baseDir, skipped));
|
|
399
|
+
files.push(...collectFiles(fullPath, baseDir, skipped, options));
|
|
389
400
|
}
|
|
390
401
|
} else if (entry.isFile()) {
|
|
391
402
|
const ext = path2.extname(entry.name).toLowerCase();
|
|
392
403
|
if (entry.name.startsWith(".")) {
|
|
393
404
|
continue;
|
|
394
405
|
}
|
|
395
|
-
|
|
406
|
+
const extensionlessData = options.includeExtensionless === true && isExtensionlessDataFile(entry.name);
|
|
407
|
+
if (!extensionlessData && !isAllowedDeployExt(entry.name)) {
|
|
396
408
|
skipped.push(normalizeRelativePath(relativePath));
|
|
397
409
|
continue;
|
|
398
410
|
}
|
|
399
|
-
const isBinary = BINARY_EXTENSIONS.has(ext);
|
|
411
|
+
const isBinary = extensionlessData || BINARY_EXTENSIONS.has(ext);
|
|
400
412
|
let content;
|
|
401
413
|
if (isBinary) {
|
|
402
414
|
content = fs2.readFileSync(fullPath).toString("base64");
|
|
@@ -438,6 +450,11 @@ function formatSkippedFilesReport(skipped, limit = SKIPPED_FILES_LIST_LIMIT) {
|
|
|
438
450
|
lines.push(
|
|
439
451
|
" \uD544\uC694\uD55C \uD30C\uC77C\uC774\uBA74 \uD5C8\uC6A9 \uD655\uC7A5\uC790\uB85C \uBC14\uAFB8\uAC70\uB098, \uD30C\uC77C \uC2A4\uD1A0\uB9AC\uC9C0(cb.storage)\uC5D0 \uC62C\uB824 URL \uB85C \uCC38\uC870\uD558\uC138\uC694."
|
|
440
452
|
);
|
|
453
|
+
if (skipped.some((p) => isExtensionlessDataFile(p))) {
|
|
454
|
+
lines.push(
|
|
455
|
+
' \uD655\uC7A5\uC790 \uC5C6\uB294 \uD30C\uC77C\uC740 --include-extensionless (\uB610\uB294 .connectbaserc \uC758 "includeExtensionless": true) \uB85C \uBC14\uC774\uB108\uB9AC(application/octet-stream)\uB85C \uBC30\uD3EC\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.'
|
|
456
|
+
);
|
|
457
|
+
}
|
|
441
458
|
return lines;
|
|
442
459
|
}
|
|
443
460
|
function reportSkippedFiles(skipped) {
|
|
@@ -766,8 +783,18 @@ async function deploy(directory, config, isDev = false, deployOpts = {}) {
|
|
|
766
783
|
}
|
|
767
784
|
info(`\uBC30\uD3EC \uC900\uBE44 \uC911... ${dir}`);
|
|
768
785
|
process.stdout.write(`${colors.dim}\uD30C\uC77C \uC218\uC9D1 \uC911...${colors.reset}`);
|
|
769
|
-
const { files, skipped } = collectDeployFiles(dir
|
|
786
|
+
const { files, skipped } = collectDeployFiles(dir, {
|
|
787
|
+
includeExtensionless: deployOpts.includeExtensionless
|
|
788
|
+
});
|
|
770
789
|
process.stdout.write(`\r${colors.dim} \r${colors.reset}`);
|
|
790
|
+
if (deployOpts.includeExtensionless) {
|
|
791
|
+
const extensionless = files.filter((f) => isExtensionlessDataFile(f.path));
|
|
792
|
+
if (extensionless.length > 0) {
|
|
793
|
+
info(
|
|
794
|
+
`\uD655\uC7A5\uC790 \uC5C6\uB294 \uD30C\uC77C ${extensionless.length}\uAC1C\uB97C \uBC14\uC774\uB108\uB9AC(application/octet-stream)\uB85C \uD3EC\uD568\uD569\uB2C8\uB2E4 (--include-extensionless)`
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
771
798
|
if (files.length === 0) {
|
|
772
799
|
reportSkippedFiles(skipped);
|
|
773
800
|
error("\uBC30\uD3EC\uD560 \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4");
|
|
@@ -3670,6 +3697,7 @@ ${colors.yellow}\uC635\uC158:${colors.reset}
|
|
|
3670
3697
|
--label <name> Endpoint binding \uC790\uB3D9 \uB4F1\uB85D (tunnel \uC804\uC6A9) \u2014 SDK \uC758 cb.endpoint.call(label) \uD638\uCD9C \uAC00\uB2A5
|
|
3671
3698
|
--description <text> Endpoint binding \uC124\uBA85 (--label \uB3D9\uBC18 \uC2DC\uB9CC)
|
|
3672
3699
|
-d, --dev QA \uD658\uACBD\uC5D0\uB9CC \uBC30\uD3EC (deploy \uC804\uC6A9, deprecated \u2014 QA \uBCC4\uCE6D)
|
|
3700
|
+
--include-extensionless \uD655\uC7A5\uC790 \uC5C6\uB294 \uD30C\uC77C\uB3C4 \uBC14\uC774\uB108\uB9AC(application/octet-stream)\uB85C \uBC30\uD3EC (deploy \uC804\uC6A9, \uAE30\uBCF8: \uC81C\uC678)
|
|
3673
3701
|
-y, --yes \uBE44\uB300\uD654\uD615 \uCD08\uAE30\uD654 \u2014 \uD504\uB86C\uD504\uD2B8 \uC5C6\uC774 \uAE30\uBCF8\uAC12/\uD50C\uB798\uADF8\uB85C \uC9C4\uD589 (init \uC804\uC6A9, CI/AI \uC5D0\uC774\uC804\uD2B8\uC6A9)
|
|
3674
3702
|
-a, --app <id> \uAE30\uC874 \uC571 ID \uC9C0\uC815 (init \uC804\uC6A9)
|
|
3675
3703
|
--secret-key <key> Secret Key (cb_sk_) \uC9C0\uC815 (init/mcp)
|
|
@@ -3727,7 +3755,8 @@ ${colors.yellow}\uC124\uC815 \uD30C\uC77C (.connectbaserc):${colors.reset}
|
|
|
3727
3755
|
"publicKey": "cb_pk_...",
|
|
3728
3756
|
"secretKey": "cb_sk_...",
|
|
3729
3757
|
"storageId": "your-storage-id",
|
|
3730
|
-
"deployDir": "./dist"
|
|
3758
|
+
"deployDir": "./dist",
|
|
3759
|
+
"includeExtensionless": false ${colors.dim}// true \uBA74 \uD655\uC7A5\uC790 \uC5C6\uB294 \uD30C\uC77C\uB3C4 \uBC30\uD3EC (= --include-extensionless)${colors.reset}
|
|
3731
3760
|
}
|
|
3732
3761
|
`);
|
|
3733
3762
|
}
|
|
@@ -3771,6 +3800,8 @@ function parseArgs(args) {
|
|
|
3771
3800
|
result.options.showToken = "true";
|
|
3772
3801
|
} else if (arg === "-d" || arg === "--dev") {
|
|
3773
3802
|
result.options.dev = "true";
|
|
3803
|
+
} else if (arg === "--include-extensionless") {
|
|
3804
|
+
result.options.includeExtensionless = "true";
|
|
3774
3805
|
} else if (arg === "--check") {
|
|
3775
3806
|
result.options.check = "true";
|
|
3776
3807
|
} else if (arg === "--skip-docs") {
|
|
@@ -3845,7 +3876,9 @@ async function main() {
|
|
|
3845
3876
|
process.exit(1);
|
|
3846
3877
|
}
|
|
3847
3878
|
const isDev = parsed.options.dev === "true";
|
|
3848
|
-
const deployOpts = {
|
|
3879
|
+
const deployOpts = {
|
|
3880
|
+
includeExtensionless: parsed.options.includeExtensionless === "true" || fileConfig.includeExtensionless === true
|
|
3881
|
+
};
|
|
3849
3882
|
if (parsed.options.timeout) {
|
|
3850
3883
|
const t = parseInt(parsed.options.timeout, 10);
|
|
3851
3884
|
if (!Number.isNaN(t) && t > 0) deployOpts.timeoutMs = t * 1e3;
|
|
@@ -3960,6 +3993,7 @@ main().catch((err) => {
|
|
|
3960
3993
|
guessMimeType,
|
|
3961
3994
|
isAllowedDeployExt,
|
|
3962
3995
|
isBinaryDeployExt,
|
|
3996
|
+
isExtensionlessDataFile,
|
|
3963
3997
|
makeRequest,
|
|
3964
3998
|
normalizeRelativePath,
|
|
3965
3999
|
packageManagerCommands,
|