@drawcall/market 0.1.13 → 0.1.14
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/install.d.ts +3 -2
- package/dist/install.d.ts.map +1 -1
- package/dist/install.js +45 -21
- package/dist/install.js.map +1 -1
- package/package.json +2 -1
- package/src/install.ts +54 -23
- package/tests/install-layout.test.ts +114 -0
package/dist/install.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Install resolved assets into a local project.
|
|
3
3
|
*
|
|
4
|
-
* 1. Downloads asset files into ./
|
|
4
|
+
* 1. Downloads public asset files into ./public/... via the oRPC client.
|
|
5
5
|
* 2. Merges npm dependencies into package.json.
|
|
6
6
|
* 3. Runs the package manager to install npm deps.
|
|
7
7
|
*
|
|
@@ -11,10 +11,11 @@
|
|
|
11
11
|
import type { MarketClient } from './client.js';
|
|
12
12
|
import type { ResolveResult } from './resolve.js';
|
|
13
13
|
export interface InstallOptions {
|
|
14
|
-
/**
|
|
14
|
+
/** Directory to start project root discovery from (default: cwd) */
|
|
15
15
|
cwd?: string;
|
|
16
16
|
/** Log progress */
|
|
17
17
|
onProgress?: (message: string) => void;
|
|
18
18
|
}
|
|
19
19
|
export declare function install(client: MarketClient, resolution: ResolveResult, opts?: InstallOptions): Promise<void>;
|
|
20
|
+
export declare function findInstallRoot(cwd?: string): Promise<string>;
|
|
20
21
|
//# sourceMappingURL=install.d.ts.map
|
package/dist/install.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../src/install.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAMH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAC/C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AASjD,MAAM,WAAW,cAAc;IAC7B,
|
|
1
|
+
{"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../src/install.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAMH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAC/C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AASjD,MAAM,WAAW,cAAc;IAC7B,oEAAoE;IACpE,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,mBAAmB;IACnB,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CACvC;AAED,wBAAsB,OAAO,CAC3B,MAAM,EAAE,YAAY,EACpB,UAAU,EAAE,aAAa,EACzB,IAAI,GAAE,cAAmB,GACxB,OAAO,CAAC,IAAI,CAAC,CAQf;AAED,wBAAsB,eAAe,CAAC,GAAG,GAAE,MAAsB,GAAG,OAAO,CAAC,MAAM,CAAC,CAgBlF"}
|
package/dist/install.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Install resolved assets into a local project.
|
|
3
3
|
*
|
|
4
|
-
* 1. Downloads asset files into ./
|
|
4
|
+
* 1. Downloads public asset files into ./public/... via the oRPC client.
|
|
5
5
|
* 2. Merges npm dependencies into package.json.
|
|
6
6
|
* 3. Runs the package manager to install npm deps.
|
|
7
7
|
*
|
|
@@ -13,42 +13,68 @@ import * as path from 'path';
|
|
|
13
13
|
import { unzipSync } from 'fflate';
|
|
14
14
|
import { detectPackageManager, installDependencies } from 'nypm';
|
|
15
15
|
export async function install(client, resolution, opts = {}) {
|
|
16
|
-
const cwd = opts.cwd ?? process.cwd();
|
|
17
16
|
const log = opts.onProgress ?? (() => { });
|
|
18
|
-
|
|
17
|
+
const installRoot = await findInstallRoot(opts.cwd ?? process.cwd());
|
|
19
18
|
await Promise.all([
|
|
20
|
-
downloadAssets(client, resolution,
|
|
21
|
-
installNpmDeps(resolution,
|
|
19
|
+
downloadAssets(client, resolution, installRoot, log),
|
|
20
|
+
installNpmDeps(resolution, installRoot, log),
|
|
22
21
|
]);
|
|
23
22
|
}
|
|
24
|
-
async function
|
|
23
|
+
export async function findInstallRoot(cwd = process.cwd()) {
|
|
24
|
+
const start = path.resolve(cwd);
|
|
25
|
+
let packageRoot = null;
|
|
26
|
+
let dir = start;
|
|
27
|
+
while (true) {
|
|
28
|
+
if (await isFile(path.join(dir, 'package.json'))) {
|
|
29
|
+
packageRoot = dir;
|
|
30
|
+
}
|
|
31
|
+
const parent = path.dirname(dir);
|
|
32
|
+
if (parent === dir)
|
|
33
|
+
break;
|
|
34
|
+
dir = parent;
|
|
35
|
+
}
|
|
36
|
+
return packageRoot ?? start;
|
|
37
|
+
}
|
|
38
|
+
async function downloadAssets(client, resolution, projectRoot, log) {
|
|
25
39
|
for (const asset of resolution.assets) {
|
|
26
|
-
const destDir = path.join(cwd, 'src', asset.name);
|
|
27
|
-
await fs.mkdir(destDir, { recursive: true });
|
|
28
40
|
log(`Downloading ${asset.name}@${asset.version}...`);
|
|
29
41
|
const zip = await client.asset.downloadZip({ name: asset.name, version: asset.version });
|
|
30
42
|
const files = unzipSync(new Uint8Array(await zip.arrayBuffer()));
|
|
43
|
+
let installedFiles = 0;
|
|
31
44
|
for (const [relativePath, content] of Object.entries(files)) {
|
|
32
|
-
const
|
|
33
|
-
if (
|
|
45
|
+
const zipPath = relativePath.replace(/\\/g, '/');
|
|
46
|
+
if (zipPath.split('/').includes('..') ||
|
|
47
|
+
path.posix.isAbsolute(zipPath) ||
|
|
48
|
+
path.win32.isAbsolute(zipPath)) {
|
|
34
49
|
throw new Error(`Zip contains an unsafe path: ${relativePath}`);
|
|
35
50
|
}
|
|
51
|
+
const normalizedPath = path.posix.normalize(zipPath);
|
|
52
|
+
if (normalizedPath === '.' ||
|
|
53
|
+
normalizedPath === 'README.md' ||
|
|
54
|
+
normalizedPath.endsWith('/')) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const filePath = path.join(projectRoot, normalizedPath);
|
|
36
58
|
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
37
59
|
await fs.writeFile(filePath, content);
|
|
60
|
+
installedFiles += 1;
|
|
38
61
|
}
|
|
39
|
-
log(`Downloaded ${
|
|
62
|
+
log(`Downloaded ${installedFiles} files.`);
|
|
40
63
|
}
|
|
41
64
|
}
|
|
42
|
-
function
|
|
43
|
-
|
|
44
|
-
|
|
65
|
+
async function isFile(file) {
|
|
66
|
+
try {
|
|
67
|
+
return (await fs.stat(file)).isFile();
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
45
72
|
}
|
|
46
|
-
async function installNpmDeps(resolution,
|
|
73
|
+
async function installNpmDeps(resolution, projectRoot, log) {
|
|
47
74
|
const deps = resolution.npmDependencies;
|
|
48
75
|
if (Object.keys(deps).length === 0)
|
|
49
76
|
return;
|
|
50
|
-
|
|
51
|
-
const pkgPath = path.join(cwd, 'package.json');
|
|
77
|
+
const pkgPath = path.join(projectRoot, 'package.json');
|
|
52
78
|
let pkg;
|
|
53
79
|
try {
|
|
54
80
|
pkg = JSON.parse(await fs.readFile(pkgPath, 'utf-8'));
|
|
@@ -56,15 +82,13 @@ async function installNpmDeps(resolution, cwd, log) {
|
|
|
56
82
|
catch {
|
|
57
83
|
pkg = { name: 'my-project', private: true, dependencies: {} };
|
|
58
84
|
}
|
|
59
|
-
// Merge dependencies
|
|
60
85
|
pkg.dependencies = { ...pkg.dependencies, ...deps };
|
|
61
86
|
await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
|
62
87
|
log(`Installing npm dependencies: ${Object.keys(deps).join(', ')}`);
|
|
63
|
-
|
|
64
|
-
const pm = await detectPackageManager(cwd).catch(() => null);
|
|
88
|
+
const pm = await detectPackageManager(projectRoot).catch(() => null);
|
|
65
89
|
const pmName = pm?.name ?? 'npm';
|
|
66
90
|
log(`Using ${pmName}...`);
|
|
67
|
-
await installDependencies({ cwd, packageManager: { name: pmName, command: pmName } });
|
|
91
|
+
await installDependencies({ cwd: projectRoot, packageManager: { name: pmName, command: pmName } });
|
|
68
92
|
log('npm dependencies installed.');
|
|
69
93
|
}
|
|
70
94
|
//# sourceMappingURL=install.js.map
|
package/dist/install.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"install.js","sourceRoot":"","sources":["../src/install.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,MAAM,aAAa,CAAA;AACjC,OAAO,KAAK,IAAI,MAAM,MAAM,CAAA;AAC5B,OAAO,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAA;AAClC,OAAO,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,MAAM,CAAA;AAkBhE,MAAM,CAAC,KAAK,UAAU,OAAO,CAC3B,MAAoB,EACpB,UAAyB,EACzB,OAAuB,EAAE;IAEzB,MAAM,GAAG,GAAG,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"install.js","sourceRoot":"","sources":["../src/install.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drawcall/market",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/drawcall-ai/market",
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"scripts": {
|
|
32
32
|
"build": "tsc",
|
|
33
33
|
"dev": "tsx src/cli.ts",
|
|
34
|
+
"test:install-layout": "tsx --test tests/install-layout.test.ts",
|
|
34
35
|
"typecheck": "tsc --noEmit"
|
|
35
36
|
},
|
|
36
37
|
"dependencies": {
|
package/src/install.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Install resolved assets into a local project.
|
|
3
3
|
*
|
|
4
|
-
* 1. Downloads asset files into ./
|
|
4
|
+
* 1. Downloads public asset files into ./public/... via the oRPC client.
|
|
5
5
|
* 2. Merges npm dependencies into package.json.
|
|
6
6
|
* 3. Runs the package manager to install npm deps.
|
|
7
7
|
*
|
|
@@ -24,7 +24,7 @@ interface PackageJson {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export interface InstallOptions {
|
|
27
|
-
/**
|
|
27
|
+
/** Directory to start project root discovery from (default: cwd) */
|
|
28
28
|
cwd?: string
|
|
29
29
|
/** Log progress */
|
|
30
30
|
onProgress?: (message: string) => void
|
|
@@ -35,59 +35,92 @@ export async function install(
|
|
|
35
35
|
resolution: ResolveResult,
|
|
36
36
|
opts: InstallOptions = {},
|
|
37
37
|
): Promise<void> {
|
|
38
|
-
const cwd = opts.cwd ?? process.cwd()
|
|
39
38
|
const log = opts.onProgress ?? (() => {})
|
|
39
|
+
const installRoot = await findInstallRoot(opts.cwd ?? process.cwd())
|
|
40
40
|
|
|
41
|
-
// Run asset file downloads and npm dep installation in parallel
|
|
42
41
|
await Promise.all([
|
|
43
|
-
downloadAssets(client, resolution,
|
|
44
|
-
installNpmDeps(resolution,
|
|
42
|
+
downloadAssets(client, resolution, installRoot, log),
|
|
43
|
+
installNpmDeps(resolution, installRoot, log),
|
|
45
44
|
])
|
|
46
45
|
}
|
|
47
46
|
|
|
47
|
+
export async function findInstallRoot(cwd: string = process.cwd()): Promise<string> {
|
|
48
|
+
const start = path.resolve(cwd)
|
|
49
|
+
let packageRoot: string | null = null
|
|
50
|
+
|
|
51
|
+
let dir = start
|
|
52
|
+
while (true) {
|
|
53
|
+
if (await isFile(path.join(dir, 'package.json'))) {
|
|
54
|
+
packageRoot = dir
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const parent = path.dirname(dir)
|
|
58
|
+
if (parent === dir) break
|
|
59
|
+
dir = parent
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return packageRoot ?? start
|
|
63
|
+
}
|
|
64
|
+
|
|
48
65
|
async function downloadAssets(
|
|
49
66
|
client: MarketClient,
|
|
50
67
|
resolution: ResolveResult,
|
|
51
|
-
|
|
68
|
+
projectRoot: string,
|
|
52
69
|
log: (msg: string) => void,
|
|
53
70
|
): Promise<void> {
|
|
54
71
|
for (const asset of resolution.assets) {
|
|
55
|
-
const destDir = path.join(cwd, 'src', asset.name)
|
|
56
|
-
await fs.mkdir(destDir, { recursive: true })
|
|
57
|
-
|
|
58
72
|
log(`Downloading ${asset.name}@${asset.version}...`)
|
|
59
73
|
|
|
60
74
|
const zip = await client.asset.downloadZip({ name: asset.name, version: asset.version })
|
|
61
75
|
const files = unzipSync(new Uint8Array(await zip.arrayBuffer()))
|
|
76
|
+
let installedFiles = 0
|
|
62
77
|
|
|
63
78
|
for (const [relativePath, content] of Object.entries(files) as [string, Uint8Array][]) {
|
|
64
|
-
const
|
|
65
|
-
if (
|
|
79
|
+
const zipPath = relativePath.replace(/\\/g, '/')
|
|
80
|
+
if (
|
|
81
|
+
zipPath.split('/').includes('..') ||
|
|
82
|
+
path.posix.isAbsolute(zipPath) ||
|
|
83
|
+
path.win32.isAbsolute(zipPath)
|
|
84
|
+
) {
|
|
66
85
|
throw new Error(`Zip contains an unsafe path: ${relativePath}`)
|
|
67
86
|
}
|
|
87
|
+
|
|
88
|
+
const normalizedPath = path.posix.normalize(zipPath)
|
|
89
|
+
if (
|
|
90
|
+
normalizedPath === '.' ||
|
|
91
|
+
normalizedPath === 'README.md' ||
|
|
92
|
+
normalizedPath.endsWith('/')
|
|
93
|
+
) {
|
|
94
|
+
continue
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const filePath = path.join(projectRoot, normalizedPath)
|
|
68
98
|
await fs.mkdir(path.dirname(filePath), { recursive: true })
|
|
69
99
|
await fs.writeFile(filePath, content)
|
|
100
|
+
installedFiles += 1
|
|
70
101
|
}
|
|
71
102
|
|
|
72
|
-
log(`Downloaded ${
|
|
103
|
+
log(`Downloaded ${installedFiles} files.`)
|
|
73
104
|
}
|
|
74
105
|
}
|
|
75
106
|
|
|
76
|
-
function
|
|
77
|
-
|
|
78
|
-
|
|
107
|
+
async function isFile(file: string): Promise<boolean> {
|
|
108
|
+
try {
|
|
109
|
+
return (await fs.stat(file)).isFile()
|
|
110
|
+
} catch {
|
|
111
|
+
return false
|
|
112
|
+
}
|
|
79
113
|
}
|
|
80
114
|
|
|
81
115
|
async function installNpmDeps(
|
|
82
116
|
resolution: ResolveResult,
|
|
83
|
-
|
|
117
|
+
projectRoot: string,
|
|
84
118
|
log: (msg: string) => void,
|
|
85
119
|
): Promise<void> {
|
|
86
120
|
const deps = resolution.npmDependencies
|
|
87
121
|
if (Object.keys(deps).length === 0) return
|
|
88
122
|
|
|
89
|
-
|
|
90
|
-
const pkgPath = path.join(cwd, 'package.json')
|
|
123
|
+
const pkgPath = path.join(projectRoot, 'package.json')
|
|
91
124
|
let pkg: PackageJson
|
|
92
125
|
try {
|
|
93
126
|
pkg = JSON.parse(await fs.readFile(pkgPath, 'utf-8')) as PackageJson
|
|
@@ -95,17 +128,15 @@ async function installNpmDeps(
|
|
|
95
128
|
pkg = { name: 'my-project', private: true, dependencies: {} }
|
|
96
129
|
}
|
|
97
130
|
|
|
98
|
-
// Merge dependencies
|
|
99
131
|
pkg.dependencies = { ...pkg.dependencies, ...deps }
|
|
100
132
|
await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
|
|
101
133
|
|
|
102
134
|
log(`Installing npm dependencies: ${Object.keys(deps).join(', ')}`)
|
|
103
135
|
|
|
104
|
-
|
|
105
|
-
const pm = await detectPackageManager(cwd).catch(() => null)
|
|
136
|
+
const pm = await detectPackageManager(projectRoot).catch(() => null)
|
|
106
137
|
const pmName = pm?.name ?? 'npm'
|
|
107
138
|
|
|
108
139
|
log(`Using ${pmName}...`)
|
|
109
|
-
await installDependencies({ cwd, packageManager: { name: pmName, command: pmName } })
|
|
140
|
+
await installDependencies({ cwd: projectRoot, packageManager: { name: pmName, command: pmName } })
|
|
110
141
|
log('npm dependencies installed.')
|
|
111
142
|
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import * as fs from 'node:fs/promises'
|
|
3
|
+
import * as os from 'node:os'
|
|
4
|
+
import * as path from 'node:path'
|
|
5
|
+
import test from 'node:test'
|
|
6
|
+
import { zipSync } from 'fflate'
|
|
7
|
+
import { findInstallRoot, install } from '../src/install.js'
|
|
8
|
+
|
|
9
|
+
const textEncoder = new TextEncoder()
|
|
10
|
+
|
|
11
|
+
test('install writes zip files into the package root', async () => {
|
|
12
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'market-install-'))
|
|
13
|
+
const appRoot = path.join(tempDir, 'app')
|
|
14
|
+
const cwd = path.join(appRoot, 'src', 'feature')
|
|
15
|
+
await fs.mkdir(path.join(appRoot, 'public'), { recursive: true })
|
|
16
|
+
await fs.mkdir(cwd, { recursive: true })
|
|
17
|
+
await fs.writeFile(path.join(appRoot, 'package.json'), '{}\n')
|
|
18
|
+
await fs.writeFile(path.join(appRoot, 'README.md'), 'project readme\n')
|
|
19
|
+
|
|
20
|
+
await install(
|
|
21
|
+
clientWithZip({
|
|
22
|
+
'public/humanoid-animation/idle-loop.glb': textEncoder.encode('glb'),
|
|
23
|
+
'src/generated/idle-loop.ts': textEncoder.encode('export const idleLoop = true\n'),
|
|
24
|
+
'README.md': textEncoder.encode('asset readme'),
|
|
25
|
+
}),
|
|
26
|
+
{
|
|
27
|
+
assets: [
|
|
28
|
+
{
|
|
29
|
+
name: 'idle-loop',
|
|
30
|
+
version: '1.0.0',
|
|
31
|
+
npmDependencies: {},
|
|
32
|
+
assetDependencies: {},
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
npmDependencies: {},
|
|
36
|
+
},
|
|
37
|
+
{ cwd },
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
assert.equal(
|
|
41
|
+
await fs.readFile(path.join(appRoot, 'public', 'humanoid-animation', 'idle-loop.glb'), 'utf-8'),
|
|
42
|
+
'glb',
|
|
43
|
+
)
|
|
44
|
+
assert.equal(
|
|
45
|
+
await fs.readFile(path.join(appRoot, 'src', 'generated', 'idle-loop.ts'), 'utf-8'),
|
|
46
|
+
'export const idleLoop = true\n',
|
|
47
|
+
)
|
|
48
|
+
assert.equal(await fs.readFile(path.join(appRoot, 'README.md'), 'utf-8'), 'project readme\n')
|
|
49
|
+
assert.equal(await exists(path.join(cwd, 'src', 'idle-loop', 'public')), false)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('install rejects zip paths that escape through parent segments', async () => {
|
|
53
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'market-install-'))
|
|
54
|
+
await fs.writeFile(path.join(tempDir, 'package.json'), '{}\n')
|
|
55
|
+
|
|
56
|
+
await assert.rejects(
|
|
57
|
+
install(
|
|
58
|
+
clientWithZip({
|
|
59
|
+
'public/../package.json': textEncoder.encode('nope\n'),
|
|
60
|
+
}),
|
|
61
|
+
{
|
|
62
|
+
assets: [
|
|
63
|
+
{
|
|
64
|
+
name: 'bad-path',
|
|
65
|
+
version: '1.0.0',
|
|
66
|
+
npmDependencies: {},
|
|
67
|
+
assetDependencies: {},
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
npmDependencies: {},
|
|
71
|
+
},
|
|
72
|
+
{ cwd: tempDir },
|
|
73
|
+
),
|
|
74
|
+
/unsafe path/u,
|
|
75
|
+
)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('findInstallRoot ignores public directories and uses the highest package.json', async () => {
|
|
79
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'market-root-'))
|
|
80
|
+
const repoRoot = path.join(tempDir, 'repo')
|
|
81
|
+
const appRoot = path.join(repoRoot, 'packages', 'app')
|
|
82
|
+
const cwd = path.join(appRoot, 'src', 'routes')
|
|
83
|
+
await fs.mkdir(path.join(appRoot, 'public'), { recursive: true })
|
|
84
|
+
await fs.mkdir(cwd, { recursive: true })
|
|
85
|
+
await fs.writeFile(path.join(repoRoot, 'package.json'), '{}\n')
|
|
86
|
+
await fs.writeFile(path.join(appRoot, 'package.json'), '{}\n')
|
|
87
|
+
|
|
88
|
+
assert.equal(await findInstallRoot(cwd), repoRoot)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('findInstallRoot falls back to cwd when no package.json exists', async () => {
|
|
92
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'market-root-'))
|
|
93
|
+
const cwd = path.join(tempDir, 'repo', 'src')
|
|
94
|
+
await fs.mkdir(cwd, { recursive: true })
|
|
95
|
+
|
|
96
|
+
assert.equal(await findInstallRoot(cwd), cwd)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
function clientWithZip(files: Record<string, Uint8Array>) {
|
|
100
|
+
return {
|
|
101
|
+
asset: {
|
|
102
|
+
downloadZip: async () => new Blob([zipSync(files)]),
|
|
103
|
+
},
|
|
104
|
+
} as never
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function exists(file: string): Promise<boolean> {
|
|
108
|
+
try {
|
|
109
|
+
await fs.stat(file)
|
|
110
|
+
return true
|
|
111
|
+
} catch {
|
|
112
|
+
return false
|
|
113
|
+
}
|
|
114
|
+
}
|