Skip to content

Commit 059e939

Browse files
committed
work
1 parent 2d64283 commit 059e939

41 files changed

Lines changed: 173 additions & 525 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.agents/skills/dev-server/SKILL.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,15 @@ return {
7979
The browser URL stays as requested (the redirect is internal). `?search` is carried through, so a page can read `location.search`. Inline `<script type="module">` in such a page is externalized/supervised exactly like any app page — that is expected, the code still runs.
8080
8181
If a `transformUrlContent.html` hook injects into every page (like the devices client), exclude your own internal pages by matching their file URL (`asUrlWithoutSearch(urlInfo.url) === pageFileUrl`), since after cooking their url is the template file url, not the `/.internal/...` request path.
82+
83+
## Out directory (`.jsenv/`) writes stay synchronous
84+
85+
Every cooked file is written into `outDirectoryUrl` (a debug aid, on by
86+
default). Making these writes asynchronous is tempting and has been tried more
87+
than once; it loses, measured on a 500-file cold load: synchronous writes block
88+
the event loop ~160ms in total, asynchronous ones queue in the threadpool behind
89+
tens of MB (content + sourcemaps) and a response waiting for its own write waits
90+
~36ms on average — 18s summed. Fire-and-forget is not an option: the last write
91+
lands after the test that cooked it has ended and the side-effect snapshots lose
92+
it. Details at `writeInsideOutDirectory` in
93+
[url_info_transformations.js](../../../src/kitchen/url_graph/url_info_transformations.js).

dist/build/build.js

Lines changed: 15 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { jsenvPluginMinification } from "@jsenv/plugin-minification";
44
import { jsenvPluginTranspilation, jsenvPluginJsModuleFallback } from "@jsenv/plugin-transpilation";
55
import { memoryUsage } from "node:process";
66
import { readFileSync, existsSync, realpathSync, readdirSync, lstatSync, statSync } from "node:fs";
7-
import { lookupPackageDirectory, urlIsOrIsInsideOf, registerDirectoryLifecycle, urlToRelativeUrl, createDetailedMessage, stringifyUrlSite, generateContentFrame, validateResponseIntegrity, ensureWindowsDriveLetter, setUrlFilename, moveUrl, getCallerPosition, urlToBasename, urlToExtension, asSpecifierWithoutSearch, asUrlWithoutSearch, injectQueryParamsIntoSpecifier, bufferToEtag, isFileSystemPath, urlToPathname, setUrlBasename, urlToFileSystemPath, writeFileSync, writeFile, createLogger, URL_META, applyNodeEsmResolution, normalizeUrl, ANSI, RUNTIME_COMPAT, CONTENT_TYPE, readPackageAtOrNull, urlToFilename, DATA_URL, errorToHTML, normalizeImportMap, composeTwoImportMaps, resolveImport, JS_QUOTES, readCustomConditionsFromProcessArgs, collectFiles, readEntryStatSync, applyFileSystemMagicResolution, getExtensionsToTry, ensurePathnameTrailingSlash, compareFileUrls, setUrlExtension, isSpecifierForNodeBuiltin, injectQueryParams, renderDetails, humanizeDuration, humanizeFileSize, renderTable, renderBigSection, distributePercentages, humanizeMemory, comparePathnames, UNICODE, escapeRegexpSpecialChars, injectQueryParamIntoSpecifierWithoutEncoding, renderUrlOrRelativeUrlFilename, assertAndNormalizeDirectoryUrl, Abort, raceProcessTeardownEvents, startMonitoringCpuUsage, startMonitoringMemoryUsage, inferRuntimeCompatFromClosestPackage, browserDefaultRuntimeCompat, nodeDefaultRuntimeCompat, clearDirectorySync, createTaskLog, createLookupPackageDirectory, ensureEmptyDirectory, updateJsonFileSync, createDynamicLog } from "./jsenv_core_packages.js";
7+
import { lookupPackageDirectory, urlIsOrIsInsideOf, registerDirectoryLifecycle, urlToRelativeUrl, createDetailedMessage, stringifyUrlSite, generateContentFrame, validateResponseIntegrity, ensureWindowsDriveLetter, setUrlFilename, moveUrl, getCallerPosition, urlToBasename, urlToExtension, asSpecifierWithoutSearch, asUrlWithoutSearch, injectQueryParamsIntoSpecifier, bufferToEtag, isFileSystemPath, urlToPathname, setUrlBasename, urlToFileSystemPath, writeFileSync, createLogger, URL_META, applyNodeEsmResolution, normalizeUrl, ANSI, RUNTIME_COMPAT, CONTENT_TYPE, readPackageAtOrNull, urlToFilename, DATA_URL, errorToHTML, normalizeImportMap, composeTwoImportMaps, resolveImport, JS_QUOTES, readCustomConditionsFromProcessArgs, collectFiles, readEntryStatSync, applyFileSystemMagicResolution, getExtensionsToTry, ensurePathnameTrailingSlash, compareFileUrls, setUrlExtension, isSpecifierForNodeBuiltin, injectQueryParams, renderDetails, humanizeDuration, humanizeFileSize, renderTable, renderBigSection, distributePercentages, humanizeMemory, comparePathnames, UNICODE, escapeRegexpSpecialChars, injectQueryParamIntoSpecifierWithoutEncoding, renderUrlOrRelativeUrlFilename, assertAndNormalizeDirectoryUrl, Abort, raceProcessTeardownEvents, startMonitoringCpuUsage, startMonitoringMemoryUsage, inferRuntimeCompatFromClosestPackage, browserDefaultRuntimeCompat, nodeDefaultRuntimeCompat, clearDirectorySync, createTaskLog, createLookupPackageDirectory, ensureEmptyDirectory, updateJsonFileSync, createDynamicLog } from "./jsenv_core_packages.js";
88
import { pathToFileURL } from "node:url";
99
import { generateSourcemapFileUrl, createMagicSource, composeTwoSourcemaps, generateSourcemapDataUrl, SOURCEMAP } from "@jsenv/sourcemap";
1010
import { createPluginsController } from "@jsenv/server/src/plugins_controller.js";
@@ -2981,69 +2981,33 @@ const createUrlInfoTransformer = ({
29812981

29822982
const applyContentEffects = (urlInfo) => {
29832983
applySourcemapOnContent(urlInfo);
2984-
return writeInsideOutDirectory(urlInfo);
2985-
};
2986-
2987-
// The out directory is a debug aid. During dev a request must not be held
2988-
// by a write blocking the event loop (every other request waits too), so
2989-
// the file is written asynchronously; the response still waits for it, so
2990-
// what is on disk is what was served. Per file, writes stay ordered: a file
2991-
// cooked twice in a row ends up holding its latest content.
2992-
// During build nothing waits behind a write: it is done synchronously.
2993-
const pendingWritePromiseMap = new Map();
2994-
const writeOutFile = (urlInfo, fileUrl, content) => {
2995-
if (!urlInfo.context.dev) {
2996-
writeFileSync(fileUrl, content, { force: true });
2997-
return undefined;
2998-
}
2999-
const previousWritePromise =
3000-
pendingWritePromiseMap.get(fileUrl) || Promise.resolve();
3001-
const writePromise = previousWritePromise.then(async () => {
3002-
try {
3003-
await writeFile(fileUrl, content);
3004-
} catch {
3005-
try {
3006-
// a directory where the file goes, or a file where a directory goes
3007-
writeFileSync(fileUrl, content, { force: true });
3008-
} catch (e) {
3009-
logger.debug(`error while writing ${fileUrl}: ${e.message}`);
3010-
}
3011-
}
3012-
});
3013-
pendingWritePromiseMap.set(fileUrl, writePromise);
3014-
writePromise.then(() => {
3015-
if (pendingWritePromiseMap.get(fileUrl) === writePromise) {
3016-
pendingWritePromiseMap.delete(fileUrl);
3017-
}
3018-
});
3019-
return writePromise;
2984+
writeInsideOutDirectory(urlInfo);
30202985
};
30212986

30222987
const writeInsideOutDirectory = (urlInfo) => {
30232988
// writing result inside ".jsenv" directory (debug purposes)
30242989
if (!outDirectoryUrl) {
3025-
return undefined;
2990+
return;
30262991
}
30272992
const { generatedUrl } = urlInfo;
30282993
if (!generatedUrl) {
3029-
return undefined;
2994+
return;
30302995
}
30312996
if (!generatedUrl.startsWith("file:")) {
3032-
return undefined;
2997+
return;
30332998
}
30342999
if (urlToPathname(generatedUrl).endsWith("/")) {
30353000
// when users explicitely request a directory
30363001
// we can't write the content returned by the server in ".jsenv" at that url
30373002
// because it would try to write a directory
30383003
// ideally we would decide a filename for this
30393004
// for now we just don't write anything
3040-
return undefined;
3005+
return;
30413006
}
30423007
if (urlInfo.type === "directory") {
30433008
// no need to write the directory
3044-
return undefined;
3009+
return;
30453010
}
3046-
const writePromises = [];
30473011
// if (urlInfo.content === undefined) {
30483012
// // Some error might lead to urlInfo.content to be null
30493013
// // (error hapenning before urlInfo.content can be set, or 404 for instance)
@@ -3068,19 +3032,15 @@ const createUrlInfoTransformer = ({
30683032
const outFileUrl = setUrlBasename(generatedUrlObject, baseName);
30693033
let outFilePath = urlToFileSystemPath(outFileUrl);
30703034
outFilePath = truncate(outFilePath, 2055); // for windows
3071-
writePromises.push(writeOutFile(urlInfo, outFilePath, urlInfo.content));
3035+
writeFileSync(outFilePath, urlInfo.content, { force: true });
30723036
}
30733037
const { sourcemapGeneratedUrl, sourcemapReference } = urlInfo;
30743038
if (sourcemapGeneratedUrl && sourcemapReference) {
3075-
writePromises.push(
3076-
writeOutFile(
3077-
urlInfo,
3078-
sourcemapGeneratedUrl,
3079-
sourcemapReference.urlInfo.content,
3080-
),
3039+
writeFileSync(
3040+
new URL(sourcemapGeneratedUrl),
3041+
sourcemapReference.urlInfo.content,
30813042
);
30823043
}
3083-
return Promise.all(writePromises);
30843044
};
30853045

30863046
const applySourcemapOnContent = (
@@ -3187,9 +3147,8 @@ const createUrlInfoTransformer = ({
31873147
);
31883148
applyTransformations(urlInfo, injectionTransformations);
31893149
}
3190-
const contentEffectsPromise = applyContentEffects(urlInfo);
3150+
applyContentEffects(urlInfo);
31913151
urlInfo.contentFinalized = true;
3192-
return contentEffectsPromise;
31933152
};
31943153

31953154
return {
@@ -3842,11 +3801,7 @@ ${ANSI.color(normalizedReturnValue, ANSI.YELLOW)}
38423801
"finalizeUrlContent",
38433802
urlInfo,
38443803
);
3845-
const outDirectoryWritePromise = urlInfoTransformer.endTransformations(
3846-
urlInfo,
3847-
finalizeReturnValue,
3848-
);
3849-
return { outDirectoryWritePromise };
3804+
urlInfoTransformer.endTransformations(urlInfo, finalizeReturnValue);
38503805
} catch (error) {
38513806
throw createFinalizeUrlContentError({
38523807
jsenvPluginsController,
@@ -3880,9 +3835,8 @@ ${ANSI.color(normalizedReturnValue, ANSI.YELLOW)}
38803835
// to cook a file goes (fetch vs transform vs finalize).
38813836
const timePhase = async (name, phase) => {
38823837
const start = performance.now();
3883-
const result = await phase();
3838+
await phase();
38843839
urlInfo.timing[name] = performance.now() - start;
3885-
return result;
38863840
};
38873841

38883842
// "fetchUrlContent" hook
@@ -3892,18 +3846,7 @@ ${ANSI.color(normalizedReturnValue, ANSI.YELLOW)}
38923846
await timePhase("transform", () => urlInfo.transformContent());
38933847

38943848
// "finalize" hook
3895-
const { outDirectoryWritePromise } = await timePhase("finalize", () =>
3896-
urlInfo.finalizeContent(),
3897-
);
3898-
3899-
// Timed apart: it is disk I/O the response waits for (see
3900-
// writeInsideOutDirectory), not part of cooking.
3901-
if (outDirectoryWritePromise) {
3902-
await timePhase(
3903-
"write in out directory",
3904-
() => outDirectoryWritePromise,
3905-
);
3906-
}
3849+
await timePhase("finalize", () => urlInfo.finalizeContent());
39073850
});
39083851
} catch (e) {
39093852
urlInfo.error = e;

dist/build/jsenv_core_packages.js

Lines changed: 14 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { createSupportsColor, isUnicodeSupported, stripAnsi, eastAsianWidth, clearTerminal, eraseLines } from "./jsenv_core_node_modules.js";
2-
import { readFileSync as readFileSync$1, existsSync, readdir, chmod, stat, lstat, chmodSync, statSync, lstatSync, promises, writeFile as writeFile$1, readdirSync, openSync, closeSync, unlinkSync, rmdirSync, mkdirSync, writeFileSync as writeFileSync$1, unlink, rmdir, watch, realpathSync } from "node:fs";
3-
import { dirname, extname } from "node:path";
2+
import { extname } from "node:path";
3+
import { readFileSync as readFileSync$1, existsSync, readdir, chmod, stat, lstat, chmodSync, statSync, lstatSync, promises, readdirSync, openSync, closeSync, unlinkSync, rmdirSync, mkdirSync, writeFileSync as writeFileSync$1, unlink, rmdir, watch, realpathSync } from "node:fs";
44
import crypto, { createHash } from "node:crypto";
55
import { pathToFileURL, fileURLToPath } from "node:url";
66
import { cpus, totalmem, freemem } from "node:os";
@@ -3867,44 +3867,6 @@ const writeDirectory = async (
38673867
}
38683868
};
38693869

3870-
const ensureParentDirectories = async (destination) => {
3871-
const destinationUrl = assertAndNormalizeFileUrl(destination);
3872-
const destinationPath = urlToFileSystemPath(destinationUrl);
3873-
const destinationParentPath = dirname(destinationPath);
3874-
3875-
await writeDirectory(destinationParentPath, {
3876-
recursive: true,
3877-
allowUseless: true,
3878-
});
3879-
};
3880-
3881-
const writeFile = async (destination, content = "") => {
3882-
const destinationUrl = assertAndNormalizeFileUrl(destination);
3883-
const destinationUrlObject = new URL(destinationUrl);
3884-
try {
3885-
await writeFileNaive(destinationUrlObject, content);
3886-
} catch (error) {
3887-
if (error.code === "ENOENT") {
3888-
await ensureParentDirectories(destinationUrl);
3889-
await writeFileNaive(destinationUrlObject, content);
3890-
return;
3891-
}
3892-
throw error;
3893-
}
3894-
};
3895-
3896-
const writeFileNaive = (urlObject, content) => {
3897-
return new Promise((resolve, reject) => {
3898-
writeFile$1(urlObject, content, (error) => {
3899-
if (error) {
3900-
reject(error);
3901-
} else {
3902-
resolve();
3903-
}
3904-
});
3905-
});
3906-
};
3907-
39083870
const mediaTypeInfos = {
39093871
"application/json": {
39103872
extensions: ["json", "map"],
@@ -5060,18 +5022,12 @@ const registerDirectoryLifecycle = (
50605022
};
50615023
const tracker = trackResources();
50625024
const infoMap = new Map();
5063-
// type is given when already known (from the directory listing it comes
5064-
// from): there is then no need to stat the entry, which matters when
5065-
// indexing a large tree at startup
5066-
const readEntryInfo = (url, { type } = {}) => {
5025+
const readEntryInfo = (url) => {
50675026
try {
50685027
const relativeUrl = urlToRelativeUrl(url, source);
50695028
const previousInfo = infoMap.get(relativeUrl);
5070-
let stat = null;
5071-
if (type === undefined) {
5072-
stat = readEntryStatSync(new URL(url));
5073-
type = statsToType(stat);
5074-
}
5029+
const stat = readEntryStatSync(new URL(url));
5030+
const type = statsToType(stat);
50755031
const patternValue = previousInfo
50765032
? previousInfo.patternValue
50775033
: getWatchPatternValue({ url, type });
@@ -5228,12 +5184,10 @@ const registerDirectoryLifecycle = (
52285184
const directoryUrl = entryInfo.url.endsWith("/")
52295185
? entryInfo.url
52305186
: `${entryInfo.url}/`;
5231-
let direntArray;
5187+
let entryNameArray;
52325188
try {
52335189
const directoryUrlObject = new URL(directoryUrl);
5234-
direntArray = readdirSync(directoryUrlObject, {
5235-
withFileTypes: true,
5236-
});
5190+
entryNameArray = readdirSync(directoryUrlObject);
52375191
} catch (e) {
52385192
if (
52395193
e.code === "ENOENT" ||
@@ -5245,17 +5199,12 @@ const registerDirectoryLifecycle = (
52455199
}
52465200
throw e;
52475201
}
5248-
for (const dirent of direntArray) {
5249-
const childEntryUrl = new URL(dirent.name, directoryUrl).href;
5202+
for (const entryName of entryNameArray) {
5203+
const childEntryUrl = new URL(entryName, directoryUrl).href;
52505204
if (seenSet.has(childEntryUrl)) {
52515205
continue;
52525206
}
5253-
// the stat is read (and its mtime reported) only when the entry is
5254-
// notified; a symlink or an exotic entry is stat'ed too, so that it
5255-
// is typed as what it points to, as for any later event
5256-
const childEntryInfo = readEntryInfo(childEntryUrl, {
5257-
type: notify ? undefined : direntToType(dirent),
5258-
});
5207+
const childEntryInfo = readEntryInfo(childEntryUrl);
52595208
if (childEntryInfo.type !== null && childEntryInfo.patternValue) {
52605209
applyEntryDiscoveredEffects(childEntryInfo);
52615210
}
@@ -5297,7 +5246,7 @@ const registerDirectoryLifecycle = (
52975246
relativeUrl: entryInfo.relativeUrl,
52985247
type: entryInfo.type,
52995248
patternValue: entryInfo.patternValue,
5300-
mtime: entryInfo.stat ? entryInfo.stat.mtimeMs : undefined,
5249+
mtime: entryInfo.stat.mtimeMs,
53015250
});
53025251
}
53035252
};
@@ -5311,7 +5260,7 @@ const registerDirectoryLifecycle = (
53115260
relativeUrl: entryInfo.relativeUrl,
53125261
type: entryInfo.type,
53135262
patternValue: entryInfo.patternValue,
5314-
mtime: entryInfo.stat ? entryInfo.stat.mtimeMs : undefined,
5263+
mtime: entryInfo.stat.mtimeMs,
53155264
});
53165265
}
53175266
};
@@ -5323,9 +5272,7 @@ const registerDirectoryLifecycle = (
53235272
type: entryInfo.type,
53245273
patternValue: entryInfo.patternValue,
53255274
mtime: entryInfo.stat.mtimeMs,
5326-
previousMtime: entryInfo.previousInfo.stat
5327-
? entryInfo.previousInfo.stat.mtimeMs
5328-
: undefined,
5275+
previousMtime: entryInfo.previousInfo.stat.mtimeMs,
53295276
});
53305277
}
53315278
};
@@ -5361,21 +5308,8 @@ ${relativeUrls.join("\n")}`,
53615308
return tracker.cleanup;
53625309
};
53635310

5364-
const direntToType = (dirent) => {
5365-
if (dirent.isFile()) {
5366-
return "file";
5367-
}
5368-
if (dirent.isDirectory()) {
5369-
return "directory";
5370-
}
5371-
return undefined;
5372-
};
5373-
53745311
const shouldCallUpdated = (entryInfo) => {
53755312
const { stat, previousInfo } = entryInfo;
5376-
if (!previousInfo.stat) {
5377-
return true;
5378-
}
53795313
if (!stat.atimeMs) {
53805314
return true;
53815315
}
@@ -11297,4 +11231,4 @@ const escapeRegexpSpecialChars = (string) => {
1129711231
});
1129811232
};
1129911233

11300-
export { ANSI, Abort, CONTENT_TYPE, DATA_URL, JS_QUOTES, RUNTIME_COMPAT, UNICODE, URL_META, applyFileSystemMagicResolution, applyNodeEsmResolution, asSpecifierWithoutSearch, asUrlWithoutSearch, assertAndNormalizeDirectoryUrl, browserDefaultRuntimeCompat, bufferToEtag, clearDirectorySync, collectFiles, compareFileUrls, comparePathnames, composeTwoImportMaps, createDetailedMessage$1 as createDetailedMessage, createDynamicLog, createLogger, createLookupPackageDirectory, createTaskLog, distributePercentages, ensureEmptyDirectory, ensurePathnameTrailingSlash, ensureWindowsDriveLetter, errorToHTML, escapeRegexpSpecialChars, generateContentFrame, getCallerPosition, getExtensionsToTry, humanizeDuration, humanizeFileSize, humanizeMemory, inferRuntimeCompatFromClosestPackage, injectQueryParamIntoSpecifierWithoutEncoding, injectQueryParams, injectQueryParamsIntoSpecifier, isFileSystemPath, isSpecifierForNodeBuiltin, lookupPackageDirectory, moveUrl, nodeDefaultRuntimeCompat, normalizeImportMap, normalizeUrl, raceProcessTeardownEvents, readCustomConditionsFromProcessArgs, readEntryStatSync, readPackageAtOrNull, registerDirectoryLifecycle, renderBigSection, renderDetails, renderTable, renderUrlOrRelativeUrlFilename, resolveImport, setUrlBasename, setUrlExtension, setUrlFilename, startMonitoringCpuUsage, startMonitoringMemoryUsage, stringifyUrlSite, updateJsonFileSync, urlIsOrIsInsideOf, urlToBasename, urlToExtension$1 as urlToExtension, urlToFileSystemPath, urlToFilename$1 as urlToFilename, urlToPathname$1 as urlToPathname, urlToRelativeUrl, validateResponseIntegrity, writeFile, writeFileSync };
11234+
export { ANSI, Abort, CONTENT_TYPE, DATA_URL, JS_QUOTES, RUNTIME_COMPAT, UNICODE, URL_META, applyFileSystemMagicResolution, applyNodeEsmResolution, asSpecifierWithoutSearch, asUrlWithoutSearch, assertAndNormalizeDirectoryUrl, browserDefaultRuntimeCompat, bufferToEtag, clearDirectorySync, collectFiles, compareFileUrls, comparePathnames, composeTwoImportMaps, createDetailedMessage$1 as createDetailedMessage, createDynamicLog, createLogger, createLookupPackageDirectory, createTaskLog, distributePercentages, ensureEmptyDirectory, ensurePathnameTrailingSlash, ensureWindowsDriveLetter, errorToHTML, escapeRegexpSpecialChars, generateContentFrame, getCallerPosition, getExtensionsToTry, humanizeDuration, humanizeFileSize, humanizeMemory, inferRuntimeCompatFromClosestPackage, injectQueryParamIntoSpecifierWithoutEncoding, injectQueryParams, injectQueryParamsIntoSpecifier, isFileSystemPath, isSpecifierForNodeBuiltin, lookupPackageDirectory, moveUrl, nodeDefaultRuntimeCompat, normalizeImportMap, normalizeUrl, raceProcessTeardownEvents, readCustomConditionsFromProcessArgs, readEntryStatSync, readPackageAtOrNull, registerDirectoryLifecycle, renderBigSection, renderDetails, renderTable, renderUrlOrRelativeUrlFilename, resolveImport, setUrlBasename, setUrlExtension, setUrlFilename, startMonitoringCpuUsage, startMonitoringMemoryUsage, stringifyUrlSite, updateJsonFileSync, urlIsOrIsInsideOf, urlToBasename, urlToExtension$1 as urlToExtension, urlToFileSystemPath, urlToFilename$1 as urlToFilename, urlToPathname$1 as urlToPathname, urlToRelativeUrl, validateResponseIntegrity, writeFileSync };

0 commit comments

Comments
 (0)