@jsenv/core 40.12.13 → 41.0.0

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.
@@ -1,20 +1,49 @@
1
1
  export const installImportMetaCssBuild = (importMeta) => {
2
- const stylesheet = new CSSStyleSheet({ baseUrl: importMeta.url });
2
+ const IMPORT_META_CSS_BUILD = "jsenv_import_meta_css_build";
3
3
 
4
- let called = false;
5
- // eslint-disable-next-line accessor-pairs
4
+ if (importMeta.css === IMPORT_META_CSS_BUILD) {
5
+ return;
6
+ }
7
+
8
+ const stylesheetMap = new Map();
9
+ const adopt = (url, value) => {
10
+ const stylesheet = new CSSStyleSheet({ baseUrl: importMeta.url });
11
+ stylesheet.replaceSync(value);
12
+ stylesheetMap.set(url, stylesheet);
13
+ document.adoptedStyleSheets = [...document.adoptedStyleSheets, stylesheet];
14
+ };
15
+ const update = (url, value) => {
16
+ stylesheetMap.get(url).replaceSync(value);
17
+ };
18
+ const remove = (url) => {
19
+ const stylesheet = stylesheetMap.get(url);
20
+ document.adoptedStyleSheets = document.adoptedStyleSheets.filter(
21
+ (s) => s !== stylesheet,
22
+ );
23
+ stylesheetMap.delete(url);
24
+ };
25
+
26
+ const currentCssSourceMap = new Map();
6
27
  Object.defineProperty(importMeta, "css", {
7
28
  configurable: true,
8
- set(value) {
9
- if (called) {
10
- throw new Error("import.meta.css setter can only be called once");
29
+ get() {
30
+ return IMPORT_META_CSS_BUILD;
31
+ },
32
+ set([value, url]) {
33
+ if (value === undefined) {
34
+ if (stylesheetMap.has(url)) {
35
+ remove(url);
36
+ currentCssSourceMap.delete(url);
37
+ }
38
+ return;
39
+ }
40
+ if (!stylesheetMap.has(url)) {
41
+ adopt(url, value);
42
+ currentCssSourceMap.set(url, value);
43
+ } else if (currentCssSourceMap.get(url) !== value) {
44
+ update(url, value);
45
+ currentCssSourceMap.set(url, value);
11
46
  }
12
- called = true;
13
- stylesheet.replaceSync(value);
14
- document.adoptedStyleSheets = [
15
- ...document.adoptedStyleSheets,
16
- stylesheet,
17
- ];
18
47
  },
19
48
  });
20
49
  };
@@ -1,46 +1,58 @@
1
1
  export const installImportMetaCssDev = (importMeta) => {
2
- let cssText = "";
3
- let stylesheet = new CSSStyleSheet({ baseUrl: importMeta.url });
4
- let adopted = false;
2
+ const IMPORT_META_CSS_DEV = "jsenv_import_meta_css_dev";
5
3
 
6
- const css = {
7
- toString: () => cssText,
8
- update: (value) => {
9
- cssText = value;
10
- cssText += `
4
+ // useless today but browser might catch up to display it in devtools
5
+ const addUrlInfo = (cssText) => {
6
+ let cssTextWithUrlInfo = cssText;
7
+ cssTextWithUrlInfo += `
11
8
  /* sourceURL=${importMeta.url} */
12
9
  /* inlined from ${importMeta.url} */`;
13
- stylesheet.replaceSync(cssText);
14
- },
15
- inject: () => {
16
- if (!adopted) {
17
- document.adoptedStyleSheets = [
18
- ...document.adoptedStyleSheets,
19
- stylesheet,
20
- ];
21
- adopted = true;
22
- }
23
- },
24
- remove: () => {
25
- if (adopted) {
26
- document.adoptedStyleSheets = document.adoptedStyleSheets.filter(
27
- (s) => s !== stylesheet,
28
- );
29
- adopted = false;
30
- }
31
- },
10
+ return cssTextWithUrlInfo;
11
+ };
12
+
13
+ let stylesheet;
14
+ const adopt = (value) => {
15
+ stylesheet = new CSSStyleSheet({ baseUrl: importMeta.url });
16
+ stylesheet.replaceSync(addUrlInfo(value));
17
+ document.adoptedStyleSheets = [...document.adoptedStyleSheets, stylesheet];
18
+ };
19
+ const update = (value) => {
20
+ stylesheet.replaceSync(addUrlInfo(value));
21
+ };
22
+ const remove = () => {
23
+ document.adoptedStyleSheets = document.adoptedStyleSheets.filter(
24
+ (s) => s !== stylesheet,
25
+ );
32
26
  };
33
27
 
28
+ let currentCssSource;
34
29
  Object.defineProperty(importMeta, "css", {
35
30
  configurable: true,
36
31
  get() {
37
- return css;
32
+ return IMPORT_META_CSS_DEV;
38
33
  },
39
34
  set(value) {
40
- css.update(value);
41
- css.inject();
35
+ if (value === undefined) {
36
+ if (stylesheet) {
37
+ remove();
38
+ stylesheet = undefined;
39
+ currentCssSource = undefined;
40
+ }
41
+ return;
42
+ }
43
+ if (!stylesheet) {
44
+ adopt(value);
45
+ currentCssSource = value;
46
+ } else if (currentCssSource !== value) {
47
+ update(value);
48
+ currentCssSource = value;
49
+ }
42
50
  },
43
51
  });
44
52
 
45
- return css.remove;
53
+ return () => {
54
+ remove();
55
+ stylesheet = undefined;
56
+ currentCssSource = undefined;
57
+ };
46
58
  };
@@ -36,11 +36,7 @@ export const jsenvPluginImportMetaCss = () => {
36
36
  appliesDuring: "*",
37
37
  transformUrlContent: {
38
38
  js_module: async (urlInfo) => {
39
- if (
40
- !urlInfo.content.includes("import.meta.css") ||
41
- // there is already our installImportMetaCssBuild in the file
42
- urlInfo.content.includes("installImportMetaCssBuild")
43
- ) {
39
+ if (!urlInfo.content.includes("import.meta.css")) {
44
40
  return null;
45
41
  }
46
42
  const { metadata } = await applyBabelPlugins({
@@ -54,22 +50,78 @@ export const jsenvPluginImportMetaCss = () => {
54
50
  if (!usesImportMetaCss) {
55
51
  return null;
56
52
  }
53
+ if (urlInfo.context.build) {
54
+ const rootDirectoryUrl = urlInfo.context.rootDirectoryUrl;
55
+ const relativeUrl = urlInfo.originalUrl.slice(
56
+ rootDirectoryUrl.length - 1,
57
+ );
58
+ const { code } = await applyBabelPlugins({
59
+ babelPlugins: [
60
+ [babelPluginRewriteImportMetaCssAssignment, { relativeUrl }],
61
+ ],
62
+ input: urlInfo.content,
63
+ inputIsJsModule: true,
64
+ inputUrl: urlInfo.originalUrl,
65
+ outputUrl: urlInfo.generatedUrl,
66
+ });
67
+ if (code === urlInfo.content) {
68
+ // all assignments were already in array form (pre-built file) — nothing to do
69
+ return null;
70
+ }
71
+ return injectImportMetaCss(urlInfo, {
72
+ content: code,
73
+ importFrom: importMetaCssBuildClientFileUrl,
74
+ importName: "installImportMetaCssBuild",
75
+ importAs: "__installImportMetaCssBuild__",
76
+ });
77
+ }
57
78
  return injectImportMetaCss(urlInfo, {
58
- importFrom: urlInfo.context.build
59
- ? importMetaCssBuildClientFileUrl
60
- : importMetaCssDevClientFileUrl,
61
- importName: urlInfo.context.build
62
- ? "installImportMetaCssBuild"
63
- : "installImportMetaCssDev",
64
- importAs: urlInfo.context.build
65
- ? "__installImportMetaCssBuild__"
66
- : "__installImportMetaCssDev__",
79
+ content: urlInfo.content,
80
+ importFrom: importMetaCssDevClientFileUrl,
81
+ importName: "installImportMetaCssDev",
82
+ importAs: "__installImportMetaCssDev__",
83
+ hot: true,
67
84
  });
68
85
  },
69
86
  },
70
87
  };
71
88
  };
72
89
 
90
+ const babelPluginRewriteImportMetaCssAssignment = (
91
+ { types: t },
92
+ { relativeUrl },
93
+ ) => {
94
+ return {
95
+ name: "rewrite-import-meta-css-assignment",
96
+ visitor: {
97
+ AssignmentExpression(path) {
98
+ const { left, right } = path.node;
99
+ if (left.type !== "MemberExpression") {
100
+ return;
101
+ }
102
+ const { object, property } = left;
103
+ if (object.type !== "MetaProperty") {
104
+ return;
105
+ }
106
+ if (object.meta.name !== "import" || object.property.name !== "meta") {
107
+ return;
108
+ }
109
+ if (property.name !== "css") {
110
+ return;
111
+ }
112
+ // already transformed (e.g. pre-built file): leave as-is
113
+ if (right.type === "ArrayExpression") {
114
+ return;
115
+ }
116
+ path.node.right = t.arrayExpression([
117
+ right,
118
+ t.stringLiteral(relativeUrl),
119
+ ]);
120
+ },
121
+ },
122
+ };
123
+ };
124
+
73
125
  const babelPluginMetadataUsesImportMetaCss = () => {
74
126
  return {
75
127
  name: "metadata-uses-import-meta-css",
@@ -101,7 +153,10 @@ const babelPluginMetadataUsesImportMetaCss = () => {
101
153
  };
102
154
  };
103
155
 
104
- const injectImportMetaCss = (urlInfo, { importFrom, importName, importAs }) => {
156
+ const injectImportMetaCss = (
157
+ urlInfo,
158
+ { content, importFrom, importName, importAs, hot },
159
+ ) => {
105
160
  const importMetaCssClientFileReference = urlInfo.dependencies.inject({
106
161
  parentUrl: urlInfo.url,
107
162
  type: "js_import",
@@ -117,7 +172,9 @@ const injectImportMetaCss = (urlInfo, { importFrom, importName, importAs }) => {
117
172
  importBeforeFrom = `{ ${importName} } }`;
118
173
  importVariableName = importName;
119
174
  }
120
- let prelude = `import ${importBeforeFrom} from ${importMetaCssClientFileReference.generatedSpecifier};
175
+
176
+ const prelude = hot
177
+ ? `import ${importBeforeFrom} from ${importMetaCssClientFileReference.generatedSpecifier};
121
178
 
122
179
  const remove = ${importVariableName}(import.meta);
123
180
  if (import.meta.hot) {
@@ -126,9 +183,13 @@ if (import.meta.hot) {
126
183
  });
127
184
  }
128
185
 
186
+ `
187
+ : `import ${importBeforeFrom} from ${importMetaCssClientFileReference.generatedSpecifier};
188
+
189
+ ${importVariableName}(import.meta);
190
+
129
191
  `;
130
192
 
131
- let content = urlInfo.content;
132
193
  return {
133
194
  content: `${prelude.replace(/\n/g, "")}${content}`,
134
195
  };
@@ -0,0 +1,197 @@
1
+ import { createPluginsController } from "@jsenv/server/src/plugins_controller.js";
2
+
3
+ import { jsenvPluginHtmlSyntaxErrorFallback } from "./html_syntax_error_fallback/jsenv_plugin_html_syntax_error_fallback.js";
4
+
5
+ export const createJsenvPluginStore = async (plugins) => {
6
+ const allServerRoutes = [];
7
+ const allServerPlugins = [];
8
+ const pluginArray = [];
9
+
10
+ const pluginPromises = [];
11
+ const addPlugin = async (plugin) => {
12
+ if (plugin && typeof plugin.then === "function") {
13
+ pluginPromises.push(plugin);
14
+ const value = await plugin;
15
+ addPlugin(value);
16
+ return;
17
+ }
18
+ if (Array.isArray(plugin)) {
19
+ for (const subplugin of plugin) {
20
+ addPlugin(subplugin);
21
+ }
22
+ return;
23
+ }
24
+ if (plugin === null || typeof plugin !== "object") {
25
+ throw new TypeError(`plugin must be objects, got ${plugin}`);
26
+ }
27
+ if (!plugin.name) {
28
+ plugin.name = "anonymous";
29
+ }
30
+ const { serverRoutes } = plugin;
31
+ if (serverRoutes) {
32
+ for (const serverRoute of serverRoutes) {
33
+ allServerRoutes.push(serverRoute);
34
+ }
35
+ }
36
+ const { serverPlugins } = plugin;
37
+ if (serverPlugins) {
38
+ const serverPlugins = plugin.serverPlugins;
39
+ for (const serverPlugin of serverPlugins) {
40
+ allServerPlugins.push(serverPlugin);
41
+ }
42
+ }
43
+ pluginArray.push(plugin);
44
+ };
45
+ addPlugin(jsenvPluginHtmlSyntaxErrorFallback());
46
+ for (const plugin of plugins) {
47
+ addPlugin(plugin);
48
+ }
49
+ await Promise.all(pluginPromises);
50
+
51
+ return {
52
+ pluginArray,
53
+ allServerRoutes,
54
+ allServerPlugins,
55
+ };
56
+ };
57
+
58
+ export const createJsenvPluginsController = async (
59
+ pluginStore,
60
+ kitchen,
61
+ { meta } = {},
62
+ ) => {
63
+ kitchen.context.getPluginMeta = (id) => pluginsController.getPluginMeta(id);
64
+ const pluginsController = await createPluginsController({
65
+ plugins: pluginStore.pluginArray,
66
+ pluginDescription: JSENV_PLUGIN_DESCRIPTION,
67
+ filterPlugin: (plugin) => testAppliesDuring(plugin, kitchen),
68
+ getInitPluginArgs: (plugin) => [kitchen.context, { plugin }],
69
+ getEffectArgs: ({ otherPlugins }) => [
70
+ { kitchenContext: kitchen.context, otherPlugins },
71
+ ],
72
+ meta,
73
+ });
74
+ return pluginsController;
75
+ };
76
+
77
+ const hook = { type: "hook" };
78
+ const nonHook = {};
79
+
80
+ const assertUrlReturnValue = (valueReturned, urlInfo, { hook }) => {
81
+ if (valueReturned instanceof URL) {
82
+ return valueReturned.href;
83
+ }
84
+ if (typeof valueReturned === "string") {
85
+ return valueReturned;
86
+ }
87
+ throw new Error(
88
+ `Unexpected value returned by hook "${hook.plugin.name}.${hook.name}()": it must be a string; got ${valueReturned}`,
89
+ );
90
+ };
91
+ const assertContentReturnValue = (valueReturned, urlInfo, { hook }) => {
92
+ if (typeof valueReturned === "string" || Buffer.isBuffer(valueReturned)) {
93
+ return { content: valueReturned };
94
+ }
95
+ if (typeof valueReturned === "object") {
96
+ const { content, body } = valueReturned;
97
+ if (urlInfo.url.startsWith("ignore:")) {
98
+ return valueReturned;
99
+ }
100
+ if (typeof content !== "string" && !Buffer.isBuffer(content) && !body) {
101
+ if (Object.hasOwn(valueReturned, "contentInjections")) {
102
+ return valueReturned;
103
+ }
104
+ throw new Error(
105
+ `Unexpected "content" returned by hook "${hook.plugin.name}.${hook.name}()": it must be a string or a buffer; got ${content}`,
106
+ );
107
+ }
108
+ return valueReturned;
109
+ }
110
+ throw new Error(
111
+ `Unexpected value returned by hook "${hook.plugin.name}.${hook.name}()": it must be a string, a buffer or an object; got ${valueReturned}`,
112
+ );
113
+ };
114
+
115
+ const JSENV_PLUGIN_DESCRIPTION = {
116
+ name: "jsenv plugin",
117
+ properties: {
118
+ // non-hook properties (silently skipped)
119
+ appliesDuring: nonHook,
120
+ serverEvents: nonHook,
121
+ mustStayFirst: nonHook,
122
+ serverRoutes: nonHook,
123
+ serverPlugins: nonHook,
124
+ // hooks
125
+ init: hook,
126
+ resolveReference: {
127
+ type: "hook",
128
+ assertAndNormalize: assertUrlReturnValue,
129
+ },
130
+ redirectReference: {
131
+ type: "hook",
132
+ assertAndNormalize: assertUrlReturnValue,
133
+ },
134
+ transformReferenceSearchParams: hook,
135
+ formatReference: hook,
136
+ urlInfoCreated: hook,
137
+ fetchUrlContent: {
138
+ type: "hook",
139
+ assertAndNormalize: assertContentReturnValue,
140
+ },
141
+ transformUrlContent: {
142
+ type: "hook",
143
+ assertAndNormalize: assertContentReturnValue,
144
+ },
145
+ finalizeUrlContent: {
146
+ type: "hook",
147
+ assertAndNormalize: assertContentReturnValue,
148
+ },
149
+ bundle: hook,
150
+ optimizeBuildUrlContent: {
151
+ type: "hook",
152
+ assertAndNormalize: assertContentReturnValue,
153
+ },
154
+ cooked: hook,
155
+ augmentResponse: hook,
156
+ destroy: hook,
157
+ effect: hook,
158
+ refineBuildUrlContent: hook,
159
+ refineBuild: hook,
160
+ // serverRoutes and serverPlugins are nonHook above
161
+ },
162
+ };
163
+
164
+ const testAppliesDuring = (plugin, kitchen) => {
165
+ const { appliesDuring } = plugin;
166
+ if (appliesDuring === undefined) {
167
+ return true;
168
+ }
169
+ if (appliesDuring === "*") {
170
+ return true;
171
+ }
172
+ if (typeof appliesDuring === "string") {
173
+ if (appliesDuring !== "dev" && appliesDuring !== "build") {
174
+ throw new TypeError(
175
+ `"appliesDuring" must be "dev" or "build", got ${appliesDuring}`,
176
+ );
177
+ }
178
+ if (kitchen.context[appliesDuring]) {
179
+ return true;
180
+ }
181
+ return false;
182
+ }
183
+ if (typeof appliesDuring === "object") {
184
+ for (const key of Object.keys(appliesDuring)) {
185
+ if (!appliesDuring[key] && kitchen.context[key]) {
186
+ return false;
187
+ }
188
+ if (appliesDuring[key] && kitchen.context[key]) {
189
+ return true;
190
+ }
191
+ }
192
+ return false;
193
+ }
194
+ throw new TypeError(
195
+ `"appliesDuring" must be an object or a string, got ${appliesDuring}`,
196
+ );
197
+ };
@@ -142,7 +142,7 @@ export const jsenvPluginDirectoryListing = ({
142
142
  };
143
143
  },
144
144
  },
145
- devServerRoutes: [
145
+ serverRoutes: [
146
146
  {
147
147
  endpoint:
148
148
  "GET /.internal/directory_content.websocket?directory=:directory",
@@ -90,7 +90,7 @@ export const jsenvPluginServerEvents = ({ clientAutoreload }) => {
90
90
  return stringifyHtmlAst(htmlAst);
91
91
  },
92
92
  },
93
- devServerRoutes: [
93
+ serverRoutes: [
94
94
  {
95
95
  endpoint: "GET /.internal/events.websocket",
96
96
  description: `Jsenv dev server emit server events on this endpoint. When a file is saved the "reload" event is sent here.`,