appstage 0.2.11 → 0.2.13

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/bin.js CHANGED
@@ -434,16 +434,17 @@ async function getEntryPoints(path) {
434
434
 
435
435
  // src/scripts/utils/buildClient.ts
436
436
  var entryClientPaths = ["ui/index", "client/index", "index", "src/index"];
437
- async function buildClient({ clientDir, watch, watchClient }, plugins) {
437
+ async function buildClient({ watch, watchClient }, plugins) {
438
438
  let clientEntries = await getEntryPoints(entryClientPaths);
439
439
  let buildOptions = {
440
440
  ...commonBuildOptions,
441
- entryPoints: clientEntries.map(({ path }) => path),
441
+ entryPoints: clientEntries.map(({ path, name }) => ({
442
+ in: path,
443
+ out: `src/entries/${name}/dist/index`
444
+ })),
442
445
  bundle: true,
443
446
  splitting: true,
444
447
  format: "esm",
445
- outdir: clientDir,
446
- outbase: "src/entries",
447
448
  minify: process.env.NODE_ENV !== "development",
448
449
  plugins
449
450
  };
@@ -460,7 +461,7 @@ async function buildClient({ clientDir, watch, watchClient }, plugins) {
460
461
  // src/scripts/utils/buildServer.ts
461
462
  import esbuild2 from "esbuild";
462
463
 
463
- // src/scripts/utils/populateEntries.ts
464
+ // src/scripts/utils/setEntriesExport.ts
464
465
  import { writeFile } from "node:fs/promises";
465
466
 
466
467
  // src/scripts/utils/toImportPath.ts
@@ -474,25 +475,24 @@ function toImportPath(relativePath, referencePath = ".") {
474
475
  return importPath;
475
476
  }
476
477
 
477
- // src/scripts/utils/populateEntries.ts
478
- async function populateEntries({ entriesPath }) {
478
+ // src/scripts/utils/setEntriesExport.ts
479
+ async function setEntriesExport({ entriesPath }) {
479
480
  if (entriesPath === null) return;
480
481
  let serverEntries = await getEntryPoints(["server", "server/index"]);
481
482
  let content = "";
482
483
  if (serverEntries.length === 0) content = "export const entries = [];";
483
484
  else {
484
485
  content = "export const entries = (\n await Promise.all([";
485
- for (let i = 0; i < serverEntries.length; i++) {
486
+ for (let i = 0; i < serverEntries.length; i++)
486
487
  content += `
487
- // ${serverEntries[i].name}
488
488
  import("${toImportPath(serverEntries[i].path, "src/server")}"),`;
489
- }
490
489
  content += "\n ])\n).map(({ server }) => server);";
491
490
  }
492
491
  await writeFile(
493
492
  entriesPath ?? "src/server/entries.ts",
494
493
  `// Populated automatically during the build phase by picking
495
- // all server exports from "src/entries/<entry_name>/server(/index)?.(js|ts)"
494
+ // all server exports from "src/entries/<entry_name>/server(/index)?.(js|ts)".
495
+ // Ignore this file if a custom set of entry exports is required.
496
496
  ${content}
497
497
  `
498
498
  );
@@ -502,7 +502,7 @@ ${content}
502
502
  var appServerEntryPoints = ["src/server/index.ts"];
503
503
  async function buildServer(params, plugins) {
504
504
  let { serverDir, watch, watchServer } = params;
505
- await populateEntries(params);
505
+ await setEntriesExport(params);
506
506
  let buildOptions = {
507
507
  ...commonBuildOptions,
508
508
  entryPoints: appServerEntryPoints,
package/dist/index.cjs CHANGED
@@ -68,6 +68,15 @@ function getLanguageList(req) {
68
68
  }
69
69
  return Array.from(langs);
70
70
  }
71
+ function matches(x, matcher) {
72
+ if (matcher === null || matcher === void 0) return true;
73
+ if (typeof matcher === "function") return matcher(x);
74
+ let patterns = Array.isArray(matcher) ? matcher : [matcher];
75
+ for (let pattern of patterns) if (pattern instanceof RegExp) {
76
+ if (pattern.test(x)) return true;
77
+ } else if (pattern === x) return true;
78
+ return false;
79
+ }
71
80
  const defaultExtensions = ["html", "htm"];
72
81
  const defaultPath = (req) => req.originalUrl.split("?")[0];
73
82
  const defaultLanguages = getLanguageList;
@@ -82,6 +91,14 @@ const files = (params) => {
82
91
  return async (req, res) => {
83
92
  let langs = (p.languages ?? defaultLanguages)(req);
84
93
  let path = typeof p.path === "string" ? p.path : (p.path ?? defaultPath)(req);
94
+ if (!matches(path, p.matches)) {
95
+ emitLog(req.app, "Unmatched path", { data: { path } });
96
+ res.status(404).send(await req.app.renderStatus?.(req, res, {
97
+ code: "unmatched_path",
98
+ path
99
+ }));
100
+ return;
101
+ }
85
102
  if (path.includes("../")) {
86
103
  emitLog(req.app, "Invalid path (potential traversal attempt)", { data: { path } });
87
104
  res.status(400).send(await req.app.renderStatus?.(req, res, {
@@ -93,10 +110,12 @@ const files = (params) => {
93
110
  let filePath = null;
94
111
  for (let k = 0; k < bases.length && filePath === null; k++) {
95
112
  let base = bases[k];
96
- for (let i = 0; i < langs.length && filePath === null; i++) filePath = await resolve(base, `${path}.${langs[i]}`);
97
- if (filePath === null) filePath = await resolve(base, path);
98
- for (let i = 0; i < langs.length && filePath === null; i++) for (let j = 0; j < exts.length && filePath === null; j++) filePath = await resolve(base, `${path}.${langs[i]}.${exts[j]}`);
99
- for (let i = 0; i < exts.length && filePath === null; i++) filePath = await resolve(base, `${path}.${exts[i]}`);
113
+ if (!path.endsWith("/")) {
114
+ for (let i = 0; i < langs.length && filePath === null; i++) filePath = await resolve(base, `${path}.${langs[i]}`);
115
+ if (filePath === null) filePath = await resolve(base, path);
116
+ for (let i = 0; i < langs.length && filePath === null; i++) for (let j = 0; j < exts.length && filePath === null; j++) filePath = await resolve(base, `${path}.${langs[i]}.${exts[j]}`);
117
+ for (let i = 0; i < exts.length && filePath === null; i++) filePath = await resolve(base, `${path}.${exts[i]}`);
118
+ }
100
119
  for (let i = 0; i < langs.length && filePath === null; i++) for (let j = 0; j < exts.length && filePath === null; j++) filePath = await resolve(base, `${path}.${langs[i]}`, `index.${exts[j]}`);
101
120
  for (let i = 0; i < langs.length && filePath === null; i++) for (let j = 0; j < exts.length && filePath === null; j++) filePath = await resolve(base, path, `index.${langs[i]}.${exts[j]}`);
102
121
  for (let i = 0; i < exts.length && filePath === null; i++) filePath = await resolve(base, path, `index.${exts[i]}`);
@@ -380,16 +399,17 @@ const entryClientPaths = [
380
399
  /**
381
400
  * Builds the client-side code.
382
401
  */
383
- async function buildClient({ clientDir, watch, watchClient }, plugins) {
402
+ async function buildClient({ watch, watchClient }, plugins) {
384
403
  let clientEntries = await getEntryPoints(entryClientPaths);
385
404
  let buildOptions = {
386
405
  ...commonBuildOptions,
387
- entryPoints: clientEntries.map(({ path }) => path),
406
+ entryPoints: clientEntries.map(({ path, name }) => ({
407
+ in: path,
408
+ out: `src/entries/${name}/dist/index`
409
+ })),
388
410
  bundle: true,
389
411
  splitting: true,
390
412
  format: "esm",
391
- outdir: clientDir,
392
- outbase: "src/entries",
393
413
  minify: process.env.NODE_ENV !== "development",
394
414
  plugins
395
415
  };
@@ -410,19 +430,19 @@ function toImportPath(relativePath, referencePath = ".") {
410
430
  return importPath;
411
431
  }
412
432
 
413
- async function populateEntries({ entriesPath }) {
433
+ async function setEntriesExport({ entriesPath }) {
414
434
  if (entriesPath === null) return;
415
435
  let serverEntries = await getEntryPoints(["server", "server/index"]);
416
436
  let content = "";
417
437
  if (serverEntries.length === 0) content = "export const entries = [];";
418
438
  else {
419
439
  content = "export const entries = (\n await Promise.all([";
420
- for (let i = 0; i < serverEntries.length; i++) content += `\n // ${serverEntries[i].name}
421
- import("${toImportPath(serverEntries[i].path, "src/server")}"),`;
440
+ for (let i = 0; i < serverEntries.length; i++) content += `\n import("${toImportPath(serverEntries[i].path, "src/server")}"),`;
422
441
  content += "\n ])\n).map(({ server }) => server);";
423
442
  }
424
443
  await (0, node_fs_promises.writeFile)(entriesPath ?? "src/server/entries.ts", `// Populated automatically during the build phase by picking
425
- // all server exports from "src/entries/<entry_name>/server(/index)?.(js|ts)"
444
+ // all server exports from "src/entries/<entry_name>/server(/index)?.(js|ts)".
445
+ // Ignore this file if a custom set of entry exports is required.
426
446
  ${content}
427
447
  `);
428
448
  }
@@ -430,7 +450,7 @@ ${content}
430
450
  const appServerEntryPoints = ["src/server/index.ts"];
431
451
  async function buildServer(params, plugins) {
432
452
  let { serverDir, watch, watchServer } = params;
433
- await populateEntries(params);
453
+ await setEntriesExport(params);
434
454
  let buildOptions = {
435
455
  ...commonBuildOptions,
436
456
  entryPoints: appServerEntryPoints,
package/dist/index.d.ts CHANGED
@@ -16,9 +16,11 @@ type TransformContent = (req: Request, res: Response, params: {
16
16
  name?: string;
17
17
  }) => string | Promise<string>;
18
18
 
19
+ type StringMatcher = string | RegExp | (string | RegExp)[] | ((x: string) => boolean) | null;
19
20
  type FilesParams = {
20
21
  base: string | string[];
21
- path?: string | ((req: Request) => string);
22
+ path?: string | ((req: Request) => string); /** Specifies which paths should be accepted. */
23
+ matches?: StringMatcher;
22
24
  extensions?: string[];
23
25
  languages?: (req: Request) => string[];
24
26
  transform?: TransformContent[];
package/dist/index.mjs CHANGED
@@ -42,6 +42,15 @@ function getLanguageList(req) {
42
42
  }
43
43
  return Array.from(langs);
44
44
  }
45
+ function matches(x, matcher) {
46
+ if (matcher === null || matcher === void 0) return true;
47
+ if (typeof matcher === "function") return matcher(x);
48
+ let patterns = Array.isArray(matcher) ? matcher : [matcher];
49
+ for (let pattern of patterns) if (pattern instanceof RegExp) {
50
+ if (pattern.test(x)) return true;
51
+ } else if (pattern === x) return true;
52
+ return false;
53
+ }
45
54
  const defaultExtensions = ["html", "htm"];
46
55
  const defaultPath = (req) => req.originalUrl.split("?")[0];
47
56
  const defaultLanguages = getLanguageList;
@@ -56,6 +65,14 @@ const files = (params) => {
56
65
  return async (req, res) => {
57
66
  let langs = (p.languages ?? defaultLanguages)(req);
58
67
  let path = typeof p.path === "string" ? p.path : (p.path ?? defaultPath)(req);
68
+ if (!matches(path, p.matches)) {
69
+ emitLog(req.app, "Unmatched path", { data: { path } });
70
+ res.status(404).send(await req.app.renderStatus?.(req, res, {
71
+ code: "unmatched_path",
72
+ path
73
+ }));
74
+ return;
75
+ }
59
76
  if (path.includes("../")) {
60
77
  emitLog(req.app, "Invalid path (potential traversal attempt)", { data: { path } });
61
78
  res.status(400).send(await req.app.renderStatus?.(req, res, {
@@ -67,10 +84,12 @@ const files = (params) => {
67
84
  let filePath = null;
68
85
  for (let k = 0; k < bases.length && filePath === null; k++) {
69
86
  let base = bases[k];
70
- for (let i = 0; i < langs.length && filePath === null; i++) filePath = await resolve(base, `${path}.${langs[i]}`);
71
- if (filePath === null) filePath = await resolve(base, path);
72
- for (let i = 0; i < langs.length && filePath === null; i++) for (let j = 0; j < exts.length && filePath === null; j++) filePath = await resolve(base, `${path}.${langs[i]}.${exts[j]}`);
73
- for (let i = 0; i < exts.length && filePath === null; i++) filePath = await resolve(base, `${path}.${exts[i]}`);
87
+ if (!path.endsWith("/")) {
88
+ for (let i = 0; i < langs.length && filePath === null; i++) filePath = await resolve(base, `${path}.${langs[i]}`);
89
+ if (filePath === null) filePath = await resolve(base, path);
90
+ for (let i = 0; i < langs.length && filePath === null; i++) for (let j = 0; j < exts.length && filePath === null; j++) filePath = await resolve(base, `${path}.${langs[i]}.${exts[j]}`);
91
+ for (let i = 0; i < exts.length && filePath === null; i++) filePath = await resolve(base, `${path}.${exts[i]}`);
92
+ }
74
93
  for (let i = 0; i < langs.length && filePath === null; i++) for (let j = 0; j < exts.length && filePath === null; j++) filePath = await resolve(base, `${path}.${langs[i]}`, `index.${exts[j]}`);
75
94
  for (let i = 0; i < langs.length && filePath === null; i++) for (let j = 0; j < exts.length && filePath === null; j++) filePath = await resolve(base, path, `index.${langs[i]}.${exts[j]}`);
76
95
  for (let i = 0; i < exts.length && filePath === null; i++) filePath = await resolve(base, path, `index.${exts[i]}`);
@@ -354,16 +373,17 @@ const entryClientPaths = [
354
373
  /**
355
374
  * Builds the client-side code.
356
375
  */
357
- async function buildClient({ clientDir, watch, watchClient }, plugins) {
376
+ async function buildClient({ watch, watchClient }, plugins) {
358
377
  let clientEntries = await getEntryPoints(entryClientPaths);
359
378
  let buildOptions = {
360
379
  ...commonBuildOptions,
361
- entryPoints: clientEntries.map(({ path }) => path),
380
+ entryPoints: clientEntries.map(({ path, name }) => ({
381
+ in: path,
382
+ out: `src/entries/${name}/dist/index`
383
+ })),
362
384
  bundle: true,
363
385
  splitting: true,
364
386
  format: "esm",
365
- outdir: clientDir,
366
- outbase: "src/entries",
367
387
  minify: process.env.NODE_ENV !== "development",
368
388
  plugins
369
389
  };
@@ -384,19 +404,19 @@ function toImportPath(relativePath, referencePath = ".") {
384
404
  return importPath;
385
405
  }
386
406
 
387
- async function populateEntries({ entriesPath }) {
407
+ async function setEntriesExport({ entriesPath }) {
388
408
  if (entriesPath === null) return;
389
409
  let serverEntries = await getEntryPoints(["server", "server/index"]);
390
410
  let content = "";
391
411
  if (serverEntries.length === 0) content = "export const entries = [];";
392
412
  else {
393
413
  content = "export const entries = (\n await Promise.all([";
394
- for (let i = 0; i < serverEntries.length; i++) content += `\n // ${serverEntries[i].name}
395
- import("${toImportPath(serverEntries[i].path, "src/server")}"),`;
414
+ for (let i = 0; i < serverEntries.length; i++) content += `\n import("${toImportPath(serverEntries[i].path, "src/server")}"),`;
396
415
  content += "\n ])\n).map(({ server }) => server);";
397
416
  }
398
417
  await writeFile(entriesPath ?? "src/server/entries.ts", `// Populated automatically during the build phase by picking
399
- // all server exports from "src/entries/<entry_name>/server(/index)?.(js|ts)"
418
+ // all server exports from "src/entries/<entry_name>/server(/index)?.(js|ts)".
419
+ // Ignore this file if a custom set of entry exports is required.
400
420
  ${content}
401
421
  `);
402
422
  }
@@ -404,7 +424,7 @@ ${content}
404
424
  const appServerEntryPoints = ["src/server/index.ts"];
405
425
  async function buildServer(params, plugins) {
406
426
  let { serverDir, watch, watchServer } = params;
407
- await populateEntries(params);
427
+ await setEntriesExport(params);
408
428
  let buildOptions = {
409
429
  ...commonBuildOptions,
410
430
  entryPoints: appServerEntryPoints,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "appstage",
3
- "version": "0.2.11",
3
+ "version": "0.2.13",
4
4
  "description": "",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.mjs",
@@ -5,6 +5,13 @@ import type { Controller } from "../types/Controller.ts";
5
5
  import type { TransformContent } from "../types/TransformContent.ts";
6
6
  import { emitLog } from "../utils/emitLog.ts";
7
7
 
8
+ type StringMatcher =
9
+ | string
10
+ | RegExp
11
+ | (string | RegExp)[]
12
+ | ((x: string) => boolean)
13
+ | null;
14
+
8
15
  const maxLanguages = 3;
9
16
 
10
17
  async function resolve(...parts: string[]) {
@@ -38,9 +45,27 @@ function getLanguageList(req: Request) {
38
45
  return Array.from(langs);
39
46
  }
40
47
 
48
+ function matches(x: string, matcher: StringMatcher | undefined) {
49
+ if (matcher === null || matcher === undefined) return true;
50
+
51
+ if (typeof matcher === "function") return matcher(x);
52
+
53
+ let patterns = Array.isArray(matcher) ? matcher : [matcher];
54
+
55
+ for (let pattern of patterns) {
56
+ if (pattern instanceof RegExp) {
57
+ if (pattern.test(x)) return true;
58
+ } else if (pattern === x) return true;
59
+ }
60
+
61
+ return false;
62
+ }
63
+
41
64
  export type FilesParams = {
42
65
  base: string | string[];
43
66
  path?: string | ((req: Request) => string);
67
+ /** Specifies which paths should be accepted. */
68
+ matches?: StringMatcher;
44
69
  extensions?: string[];
45
70
  languages?: (req: Request) => string[];
46
71
  transform?: TransformContent[];
@@ -66,6 +91,19 @@ export const files: Controller<string | FilesParams> = (params) => {
66
91
  let path =
67
92
  typeof p.path === "string" ? p.path : (p.path ?? defaultPath)(req);
68
93
 
94
+ if (!matches(path, p.matches)) {
95
+ emitLog(req.app, "Unmatched path", { data: { path } });
96
+
97
+ res.status(404).send(
98
+ await req.app.renderStatus?.(req, res, {
99
+ code: "unmatched_path",
100
+ path,
101
+ }),
102
+ );
103
+
104
+ return;
105
+ }
106
+
69
107
  if (path.includes("../")) {
70
108
  emitLog(req.app, "Invalid path (potential traversal attempt)", {
71
109
  data: { path },
@@ -88,22 +126,24 @@ export const files: Controller<string | FilesParams> = (params) => {
88
126
  for (let k = 0; k < bases.length && filePath === null; k++) {
89
127
  let base = bases[k];
90
128
 
91
- // /x.en /x.ru
92
- for (let i = 0; i < langs.length && filePath === null; i++)
93
- filePath = await resolve(base, `${path}.${langs[i]}`);
129
+ if (!path.endsWith("/")) {
130
+ // /x.en /x.ru
131
+ for (let i = 0; i < langs.length && filePath === null; i++)
132
+ filePath = await resolve(base, `${path}.${langs[i]}`);
94
133
 
95
- // /x
96
- if (filePath === null) filePath = await resolve(base, path);
134
+ // /x
135
+ if (filePath === null) filePath = await resolve(base, path);
97
136
 
98
- // /x.en.html /x.en.htm /x.ru.html /x.ru.htm
99
- for (let i = 0; i < langs.length && filePath === null; i++) {
100
- for (let j = 0; j < exts.length && filePath === null; j++)
101
- filePath = await resolve(base, `${path}.${langs[i]}.${exts[j]}`);
102
- }
137
+ // /x.en.html /x.en.htm /x.ru.html /x.ru.htm
138
+ for (let i = 0; i < langs.length && filePath === null; i++) {
139
+ for (let j = 0; j < exts.length && filePath === null; j++)
140
+ filePath = await resolve(base, `${path}.${langs[i]}.${exts[j]}`);
141
+ }
103
142
 
104
- // /x.html /x.htm
105
- for (let i = 0; i < exts.length && filePath === null; i++)
106
- filePath = await resolve(base, `${path}.${exts[i]}`);
143
+ // /x.html /x.htm
144
+ for (let i = 0; i < exts.length && filePath === null; i++)
145
+ filePath = await resolve(base, `${path}.${exts[i]}`);
146
+ }
107
147
 
108
148
  // /x.en/index.html /x.en/index.htm /x.ru/index.html /x.ru/index.htm
109
149
  for (let i = 0; i < langs.length && filePath === null; i++) {
@@ -9,19 +9,20 @@ const entryClientPaths = ["ui/index", "client/index", "index", "src/index"];
9
9
  * Builds the client-side code.
10
10
  */
11
11
  export async function buildClient(
12
- { clientDir, watch, watchClient }: BuildParams,
12
+ { watch, watchClient }: BuildParams,
13
13
  plugins?: Plugin[],
14
14
  ) {
15
15
  let clientEntries = await getEntryPoints(entryClientPaths);
16
16
 
17
17
  let buildOptions: BuildOptions = {
18
18
  ...commonBuildOptions,
19
- entryPoints: clientEntries.map(({ path }) => path),
19
+ entryPoints: clientEntries.map(({ path, name }) => ({
20
+ in: path,
21
+ out: `src/entries/${name}/dist/index`,
22
+ })),
20
23
  bundle: true,
21
24
  splitting: true,
22
25
  format: "esm",
23
- outdir: clientDir,
24
- outbase: "src/entries",
25
26
  minify: process.env.NODE_ENV !== "development",
26
27
  plugins,
27
28
  };
@@ -1,14 +1,14 @@
1
1
  import esbuild, { type BuildOptions, type Plugin } from "esbuild";
2
2
  import { commonBuildOptions } from "../const/commonBuildOptions.ts";
3
3
  import type { BuildParams } from "../types/BuildParams.ts";
4
- import { populateEntries } from "./populateEntries.ts";
4
+ import { setEntriesExport } from "./setEntriesExport.ts";
5
5
 
6
6
  const appServerEntryPoints = ["src/server/index.ts"];
7
7
 
8
8
  export async function buildServer(params: BuildParams, plugins?: Plugin[]) {
9
9
  let { serverDir, watch, watchServer } = params;
10
10
 
11
- await populateEntries(params);
11
+ await setEntriesExport(params);
12
12
 
13
13
  let buildOptions: BuildOptions = {
14
14
  ...commonBuildOptions,
@@ -3,7 +3,7 @@ import type { BuildParams } from "../types/BuildParams.ts";
3
3
  import { getEntryPoints } from "./getEntryPoints.ts";
4
4
  import { toImportPath } from "./toImportPath.ts";
5
5
 
6
- export async function populateEntries({ entriesPath }: BuildParams) {
6
+ export async function setEntriesExport({ entriesPath }: BuildParams) {
7
7
  if (entriesPath === null) return;
8
8
 
9
9
  let serverEntries = await getEntryPoints(["server", "server/index"]);
@@ -13,10 +13,8 @@ export async function populateEntries({ entriesPath }: BuildParams) {
13
13
  else {
14
14
  content = "export const entries = (\n await Promise.all([";
15
15
 
16
- for (let i = 0; i < serverEntries.length; i++) {
17
- content += `\n // ${serverEntries[i].name}
18
- import("${toImportPath(serverEntries[i].path, "src/server")}"),`;
19
- }
16
+ for (let i = 0; i < serverEntries.length; i++)
17
+ content += `\n import("${toImportPath(serverEntries[i].path, "src/server")}"),`;
20
18
 
21
19
  content += "\n ])\n).map(({ server }) => server);";
22
20
  }
@@ -24,7 +22,8 @@ export async function populateEntries({ entriesPath }: BuildParams) {
24
22
  await writeFile(
25
23
  entriesPath ?? "src/server/entries.ts",
26
24
  `// Populated automatically during the build phase by picking
27
- // all server exports from "src/entries/<entry_name>/server(/index)?.(js|ts)"
25
+ // all server exports from "src/entries/<entry_name>/server(/index)?.(js|ts)".
26
+ // Ignore this file if a custom set of entry exports is required.
28
27
  ${content}
29
28
  `,
30
29
  );