@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
package/dist/build/build.js
CHANGED
|
@@ -196,12 +196,21 @@ const createStatus = (
|
|
|
196
196
|
declaredVersion,
|
|
197
197
|
declaredBy,
|
|
198
198
|
installedVersion: null,
|
|
199
|
+
// the file telling this dependency apart; it is what an install rewrites and
|
|
200
|
+
// what the dev server looks at to know the dependency became the declared one
|
|
201
|
+
watchedPath: null,
|
|
199
202
|
state: "missing",
|
|
200
203
|
};
|
|
201
204
|
const installedDirectoryUrl = findInstalledDirectoryUrl(
|
|
202
205
|
declaringDirectoryUrl,
|
|
203
206
|
packageName,
|
|
204
207
|
);
|
|
208
|
+
status.watchedPath = watchedPathFromDirectoryUrl(
|
|
209
|
+
packageDirectory,
|
|
210
|
+
// not installed yet: name the place where it is expected to appear
|
|
211
|
+
installedDirectoryUrl ||
|
|
212
|
+
`${declaringDirectoryUrl}node_modules/${packageName}/`,
|
|
213
|
+
);
|
|
205
214
|
if (!installedDirectoryUrl) {
|
|
206
215
|
return status;
|
|
207
216
|
}
|
|
@@ -220,6 +229,14 @@ const createStatus = (
|
|
|
220
229
|
return status;
|
|
221
230
|
};
|
|
222
231
|
|
|
232
|
+
const watchedPathFromDirectoryUrl = (packageDirectory, directoryUrl) => {
|
|
233
|
+
const packageJsonUrl = `${directoryUrl}package.json`;
|
|
234
|
+
if (!packageDirectory.url) {
|
|
235
|
+
return packageJsonUrl;
|
|
236
|
+
}
|
|
237
|
+
return urlToRelativeUrl(packageJsonUrl, packageDirectory.url);
|
|
238
|
+
};
|
|
239
|
+
|
|
223
240
|
const findInstalledDirectoryUrl = (declaringDirectoryUrl, packageName) => {
|
|
224
241
|
let directoryUrl = declaringDirectoryUrl;
|
|
225
242
|
while (directoryUrl) {
|
|
@@ -9298,6 +9315,7 @@ const clientFileUrl = import.meta.resolve("../js/dependency_status.js");
|
|
|
9298
9315
|
const jsenvPluginDependencyStatus = ({
|
|
9299
9316
|
dependencyProblemEventEmitter,
|
|
9300
9317
|
getDependencyProblems,
|
|
9318
|
+
getDependencyWatchInfo = () => ({}),
|
|
9301
9319
|
}) => {
|
|
9302
9320
|
return {
|
|
9303
9321
|
name: "jsenv:dependency_status",
|
|
@@ -9334,7 +9352,10 @@ const jsenvPluginDependencyStatus = ({
|
|
|
9334
9352
|
src: clientReference.generatedSpecifier,
|
|
9335
9353
|
initCall: {
|
|
9336
9354
|
callee: "initDependencyStatus",
|
|
9337
|
-
params: {
|
|
9355
|
+
params: {
|
|
9356
|
+
problems: getDependencyProblems(),
|
|
9357
|
+
watchInfo: getDependencyWatchInfo(),
|
|
9358
|
+
},
|
|
9338
9359
|
},
|
|
9339
9360
|
pluginName: "jsenv:dependency_status",
|
|
9340
9361
|
});
|
|
@@ -9408,6 +9429,12 @@ const jsenvPluginCustomElementsRedefine = () => {
|
|
|
9408
9429
|
const jsenvPluginRibbon = ({
|
|
9409
9430
|
rootDirectoryUrl,
|
|
9410
9431
|
htmlInclude = "/**/*.html",
|
|
9432
|
+
text,
|
|
9433
|
+
color,
|
|
9434
|
+
textColor,
|
|
9435
|
+
href,
|
|
9436
|
+
target,
|
|
9437
|
+
position,
|
|
9411
9438
|
}) => {
|
|
9412
9439
|
const ribbonClientFileUrl = import.meta.resolve("../client/ribbon/ribbon.js");
|
|
9413
9440
|
const associations = URL_META.resolveAssociations(
|
|
@@ -9420,7 +9447,7 @@ const jsenvPluginRibbon = ({
|
|
|
9420
9447
|
);
|
|
9421
9448
|
return {
|
|
9422
9449
|
name: "jsenv:ribbon",
|
|
9423
|
-
appliesDuring: "
|
|
9450
|
+
appliesDuring: "*",
|
|
9424
9451
|
transformUrlContent: {
|
|
9425
9452
|
html: (urlInfo) => {
|
|
9426
9453
|
const jsenvToolbarHtmlClientFileUrl = urlInfo.context.getPluginMeta(
|
|
@@ -9455,9 +9482,19 @@ const jsenvPluginRibbon = ({
|
|
|
9455
9482
|
src: ribbonClientFileReference.generatedSpecifier,
|
|
9456
9483
|
initCall: {
|
|
9457
9484
|
callee: "injectRibbon",
|
|
9458
|
-
params: {
|
|
9459
|
-
text:
|
|
9460
|
-
|
|
9485
|
+
params: withoutUndefinedValues({
|
|
9486
|
+
text:
|
|
9487
|
+
text === undefined
|
|
9488
|
+
? urlInfo.context.dev
|
|
9489
|
+
? "DEV"
|
|
9490
|
+
: "BUILD"
|
|
9491
|
+
: text,
|
|
9492
|
+
color,
|
|
9493
|
+
textColor,
|
|
9494
|
+
href,
|
|
9495
|
+
target,
|
|
9496
|
+
position,
|
|
9497
|
+
}),
|
|
9461
9498
|
},
|
|
9462
9499
|
pluginName: "jsenv:ribbon",
|
|
9463
9500
|
});
|
|
@@ -9467,6 +9504,16 @@ const jsenvPluginRibbon = ({
|
|
|
9467
9504
|
};
|
|
9468
9505
|
};
|
|
9469
9506
|
|
|
9507
|
+
const withoutUndefinedValues = (object) => {
|
|
9508
|
+
const objectWithoutUndefinedValues = {};
|
|
9509
|
+
for (const key of Object.keys(object)) {
|
|
9510
|
+
if (object[key] !== undefined) {
|
|
9511
|
+
objectWithoutUndefinedValues[key] = object[key];
|
|
9512
|
+
}
|
|
9513
|
+
}
|
|
9514
|
+
return objectWithoutUndefinedValues;
|
|
9515
|
+
};
|
|
9516
|
+
|
|
9470
9517
|
/**
|
|
9471
9518
|
* HTML page server by jsenv dev server will listen for drop events
|
|
9472
9519
|
* and redirect the browser to the dropped file location.
|
|
@@ -12153,6 +12200,18 @@ const jsenvPluginMappings = (mappings) => {
|
|
|
12153
12200
|
* How URLs are versioned for this entry point (defaults to "search_param")
|
|
12154
12201
|
* @param {('none'|'inline'|'file'|'programmatic')} [entryPoint.sourcemaps]
|
|
12155
12202
|
* Sourcemap generation strategy for this entry point (defaults to "none")
|
|
12203
|
+
* @param {boolean|object} [entryPoint.ribbon=false]
|
|
12204
|
+
* Inject a ribbon marking the page as non-production. Disabled by default
|
|
12205
|
+
* because the ribbon ends up inside the build directory: enable it on the
|
|
12206
|
+
* builds meant for a preview/review app, not on the production build.
|
|
12207
|
+
*
|
|
12208
|
+
* ribbon: {
|
|
12209
|
+
* text: "preview",
|
|
12210
|
+
* color: "#7c3aed",
|
|
12211
|
+
* href: "https://github.com/org/repo/pull/42",
|
|
12212
|
+
* }
|
|
12213
|
+
*
|
|
12214
|
+
* See startDevServer "ribbon" param for the full list of options.
|
|
12156
12215
|
* @param {object} [entryPoint.injections]
|
|
12157
12216
|
* Values to inject into files, as { urlPattern: getInjections }.
|
|
12158
12217
|
* Keys are url patterns relative to sourceDirectoryUrl ("./index.html", "**\/*.js"),
|
|
@@ -12970,6 +13029,7 @@ const entryPointDefaultParams = {
|
|
|
12970
13029
|
magicDirectoryIndex: undefined,
|
|
12971
13030
|
directoryReferenceEffect: undefined,
|
|
12972
13031
|
scenarioPlaceholders: undefined,
|
|
13032
|
+
ribbon: false,
|
|
12973
13033
|
injections: undefined,
|
|
12974
13034
|
transpilation: {},
|
|
12975
13035
|
preserveComments: undefined,
|
|
@@ -13024,6 +13084,7 @@ const prepareEntryPointBuild = async (
|
|
|
13024
13084
|
magicDirectoryIndex,
|
|
13025
13085
|
directoryReferenceEffect,
|
|
13026
13086
|
scenarioPlaceholders,
|
|
13087
|
+
ribbon,
|
|
13027
13088
|
injections,
|
|
13028
13089
|
transpilation,
|
|
13029
13090
|
preserveComments,
|
|
@@ -13200,6 +13261,7 @@ const prepareEntryPointBuild = async (
|
|
|
13200
13261
|
inlining: false,
|
|
13201
13262
|
http,
|
|
13202
13263
|
scenarioPlaceholders,
|
|
13264
|
+
ribbon,
|
|
13203
13265
|
packageSideEffects,
|
|
13204
13266
|
}),
|
|
13205
13267
|
]);
|
|
@@ -1,49 +1,117 @@
|
|
|
1
|
-
|
|
1
|
+
/*
|
|
2
|
+
* A ribbon marking a page as non-production: "DEV" while developing, whatever
|
|
3
|
+
* the app wants otherwise ("preview", a version number, a branch name).
|
|
4
|
+
*
|
|
5
|
+
* Two shapes, picked by "position":
|
|
6
|
+
* - a corner ("top-right", "top-left", "bottom-right", "bottom-left") draws the
|
|
7
|
+
* diagonal band clipped by a 100x100 square in that corner
|
|
8
|
+
* - an edge ("top", "bottom") draws a full width horizontal band
|
|
9
|
+
*
|
|
10
|
+
* The container never captures pointer events; only the visible band does, and
|
|
11
|
+
* only when "href" turns it into a link. A corner ribbon therefore costs the app
|
|
12
|
+
* nothing: the rest of the 100x100 square keeps letting clicks through to the
|
|
13
|
+
* interface underneath, which usually lives exactly there.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const CORNER_POSITIONS = {
|
|
17
|
+
"top-right": {
|
|
18
|
+
placement: `top: 0; right: 0;`,
|
|
19
|
+
transform: `translate(26px, -26px) rotate(45deg)`,
|
|
20
|
+
},
|
|
21
|
+
"top-left": {
|
|
22
|
+
placement: `top: 0; left: 0;`,
|
|
23
|
+
transform: `translate(-26px, -26px) rotate(-45deg)`,
|
|
24
|
+
},
|
|
25
|
+
"bottom-right": {
|
|
26
|
+
placement: `bottom: 0; right: 0;`,
|
|
27
|
+
transform: `translate(26px, 26px) rotate(-45deg)`,
|
|
28
|
+
},
|
|
29
|
+
"bottom-left": {
|
|
30
|
+
placement: `bottom: 0; left: 0;`,
|
|
31
|
+
transform: `translate(-26px, 26px) rotate(45deg)`,
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
const EDGE_POSITIONS = {
|
|
35
|
+
top: `top: 0; left: 0; right: 0;`,
|
|
36
|
+
bottom: `bottom: 0; left: 0; right: 0;`,
|
|
37
|
+
};
|
|
38
|
+
const DARK_TEXT_COLOR = "rgb(55, 7, 7)";
|
|
39
|
+
const LIGHT_TEXT_COLOR = "rgb(255, 255, 255)";
|
|
40
|
+
|
|
41
|
+
const injectRibbon = ({
|
|
42
|
+
text = "DEV",
|
|
43
|
+
color = "orange",
|
|
44
|
+
textColor,
|
|
45
|
+
href,
|
|
46
|
+
target = "_blank",
|
|
47
|
+
position = "top-right",
|
|
48
|
+
}) => {
|
|
49
|
+
if (!CORNER_POSITIONS[position] && !EDGE_POSITIONS[position]) {
|
|
50
|
+
console.warn(
|
|
51
|
+
`unknown ribbon position "${position}", falling back to "top-right"`,
|
|
52
|
+
);
|
|
53
|
+
position = "top-right";
|
|
54
|
+
}
|
|
55
|
+
if (textColor === undefined) {
|
|
56
|
+
textColor = pickTextColorFor(color);
|
|
57
|
+
}
|
|
58
|
+
const corner = CORNER_POSITIONS[position];
|
|
2
59
|
const css = /* css */ `
|
|
3
60
|
#jsenv_ribbon_container {
|
|
4
61
|
position: fixed;
|
|
5
|
-
top: 0;
|
|
6
|
-
right: 0;
|
|
7
62
|
z-index: 1001;
|
|
8
|
-
width: 100px;
|
|
9
|
-
height: 100px;
|
|
10
|
-
opacity: 0.5;
|
|
11
63
|
pointer-events: none;
|
|
12
64
|
overflow: hidden;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
top: -10px;
|
|
17
|
-
right: -10px;
|
|
18
|
-
width: 100%;
|
|
19
|
-
height: 100%;
|
|
65
|
+
${corner
|
|
66
|
+
? `width: 100px; height: 100px; ${corner.placement}`
|
|
67
|
+
: EDGE_POSITIONS[position]}
|
|
20
68
|
}
|
|
21
69
|
#jsenv_ribbon_text {
|
|
22
|
-
position: absolute;
|
|
23
|
-
top: 20px;
|
|
24
|
-
left: 0px;
|
|
25
70
|
display: block;
|
|
26
|
-
|
|
27
|
-
color: rgb(55, 7, 7);
|
|
71
|
+
color: ${textColor};
|
|
28
72
|
font-weight: 700;
|
|
29
73
|
font-size: 16px;
|
|
30
74
|
font-family: "Lato", sans-serif;
|
|
31
75
|
text-align: center;
|
|
76
|
+
text-decoration: none;
|
|
32
77
|
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
|
|
33
|
-
|
|
34
|
-
background-color: orange;
|
|
78
|
+
background-color: ${color};
|
|
35
79
|
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.1);
|
|
36
|
-
|
|
80
|
+
opacity: 0.8;
|
|
81
|
+
transition: opacity 150ms ease;
|
|
37
82
|
user-select: none;
|
|
83
|
+
${corner
|
|
84
|
+
? `
|
|
85
|
+
position: absolute;
|
|
86
|
+
top: 50%;
|
|
87
|
+
left: 50%;
|
|
88
|
+
width: 150px;
|
|
89
|
+
margin-top: -18px;
|
|
90
|
+
margin-left: -75px;
|
|
91
|
+
line-height: 36px;
|
|
92
|
+
transform: ${corner.transform};`
|
|
93
|
+
: `
|
|
94
|
+
position: relative;
|
|
95
|
+
width: 100%;
|
|
96
|
+
line-height: 28px;`}
|
|
97
|
+
}
|
|
98
|
+
/* A ribbon without href never receives pointer events, so it stays at 0.8:
|
|
99
|
+
covering enough to be read in one go, transparent enough to guess what is
|
|
100
|
+
underneath */
|
|
101
|
+
#jsenv_ribbon_text:hover,
|
|
102
|
+
#jsenv_ribbon_text:focus-visible {
|
|
103
|
+
opacity: 1;
|
|
38
104
|
}
|
|
39
105
|
`;
|
|
106
|
+
const tagName = href ? "a" : "div";
|
|
107
|
+
const linkAttributes = href
|
|
108
|
+
? ` href="${escapeHtmlAttributeValue(href)}" target="${escapeHtmlAttributeValue(target)}" rel="noopener noreferrer" style="pointer-events: auto;"`
|
|
109
|
+
: "";
|
|
40
110
|
const html = /* html */ `<div id="jsenv_ribbon_container">
|
|
41
111
|
<style>
|
|
42
112
|
${css}
|
|
43
113
|
</style>
|
|
44
|
-
|
|
45
|
-
<div id="jsenv_ribbon_text">${text}</div>
|
|
46
|
-
</div>
|
|
114
|
+
<${tagName} id="jsenv_ribbon_text"${linkAttributes}>${text}</${tagName}>
|
|
47
115
|
</div>`;
|
|
48
116
|
class JsenvRibbonHtmlElement extends HTMLElement {
|
|
49
117
|
constructor({ hidden }) {
|
|
@@ -72,6 +140,34 @@ const injectRibbon = ({ text }) => {
|
|
|
72
140
|
);
|
|
73
141
|
};
|
|
74
142
|
|
|
143
|
+
// The browser is the only one knowing what "orange" or "oklch(...)" resolves to,
|
|
144
|
+
// so we let it resolve the color before deciding between dark and light text
|
|
145
|
+
const pickTextColorFor = (color) => {
|
|
146
|
+
const probeElement = document.createElement("div");
|
|
147
|
+
probeElement.style.color = color;
|
|
148
|
+
document.body.appendChild(probeElement);
|
|
149
|
+
const colorComputed = getComputedStyle(probeElement).color;
|
|
150
|
+
probeElement.remove();
|
|
151
|
+
const rgbMatch = /rgba?\(\s*([\d.]+)[,\s]+([\d.]+)[,\s]+([\d.]+)/.exec(
|
|
152
|
+
colorComputed,
|
|
153
|
+
);
|
|
154
|
+
if (!rgbMatch) {
|
|
155
|
+
return DARK_TEXT_COLOR;
|
|
156
|
+
}
|
|
157
|
+
const r = parseFloat(rgbMatch[1]);
|
|
158
|
+
const g = parseFloat(rgbMatch[2]);
|
|
159
|
+
const b = parseFloat(rgbMatch[3]);
|
|
160
|
+
const brightness = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
|
161
|
+
if (brightness > 0.55) {
|
|
162
|
+
return DARK_TEXT_COLOR;
|
|
163
|
+
}
|
|
164
|
+
return LIGHT_TEXT_COLOR;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const escapeHtmlAttributeValue = (value) => {
|
|
168
|
+
return String(value).replace(/&/g, "&").replace(/"/g, """);
|
|
169
|
+
};
|
|
170
|
+
|
|
75
171
|
const appendIntoRespectingLineBreaksAndIndentation = (
|
|
76
172
|
node,
|
|
77
173
|
parentNode,
|
|
@@ -3,11 +3,22 @@
|
|
|
3
3
|
*
|
|
4
4
|
* A missing dependency is not handled here: it makes the import fail, so the
|
|
5
5
|
* error overlay already says it, with more precision (which import, where).
|
|
6
|
+
*
|
|
7
|
+
* The overlay does not only describe the problem, it shows the dev server
|
|
8
|
+
* watching for the fix: the paths being looked at are named and kept alive on
|
|
9
|
+
* screen, so waiting for "npm install" feels like waiting for something that is
|
|
10
|
+
* actually going to happen.
|
|
6
11
|
*/
|
|
7
12
|
|
|
8
13
|
let removeOverlay = () => {};
|
|
14
|
+
let watchInfoFromServer = {};
|
|
15
|
+
// packages listed as outdated at some point, and the path watched for each;
|
|
16
|
+
// what leaves this map has been installed while the overlay was open, which is
|
|
17
|
+
// worth showing as such
|
|
18
|
+
const outdatedPathMap = new Map();
|
|
9
19
|
|
|
10
|
-
const initDependencyStatus = ({ problems }) => {
|
|
20
|
+
const initDependencyStatus = ({ problems, watchInfo }) => {
|
|
21
|
+
watchInfoFromServer = watchInfo || {};
|
|
11
22
|
render(problems);
|
|
12
23
|
// without the server events channel the page only knows the state it was
|
|
13
24
|
// served with, which is still better than nothing
|
|
@@ -24,9 +35,19 @@ const render = (problems) => {
|
|
|
24
35
|
const outdatedList = problems.filter(({ state }) => state === "outdated");
|
|
25
36
|
removeOverlay();
|
|
26
37
|
removeOverlay = () => {};
|
|
38
|
+
const installedList = [];
|
|
39
|
+
for (const [packageName, watchedPath] of outdatedPathMap) {
|
|
40
|
+
if (!outdatedList.some((problem) => problem.packageName === packageName)) {
|
|
41
|
+
installedList.push({ packageName, watchedPath });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
27
44
|
if (outdatedList.length === 0) {
|
|
45
|
+
outdatedPathMap.clear();
|
|
28
46
|
return;
|
|
29
47
|
}
|
|
48
|
+
for (const { packageName, watchedPath } of outdatedList) {
|
|
49
|
+
outdatedPathMap.set(packageName, watchedPath);
|
|
50
|
+
}
|
|
30
51
|
const supervisor = window.__supervisor__;
|
|
31
52
|
if (!supervisor || !supervisor.reportWarning) {
|
|
32
53
|
console.warn(summarize(outdatedList));
|
|
@@ -41,8 +62,8 @@ const render = (problems) => {
|
|
|
41
62
|
details: [
|
|
42
63
|
"The page is running with what is installed in node_modules, which is not what package.json asks for. It may work, but it is not the code the project expects.",
|
|
43
64
|
"Run npm install to fix it. If an install is already running, there is nothing to do but wait.",
|
|
44
|
-
"This page reloads on its own as soon as node_modules matches package.json.",
|
|
45
65
|
],
|
|
66
|
+
node: createWatchNode(outdatedList, installedList),
|
|
46
67
|
});
|
|
47
68
|
};
|
|
48
69
|
|
|
@@ -58,4 +79,214 @@ const summarize = (outdatedList) => {
|
|
|
58
79
|
.join(", ")}`;
|
|
59
80
|
};
|
|
60
81
|
|
|
82
|
+
const createWatchNode = (outdatedList, installedList) => {
|
|
83
|
+
const { packageJsonPath = "package.json", pollInterval } =
|
|
84
|
+
watchInfoFromServer;
|
|
85
|
+
const node = document.createElement("div");
|
|
86
|
+
node.className = "dependency_watch";
|
|
87
|
+
const rows = [];
|
|
88
|
+
rows.push(
|
|
89
|
+
createRow({
|
|
90
|
+
state: "watching",
|
|
91
|
+
path: packageJsonPath,
|
|
92
|
+
// a file watcher, not a poll: an edit here is what puts a dependency out
|
|
93
|
+
// of date in the first place
|
|
94
|
+
note: "on every change",
|
|
95
|
+
pollInterval,
|
|
96
|
+
}),
|
|
97
|
+
);
|
|
98
|
+
for (const { packageName, declaredVersion, watchedPath } of outdatedList) {
|
|
99
|
+
rows.push(
|
|
100
|
+
createRow({
|
|
101
|
+
state: "watching",
|
|
102
|
+
path: watchedPath || `node_modules/${packageName}/package.json`,
|
|
103
|
+
note: pollInterval
|
|
104
|
+
? `waiting for ${declaredVersion}, read every ${pollInterval}ms`
|
|
105
|
+
: `waiting for ${declaredVersion}`,
|
|
106
|
+
pollInterval,
|
|
107
|
+
}),
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
for (const { packageName, watchedPath } of installedList) {
|
|
111
|
+
rows.push(
|
|
112
|
+
createRow({
|
|
113
|
+
state: "done",
|
|
114
|
+
path: watchedPath || `node_modules/${packageName}/package.json`,
|
|
115
|
+
note: "installed",
|
|
116
|
+
}),
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
node.innerHTML = `
|
|
120
|
+
<style>
|
|
121
|
+
${watchCSS}
|
|
122
|
+
</style>
|
|
123
|
+
<div class="dependency_watch_head">
|
|
124
|
+
<span class="dependency_watch_live">
|
|
125
|
+
<span class="dependency_watch_live_dot"></span>watching
|
|
126
|
+
</span>
|
|
127
|
+
<span class="dependency_watch_head_text">
|
|
128
|
+
the files that will tell the install is over
|
|
129
|
+
</span>
|
|
130
|
+
</div>
|
|
131
|
+
<div class="dependency_watch_rows">
|
|
132
|
+
${rows.join("\n ")}
|
|
133
|
+
</div>
|
|
134
|
+
<div class="dependency_watch_effect" data-copy-line>
|
|
135
|
+
As soon as node_modules matches package.json, this page reloads by itself.
|
|
136
|
+
</div>`;
|
|
137
|
+
return node;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const createRow = ({ state, path, note, pollInterval }) => {
|
|
141
|
+
const sweepStyle = pollInterval
|
|
142
|
+
? ` style="animation-duration: ${pollInterval}ms"`
|
|
143
|
+
: "";
|
|
144
|
+
return `<div class="dependency_watch_row" data-state="${state}">
|
|
145
|
+
<span class="dependency_watch_row_icon"></span>
|
|
146
|
+
<code class="dependency_watch_row_path">${escapeHtml(path)}</code>
|
|
147
|
+
<span class="dependency_watch_row_note">${escapeHtml(note)}</span>
|
|
148
|
+
${
|
|
149
|
+
state === "watching"
|
|
150
|
+
? `<span class="dependency_watch_row_sweep"${sweepStyle}></span>`
|
|
151
|
+
: ""
|
|
152
|
+
}
|
|
153
|
+
</div>`;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const escapeHtml = (string) => {
|
|
157
|
+
return String(string)
|
|
158
|
+
.replace(/&/g, "&")
|
|
159
|
+
.replace(/</g, "<")
|
|
160
|
+
.replace(/>/g, ">")
|
|
161
|
+
.replace(/"/g, """)
|
|
162
|
+
.replace(/'/g, "'");
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const watchCSS = /* css */ `
|
|
166
|
+
.dependency_watch_head {
|
|
167
|
+
display: flex;
|
|
168
|
+
margin-bottom: 8px;
|
|
169
|
+
align-items: center;
|
|
170
|
+
gap: 8px;
|
|
171
|
+
}
|
|
172
|
+
.dependency_watch_live {
|
|
173
|
+
display: flex;
|
|
174
|
+
padding: 2px 8px;
|
|
175
|
+
align-items: center;
|
|
176
|
+
gap: 6px;
|
|
177
|
+
color: #ffab40;
|
|
178
|
+
font-size: 12px;
|
|
179
|
+
text-transform: uppercase;
|
|
180
|
+
letter-spacing: 0.08em;
|
|
181
|
+
border: 1px solid currentColor;
|
|
182
|
+
border-radius: 999px;
|
|
183
|
+
}
|
|
184
|
+
.dependency_watch_live_dot {
|
|
185
|
+
width: 7px;
|
|
186
|
+
height: 7px;
|
|
187
|
+
background: currentColor;
|
|
188
|
+
border-radius: 50%;
|
|
189
|
+
animation: dependency_watch_pulse 1.4s ease-in-out infinite;
|
|
190
|
+
}
|
|
191
|
+
@keyframes dependency_watch_pulse {
|
|
192
|
+
0%,
|
|
193
|
+
100% {
|
|
194
|
+
opacity: 0.25;
|
|
195
|
+
transform: scale(0.8);
|
|
196
|
+
}
|
|
197
|
+
50% {
|
|
198
|
+
opacity: 1;
|
|
199
|
+
transform: scale(1.15);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
.dependency_watch_head_text {
|
|
203
|
+
font-size: 13px;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.dependency_watch_rows {
|
|
207
|
+
display: flex;
|
|
208
|
+
flex-direction: column;
|
|
209
|
+
gap: 4px;
|
|
210
|
+
}
|
|
211
|
+
.dependency_watch_row {
|
|
212
|
+
position: relative;
|
|
213
|
+
display: flex;
|
|
214
|
+
padding: 6px 10px;
|
|
215
|
+
align-items: center;
|
|
216
|
+
gap: 8px;
|
|
217
|
+
font-size: 13px;
|
|
218
|
+
background: rgba(255, 255, 255, 0.04);
|
|
219
|
+
border: 1px solid #333;
|
|
220
|
+
border-radius: 4px;
|
|
221
|
+
overflow: hidden;
|
|
222
|
+
}
|
|
223
|
+
.dependency_watch_row_icon {
|
|
224
|
+
width: 8px;
|
|
225
|
+
height: 8px;
|
|
226
|
+
flex: none;
|
|
227
|
+
border: 1px solid #ffab40;
|
|
228
|
+
border-radius: 50%;
|
|
229
|
+
}
|
|
230
|
+
.dependency_watch_row[data-state="done"] .dependency_watch_row_icon {
|
|
231
|
+
background: #7fd67f;
|
|
232
|
+
border-color: #7fd67f;
|
|
233
|
+
}
|
|
234
|
+
.dependency_watch_row_path {
|
|
235
|
+
color: #eee;
|
|
236
|
+
text-overflow: ellipsis;
|
|
237
|
+
white-space: nowrap;
|
|
238
|
+
overflow: hidden;
|
|
239
|
+
}
|
|
240
|
+
.dependency_watch_row_note {
|
|
241
|
+
margin-left: auto;
|
|
242
|
+
padding-left: 8px;
|
|
243
|
+
font-size: 12px;
|
|
244
|
+
white-space: nowrap;
|
|
245
|
+
opacity: 0.8;
|
|
246
|
+
}
|
|
247
|
+
.dependency_watch_row[data-state="done"] .dependency_watch_row_note {
|
|
248
|
+
color: #7fd67f;
|
|
249
|
+
opacity: 1;
|
|
250
|
+
}
|
|
251
|
+
/* the sweep is the watching made visible: one pass per read */
|
|
252
|
+
.dependency_watch_row_sweep {
|
|
253
|
+
position: absolute;
|
|
254
|
+
top: 0;
|
|
255
|
+
left: 0;
|
|
256
|
+
width: 35%;
|
|
257
|
+
height: 100%;
|
|
258
|
+
background: linear-gradient(
|
|
259
|
+
90deg,
|
|
260
|
+
transparent,
|
|
261
|
+
rgba(255, 171, 64, 0.16),
|
|
262
|
+
transparent
|
|
263
|
+
);
|
|
264
|
+
animation: dependency_watch_sweep 2s linear infinite;
|
|
265
|
+
pointer-events: none;
|
|
266
|
+
}
|
|
267
|
+
@keyframes dependency_watch_sweep {
|
|
268
|
+
from {
|
|
269
|
+
transform: translateX(-100%);
|
|
270
|
+
}
|
|
271
|
+
to {
|
|
272
|
+
transform: translateX(340%);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.dependency_watch_effect {
|
|
277
|
+
margin-top: 8px;
|
|
278
|
+
color: #ffab40;
|
|
279
|
+
font-size: 13px;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
@media (prefers-reduced-motion: reduce) {
|
|
283
|
+
.dependency_watch_live_dot {
|
|
284
|
+
animation: none;
|
|
285
|
+
}
|
|
286
|
+
.dependency_watch_row_sweep {
|
|
287
|
+
display: none;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
`;
|
|
291
|
+
|
|
61
292
|
export { initDependencyStatus };
|