appstage 0.2.11 → 0.2.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/bin.js CHANGED
@@ -460,7 +460,7 @@ async function buildClient({ clientDir, watch, watchClient }, plugins) {
460
460
  // src/scripts/utils/buildServer.ts
461
461
  import esbuild2 from "esbuild";
462
462
 
463
- // src/scripts/utils/populateEntries.ts
463
+ // src/scripts/utils/setEntriesExport.ts
464
464
  import { writeFile } from "node:fs/promises";
465
465
 
466
466
  // src/scripts/utils/toImportPath.ts
@@ -474,25 +474,24 @@ function toImportPath(relativePath, referencePath = ".") {
474
474
  return importPath;
475
475
  }
476
476
 
477
- // src/scripts/utils/populateEntries.ts
478
- async function populateEntries({ entriesPath }) {
477
+ // src/scripts/utils/setEntriesExport.ts
478
+ async function setEntriesExport({ entriesPath }) {
479
479
  if (entriesPath === null) return;
480
480
  let serverEntries = await getEntryPoints(["server", "server/index"]);
481
481
  let content = "";
482
482
  if (serverEntries.length === 0) content = "export const entries = [];";
483
483
  else {
484
484
  content = "export const entries = (\n await Promise.all([";
485
- for (let i = 0; i < serverEntries.length; i++) {
485
+ for (let i = 0; i < serverEntries.length; i++)
486
486
  content += `
487
- // ${serverEntries[i].name}
488
487
  import("${toImportPath(serverEntries[i].path, "src/server")}"),`;
489
- }
490
488
  content += "\n ])\n).map(({ server }) => server);";
491
489
  }
492
490
  await writeFile(
493
491
  entriesPath ?? "src/server/entries.ts",
494
492
  `// Populated automatically during the build phase by picking
495
- // all server exports from "src/entries/<entry_name>/server(/index)?.(js|ts)"
493
+ // all server exports from "src/entries/<entry_name>/server(/index)?.(js|ts)".
494
+ // Ignore this file if a custom set of entry exports is required.
496
495
  ${content}
497
496
  `
498
497
  );
@@ -502,7 +501,7 @@ ${content}
502
501
  var appServerEntryPoints = ["src/server/index.ts"];
503
502
  async function buildServer(params, plugins) {
504
503
  let { serverDir, watch, watchServer } = params;
505
- await populateEntries(params);
504
+ await setEntriesExport(params);
506
505
  let buildOptions = {
507
506
  ...commonBuildOptions,
508
507
  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]}`);
@@ -410,19 +429,19 @@ function toImportPath(relativePath, referencePath = ".") {
410
429
  return importPath;
411
430
  }
412
431
 
413
- async function populateEntries({ entriesPath }) {
432
+ async function setEntriesExport({ entriesPath }) {
414
433
  if (entriesPath === null) return;
415
434
  let serverEntries = await getEntryPoints(["server", "server/index"]);
416
435
  let content = "";
417
436
  if (serverEntries.length === 0) content = "export const entries = [];";
418
437
  else {
419
438
  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")}"),`;
439
+ for (let i = 0; i < serverEntries.length; i++) content += `\n import("${toImportPath(serverEntries[i].path, "src/server")}"),`;
422
440
  content += "\n ])\n).map(({ server }) => server);";
423
441
  }
424
442
  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)"
443
+ // all server exports from "src/entries/<entry_name>/server(/index)?.(js|ts)".
444
+ // Ignore this file if a custom set of entry exports is required.
426
445
  ${content}
427
446
  `);
428
447
  }
@@ -430,7 +449,7 @@ ${content}
430
449
  const appServerEntryPoints = ["src/server/index.ts"];
431
450
  async function buildServer(params, plugins) {
432
451
  let { serverDir, watch, watchServer } = params;
433
- await populateEntries(params);
452
+ await setEntriesExport(params);
434
453
  let buildOptions = {
435
454
  ...commonBuildOptions,
436
455
  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]}`);
@@ -384,19 +403,19 @@ function toImportPath(relativePath, referencePath = ".") {
384
403
  return importPath;
385
404
  }
386
405
 
387
- async function populateEntries({ entriesPath }) {
406
+ async function setEntriesExport({ entriesPath }) {
388
407
  if (entriesPath === null) return;
389
408
  let serverEntries = await getEntryPoints(["server", "server/index"]);
390
409
  let content = "";
391
410
  if (serverEntries.length === 0) content = "export const entries = [];";
392
411
  else {
393
412
  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")}"),`;
413
+ for (let i = 0; i < serverEntries.length; i++) content += `\n import("${toImportPath(serverEntries[i].path, "src/server")}"),`;
396
414
  content += "\n ])\n).map(({ server }) => server);";
397
415
  }
398
416
  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)"
417
+ // all server exports from "src/entries/<entry_name>/server(/index)?.(js|ts)".
418
+ // Ignore this file if a custom set of entry exports is required.
400
419
  ${content}
401
420
  `);
402
421
  }
@@ -404,7 +423,7 @@ ${content}
404
423
  const appServerEntryPoints = ["src/server/index.ts"];
405
424
  async function buildServer(params, plugins) {
406
425
  let { serverDir, watch, watchServer } = params;
407
- await populateEntries(params);
426
+ await setEntriesExport(params);
408
427
  let buildOptions = {
409
428
  ...commonBuildOptions,
410
429
  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.12",
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++) {
@@ -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
  );