jamdesk 1.1.40 → 1.1.42
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/__tests__/unit/clean.test.d.ts +2 -0
- package/dist/__tests__/unit/clean.test.d.ts.map +1 -0
- package/dist/__tests__/unit/clean.test.js +59 -0
- package/dist/__tests__/unit/clean.test.js.map +1 -0
- package/dist/__tests__/unit/dev-cache-cleanup.test.js +2 -2
- package/dist/__tests__/unit/dev-cache-cleanup.test.js.map +1 -1
- package/dist/__tests__/unit/dev-lock.test.d.ts +2 -0
- package/dist/__tests__/unit/dev-lock.test.d.ts.map +1 -0
- package/dist/__tests__/unit/dev-lock.test.js +70 -0
- package/dist/__tests__/unit/dev-lock.test.js.map +1 -0
- package/dist/__tests__/unit/dev-sync-vendored.test.d.ts +2 -0
- package/dist/__tests__/unit/dev-sync-vendored.test.d.ts.map +1 -0
- package/dist/__tests__/unit/dev-sync-vendored.test.js +58 -0
- package/dist/__tests__/unit/dev-sync-vendored.test.js.map +1 -0
- package/dist/__tests__/unit/run-build-script.test.d.ts +2 -0
- package/dist/__tests__/unit/run-build-script.test.d.ts.map +1 -0
- package/dist/__tests__/unit/run-build-script.test.js +58 -0
- package/dist/__tests__/unit/run-build-script.test.js.map +1 -0
- package/dist/__tests__/unit/vendored-sync.test.js +4 -0
- package/dist/__tests__/unit/vendored-sync.test.js.map +1 -1
- package/dist/__tests__/unit/workspace-paths.test.d.ts +2 -0
- package/dist/__tests__/unit/workspace-paths.test.d.ts.map +1 -0
- package/dist/__tests__/unit/workspace-paths.test.js +104 -0
- package/dist/__tests__/unit/workspace-paths.test.js.map +1 -0
- package/dist/commands/clean.d.ts +17 -2
- package/dist/commands/clean.d.ts.map +1 -1
- package/dist/commands/clean.js +47 -27
- package/dist/commands/clean.js.map +1 -1
- package/dist/commands/dev.d.ts +10 -1
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +111 -141
- package/dist/commands/dev.js.map +1 -1
- package/dist/index.js +10 -11
- package/dist/index.js.map +1 -1
- package/dist/lib/deps.d.ts +0 -4
- package/dist/lib/deps.d.ts.map +1 -1
- package/dist/lib/deps.js +0 -6
- package/dist/lib/deps.js.map +1 -1
- package/dist/lib/dev-lock.d.ts +15 -0
- package/dist/lib/dev-lock.d.ts.map +1 -0
- package/dist/lib/dev-lock.js +95 -0
- package/dist/lib/dev-lock.js.map +1 -0
- package/dist/lib/run-build-script.d.ts +13 -0
- package/dist/lib/run-build-script.d.ts.map +1 -0
- package/dist/lib/run-build-script.js +36 -0
- package/dist/lib/run-build-script.js.map +1 -0
- package/dist/lib/safe-fs.d.ts +18 -0
- package/dist/lib/safe-fs.d.ts.map +1 -0
- package/dist/lib/safe-fs.js +55 -0
- package/dist/lib/safe-fs.js.map +1 -0
- package/dist/lib/workspace-paths.d.ts +37 -0
- package/dist/lib/workspace-paths.d.ts.map +1 -0
- package/dist/lib/workspace-paths.js +64 -0
- package/dist/lib/workspace-paths.js.map +1 -0
- package/package.json +1 -1
- package/vendored/app/[[...slug]]/page.tsx +41 -28
- package/vendored/app/api/isr-health/route.ts +6 -4
- package/vendored/components/mdx/StepSlugContext.tsx +57 -0
- package/vendored/components/mdx/Steps.tsx +2 -2
- package/vendored/components/navigation/TableOfContents.tsx +77 -5
- package/vendored/lib/cache-tags.ts +25 -0
- package/vendored/lib/cache-utils.ts +19 -0
- package/vendored/lib/heading-extractor.ts +25 -6
- package/vendored/lib/indexnow.ts +1 -1
- package/vendored/lib/navigation-resolver.ts +1 -1
- package/vendored/lib/openapi-isr.ts +13 -8
- package/vendored/lib/r2-cleanup.ts +70 -0
- package/vendored/lib/r2-content.ts +0 -24
- package/vendored/lib/r2-manifest.ts +13 -3
- package/vendored/lib/revalidation-helpers.ts +41 -11
- package/vendored/lib/revalidation-trigger.ts +104 -28
- package/vendored/lib/scanner-blocklist.ts +256 -0
- package/vendored/lib/snippet-compiler-isr.ts +5 -2
- package/vendored/scripts/validate-links.cjs +17 -6
- package/vendored/workspace-package-lock.json +9 -9
- package/vendored/lib/cache-keys.ts +0 -117
package/dist/commands/clean.js
CHANGED
|
@@ -1,49 +1,69 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Clean Command
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Default: clears the current project's workspace
|
|
5
|
+
* (`<jamdeskDir>/workspaces/<slug>/`).
|
|
6
|
+
*
|
|
7
|
+
* `--all`: clears everything under `<jamdeskDir>/`, including shared
|
|
8
|
+
* `node_modules` (next dev will reinstall on first run).
|
|
9
|
+
*
|
|
10
|
+
* Both modes use safeRemoveCache so a still-running dev session in the
|
|
11
|
+
* target tree surfaces a clear "another dev is running" message rather
|
|
12
|
+
* than crashing ENOTEMPTY.
|
|
5
13
|
*/
|
|
6
14
|
import fs from 'fs-extra';
|
|
7
|
-
import path from 'path';
|
|
8
|
-
import { homedir } from 'os';
|
|
9
15
|
import { output } from '../lib/output.js';
|
|
10
16
|
import { spinner } from '../lib/spinner.js';
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
17
|
+
import { getJamdeskDir } from '../lib/deps.js';
|
|
18
|
+
import { getProjectWorkspaceDir } from '../lib/workspace-paths.js';
|
|
19
|
+
import { safeRemoveCache } from '../lib/safe-fs.js';
|
|
20
|
+
export async function clean(options = {}, ctx = { jamdeskDir: getJamdeskDir(), projectDir: process.cwd() }) {
|
|
21
|
+
const { jamdeskDir, projectDir } = ctx;
|
|
22
|
+
if (options.all) {
|
|
23
|
+
if (!fs.existsSync(jamdeskDir)) {
|
|
24
|
+
output.info(`Nothing to clean — ${jamdeskDir} does not exist`);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const spin = spinner('Calculating cache size...');
|
|
28
|
+
const size = await getDirectorySize(jamdeskDir);
|
|
29
|
+
spin.text = `Removing ${jamdeskDir} (${formatBytes(size)})...`;
|
|
30
|
+
await safeRemoveCache(jamdeskDir);
|
|
31
|
+
spin.succeed(`Cleared ${formatBytes(size)} of cache`);
|
|
32
|
+
console.log('\nCleared:');
|
|
33
|
+
console.log(` - ${jamdeskDir}/workspaces/ (all per-project caches)`);
|
|
34
|
+
console.log(` - ${jamdeskDir}/node_modules/ (installed dependencies)`);
|
|
35
|
+
console.log(` - ${jamdeskDir}/version-cache.json (update check cache)`);
|
|
36
|
+
console.log('\nNote: any currently-running `jamdesk dev` sessions will fail until you restart them.');
|
|
37
|
+
console.log('\nNext `jamdesk dev` will reinstall dependencies.');
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const workspaceDir = getProjectWorkspaceDir(jamdeskDir, projectDir);
|
|
41
|
+
if (!fs.existsSync(workspaceDir)) {
|
|
42
|
+
output.info(`Nothing to clean — no workspace at ${workspaceDir}`);
|
|
15
43
|
return;
|
|
16
44
|
}
|
|
17
|
-
const spin = spinner('Calculating
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
console.log('\nCleared:');
|
|
25
|
-
console.log(' - ~/.jamdesk/workspace/ (Build cache)');
|
|
26
|
-
console.log(' - ~/.jamdesk/node_modules/ (installed dependencies)');
|
|
27
|
-
console.log(' - ~/.jamdesk/version-cache.json (update check cache)');
|
|
28
|
-
console.log('\nNext `jamdesk dev` will reinstall dependencies.');
|
|
45
|
+
const spin = spinner('Calculating workspace size...');
|
|
46
|
+
const size = await getDirectorySize(workspaceDir);
|
|
47
|
+
spin.text = `Removing ${workspaceDir} (${formatBytes(size)})...`;
|
|
48
|
+
await safeRemoveCache(workspaceDir);
|
|
49
|
+
spin.succeed(`Cleared ${formatBytes(size)} from this project's workspace`);
|
|
50
|
+
console.log(`\nNext \`jamdesk dev\` in this directory will recompile from scratch.`);
|
|
51
|
+
console.log(`Use \`jamdesk clean --all\` to also wipe shared node_modules.`);
|
|
29
52
|
}
|
|
30
53
|
async function getDirectorySize(dir) {
|
|
31
54
|
let size = 0;
|
|
32
55
|
try {
|
|
33
56
|
const files = await fs.readdir(dir, { withFileTypes: true });
|
|
34
57
|
for (const file of files) {
|
|
35
|
-
const filePath =
|
|
36
|
-
if (file.isDirectory())
|
|
58
|
+
const filePath = `${dir}/${file.name}`;
|
|
59
|
+
if (file.isDirectory())
|
|
37
60
|
size += await getDirectorySize(filePath);
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const stats = await fs.stat(filePath);
|
|
41
|
-
size += stats.size;
|
|
42
|
-
}
|
|
61
|
+
else
|
|
62
|
+
size += (await fs.stat(filePath)).size;
|
|
43
63
|
}
|
|
44
64
|
}
|
|
45
65
|
catch {
|
|
46
|
-
//
|
|
66
|
+
// ignore (permissions, vanished entries)
|
|
47
67
|
}
|
|
48
68
|
return size;
|
|
49
69
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"clean.js","sourceRoot":"","sources":["../../src/commands/clean.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"clean.js","sourceRoot":"","sources":["../../src/commands/clean.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,MAAM,UAAU,CAAC;AAC1B,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC1C,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AACnE,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAWpD,MAAM,CAAC,KAAK,UAAU,KAAK,CACzB,UAAwB,EAAE,EAC1B,MAAoB,EAAE,UAAU,EAAE,aAAa,EAAE,EAAE,UAAU,EAAE,OAAO,CAAC,GAAG,EAAE,EAAE;IAE9E,MAAM,EAAE,UAAU,EAAE,UAAU,EAAE,GAAG,GAAG,CAAC;IAEvC,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;QAChB,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;YAC/B,MAAM,CAAC,IAAI,CAAC,sBAAsB,UAAU,iBAAiB,CAAC,CAAC;YAC/D,OAAO;QACT,CAAC;QACD,MAAM,IAAI,GAAG,OAAO,CAAC,2BAA2B,CAAC,CAAC;QAClD,MAAM,IAAI,GAAG,MAAM,gBAAgB,CAAC,UAAU,CAAC,CAAC;QAChD,IAAI,CAAC,IAAI,GAAG,YAAY,UAAU,KAAK,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC;QAC/D,MAAM,eAAe,CAAC,UAAU,CAAC,CAAC;QAClC,IAAI,CAAC,OAAO,CAAC,WAAW,WAAW,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QACtD,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;QAC1B,OAAO,CAAC,GAAG,CAAC,OAAO,UAAU,uCAAuC,CAAC,CAAC;QACtE,OAAO,CAAC,GAAG,CAAC,OAAO,UAAU,yCAAyC,CAAC,CAAC;QACxE,OAAO,CAAC,GAAG,CAAC,OAAO,UAAU,0CAA0C,CAAC,CAAC;QACzE,OAAO,CAAC,GAAG,CAAC,wFAAwF,CAAC,CAAC;QACtG,OAAO,CAAC,GAAG,CAAC,mDAAmD,CAAC,CAAC;QACjE,OAAO;IACT,CAAC;IAED,MAAM,YAAY,GAAG,sBAAsB,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC;IACpE,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE,CAAC;QACjC,MAAM,CAAC,IAAI,CAAC,sCAAsC,YAAY,EAAE,CAAC,CAAC;QAClE,OAAO;IACT,CAAC;IAED,MAAM,IAAI,GAAG,OAAO,CAAC,+BAA+B,CAAC,CAAC;IACtD,MAAM,IAAI,GAAG,MAAM,gBAAgB,CAAC,YAAY,CAAC,CAAC;IAClD,IAAI,CAAC,IAAI,GAAG,YAAY,YAAY,KAAK,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC;IACjE,MAAM,eAAe,CAAC,YAAY,CAAC,CAAC;IACpC,IAAI,CAAC,OAAO,CAAC,WAAW,WAAW,CAAC,IAAI,CAAC,gCAAgC,CAAC,CAAC;IAC3E,OAAO,CAAC,GAAG,CAAC,uEAAuE,CAAC,CAAC;IACrF,OAAO,CAAC,GAAG,CAAC,+DAA+D,CAAC,CAAC;AAC/E,CAAC;AAED,KAAK,UAAU,gBAAgB,CAAC,GAAW;IACzC,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;QAC7D,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,MAAM,QAAQ,GAAG,GAAG,GAAG,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;YACvC,IAAI,IAAI,CAAC,WAAW,EAAE;gBAAE,IAAI,IAAI,MAAM,gBAAgB,CAAC,QAAQ,CAAC,CAAC;;gBAC5D,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC;QAC9C,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,yCAAyC;IAC3C,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,WAAW,CAAC,KAAa;IAChC,IAAI,KAAK,GAAG,IAAI;QAAE,OAAO,GAAG,KAAK,IAAI,CAAC;IACtC,IAAI,KAAK,GAAG,IAAI,GAAG,IAAI;QAAE,OAAO,GAAG,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC;IAClE,IAAI,KAAK,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI;QAAE,OAAO,GAAG,CAAC,KAAK,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC;IAClF,OAAO,GAAG,CAAC,KAAK,GAAG,CAAC,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC;AAC3D,CAAC"}
|
package/dist/commands/dev.d.ts
CHANGED
|
@@ -27,7 +27,16 @@ export interface DevOptions {
|
|
|
27
27
|
webpack?: boolean;
|
|
28
28
|
clean?: boolean;
|
|
29
29
|
}
|
|
30
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Sync vendored files to workspace, only copying files that changed.
|
|
32
|
+
* This preserves file timestamps for unchanged files, allowing Turbopack
|
|
33
|
+
* to reuse its cache and avoid 35s recompilation on every start.
|
|
34
|
+
*/
|
|
35
|
+
export declare function syncVendoredFiles(srcDir: string, destDir: string, filter: (relativePath: string) => boolean): Promise<{
|
|
36
|
+
copied: number;
|
|
37
|
+
skipped: number;
|
|
38
|
+
removed: number;
|
|
39
|
+
}>;
|
|
31
40
|
/**
|
|
32
41
|
* Validates the .next cache and cleans it if corrupted or stale.
|
|
33
42
|
* Returns true if cache is valid and usable, false if it was cleared.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/commands/dev.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;
|
|
1
|
+
{"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/commands/dev.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAyBH;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAO/E;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAiJD;;;;GAIG;AAGH,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,GACxC,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAuE/D;AAED;;;;;GAKG;AACH,wBAAsB,qBAAqB,CACzC,YAAY,EAAE,MAAM,EACpB,cAAc,EAAE,MAAM,EACtB,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,OAAO,GACf,OAAO,CAAC,OAAO,CAAC,CA6BlB;AAED,wBAAsB,GAAG,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAqtB5D"}
|
package/dist/commands/dev.js
CHANGED
|
@@ -5,17 +5,21 @@
|
|
|
5
5
|
* Wraps the dev-project.cjs logic from build-service.
|
|
6
6
|
*/
|
|
7
7
|
import { execSync, spawn } from 'child_process';
|
|
8
|
+
import { runBuildScript } from '../lib/run-build-script.js';
|
|
8
9
|
import fs from 'fs-extra';
|
|
9
10
|
import path from 'path';
|
|
10
11
|
import { createRequire } from 'module';
|
|
11
12
|
import { output } from '../lib/output.js';
|
|
12
|
-
import { spinner
|
|
13
|
-
import { ensureDependencies,
|
|
13
|
+
import { spinner } from '../lib/spinner.js';
|
|
14
|
+
import { ensureDependencies, getJamdeskDir } from '../lib/deps.js';
|
|
15
|
+
import { getProjectWorkspaceDir, migrateLegacyWorkspace } from '../lib/workspace-paths.js';
|
|
16
|
+
import { acquireDevLock, releaseDevLock } from '../lib/dev-lock.js';
|
|
14
17
|
import { validateOpenApiSpec, formatErrorForCli, clearSpecCache } from '../lib/openapi.js';
|
|
15
18
|
import { validateConfig } from '../lib/docs-config.js';
|
|
16
19
|
import { validateBrandingAssets } from '../lib/validate-branding.js';
|
|
17
20
|
import { findAvailablePort } from '../lib/port.js';
|
|
18
21
|
import { createLoadingServer } from '../lib/dev-loading-server.js';
|
|
22
|
+
import { safeRemoveCache } from '../lib/safe-fs.js';
|
|
19
23
|
import { fileURLToPath } from 'url';
|
|
20
24
|
const __filename = fileURLToPath(import.meta.url);
|
|
21
25
|
const __dirname = path.dirname(__filename);
|
|
@@ -151,7 +155,9 @@ async function syncDirectory(srcDir, destDir) {
|
|
|
151
155
|
* This preserves file timestamps for unchanged files, allowing Turbopack
|
|
152
156
|
* to reuse its cache and avoid 35s recompilation on every start.
|
|
153
157
|
*/
|
|
154
|
-
|
|
158
|
+
// Exported for unit testing — verifies the skip list preserves CLI metadata
|
|
159
|
+
// like the dev lockfile, which acquireDevLock writes before this runs.
|
|
160
|
+
export async function syncVendoredFiles(srcDir, destDir, filter) {
|
|
155
161
|
const stats = { copied: 0, skipped: 0, removed: 0 };
|
|
156
162
|
// Build set of source files (relative paths)
|
|
157
163
|
const sourceFiles = new Set();
|
|
@@ -188,9 +194,14 @@ async function syncVendoredFiles(srcDir, destDir, filter) {
|
|
|
188
194
|
// - node_modules: managed separately via NODE_PATH
|
|
189
195
|
// - projects: symlink to user's project
|
|
190
196
|
// - public: contains project-specific files added after sync (docs.json, images)
|
|
197
|
+
// Plus any CLI-managed dotfile written before sync (e.g. the dev lockfile
|
|
198
|
+
// acquireDevLock writes at workspace root) — don't sweep our own metadata.
|
|
191
199
|
if (entry.name === '.next' || entry.name === 'node_modules' || entry.name === 'projects' || entry.name === 'public') {
|
|
192
200
|
continue;
|
|
193
201
|
}
|
|
202
|
+
if (entry.name === '.jamdesk-dev.lock' || entry.name === '.content-hash') {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
194
205
|
const destPath = path.join(dir, entry.name);
|
|
195
206
|
if (entry.isDirectory()) {
|
|
196
207
|
await walkDest(destPath, relativePath);
|
|
@@ -209,55 +220,6 @@ async function syncVendoredFiles(srcDir, destDir, filter) {
|
|
|
209
220
|
await walkDest(destDir);
|
|
210
221
|
return stats;
|
|
211
222
|
}
|
|
212
|
-
/**
|
|
213
|
-
* Recursively removes a directory with retries.
|
|
214
|
-
*
|
|
215
|
-
* `fs-extra`'s legacy `remove` walks files then `rmdir`s the parent — if a
|
|
216
|
-
* concurrent process (e.g. another `jamdesk dev` instance writing to its
|
|
217
|
-
* Turbopack cache) repopulates the directory mid-walk, the final `rmdir`
|
|
218
|
-
* fails with ENOTEMPTY/EBUSY and crashes the CLI as an unhandled rejection.
|
|
219
|
-
*
|
|
220
|
-
* `fs.rm` with `maxRetries` handles the race; on terminal failure we surface
|
|
221
|
-
* a friendly message naming the most likely cause (another dev session
|
|
222
|
-
* holding the workspace) and exit cleanly instead of stack-trace-crashing.
|
|
223
|
-
*
|
|
224
|
-
* Exported for unit testing.
|
|
225
|
-
*/
|
|
226
|
-
// ENOTEMPTY/EBUSY are the documented APFS racy-rmdir codes; EPERM shows up
|
|
227
|
-
// under macOS for files held open by a child process. EACCES is treated
|
|
228
|
-
// separately — it almost always means a real permissions problem (e.g.
|
|
229
|
-
// workspace owned by another user after a `sudo jamdesk dev` accident).
|
|
230
|
-
const RACE_CODES = ['ENOTEMPTY', 'EBUSY', 'EPERM'];
|
|
231
|
-
export async function safeRemoveCache(dirPath) {
|
|
232
|
-
try {
|
|
233
|
-
await fs.rm(dirPath, {
|
|
234
|
-
recursive: true,
|
|
235
|
-
force: true,
|
|
236
|
-
maxRetries: 2,
|
|
237
|
-
retryDelay: 200,
|
|
238
|
-
});
|
|
239
|
-
}
|
|
240
|
-
catch (err) {
|
|
241
|
-
const e = err;
|
|
242
|
-
if (e.code && RACE_CODES.includes(e.code)) {
|
|
243
|
-
stopSpinner();
|
|
244
|
-
output.error(`Could not clear cache at ${dirPath} (${e.code}).\n` +
|
|
245
|
-
` Another \`jamdesk dev\` instance is likely still running and ` +
|
|
246
|
-
`writing to this workspace.\n\n` +
|
|
247
|
-
` Fix: pkill -f "jamdesk dev|next dev" && jamdesk dev`);
|
|
248
|
-
process.exit(1);
|
|
249
|
-
}
|
|
250
|
-
if (e.code === 'EACCES') {
|
|
251
|
-
stopSpinner();
|
|
252
|
-
output.error(`Permission denied clearing cache at ${dirPath} (EACCES).\n` +
|
|
253
|
-
` The workspace is likely owned by another user — check ownership ` +
|
|
254
|
-
`with \`ls -ld ${dirPath}\` and \`chown\` it back to your user, ` +
|
|
255
|
-
`or remove the directory manually.`);
|
|
256
|
-
process.exit(1);
|
|
257
|
-
}
|
|
258
|
-
throw err;
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
223
|
/**
|
|
262
224
|
* Validates the .next cache and cleans it if corrupted or stale.
|
|
263
225
|
* Returns true if cache is valid and usable, false if it was cleared.
|
|
@@ -402,7 +364,38 @@ export async function dev(options) {
|
|
|
402
364
|
// Step 2: Prepare workspace
|
|
403
365
|
spin = spinner('Preparing workspace...');
|
|
404
366
|
const jamdeskDir = getJamdeskDir();
|
|
405
|
-
|
|
367
|
+
// Drop the legacy single-workspace layout (CLI ≤1.1.40). safeRemoveCache
|
|
368
|
+
// surfaces a friendly message if a 1.1.40 dev session is still holding it.
|
|
369
|
+
if (await migrateLegacyWorkspace(jamdeskDir)) {
|
|
370
|
+
if (verbose)
|
|
371
|
+
output.info('Migrated legacy ~/.jamdesk/workspace/ to per-project layout');
|
|
372
|
+
}
|
|
373
|
+
const workspaceDir = getProjectWorkspaceDir(jamdeskDir, projectDir);
|
|
374
|
+
// Same-project guard: refuse to start if another dev is using this workspace.
|
|
375
|
+
// Acquired BEFORE --clean so a concurrent `dev --clean` can't wipe our
|
|
376
|
+
// lockfile out from under us.
|
|
377
|
+
const lock = acquireDevLock(workspaceDir);
|
|
378
|
+
if (!lock.acquired) {
|
|
379
|
+
spin.fail('Another `jamdesk dev` is already running for this project');
|
|
380
|
+
output.error(` PID ${lock.heldBy} holds the lock at ${path.join(workspaceDir, '.jamdesk-dev.lock')}.\n` +
|
|
381
|
+
` Stop it first (Ctrl-C in that terminal, or kill ${lock.heldBy}), then retry.`);
|
|
382
|
+
process.exit(1);
|
|
383
|
+
}
|
|
384
|
+
// Release the lock on any normal exit path. SIGINT/SIGTERM additionally
|
|
385
|
+
// run `cleanup` further below, which calls release() before tree-killing
|
|
386
|
+
// the spawned next dev process group.
|
|
387
|
+
const release = () => releaseDevLock(workspaceDir);
|
|
388
|
+
process.once('exit', release);
|
|
389
|
+
// Handle --clean: wipe workspace contents but preserve the lockfile we just
|
|
390
|
+
// acquired. safeRemoveCache surfaces the friendly message if any path is
|
|
391
|
+
// still held open.
|
|
392
|
+
if (options.clean) {
|
|
393
|
+
output.info('Clearing workspace cache...');
|
|
394
|
+
const entries = await fs.readdir(workspaceDir);
|
|
395
|
+
await Promise.all(entries
|
|
396
|
+
.filter((name) => name !== '.jamdesk-dev.lock')
|
|
397
|
+
.map((name) => safeRemoveCache(path.join(workspaceDir, name))));
|
|
398
|
+
}
|
|
406
399
|
const vendoredDir = path.join(__dirname, '../../vendored');
|
|
407
400
|
// Check if vendored directory exists
|
|
408
401
|
if (!fs.existsSync(vendoredDir)) {
|
|
@@ -414,58 +407,46 @@ export async function dev(options) {
|
|
|
414
407
|
// Only remove and recopy vendored files, keeping the .next directory
|
|
415
408
|
const nextCacheDir = path.join(workspaceDir, '.next');
|
|
416
409
|
const cliVersionFile = path.join(nextCacheDir, '.jamdesk-cli-version');
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
output.info('Clearing workspace cache...');
|
|
420
|
-
await fs.remove(workspaceDir);
|
|
421
|
-
}
|
|
422
|
-
// Determine if we have usable cached compilation
|
|
423
|
-
let hadCache = fs.existsSync(nextCacheDir);
|
|
424
|
-
if (hadCache) {
|
|
425
|
-
hadCache = await validateAndCleanCache(nextCacheDir, cliVersionFile, workspaceDir, verbose);
|
|
426
|
-
}
|
|
410
|
+
const hadCache = fs.existsSync(nextCacheDir)
|
|
411
|
+
&& await validateAndCleanCache(nextCacheDir, cliVersionFile, workspaceDir, verbose);
|
|
427
412
|
// Ensure workspace directory exists
|
|
428
413
|
await fs.ensureDir(workspaceDir);
|
|
429
414
|
// Sync vendored files - only copy files that actually changed
|
|
430
415
|
// This preserves timestamps for unchanged files, allowing Turbopack to reuse cache
|
|
431
416
|
const syncStats = await syncVendoredFiles(vendoredDir, workspaceDir, (relativePath) => !relativePath.includes('node_modules') && !relativePath.includes('.next'));
|
|
432
|
-
// Log sync stats in verbose mode only (internal detail)
|
|
433
417
|
if (verbose && syncStats.copied > 0) {
|
|
434
418
|
output.info(`Synced ${syncStats.copied} build files (${syncStats.skipped} unchanged)`);
|
|
435
419
|
}
|
|
436
|
-
//
|
|
437
|
-
//
|
|
438
|
-
// NODE_PATH tells Node.js where to find modules without needing a symlink
|
|
439
|
-
const jamdeskNodeModules = path.join(path.dirname(workspaceDir), 'node_modules');
|
|
440
|
-
// Clean up any stale node_modules symlink (Turbopack doesn't allow external symlinks)
|
|
420
|
+
// Turbopack rejects external symlinks (`Symlink node_modules is invalid`),
|
|
421
|
+
// so use NODE_PATH=depsDir below instead and clear any stale symlink here.
|
|
441
422
|
const workspaceNodeModules = path.join(workspaceDir, 'node_modules');
|
|
442
|
-
|
|
423
|
+
try {
|
|
443
424
|
const stats = await fs.lstat(workspaceNodeModules);
|
|
444
|
-
if (stats.isSymbolicLink())
|
|
425
|
+
if (stats.isSymbolicLink())
|
|
445
426
|
await fs.remove(workspaceNodeModules);
|
|
446
|
-
|
|
427
|
+
}
|
|
428
|
+
catch (err) {
|
|
429
|
+
if (err.code !== 'ENOENT')
|
|
430
|
+
throw err;
|
|
447
431
|
}
|
|
448
432
|
// Symlink public directory to parent so Turbopack can find static files
|
|
449
433
|
// (outputFileTracingRoot and turbopack.root are set to parent directory)
|
|
450
434
|
const parentPublic = path.join(jamdeskDir, 'public');
|
|
451
435
|
const workspacePublic = path.join(workspaceDir, 'public');
|
|
452
436
|
await fs.ensureDir(workspacePublic);
|
|
453
|
-
// Only recreate symlink if target changed
|
|
437
|
+
// Only recreate symlink if target changed (preserves Turbopack cache mtimes)
|
|
454
438
|
let needsPublicSymlink = true;
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
const currentTarget = await fs.readlink(parentPublic);
|
|
460
|
-
if (currentTarget === workspacePublic) {
|
|
461
|
-
needsPublicSymlink = false;
|
|
462
|
-
}
|
|
463
|
-
else {
|
|
464
|
-
await fs.remove(parentPublic);
|
|
465
|
-
}
|
|
466
|
-
}
|
|
439
|
+
try {
|
|
440
|
+
const stats = await fs.lstat(parentPublic);
|
|
441
|
+
if (stats.isSymbolicLink() && (await fs.readlink(parentPublic)) === workspacePublic) {
|
|
442
|
+
needsPublicSymlink = false;
|
|
467
443
|
}
|
|
468
|
-
|
|
444
|
+
else {
|
|
445
|
+
await fs.remove(parentPublic);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
catch (err) {
|
|
449
|
+
if (err.code !== 'ENOENT') {
|
|
469
450
|
await fs.remove(parentPublic);
|
|
470
451
|
}
|
|
471
452
|
}
|
|
@@ -478,28 +459,23 @@ export async function dev(options) {
|
|
|
478
459
|
const workspaceProjectsDir = path.join(workspaceDir, 'projects');
|
|
479
460
|
const workspaceProjectDir = path.join(workspaceProjectsDir, projectName);
|
|
480
461
|
await fs.ensureDir(workspaceProjectsDir);
|
|
481
|
-
// Only recreate symlink if target changed (preserves timestamps for Turbopack cache)
|
|
482
462
|
let needsSymlink = true;
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
if (currentTarget === projectDir) {
|
|
487
|
-
needsSymlink = false; // Symlink already points to correct location
|
|
488
|
-
}
|
|
489
|
-
else {
|
|
490
|
-
await fs.remove(workspaceProjectDir);
|
|
491
|
-
}
|
|
463
|
+
try {
|
|
464
|
+
if ((await fs.readlink(workspaceProjectDir)) === projectDir) {
|
|
465
|
+
needsSymlink = false;
|
|
492
466
|
}
|
|
493
|
-
|
|
494
|
-
|
|
467
|
+
else {
|
|
468
|
+
await fs.remove(workspaceProjectDir);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
catch (err) {
|
|
472
|
+
if (err.code !== 'ENOENT') {
|
|
495
473
|
await fs.remove(workspaceProjectDir);
|
|
496
474
|
}
|
|
497
475
|
}
|
|
498
476
|
if (needsSymlink) {
|
|
499
477
|
await fs.symlink(projectDir, workspaceProjectDir, 'junction');
|
|
500
478
|
}
|
|
501
|
-
// Copy project's docs.json to workspace/public/ (only if changed)
|
|
502
|
-
await fs.ensureDir(path.join(workspaceDir, 'public'));
|
|
503
479
|
await copyIfChanged(docsJsonPath, path.join(workspaceDir, 'public/docs.json'));
|
|
504
480
|
spin.succeed('Workspace prepared');
|
|
505
481
|
// Step 3: Copy project assets (images, etc.) - only copy changed files
|
|
@@ -551,30 +527,31 @@ export async function dev(options) {
|
|
|
551
527
|
// Compute content hash to detect changes (based on file mtimes for speed)
|
|
552
528
|
const crypto = await import('crypto');
|
|
553
529
|
const { glob } = await import('glob');
|
|
530
|
+
const statMtime = async (file, root) => {
|
|
531
|
+
try {
|
|
532
|
+
const stat = await fs.stat(path.join(root, file));
|
|
533
|
+
return { key: file, mtimeMs: stat.mtimeMs };
|
|
534
|
+
}
|
|
535
|
+
catch {
|
|
536
|
+
return null;
|
|
537
|
+
}
|
|
538
|
+
};
|
|
554
539
|
const computeContentHash = async () => {
|
|
555
540
|
const hash = crypto.createHash('md5');
|
|
556
|
-
// Hash docs.json content
|
|
557
541
|
hash.update(await fs.readFile(docsJsonPath, 'utf-8'));
|
|
558
|
-
// Glob only MDX/MD files
|
|
559
|
-
|
|
560
|
-
const
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
const stat = await fs.stat(path.join(projectDir, file));
|
|
565
|
-
hash.update(`${file}:${stat.mtimeMs}`);
|
|
566
|
-
}
|
|
567
|
-
catch { /* skip inaccessible files */ }
|
|
542
|
+
// Glob only MDX/MD files — recursive readdir walks node_modules/.git.
|
|
543
|
+
const mdxFiles = (await glob('**/*.{mdx,md}', { cwd: projectDir, nodir: true })).sort();
|
|
544
|
+
const mdxStats = await Promise.all(mdxFiles.map((f) => statMtime(f, projectDir)));
|
|
545
|
+
for (const s of mdxStats) {
|
|
546
|
+
if (s)
|
|
547
|
+
hash.update(`${s.key}:${s.mtimeMs}`);
|
|
568
548
|
}
|
|
569
|
-
// Hash snippets mtimes if they exist
|
|
570
549
|
if (hasSnippets) {
|
|
571
|
-
const snippetFiles = await fs.readdir(snippetsDir);
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
hash.update(`snippets/${
|
|
576
|
-
}
|
|
577
|
-
catch { /* skip */ }
|
|
550
|
+
const snippetFiles = (await fs.readdir(snippetsDir)).sort();
|
|
551
|
+
const snippetStats = await Promise.all(snippetFiles.map((f) => statMtime(f, snippetsDir)));
|
|
552
|
+
for (const s of snippetStats) {
|
|
553
|
+
if (s)
|
|
554
|
+
hash.update(`snippets/${s.key}:${s.mtimeMs}`);
|
|
578
555
|
}
|
|
579
556
|
}
|
|
580
557
|
return hash.digest('hex');
|
|
@@ -595,34 +572,26 @@ export async function dev(options) {
|
|
|
595
572
|
const contentChanged = currentHash !== cachedHash;
|
|
596
573
|
if (contentChanged) {
|
|
597
574
|
spin.text = 'Processing navigation, snippets, and search index...';
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
execSync(`node scripts/${script}`, {
|
|
603
|
-
cwd: workspaceDir,
|
|
604
|
-
stdio: verbose ? 'inherit' : 'pipe',
|
|
605
|
-
env: { ...env, PROJECT_NAME: path.basename(projectDir) },
|
|
606
|
-
});
|
|
607
|
-
resolve({ script, success: true });
|
|
608
|
-
}
|
|
609
|
-
catch (error) {
|
|
610
|
-
resolve({ script, success: false, error });
|
|
611
|
-
}
|
|
612
|
-
});
|
|
575
|
+
const scriptOpts = {
|
|
576
|
+
cwd: workspaceDir,
|
|
577
|
+
env: { ...env, PROJECT_NAME: path.basename(projectDir) },
|
|
578
|
+
verbose,
|
|
613
579
|
};
|
|
614
|
-
// Run all three scripts in parallel
|
|
615
580
|
const [navResult, snippetsResult, searchResult] = await Promise.all([
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
581
|
+
runBuildScript('enhance-navigation.cjs', scriptOpts),
|
|
582
|
+
runBuildScript('compile-snippets.cjs', scriptOpts),
|
|
583
|
+
runBuildScript('build-search-index.cjs', scriptOpts),
|
|
619
584
|
]);
|
|
620
|
-
// Check for failures
|
|
621
585
|
const failures = [navResult, snippetsResult, searchResult].filter((r) => !r.success);
|
|
622
586
|
if (failures.length > 0) {
|
|
623
587
|
spin.fail('Build step failed');
|
|
624
588
|
for (const f of failures) {
|
|
625
589
|
output.error(`Failed: ${f.script}`);
|
|
590
|
+
// Surface child stderr (or stdout) on every failure so non-verbose users
|
|
591
|
+
// see the actual error, not just the script name.
|
|
592
|
+
const tail = (f.stderr || f.stdout || '').trim();
|
|
593
|
+
if (tail)
|
|
594
|
+
console.error(tail.split('\n').slice(-20).join('\n'));
|
|
626
595
|
if (verbose && f.error)
|
|
627
596
|
console.error(f.error);
|
|
628
597
|
}
|
|
@@ -908,6 +877,7 @@ export async function dev(options) {
|
|
|
908
877
|
});
|
|
909
878
|
// Handle Ctrl+C - be aggressive about cleanup
|
|
910
879
|
function cleanup() {
|
|
880
|
+
release(); // Release the workspace lock first so a subsequent dev isn't blocked.
|
|
911
881
|
if (compileSpinner) {
|
|
912
882
|
clearInterval(compileSpinner);
|
|
913
883
|
process.stdout.write('\r\x1b[K');
|
|
@@ -933,7 +903,7 @@ export async function dev(options) {
|
|
|
933
903
|
}
|
|
934
904
|
process.exit(0);
|
|
935
905
|
}
|
|
936
|
-
process.
|
|
937
|
-
process.
|
|
906
|
+
process.once('SIGINT', cleanup);
|
|
907
|
+
process.once('SIGTERM', cleanup);
|
|
938
908
|
}
|
|
939
909
|
//# sourceMappingURL=dev.js.map
|