@jsenv/core 39.2.19 → 39.3.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/core",
3
- "version": "39.2.19",
3
+ "version": "39.3.1",
4
4
  "description": "Tool to develop, test and build js projects",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -69,7 +69,7 @@
69
69
  "@financial-times/polyfill-useragent-normaliser": "1.10.2",
70
70
  "@jsenv/abort": "4.3.0",
71
71
  "@jsenv/ast": "6.2.15",
72
- "@jsenv/filesystem": "4.9.10",
72
+ "@jsenv/filesystem": "4.10.0",
73
73
  "@jsenv/humanize": "1.2.8",
74
74
  "@jsenv/importmap": "1.2.1",
75
75
  "@jsenv/integrity": "0.0.2",
@@ -27,18 +27,19 @@ import { createLogger, createTaskLog } from "@jsenv/humanize";
27
27
  import { jsenvPluginBundling } from "@jsenv/plugin-bundling";
28
28
  import { jsenvPluginMinification } from "@jsenv/plugin-minification";
29
29
  import { jsenvPluginJsModuleFallback } from "@jsenv/plugin-transpilation";
30
-
30
+ import { urlIsInsideOf } from "@jsenv/urls";
31
31
  import { lookupPackageDirectory } from "../helpers/lookup_package_directory.js";
32
32
  import { watchSourceFiles } from "../helpers/watch_source_files.js";
33
+ import { jsenvCoreDirectoryUrl } from "../jsenv_core_directory_url.js";
33
34
  import { createKitchen } from "../kitchen/kitchen.js";
34
35
  import { createUrlGraphSummary } from "../kitchen/url_graph/url_graph_report.js";
35
36
  import { GRAPH_VISITOR } from "../kitchen/url_graph/url_graph_visitor.js";
37
+ import { jsenvPluginDirectoryReferenceEffect } from "../plugins/directory_reference_effect/jsenv_plugin_directory_reference_effect.js";
36
38
  import { jsenvPluginInlining } from "../plugins/inlining/jsenv_plugin_inlining.js";
37
39
  import { getCorePlugins } from "../plugins/plugins.js";
38
40
  import { jsenvPluginReferenceAnalysis } from "../plugins/reference_analysis/jsenv_plugin_reference_analysis.js";
39
- import { jsenvPluginLineBreakNormalization } from "./jsenv_plugin_line_break_normalization.js";
40
-
41
41
  import { createBuildSpecifierManager } from "./build_specifier_manager.js";
42
+ import { jsenvPluginLineBreakNormalization } from "./jsenv_plugin_line_break_normalization.js";
42
43
 
43
44
  // default runtimeCompat corresponds to
44
45
  // "we can keep <script type="module"> intact":
@@ -149,7 +150,10 @@ export const build = async ({
149
150
  "buildDirectoryUrl",
150
151
  );
151
152
  if (outDirectoryUrl === undefined) {
152
- if (process.env.CAPTURING_SIDE_EFFECTS) {
153
+ if (
154
+ process.env.CAPTURING_SIDE_EFFECTS ||
155
+ urlIsInsideOf(sourceDirectoryUrl, jsenvCoreDirectoryUrl)
156
+ ) {
153
157
  outDirectoryUrl = new URL("../.jsenv/", sourceDirectoryUrl);
154
158
  } else {
155
159
  const packageDirectoryUrl = lookupPackageDirectory(sourceDirectoryUrl);
@@ -363,6 +367,7 @@ build ${entryPointKeys.length} entry points`);
363
367
  fetchInlineUrls: false,
364
368
  // inlineContent: false,
365
369
  }),
370
+ jsenvPluginDirectoryReferenceEffect(directoryReferenceEffect),
366
371
  ...(lineBreakNormalization
367
372
  ? [jsenvPluginLineBreakNormalization()]
368
373
  : []),
@@ -16,6 +16,7 @@ import {
16
16
  ensurePathnameTrailingSlash,
17
17
  injectQueryParamIntoSpecifierWithoutEncoding,
18
18
  renderUrlOrRelativeUrlFilename,
19
+ urlIsInsideOf,
19
20
  urlToRelativeUrl,
20
21
  } from "@jsenv/urls";
21
22
  import { CONTENT_TYPE } from "@jsenv/utils/src/content_type/content_type.js";
@@ -203,7 +204,7 @@ export const createBuildSpecifierManager = ({
203
204
  if (!generatedUrl.startsWith("file:")) {
204
205
  return null;
205
206
  }
206
- if (reference.isWeak) {
207
+ if (reference.isWeak && reference.expectedType !== "directory") {
207
208
  return null;
208
209
  }
209
210
  if (reference.type === "sourcemap_comment") {
@@ -468,6 +469,9 @@ export const createBuildSpecifierManager = ({
468
469
  if (reference.type === "sourcemap_comment") {
469
470
  return false;
470
471
  }
472
+ if (reference.expectedType === "directory") {
473
+ return true;
474
+ }
471
475
  // specifier comes from "normalize" hook done a bit earlier in this file
472
476
  // we want to get back their build url to access their infos
473
477
  const referencedUrlInfo = reference.urlInfo;
@@ -480,6 +484,7 @@ export const createBuildSpecifierManager = ({
480
484
  const prepareVersioning = () => {
481
485
  const contentOnlyVersionMap = new Map();
482
486
  const urlInfoToContainedPlaceholderSetMap = new Map();
487
+ const directoryUrlInfoSet = new Set();
483
488
  generate_content_only_versions: {
484
489
  GRAPH_VISITOR.forEachUrlInfoStronglyReferenced(
485
490
  finalKitchen.graph.rootUrlInfo,
@@ -536,6 +541,9 @@ export const createBuildSpecifierManager = ({
536
541
  const contentVersion = generateVersion([content], versionLength);
537
542
  contentOnlyVersionMap.set(urlInfo, contentVersion);
538
543
  },
544
+ {
545
+ directoryUrlInfoSet,
546
+ },
539
547
  );
540
548
  }
541
549
 
@@ -582,9 +590,13 @@ export const createBuildSpecifierManager = ({
582
590
  }
583
591
  return setOfUrlInfluencingVersion;
584
592
  };
585
- for (const [urlInfo, contentOnlyVersion] of contentOnlyVersionMap) {
593
+
594
+ for (const [
595
+ contentOnlyUrlInfo,
596
+ contentOnlyVersion,
597
+ ] of contentOnlyVersionMap) {
586
598
  const setOfUrlInfoInfluencingVersion =
587
- getSetOfUrlInfoInfluencingVersion(urlInfo);
599
+ getSetOfUrlInfoInfluencingVersion(contentOnlyUrlInfo);
588
600
  const versionPartSet = new Set();
589
601
  versionPartSet.add(contentOnlyVersion);
590
602
  for (const urlInfoInfluencingVersion of setOfUrlInfoInfluencingVersion) {
@@ -593,13 +605,42 @@ export const createBuildSpecifierManager = ({
593
605
  );
594
606
  if (!otherUrlInfoContentVersion) {
595
607
  throw new Error(
596
- `cannot find content version for ${urlInfoInfluencingVersion.url} (used by ${urlInfo.url})`,
608
+ `cannot find content version for ${urlInfoInfluencingVersion.url} (used by ${contentOnlyUrlInfo.url})`,
597
609
  );
598
610
  }
599
611
  versionPartSet.add(otherUrlInfoContentVersion);
600
612
  }
601
613
  const version = generateVersion(versionPartSet, versionLength);
602
- versionMap.set(urlInfo, version);
614
+ versionMap.set(contentOnlyUrlInfo, version);
615
+ }
616
+ }
617
+
618
+ generate_directory_versions: {
619
+ // we should grab all the files inside this directory
620
+ // they will influence his versioning
621
+ for (const directoryUrlInfo of directoryUrlInfoSet) {
622
+ const directoryUrl = directoryUrlInfo.url;
623
+ // const urlInfoInsideThisDirectorySet = new Set();
624
+ const versionsInfluencingThisDirectorySet = new Set();
625
+ for (const [url, urlInfo] of finalKitchen.graph.urlInfoMap) {
626
+ if (!urlIsInsideOf(url, directoryUrl)) {
627
+ continue;
628
+ }
629
+ // ideally we should exclude eventual directories as the are redundant
630
+ // with the file they contains
631
+ const version = versionMap.get(urlInfo);
632
+ if (version !== undefined) {
633
+ versionsInfluencingThisDirectorySet.add(version);
634
+ }
635
+ }
636
+ const contentVersion =
637
+ versionsInfluencingThisDirectorySet.size === 0
638
+ ? "empty"
639
+ : generateVersion(
640
+ versionsInfluencingThisDirectorySet,
641
+ versionLength,
642
+ );
643
+ versionMap.set(directoryUrlInfo, contentVersion);
603
644
  }
604
645
  }
605
646
  };
@@ -613,6 +654,9 @@ export const createBuildSpecifierManager = ({
613
654
  return buildSpecifier;
614
655
  }
615
656
  const version = versionMap.get(reference.urlInfo);
657
+ if (version === undefined) {
658
+ return buildSpecifier;
659
+ }
616
660
  const buildSpecifierVersioned = injectVersionIntoBuildSpecifier({
617
661
  buildSpecifier,
618
662
  versioningMethod,
@@ -722,9 +766,9 @@ export const createBuildSpecifierManager = ({
722
766
  generateReplacement(urlInfo.firstReference);
723
767
  }
724
768
  if (urlInfo.firstReference.type === "side_effect_file") {
769
+ // side effect stuff must be generated too
725
770
  generateReplacement(urlInfo.firstReference);
726
771
  }
727
- // side effect stuff must be generated too
728
772
  if (mayUsePlaceholder(urlInfo)) {
729
773
  const contentBeforeReplace = urlInfo.content;
730
774
  const { content, sourcemap } = placeholderAPI.replaceAll(
@@ -33,9 +33,14 @@ ${ANSI.color(buildUrl, ANSI.MAGENTA)}
33
33
  if (buildUrlFromCache) {
34
34
  return buildUrlFromCache;
35
35
  }
36
- if (urlInfo.type === "directory") {
36
+ if (
37
+ urlInfo.type === "directory" ||
38
+ (urlInfo.type === undefined && urlInfo.typeHint === "directory")
39
+ ) {
37
40
  let directoryPath;
38
- if (urlInfo.filenameHint) {
41
+ if (url === sourceDirectoryUrl) {
42
+ directoryPath = "";
43
+ } else if (urlInfo.filenameHint) {
39
44
  directoryPath = urlInfo.filenameHint;
40
45
  } else {
41
46
  directoryPath = urlToRelativeUrl(url, sourceDirectoryUrl);
@@ -13,7 +13,7 @@ import {
13
13
  } from "@jsenv/server";
14
14
  import { convertFileSystemErrorToResponseProperties } from "@jsenv/server/src/internal/convertFileSystemErrorToResponseProperties.js";
15
15
  import { URL_META } from "@jsenv/url-meta";
16
- import { urlToRelativeUrl } from "@jsenv/urls";
16
+ import { urlIsInsideOf, urlToRelativeUrl } from "@jsenv/urls";
17
17
  import { existsSync, readFileSync } from "node:fs";
18
18
 
19
19
  import { defaultRuntimeCompat } from "../build/build.js";
@@ -21,6 +21,7 @@ import { createEventEmitter } from "../helpers/event_emitter.js";
21
21
  import { lookupPackageDirectory } from "../helpers/lookup_package_directory.js";
22
22
  import { watchSourceFiles } from "../helpers/watch_source_files.js";
23
23
  import { WEB_URL_CONVERTER } from "../helpers/web_url_converter.js";
24
+ import { jsenvCoreDirectoryUrl } from "../jsenv_core_directory_url.js";
24
25
  import { createKitchen } from "../kitchen/kitchen.js";
25
26
  import { getCorePlugins } from "../plugins/plugins.js";
26
27
  import { jsenvPluginServerEventsClientInjection } from "../plugins/server_events/jsenv_plugin_server_events_client_injection.js";
@@ -105,7 +106,10 @@ export const startDevServer = async ({
105
106
  sourceDirectoryUrl,
106
107
  );
107
108
  if (outDirectoryUrl === undefined) {
108
- if (process.env.CAPTURING_SIDE_EFFECTS) {
109
+ if (
110
+ process.env.CAPTURING_SIDE_EFFECTS ||
111
+ urlIsInsideOf(sourceDirectoryUrl, jsenvCoreDirectoryUrl)
112
+ ) {
109
113
  outDirectoryUrl = new URL("../.jsenv/", sourceDirectoryUrl);
110
114
  } else {
111
115
  const packageDirectoryUrl = lookupPackageDirectory(sourceDirectoryUrl);
@@ -0,0 +1 @@
1
+ export const jsenvCoreDirectoryUrl = new URL("../", import.meta.url);
@@ -728,6 +728,9 @@ const applyReferenceEffectsOnUrlInfo = (reference) => {
728
728
  if (reference.filenameHint && !referencedUrlInfo.filenameHint) {
729
729
  referencedUrlInfo.filenameHint = reference.filenameHint;
730
730
  }
731
+ if (reference.dirnameHint && !referencedUrlInfo.dirnameHint) {
732
+ referencedUrlInfo.dirnameHint = reference.dirnameHint;
733
+ }
731
734
  if (reference.debug) {
732
735
  referencedUrlInfo.debug = true;
733
736
  }
@@ -106,18 +106,28 @@ GRAPH_VISITOR.findDependency = (urlInfo, visitor) => {
106
106
  // because we start from root and ignore weak ref
107
107
  // The alternative would be to iterate on urlInfoMap
108
108
  // and call urlInfo.isUsed() but that would be more expensive
109
- GRAPH_VISITOR.forEachUrlInfoStronglyReferenced = (initialUrlInfo, callback) => {
109
+ GRAPH_VISITOR.forEachUrlInfoStronglyReferenced = (
110
+ initialUrlInfo,
111
+ callback,
112
+ { directoryUrlInfoSet } = {},
113
+ ) => {
110
114
  const seen = new Set();
111
115
  seen.add(initialUrlInfo);
112
116
  const iterateOnReferences = (urlInfo) => {
113
117
  for (const referenceToOther of urlInfo.referenceToOthersSet) {
114
- if (referenceToOther.isWeak) {
115
- continue;
116
- }
117
118
  if (referenceToOther.gotInlined()) {
118
119
  continue;
119
120
  }
120
121
  const referencedUrlInfo = referenceToOther.urlInfo;
122
+ if (
123
+ directoryUrlInfoSet &&
124
+ referenceToOther.expectedType === "directory"
125
+ ) {
126
+ directoryUrlInfoSet.add(referencedUrlInfo);
127
+ }
128
+ if (referenceToOther.isWeak) {
129
+ continue;
130
+ }
121
131
  if (seen.has(referencedUrlInfo)) {
122
132
  continue;
123
133
  }
@@ -0,0 +1,63 @@
1
+ import { urlToFilename } from "@jsenv/urls";
2
+
3
+ export const jsenvPluginDirectoryReferenceEffect = (
4
+ directoryReferenceEffect = "error",
5
+ ) => {
6
+ return {
7
+ name: "jsenv:directory_reference_effect",
8
+ appliesDuring: "*",
9
+ redirectReference: (reference) => {
10
+ // http, https, data, about, ...
11
+ if (!reference.url.startsWith("file:")) {
12
+ return null;
13
+ }
14
+ if (reference.isInline) {
15
+ return null;
16
+ }
17
+ if (reference.ownerUrlInfo.type === "directory") {
18
+ reference.dirnameHint = reference.ownerUrlInfo.filenameHint;
19
+ }
20
+ const { pathname } = new URL(reference.url);
21
+ if (pathname[pathname.length - 1] !== "/") {
22
+ return null;
23
+ }
24
+ reference.leadsToADirectory = true;
25
+ reference.expectedType = "directory";
26
+ if (reference.ownerUrlInfo.type === "directory") {
27
+ reference.dirnameHint = reference.ownerUrlInfo.filenameHint;
28
+ }
29
+ if (reference.type === "filesystem") {
30
+ reference.filenameHint = `${
31
+ reference.ownerUrlInfo.filenameHint
32
+ }${urlToFilename(reference.url)}/`;
33
+ } else {
34
+ reference.filenameHint = `${urlToFilename(reference.url)}/`;
35
+ }
36
+ let actionForDirectory;
37
+ if (reference.type === "a_href") {
38
+ actionForDirectory = "copy";
39
+ } else if (reference.type === "filesystem") {
40
+ actionForDirectory = "copy";
41
+ } else if (typeof directoryReferenceEffect === "string") {
42
+ actionForDirectory = directoryReferenceEffect;
43
+ } else if (typeof directoryReferenceEffect === "function") {
44
+ actionForDirectory = directoryReferenceEffect(reference);
45
+ } else {
46
+ actionForDirectory = "error";
47
+ }
48
+ reference.actionForDirectory = actionForDirectory;
49
+ if (actionForDirectory !== "copy") {
50
+ reference.isWeak = true;
51
+ }
52
+ if (actionForDirectory === "error") {
53
+ const error = new Error("Reference leads to a directory");
54
+ error.code = "DIRECTORY_REFERENCE_NOT_ALLOWED";
55
+ throw error;
56
+ }
57
+ if (actionForDirectory === "preserve") {
58
+ return `ignore:${reference.specifier}`;
59
+ }
60
+ return null;
61
+ },
62
+ };
63
+ };
@@ -181,13 +181,13 @@ export const createPluginController = (
181
181
  currentPlugin = hook.plugin;
182
182
  currentHookName = hook.name;
183
183
  let valueReturned = hookFn(info);
184
- currentPlugin = null;
185
- currentHookName = null;
186
184
  if (info.timing) {
187
185
  info.timing[`${hook.name}-${hook.plugin.name.replace("jsenv:", "")}`] =
188
186
  performance.now() - startTimestamp;
189
187
  }
190
188
  valueReturned = assertAndNormalizeReturnValue(hook, valueReturned, info);
189
+ currentPlugin = null;
190
+ currentHookName = null;
191
191
  return valueReturned;
192
192
  };
193
193
  const callAsyncHook = async (hook, info) => {
@@ -204,13 +204,13 @@ export const createPluginController = (
204
204
  currentPlugin = hook.plugin;
205
205
  currentHookName = hook.name;
206
206
  let valueReturned = await hookFn(info);
207
- currentPlugin = null;
208
- currentHookName = null;
209
207
  if (info.timing) {
210
208
  info.timing[`${hook.name}-${hook.plugin.name.replace("jsenv:", "")}`] =
211
209
  performance.now() - startTimestamp;
212
210
  }
213
211
  valueReturned = assertAndNormalizeReturnValue(hook, valueReturned, info);
212
+ currentPlugin = null;
213
+ currentHookName = null;
214
214
  return valueReturned;
215
215
  };
216
216
 
@@ -9,6 +9,7 @@ import { jsenvPluginWebResolution } from "./resolution_web/jsenv_plugin_web_reso
9
9
  import { jsenvPluginVersionSearchParam } from "./version_search_param/jsenv_plugin_version_search_param.js";
10
10
  import { jsenvPluginProtocolFile } from "./protocol_file/jsenv_plugin_protocol_file.js";
11
11
  import { jsenvPluginProtocolHttp } from "./protocol_http/jsenv_plugin_protocol_http.js";
12
+ import { jsenvPluginDirectoryReferenceEffect } from "./directory_reference_effect/jsenv_plugin_directory_reference_effect.js";
12
13
  import { jsenvPluginInjections } from "./injections/jsenv_plugin_injections.js";
13
14
  import { jsenvPluginInlining } from "./inlining/jsenv_plugin_inlining.js";
14
15
  import { jsenvPluginCommonJsGlobals } from "./commonjs_globals/jsenv_plugin_commonjs_globals.js";
@@ -66,7 +67,6 @@ export const getCorePlugins = ({
66
67
  - All the rest uses web standard url resolution
67
68
  */
68
69
  jsenvPluginProtocolFile({
69
- directoryReferenceEffect,
70
70
  magicExtensions,
71
71
  magicDirectoryIndex,
72
72
  }),
@@ -75,6 +75,7 @@ export const getCorePlugins = ({
75
75
  ? [jsenvPluginNodeEsmResolution(nodeEsmResolution)]
76
76
  : []),
77
77
  jsenvPluginWebResolution(),
78
+ jsenvPluginDirectoryReferenceEffect(directoryReferenceEffect),
78
79
 
79
80
  jsenvPluginVersionSearchParam(),
80
81
  jsenvPluginCommonJsGlobals(),
@@ -0,0 +1,108 @@
1
+ import {
2
+ applyFileSystemMagicResolution,
3
+ getExtensionsToTry,
4
+ } from "@jsenv/node-esm-resolution";
5
+ import { realpathSync, statSync } from "node:fs";
6
+ import { pathToFileURL } from "node:url";
7
+
8
+ export const jsenvPluginFsRedirection = ({
9
+ magicExtensions = ["inherit", ".js"],
10
+ magicDirectoryIndex = true,
11
+ preserveSymlinks = false,
12
+ }) => {
13
+ return {
14
+ name: "jsenv:fs_redirection",
15
+ appliesDuring: "*",
16
+ redirectReference: (reference) => {
17
+ // http, https, data, about, ...
18
+ if (!reference.url.startsWith("file:")) {
19
+ return null;
20
+ }
21
+ if (reference.isInline) {
22
+ return null;
23
+ }
24
+ if (reference.url === "file:///" || reference.url === "file://") {
25
+ reference.leadsToADirectory = true;
26
+ return `ignore:file:///`;
27
+ }
28
+ // ignore all new URL second arg
29
+ if (reference.subtype === "new_url_second_arg") {
30
+ return `ignore:${reference.url}`;
31
+ }
32
+ // ignore "./" on new URL("./")
33
+ // if (
34
+ // reference.subtype === "new_url_first_arg" &&
35
+ // reference.specifier === "./"
36
+ // ) {
37
+ // return `ignore:${reference.url}`;
38
+ // }
39
+ const urlObject = new URL(reference.url);
40
+ let stat;
41
+ try {
42
+ stat = statSync(urlObject);
43
+ } catch (e) {
44
+ if (e.code === "ENOENT") {
45
+ stat = null;
46
+ } else {
47
+ throw e;
48
+ }
49
+ }
50
+ const { search, hash } = urlObject;
51
+ urlObject.search = "";
52
+ urlObject.hash = "";
53
+ applyStatEffectsOnUrlObject(urlObject, stat);
54
+ const shouldApplyFilesystemMagicResolution =
55
+ reference.type === "js_import";
56
+ if (shouldApplyFilesystemMagicResolution) {
57
+ const filesystemResolution = applyFileSystemMagicResolution(
58
+ urlObject.href,
59
+ {
60
+ fileStat: stat,
61
+ magicDirectoryIndex,
62
+ magicExtensions: getExtensionsToTry(
63
+ magicExtensions,
64
+ reference.ownerUrlInfo.url,
65
+ ),
66
+ },
67
+ );
68
+ if (filesystemResolution.stat) {
69
+ stat = filesystemResolution.stat;
70
+ urlObject.href = filesystemResolution.url;
71
+ applyStatEffectsOnUrlObject(urlObject, stat);
72
+ }
73
+ }
74
+ if (!stat) {
75
+ return null;
76
+ }
77
+ const urlRaw = preserveSymlinks
78
+ ? urlObject.href
79
+ : resolveSymlink(urlObject.href);
80
+ const resolvedUrl = `${urlRaw}${search}${hash}`;
81
+ return resolvedUrl;
82
+ },
83
+ };
84
+ };
85
+
86
+ const applyStatEffectsOnUrlObject = (urlObject, stat) => {
87
+ const { pathname } = urlObject;
88
+ const pathnameUsesTrailingSlash = pathname.endsWith("/");
89
+ // force trailing slash on directories
90
+ if (stat && stat.isDirectory() && !pathnameUsesTrailingSlash) {
91
+ urlObject.pathname = `${pathname}/`;
92
+ }
93
+ // otherwise remove trailing slash if any
94
+ if (stat && !stat.isDirectory() && pathnameUsesTrailingSlash) {
95
+ // a warning here? (because it's strange to reference a file with a trailing slash)
96
+ urlObject.pathname = pathname.slice(0, -1);
97
+ }
98
+ };
99
+
100
+ const resolveSymlink = (fileUrl) => {
101
+ const urlObject = new URL(fileUrl);
102
+ const realpath = realpathSync(urlObject);
103
+ const realUrlObject = pathToFileURL(realpath);
104
+ if (urlObject.pathname.endsWith("/")) {
105
+ realUrlObject.pathname += `/`;
106
+ }
107
+ return realUrlObject.href;
108
+ };