@jsenv/core 41.4.10 → 41.4.12
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/build/build.js +67 -5
- package/dist/client/ribbon/ribbon.js +120 -24
- package/dist/js/dependency_status.js +233 -2
- package/dist/start_build_server/start_build_server.js +94 -4
- package/dist/start_dev_server/start_dev_server.js +60 -7
- package/package.json +6 -6
- package/src/build/build.js +15 -0
- package/src/build/start_build_server.js +56 -3
- package/src/dev/start_dev_server.js +2 -1
- package/src/helpers/watch_dependencies.js +5 -0
- package/src/kitchen/package_dependencies.js +18 -0
- package/src/plugins/dependency_status/client/dependency_status.js +233 -2
- package/src/plugins/dependency_status/jsenv_plugin_dependency_status.js +5 -1
- package/src/plugins/ribbon/client/ribbon.js +120 -24
- package/src/plugins/ribbon/jsenv_plugin_ribbon.js +30 -4
- package/src/plugins/ribbon/ribbon_html_injection.js +38 -0
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createFileSystemFetch, startServer, serverPluginCORS, jsenvAccessControlAllowedHeaders, serverPluginErrorHandler } from "@jsenv/server";
|
|
2
|
+
import { parseHtml, injectJsenvScript, stringifyHtmlAst } from "@jsenv/ast";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
2
4
|
import { createLogger, Abort, raceProcessTeardownEvents, createTaskLog } from "./jsenv_core_packages.js";
|
|
3
5
|
import "./jsenv_core_node_modules.js";
|
|
4
6
|
import "node:process";
|
|
@@ -6,6 +8,43 @@ import "node:os";
|
|
|
6
8
|
import "node:tty";
|
|
7
9
|
import "node:util";
|
|
8
10
|
|
|
11
|
+
/*
|
|
12
|
+
* Inject the ribbon into an html string, inlining the client code.
|
|
13
|
+
*
|
|
14
|
+
* Used when serving files that must not be modified on the filesystem (the build
|
|
15
|
+
* directory served by startBuildServer): the ribbon exists only in what the
|
|
16
|
+
* server sends, never in the artifact that gets deployed.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
const ribbonClientFileUrl = import.meta.resolve("../client/ribbon/ribbon.js");
|
|
21
|
+
let ribbonClientFileContent;
|
|
22
|
+
|
|
23
|
+
const injectRibbonIntoHtml = (
|
|
24
|
+
html,
|
|
25
|
+
{ text = "BUILD", color, textColor, href, target, position } = {},
|
|
26
|
+
) => {
|
|
27
|
+
if (ribbonClientFileContent === undefined) {
|
|
28
|
+
ribbonClientFileContent = String(
|
|
29
|
+
readFileSync(new URL(ribbonClientFileUrl)),
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
const htmlAst = parseHtml({ html, url: "file:///ribbon.html" });
|
|
33
|
+
const params = { text, color, textColor, href, target, position };
|
|
34
|
+
for (const key of Object.keys(params)) {
|
|
35
|
+
if (params[key] === undefined) {
|
|
36
|
+
delete params[key];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
injectJsenvScript(htmlAst, {
|
|
40
|
+
type: "module",
|
|
41
|
+
content: `${ribbonClientFileContent}
|
|
42
|
+
injectRibbon(${JSON.stringify(params)});`,
|
|
43
|
+
pluginName: "jsenv:ribbon",
|
|
44
|
+
});
|
|
45
|
+
return stringifyHtmlAst(htmlAst);
|
|
46
|
+
};
|
|
47
|
+
|
|
9
48
|
/*
|
|
10
49
|
* startBuildServer is mean to interact with the build files;
|
|
11
50
|
* files that will be deployed to production server(s).
|
|
@@ -26,11 +65,16 @@ import "node:util";
|
|
|
26
65
|
* Start a server for build files.
|
|
27
66
|
* @param {Object} buildServerParameters
|
|
28
67
|
* @param {string|url} buildServerParameters.buildDirectoryUrl Directory where build files are written
|
|
68
|
+
* @param {boolean|object} [buildServerParameters.ribbon=false] Inject a ribbon in the html responses,
|
|
69
|
+
* marking the page as non-production. The build directory itself is left untouched.
|
|
70
|
+
* As an object: `text`, `color`, `textColor`, `href`, `target`, `position`
|
|
71
|
+
* (see startDevServer "ribbon" param).
|
|
29
72
|
* @return {Object} A build server object
|
|
30
73
|
*/
|
|
31
74
|
const startBuildServer = async ({
|
|
32
75
|
buildDirectoryUrl,
|
|
33
76
|
buildDirectoryMainFileRelativeUrl = "index.html",
|
|
77
|
+
ribbon = false,
|
|
34
78
|
port = 9779,
|
|
35
79
|
routes = [],
|
|
36
80
|
serverPlugins = [],
|
|
@@ -46,6 +90,9 @@ const startBuildServer = async ({
|
|
|
46
90
|
keepProcessAlive = true,
|
|
47
91
|
}) => {
|
|
48
92
|
const logger = createLogger({ logLevel });
|
|
93
|
+
if (ribbon === true) {
|
|
94
|
+
ribbon = {};
|
|
95
|
+
}
|
|
49
96
|
const operation = Abort.startOperation();
|
|
50
97
|
operation.addAbortSignal(signal);
|
|
51
98
|
if (handleSIGINT) {
|
|
@@ -58,6 +105,9 @@ const startBuildServer = async ({
|
|
|
58
105
|
});
|
|
59
106
|
}
|
|
60
107
|
|
|
108
|
+
const fileSystemFetch = createFileSystemFetch(buildDirectoryUrl, {
|
|
109
|
+
mainFileRelativeUrl: buildDirectoryMainFileRelativeUrl,
|
|
110
|
+
});
|
|
61
111
|
const startBuildServerTask = createTaskLog("start build server", {
|
|
62
112
|
disabled: !logger.levels.info,
|
|
63
113
|
});
|
|
@@ -96,9 +146,9 @@ const startBuildServer = async ({
|
|
|
96
146
|
{
|
|
97
147
|
endpoint: "GET /",
|
|
98
148
|
description: "Serve build files",
|
|
99
|
-
fetch:
|
|
100
|
-
|
|
101
|
-
|
|
149
|
+
fetch: ribbon
|
|
150
|
+
? withRibbonInjectedInHtml(fileSystemFetch, ribbon)
|
|
151
|
+
: fileSystemFetch,
|
|
102
152
|
},
|
|
103
153
|
],
|
|
104
154
|
});
|
|
@@ -120,4 +170,44 @@ const startBuildServer = async ({
|
|
|
120
170
|
};
|
|
121
171
|
};
|
|
122
172
|
|
|
173
|
+
const withRibbonInjectedInHtml = (fetch, ribbon) => {
|
|
174
|
+
return async (request, helpers) => {
|
|
175
|
+
const response = await fetch(request, helpers);
|
|
176
|
+
if (!response || response.status !== 200) {
|
|
177
|
+
return response;
|
|
178
|
+
}
|
|
179
|
+
const contentType = response.headers?.["content-type"];
|
|
180
|
+
if (!contentType || !contentType.startsWith("text/html")) {
|
|
181
|
+
return response;
|
|
182
|
+
}
|
|
183
|
+
const html = await readResponseBodyAsString(response.body);
|
|
184
|
+
const htmlWithRibbon = injectRibbonIntoHtml(html, ribbon);
|
|
185
|
+
const bodyBuffer = Buffer.from(htmlWithRibbon);
|
|
186
|
+
const headers = {
|
|
187
|
+
...response.headers,
|
|
188
|
+
"content-length": bodyBuffer.length,
|
|
189
|
+
};
|
|
190
|
+
// the body is generated per request; what the file on disk hashes to or when
|
|
191
|
+
// it was modified does not describe it
|
|
192
|
+
delete headers.etag;
|
|
193
|
+
delete headers["last-modified"];
|
|
194
|
+
return {
|
|
195
|
+
...response,
|
|
196
|
+
headers,
|
|
197
|
+
body: bodyBuffer,
|
|
198
|
+
};
|
|
199
|
+
};
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const readResponseBodyAsString = async (body) => {
|
|
203
|
+
if (typeof body === "string" || Buffer.isBuffer(body)) {
|
|
204
|
+
return String(body);
|
|
205
|
+
}
|
|
206
|
+
const chunks = [];
|
|
207
|
+
for await (const chunk of body) {
|
|
208
|
+
chunks.push(Buffer.from(chunk));
|
|
209
|
+
}
|
|
210
|
+
return String(Buffer.concat(chunks));
|
|
211
|
+
};
|
|
212
|
+
|
|
123
213
|
export { startBuildServer };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { WebSocketResponse, pickContentType, ServerEvents, serverPluginErrorHandler, composeTwoResponses, fetchDirectory, serverPluginCORS, jsenvAccessControlAllowedHeaders, startServer } from "@jsenv/server";
|
|
2
2
|
import { existsSync, readFileSync, readdirSync, lstatSync, realpathSync } from "node:fs";
|
|
3
|
-
import { registerFileLifecycle, lookupPackageDirectory, readPackageAtOrNull, generateContentFrame,
|
|
3
|
+
import { urlToRelativeUrl, registerFileLifecycle, lookupPackageDirectory, readPackageAtOrNull, generateContentFrame, errorToHTML, DATA_URL, CONTENT_TYPE, normalizeImportMap, composeTwoImportMaps, resolveImport, JS_QUOTES, urlToExtension, urlToBasename, applyNodeEsmResolution, URL_META, readCustomConditionsFromProcessArgs, urlIsOrIsInsideOf, collectFiles, registerDirectoryLifecycle, asUrlWithoutSearch, readEntryStatSync, ensurePathnameTrailingSlash, compareFileUrls, urlToFilename, applyFileSystemMagicResolution, getExtensionsToTry, setUrlExtension, createDetailedMessage, stringifyUrlSite, injectQueryParamsIntoSpecifier, isSpecifierForNodeBuiltin, injectQueryParams, urlToFileSystemPath, writeFileSync, moveUrl, ensureWindowsDriveLetter, validateResponseIntegrity, setUrlFilename, getCallerPosition, asSpecifierWithoutSearch, bufferToEtag, isFileSystemPath, urlToPathname, setUrlBasename, createLogger, normalizeUrl, ANSI, RUNTIME_COMPAT, formatError, assertAndNormalizeDirectoryUrl, createTaskLog } from "./jsenv_core_packages.js";
|
|
4
4
|
import { createPluginsController } from "@jsenv/server/src/plugins_controller.js";
|
|
5
5
|
import { parseHtml, injectJsenvScript, stringifyHtmlAst, parseCssUrls, getHtmlNodeAttribute, getHtmlNodePosition, getHtmlNodeAttributePosition, setHtmlNodeAttributes, parseSrcSet, getUrlForContentInsideHtml, removeHtmlNodeText, setHtmlNodeText, getHtmlNodeText, analyzeScriptNode, visitHtmlNodes, parseJsUrls, getUrlForContentInsideJs, applyBabelPlugins, analyzeLinkNode, injectHtmlNodeAsEarlyAsPossible, createHtmlNode, generateUrlForInlineContent, parseJsWithAcorn } from "@jsenv/ast";
|
|
6
6
|
import { jsenvPluginSupervisor } from "@jsenv/plugin-supervisor";
|
|
@@ -144,12 +144,21 @@ const createStatus = (
|
|
|
144
144
|
declaredVersion,
|
|
145
145
|
declaredBy,
|
|
146
146
|
installedVersion: null,
|
|
147
|
+
// the file telling this dependency apart; it is what an install rewrites and
|
|
148
|
+
// what the dev server looks at to know the dependency became the declared one
|
|
149
|
+
watchedPath: null,
|
|
147
150
|
state: "missing",
|
|
148
151
|
};
|
|
149
152
|
const installedDirectoryUrl = findInstalledDirectoryUrl(
|
|
150
153
|
declaringDirectoryUrl,
|
|
151
154
|
packageName,
|
|
152
155
|
);
|
|
156
|
+
status.watchedPath = watchedPathFromDirectoryUrl(
|
|
157
|
+
packageDirectory,
|
|
158
|
+
// not installed yet: name the place where it is expected to appear
|
|
159
|
+
installedDirectoryUrl ||
|
|
160
|
+
`${declaringDirectoryUrl}node_modules/${packageName}/`,
|
|
161
|
+
);
|
|
153
162
|
if (!installedDirectoryUrl) {
|
|
154
163
|
return status;
|
|
155
164
|
}
|
|
@@ -168,6 +177,14 @@ const createStatus = (
|
|
|
168
177
|
return status;
|
|
169
178
|
};
|
|
170
179
|
|
|
180
|
+
const watchedPathFromDirectoryUrl = (packageDirectory, directoryUrl) => {
|
|
181
|
+
const packageJsonUrl = `${directoryUrl}package.json`;
|
|
182
|
+
if (!packageDirectory.url) {
|
|
183
|
+
return packageJsonUrl;
|
|
184
|
+
}
|
|
185
|
+
return urlToRelativeUrl(packageJsonUrl, packageDirectory.url);
|
|
186
|
+
};
|
|
187
|
+
|
|
171
188
|
const findInstalledDirectoryUrl = (declaringDirectoryUrl, packageName) => {
|
|
172
189
|
let directoryUrl = declaringDirectoryUrl;
|
|
173
190
|
while (directoryUrl) {
|
|
@@ -232,8 +249,13 @@ const watchDependencies = (
|
|
|
232
249
|
{ onProblem, onInstalled, onChange, pollInterval = POLL_INTERVAL },
|
|
233
250
|
) => {
|
|
234
251
|
let problemMap = new Map();
|
|
252
|
+
// every path given to the browser is relative to the package directory, the
|
|
253
|
+
// one holding the package.json being watched
|
|
254
|
+
const packageJsonPath = "package.json";
|
|
235
255
|
const watcher = {
|
|
236
256
|
getProblems: () => Array.from(problemMap.values()),
|
|
257
|
+
// what the browser needs to display the watching in progress
|
|
258
|
+
getWatchInfo: () => ({ packageJsonPath, pollInterval }),
|
|
237
259
|
stop: () => {},
|
|
238
260
|
};
|
|
239
261
|
if (!packageDirectory.url) {
|
|
@@ -7122,6 +7144,7 @@ const clientFileUrl = import.meta.resolve("../js/dependency_status.js");
|
|
|
7122
7144
|
const jsenvPluginDependencyStatus = ({
|
|
7123
7145
|
dependencyProblemEventEmitter,
|
|
7124
7146
|
getDependencyProblems,
|
|
7147
|
+
getDependencyWatchInfo = () => ({}),
|
|
7125
7148
|
}) => {
|
|
7126
7149
|
return {
|
|
7127
7150
|
name: "jsenv:dependency_status",
|
|
@@ -7158,7 +7181,10 @@ const jsenvPluginDependencyStatus = ({
|
|
|
7158
7181
|
src: clientReference.generatedSpecifier,
|
|
7159
7182
|
initCall: {
|
|
7160
7183
|
callee: "initDependencyStatus",
|
|
7161
|
-
params: {
|
|
7184
|
+
params: {
|
|
7185
|
+
problems: getDependencyProblems(),
|
|
7186
|
+
watchInfo: getDependencyWatchInfo(),
|
|
7187
|
+
},
|
|
7162
7188
|
},
|
|
7163
7189
|
pluginName: "jsenv:dependency_status",
|
|
7164
7190
|
});
|
|
@@ -7232,6 +7258,12 @@ const jsenvPluginCustomElementsRedefine = () => {
|
|
|
7232
7258
|
const jsenvPluginRibbon = ({
|
|
7233
7259
|
rootDirectoryUrl,
|
|
7234
7260
|
htmlInclude = "/**/*.html",
|
|
7261
|
+
text,
|
|
7262
|
+
color,
|
|
7263
|
+
textColor,
|
|
7264
|
+
href,
|
|
7265
|
+
target,
|
|
7266
|
+
position,
|
|
7235
7267
|
}) => {
|
|
7236
7268
|
const ribbonClientFileUrl = import.meta.resolve("../client/ribbon/ribbon.js");
|
|
7237
7269
|
const associations = URL_META.resolveAssociations(
|
|
@@ -7244,7 +7276,7 @@ const jsenvPluginRibbon = ({
|
|
|
7244
7276
|
);
|
|
7245
7277
|
return {
|
|
7246
7278
|
name: "jsenv:ribbon",
|
|
7247
|
-
appliesDuring: "
|
|
7279
|
+
appliesDuring: "*",
|
|
7248
7280
|
transformUrlContent: {
|
|
7249
7281
|
html: (urlInfo) => {
|
|
7250
7282
|
const jsenvToolbarHtmlClientFileUrl = urlInfo.context.getPluginMeta(
|
|
@@ -7279,9 +7311,19 @@ const jsenvPluginRibbon = ({
|
|
|
7279
7311
|
src: ribbonClientFileReference.generatedSpecifier,
|
|
7280
7312
|
initCall: {
|
|
7281
7313
|
callee: "injectRibbon",
|
|
7282
|
-
params: {
|
|
7283
|
-
text:
|
|
7284
|
-
|
|
7314
|
+
params: withoutUndefinedValues({
|
|
7315
|
+
text:
|
|
7316
|
+
text === undefined
|
|
7317
|
+
? urlInfo.context.dev
|
|
7318
|
+
? "DEV"
|
|
7319
|
+
: "BUILD"
|
|
7320
|
+
: text,
|
|
7321
|
+
color,
|
|
7322
|
+
textColor,
|
|
7323
|
+
href,
|
|
7324
|
+
target,
|
|
7325
|
+
position,
|
|
7326
|
+
}),
|
|
7285
7327
|
},
|
|
7286
7328
|
pluginName: "jsenv:ribbon",
|
|
7287
7329
|
});
|
|
@@ -7291,6 +7333,16 @@ const jsenvPluginRibbon = ({
|
|
|
7291
7333
|
};
|
|
7292
7334
|
};
|
|
7293
7335
|
|
|
7336
|
+
const withoutUndefinedValues = (object) => {
|
|
7337
|
+
const objectWithoutUndefinedValues = {};
|
|
7338
|
+
for (const key of Object.keys(object)) {
|
|
7339
|
+
if (object[key] !== undefined) {
|
|
7340
|
+
objectWithoutUndefinedValues[key] = object[key];
|
|
7341
|
+
}
|
|
7342
|
+
}
|
|
7343
|
+
return objectWithoutUndefinedValues;
|
|
7344
|
+
};
|
|
7345
|
+
|
|
7294
7346
|
/**
|
|
7295
7347
|
* HTML page server by jsenv dev server will listen for drop events
|
|
7296
7348
|
* and redirect the browser to the dropped file location.
|
|
@@ -11666,7 +11718,7 @@ const EXECUTED_BY_TEST_PLAN = process.argv.includes("--jsenv-test");
|
|
|
11666
11718
|
* @param {Array} [params.serverPlugins=[]] - `@jsenv/server`-level plugins.
|
|
11667
11719
|
* @param {boolean|object} [params.clientAutoreload=true] - Live reload; also gates the server-events channel.
|
|
11668
11720
|
* @param {boolean|object} [params.serverTiming={ minDuration: 0.5 }] - server-timing response headers; `minDuration` (ms) drops entries that took less (0 when run by the test plan, so tests see every entry).
|
|
11669
|
-
* @param {boolean} [params.ribbon=true] - The
|
|
11721
|
+
* @param {boolean|object} [params.ribbon=true] - The "ribbon" overlay marking the page as non-production. As an object: `text` (defaults to `"DEV"`), `color` (background, defaults to `"orange"`), `textColor` (defaults to dark or light depending on `color`), `href` (turns the ribbon into a link; only then does it capture clicks), `target` (defaults to `"_blank"`), `position` (`"top-right"` (default), `"top-left"`, `"bottom-right"`, `"bottom-left"` draw a diagonal corner ribbon; `"top"`, `"bottom"` draw a full width band), `htmlInclude` (defaults to `"/**\/*.html"`).
|
|
11670
11722
|
* @param {boolean} [params.supervisor=true] - Script supervisor (better error reporting).
|
|
11671
11723
|
* @param {boolean|object} [params.directoryListing=true] - Directory listing pages.
|
|
11672
11724
|
* @param {object} [params.injections] - Values to inject into files, as `{ urlPattern: getInjections }`. Keys are url patterns relative to sourceDirectoryUrl (`"./index.html"`, `"**\/*.js"`), values are functions receiving `urlInfo` and returning (or resolving to) an object of placeholders to replace, named `__LIKE_THIS__` by convention. In JS the value is injected as a JS literal (a string brings its own quotes), everywhere else as-is so it can be concatenated: `href="__BACKEND_URL__/users/me"`. An html url pattern also covers what is inlined in that html, so `<script>window.backendUrl = __BACKEND_URL__;</script>` shares the value with every js file of the page. See `INJECTIONS.optional` and `INJECTIONS.global`.
|
|
@@ -11882,6 +11934,7 @@ const startDevServer = async ({
|
|
|
11882
11934
|
dependencyStatus: {
|
|
11883
11935
|
dependencyProblemEventEmitter,
|
|
11884
11936
|
getDependencyProblems: dependencyWatcher.getProblems,
|
|
11937
|
+
getDependencyWatchInfo: dependencyWatcher.getWatchInfo,
|
|
11885
11938
|
},
|
|
11886
11939
|
cacheControl,
|
|
11887
11940
|
ribbon,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jsenv/core",
|
|
3
|
-
"version": "41.4.
|
|
3
|
+
"version": "41.4.12",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Tool to develop, test and build js projects",
|
|
6
6
|
"repository": {
|
|
@@ -72,12 +72,12 @@
|
|
|
72
72
|
"test:snapshot_clear": "npx @jsenv/filesystem clear **/tests/**/side_effects/"
|
|
73
73
|
},
|
|
74
74
|
"dependencies": {
|
|
75
|
-
"@jsenv/ast": "6.
|
|
76
|
-
"@jsenv/js-module-fallback": "1.4.
|
|
75
|
+
"@jsenv/ast": "6.9.0",
|
|
76
|
+
"@jsenv/js-module-fallback": "1.4.39",
|
|
77
77
|
"@jsenv/plugin-bundling": "2.10.16",
|
|
78
|
-
"@jsenv/plugin-minification": "1.7.
|
|
79
|
-
"@jsenv/plugin-supervisor": "1.8.
|
|
80
|
-
"@jsenv/plugin-transpilation": "1.5.
|
|
78
|
+
"@jsenv/plugin-minification": "1.7.7",
|
|
79
|
+
"@jsenv/plugin-supervisor": "1.8.10",
|
|
80
|
+
"@jsenv/plugin-transpilation": "1.5.80",
|
|
81
81
|
"@jsenv/server": "17.6.0",
|
|
82
82
|
"@jsenv/sourcemap": "1.4.2",
|
|
83
83
|
"react-table": "7.8.0"
|
package/src/build/build.js
CHANGED
|
@@ -134,6 +134,18 @@ import { jsenvPluginMappings } from "./jsenv_plugin_mappings.js";
|
|
|
134
134
|
* How URLs are versioned for this entry point (defaults to "search_param")
|
|
135
135
|
* @param {('none'|'inline'|'file'|'programmatic')} [entryPoint.sourcemaps]
|
|
136
136
|
* Sourcemap generation strategy for this entry point (defaults to "none")
|
|
137
|
+
* @param {boolean|object} [entryPoint.ribbon=false]
|
|
138
|
+
* Inject a ribbon marking the page as non-production. Disabled by default
|
|
139
|
+
* because the ribbon ends up inside the build directory: enable it on the
|
|
140
|
+
* builds meant for a preview/review app, not on the production build.
|
|
141
|
+
*
|
|
142
|
+
* ribbon: {
|
|
143
|
+
* text: "preview",
|
|
144
|
+
* color: "#7c3aed",
|
|
145
|
+
* href: "https://github.com/org/repo/pull/42",
|
|
146
|
+
* }
|
|
147
|
+
*
|
|
148
|
+
* See startDevServer "ribbon" param for the full list of options.
|
|
137
149
|
* @param {object} [entryPoint.injections]
|
|
138
150
|
* Values to inject into files, as { urlPattern: getInjections }.
|
|
139
151
|
* Keys are url patterns relative to sourceDirectoryUrl ("./index.html", "**\/*.js"),
|
|
@@ -953,6 +965,7 @@ const entryPointDefaultParams = {
|
|
|
953
965
|
magicDirectoryIndex: undefined,
|
|
954
966
|
directoryReferenceEffect: undefined,
|
|
955
967
|
scenarioPlaceholders: undefined,
|
|
968
|
+
ribbon: false,
|
|
956
969
|
injections: undefined,
|
|
957
970
|
transpilation: {},
|
|
958
971
|
preserveComments: undefined,
|
|
@@ -1007,6 +1020,7 @@ const prepareEntryPointBuild = async (
|
|
|
1007
1020
|
magicDirectoryIndex,
|
|
1008
1021
|
directoryReferenceEffect,
|
|
1009
1022
|
scenarioPlaceholders,
|
|
1023
|
+
ribbon,
|
|
1010
1024
|
injections,
|
|
1011
1025
|
transpilation,
|
|
1012
1026
|
preserveComments,
|
|
@@ -1183,6 +1197,7 @@ const prepareEntryPointBuild = async (
|
|
|
1183
1197
|
inlining: false,
|
|
1184
1198
|
http,
|
|
1185
1199
|
scenarioPlaceholders,
|
|
1200
|
+
ribbon,
|
|
1186
1201
|
packageSideEffects,
|
|
1187
1202
|
}),
|
|
1188
1203
|
]);
|
|
@@ -23,15 +23,22 @@ import {
|
|
|
23
23
|
startServer,
|
|
24
24
|
} from "@jsenv/server";
|
|
25
25
|
|
|
26
|
+
import { injectRibbonIntoHtml } from "../plugins/ribbon/ribbon_html_injection.js";
|
|
27
|
+
|
|
26
28
|
/**
|
|
27
29
|
* Start a server for build files.
|
|
28
30
|
* @param {Object} buildServerParameters
|
|
29
31
|
* @param {string|url} buildServerParameters.buildDirectoryUrl Directory where build files are written
|
|
32
|
+
* @param {boolean|object} [buildServerParameters.ribbon=false] Inject a ribbon in the html responses,
|
|
33
|
+
* marking the page as non-production. The build directory itself is left untouched.
|
|
34
|
+
* As an object: `text`, `color`, `textColor`, `href`, `target`, `position`
|
|
35
|
+
* (see startDevServer "ribbon" param).
|
|
30
36
|
* @return {Object} A build server object
|
|
31
37
|
*/
|
|
32
38
|
export const startBuildServer = async ({
|
|
33
39
|
buildDirectoryUrl,
|
|
34
40
|
buildDirectoryMainFileRelativeUrl = "index.html",
|
|
41
|
+
ribbon = false,
|
|
35
42
|
port = 9779,
|
|
36
43
|
routes = [],
|
|
37
44
|
serverPlugins = [],
|
|
@@ -47,6 +54,9 @@ export const startBuildServer = async ({
|
|
|
47
54
|
keepProcessAlive = true,
|
|
48
55
|
}) => {
|
|
49
56
|
const logger = createLogger({ logLevel });
|
|
57
|
+
if (ribbon === true) {
|
|
58
|
+
ribbon = {};
|
|
59
|
+
}
|
|
50
60
|
const operation = Abort.startOperation();
|
|
51
61
|
operation.addAbortSignal(signal);
|
|
52
62
|
if (handleSIGINT) {
|
|
@@ -60,6 +70,9 @@ export const startBuildServer = async ({
|
|
|
60
70
|
});
|
|
61
71
|
}
|
|
62
72
|
|
|
73
|
+
const fileSystemFetch = createFileSystemFetch(buildDirectoryUrl, {
|
|
74
|
+
mainFileRelativeUrl: buildDirectoryMainFileRelativeUrl,
|
|
75
|
+
});
|
|
63
76
|
const startBuildServerTask = createTaskLog("start build server", {
|
|
64
77
|
disabled: !logger.levels.info,
|
|
65
78
|
});
|
|
@@ -98,9 +111,9 @@ export const startBuildServer = async ({
|
|
|
98
111
|
{
|
|
99
112
|
endpoint: "GET /",
|
|
100
113
|
description: "Serve build files",
|
|
101
|
-
fetch:
|
|
102
|
-
|
|
103
|
-
|
|
114
|
+
fetch: ribbon
|
|
115
|
+
? withRibbonInjectedInHtml(fileSystemFetch, ribbon)
|
|
116
|
+
: fileSystemFetch,
|
|
104
117
|
},
|
|
105
118
|
],
|
|
106
119
|
});
|
|
@@ -121,3 +134,43 @@ export const startBuildServer = async ({
|
|
|
121
134
|
},
|
|
122
135
|
};
|
|
123
136
|
};
|
|
137
|
+
|
|
138
|
+
const withRibbonInjectedInHtml = (fetch, ribbon) => {
|
|
139
|
+
return async (request, helpers) => {
|
|
140
|
+
const response = await fetch(request, helpers);
|
|
141
|
+
if (!response || response.status !== 200) {
|
|
142
|
+
return response;
|
|
143
|
+
}
|
|
144
|
+
const contentType = response.headers?.["content-type"];
|
|
145
|
+
if (!contentType || !contentType.startsWith("text/html")) {
|
|
146
|
+
return response;
|
|
147
|
+
}
|
|
148
|
+
const html = await readResponseBodyAsString(response.body);
|
|
149
|
+
const htmlWithRibbon = injectRibbonIntoHtml(html, ribbon);
|
|
150
|
+
const bodyBuffer = Buffer.from(htmlWithRibbon);
|
|
151
|
+
const headers = {
|
|
152
|
+
...response.headers,
|
|
153
|
+
"content-length": bodyBuffer.length,
|
|
154
|
+
};
|
|
155
|
+
// the body is generated per request; what the file on disk hashes to or when
|
|
156
|
+
// it was modified does not describe it
|
|
157
|
+
delete headers.etag;
|
|
158
|
+
delete headers["last-modified"];
|
|
159
|
+
return {
|
|
160
|
+
...response,
|
|
161
|
+
headers,
|
|
162
|
+
body: bodyBuffer,
|
|
163
|
+
};
|
|
164
|
+
};
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const readResponseBodyAsString = async (body) => {
|
|
168
|
+
if (typeof body === "string" || Buffer.isBuffer(body)) {
|
|
169
|
+
return String(body);
|
|
170
|
+
}
|
|
171
|
+
const chunks = [];
|
|
172
|
+
for await (const chunk of body) {
|
|
173
|
+
chunks.push(Buffer.from(chunk));
|
|
174
|
+
}
|
|
175
|
+
return String(Buffer.concat(chunks));
|
|
176
|
+
};
|
|
@@ -48,7 +48,7 @@ const EXECUTED_BY_TEST_PLAN = process.argv.includes("--jsenv-test");
|
|
|
48
48
|
* @param {Array} [params.serverPlugins=[]] - `@jsenv/server`-level plugins.
|
|
49
49
|
* @param {boolean|object} [params.clientAutoreload=true] - Live reload; also gates the server-events channel.
|
|
50
50
|
* @param {boolean|object} [params.serverTiming={ minDuration: 0.5 }] - server-timing response headers; `minDuration` (ms) drops entries that took less (0 when run by the test plan, so tests see every entry).
|
|
51
|
-
* @param {boolean} [params.ribbon=true] - The
|
|
51
|
+
* @param {boolean|object} [params.ribbon=true] - The "ribbon" overlay marking the page as non-production. As an object: `text` (defaults to `"DEV"`), `color` (background, defaults to `"orange"`), `textColor` (defaults to dark or light depending on `color`), `href` (turns the ribbon into a link; only then does it capture clicks), `target` (defaults to `"_blank"`), `position` (`"top-right"` (default), `"top-left"`, `"bottom-right"`, `"bottom-left"` draw a diagonal corner ribbon; `"top"`, `"bottom"` draw a full width band), `htmlInclude` (defaults to `"/**\/*.html"`).
|
|
52
52
|
* @param {boolean} [params.supervisor=true] - Script supervisor (better error reporting).
|
|
53
53
|
* @param {boolean|object} [params.directoryListing=true] - Directory listing pages.
|
|
54
54
|
* @param {object} [params.injections] - Values to inject into files, as `{ urlPattern: getInjections }`. Keys are url patterns relative to sourceDirectoryUrl (`"./index.html"`, `"**\/*.js"`), values are functions receiving `urlInfo` and returning (or resolving to) an object of placeholders to replace, named `__LIKE_THIS__` by convention. In JS the value is injected as a JS literal (a string brings its own quotes), everywhere else as-is so it can be concatenated: `href="__BACKEND_URL__/users/me"`. An html url pattern also covers what is inlined in that html, so `<script>window.backendUrl = __BACKEND_URL__;</script>` shares the value with every js file of the page. See `INJECTIONS.optional` and `INJECTIONS.global`.
|
|
@@ -265,6 +265,7 @@ export const startDevServer = async ({
|
|
|
265
265
|
dependencyStatus: {
|
|
266
266
|
dependencyProblemEventEmitter,
|
|
267
267
|
getDependencyProblems: dependencyWatcher.getProblems,
|
|
268
|
+
getDependencyWatchInfo: dependencyWatcher.getWatchInfo,
|
|
268
269
|
},
|
|
269
270
|
cacheControl,
|
|
270
271
|
ribbon,
|
|
@@ -23,8 +23,13 @@ export const watchDependencies = (
|
|
|
23
23
|
{ onProblem, onInstalled, onChange, pollInterval = POLL_INTERVAL },
|
|
24
24
|
) => {
|
|
25
25
|
let problemMap = new Map();
|
|
26
|
+
// every path given to the browser is relative to the package directory, the
|
|
27
|
+
// one holding the package.json being watched
|
|
28
|
+
const packageJsonPath = "package.json";
|
|
26
29
|
const watcher = {
|
|
27
30
|
getProblems: () => Array.from(problemMap.values()),
|
|
31
|
+
// what the browser needs to display the watching in progress
|
|
32
|
+
getWatchInfo: () => ({ packageJsonPath, pollInterval }),
|
|
28
33
|
stop: () => {},
|
|
29
34
|
};
|
|
30
35
|
if (!packageDirectory.url) {
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* would pick, which is way beyond what is needed here.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
import { urlToRelativeUrl } from "@jsenv/urls";
|
|
15
16
|
import { existsSync } from "node:fs";
|
|
16
17
|
|
|
17
18
|
const DEPENDENCY_FIELDS = [
|
|
@@ -93,12 +94,21 @@ const createStatus = (
|
|
|
93
94
|
declaredVersion,
|
|
94
95
|
declaredBy,
|
|
95
96
|
installedVersion: null,
|
|
97
|
+
// the file telling this dependency apart; it is what an install rewrites and
|
|
98
|
+
// what the dev server looks at to know the dependency became the declared one
|
|
99
|
+
watchedPath: null,
|
|
96
100
|
state: "missing",
|
|
97
101
|
};
|
|
98
102
|
const installedDirectoryUrl = findInstalledDirectoryUrl(
|
|
99
103
|
declaringDirectoryUrl,
|
|
100
104
|
packageName,
|
|
101
105
|
);
|
|
106
|
+
status.watchedPath = watchedPathFromDirectoryUrl(
|
|
107
|
+
packageDirectory,
|
|
108
|
+
// not installed yet: name the place where it is expected to appear
|
|
109
|
+
installedDirectoryUrl ||
|
|
110
|
+
`${declaringDirectoryUrl}node_modules/${packageName}/`,
|
|
111
|
+
);
|
|
102
112
|
if (!installedDirectoryUrl) {
|
|
103
113
|
return status;
|
|
104
114
|
}
|
|
@@ -117,6 +127,14 @@ const createStatus = (
|
|
|
117
127
|
return status;
|
|
118
128
|
};
|
|
119
129
|
|
|
130
|
+
const watchedPathFromDirectoryUrl = (packageDirectory, directoryUrl) => {
|
|
131
|
+
const packageJsonUrl = `${directoryUrl}package.json`;
|
|
132
|
+
if (!packageDirectory.url) {
|
|
133
|
+
return packageJsonUrl;
|
|
134
|
+
}
|
|
135
|
+
return urlToRelativeUrl(packageJsonUrl, packageDirectory.url);
|
|
136
|
+
};
|
|
137
|
+
|
|
120
138
|
const findInstalledDirectoryUrl = (declaringDirectoryUrl, packageName) => {
|
|
121
139
|
let directoryUrl = declaringDirectoryUrl;
|
|
122
140
|
while (directoryUrl) {
|