@primate/core 0.8.8 → 0.9.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.
Files changed (65) hide show
  1. package/lib/private/App.d.ts +3 -3
  2. package/lib/private/App.js +10 -3
  3. package/lib/private/build/App.d.ts +15 -6
  4. package/lib/private/build/App.js +61 -26
  5. package/lib/private/build/Loader.d.ts +17 -0
  6. package/lib/private/build/Loader.js +2 -0
  7. package/lib/private/build/client/index.js +4 -2
  8. package/lib/private/build/client/plugin/app-request.js +2 -2
  9. package/lib/private/build/client/plugin/frontend.js +2 -1
  10. package/lib/private/build/client/plugin/route-module.js +4 -4
  11. package/lib/private/build/client/plugin/view.js +4 -0
  12. package/lib/private/build/hook.js +1 -1
  13. package/lib/private/build/server/index.js +3 -3
  14. package/lib/private/build/server/plugin/app-request.js +2 -2
  15. package/lib/private/build/server/plugin/frontend.js +6 -6
  16. package/lib/private/build/server/plugin/route.js +4 -5
  17. package/lib/private/build/server/plugin/view.js +14 -7
  18. package/lib/private/build/server/plugin/views.js +44 -0
  19. package/lib/private/build/server/plugin/virtual-routes.js +4 -1
  20. package/lib/private/build/server/plugin/virtual-templates.d.ts +4 -0
  21. package/lib/private/build/server/plugin/{virtual-pages.js → virtual-templates.js} +14 -14
  22. package/lib/private/build/shared/plugin/app-request.js +2 -2
  23. package/lib/private/client/ViewOptions.d.ts +1 -1
  24. package/lib/private/client/navigate.d.ts +2 -1
  25. package/lib/private/config/schema.d.ts +2 -2
  26. package/lib/private/config/schema.js +3 -2
  27. package/lib/private/db/test.js +47 -0
  28. package/lib/private/errors.d.ts +9 -0
  29. package/lib/private/errors.js +12 -0
  30. package/lib/private/events.d.ts +11 -0
  31. package/lib/private/events.js +24 -0
  32. package/lib/private/frontend.js +19 -8
  33. package/lib/private/location.d.ts +1 -1
  34. package/lib/private/location.js +2 -2
  35. package/lib/private/request/RequestBody.d.ts +2 -2
  36. package/lib/private/request/RequestBody.js +5 -5
  37. package/lib/private/request/route.js +6 -3
  38. package/lib/private/request/router.d.ts +1 -1
  39. package/lib/private/request/router.js +15 -4
  40. package/lib/private/response/ResponseFunction.d.ts +4 -1
  41. package/lib/private/response/ResponseLike.d.ts +1 -1
  42. package/lib/private/response/error.d.ts +2 -2
  43. package/lib/private/response/error.js +2 -2
  44. package/lib/private/response/page.d.ts +6 -0
  45. package/lib/private/response/page.js +12 -0
  46. package/lib/private/response/sse.d.ts +4 -5
  47. package/lib/private/response/sse.js +17 -14
  48. package/lib/private/response/view.d.ts +4 -4
  49. package/lib/private/route/NarrowedRequest.d.ts +7 -5
  50. package/lib/private/route/Options.d.ts +5 -3
  51. package/lib/private/route/router.js +2 -1
  52. package/lib/private/route.client.d.ts +11 -6
  53. package/lib/private/route.client.js +10 -2
  54. package/lib/private/route.d.ts +17 -6
  55. package/lib/private/serve/App.d.ts +11 -3
  56. package/lib/private/serve/App.js +28 -19
  57. package/lib/private/serve/Init.d.ts +2 -1
  58. package/lib/public/events.d.ts +2 -0
  59. package/lib/public/events.js +2 -0
  60. package/lib/public/response.d.ts +5 -6
  61. package/lib/public/response.js +2 -0
  62. package/package.json +20 -20
  63. package/lib/private/Binder.d.ts +0 -11
  64. package/lib/private/Binder.js +0 -2
  65. package/lib/private/build/server/plugin/virtual-pages.d.ts +0 -4
@@ -19,7 +19,7 @@ export default class App {
19
19
  error(error: unknown): void;
20
20
  };
21
21
  init(): Promise<this>;
22
- register(created: Created): void;
22
+ module(created: Created): void;
23
23
  build_hooks(app: BuildApp): Promise<BuildApp>;
24
24
  serve_hooks(app: ServeApp): Promise<ServeApp>;
25
25
  get handle_hooks(): import("./index.js").HandleHook[];
@@ -29,7 +29,7 @@ export default class App {
29
29
  client: "client";
30
30
  config: "config";
31
31
  error_html: "error.html";
32
- pages: "pages";
32
+ templates: "templates";
33
33
  routes: "routes";
34
34
  static: "static";
35
35
  stores: "stores";
@@ -43,7 +43,7 @@ export default class App {
43
43
  readonly client: FileRef;
44
44
  readonly config: FileRef;
45
45
  readonly error_html: FileRef;
46
- readonly pages: FileRef;
46
+ readonly templates: FileRef;
47
47
  readonly routes: FileRef;
48
48
  readonly static: FileRef;
49
49
  readonly stores: FileRef;
@@ -4,6 +4,7 @@ import logger from "#logger";
4
4
  import create from "#module/create";
5
5
  import TargetManager from "#target/Manager";
6
6
  import dict from "@rcompat/dict";
7
+ import fs from "@rcompat/fs";
7
8
  import is from "@rcompat/is";
8
9
  import p from "pema";
9
10
  export default class App {
@@ -31,11 +32,17 @@ export default class App {
31
32
  this.#root = root;
32
33
  this.#config = config;
33
34
  for (const module of config.modules)
34
- this.register(create(module));
35
+ this.module(create(module));
35
36
  this.#path = dict.map({
36
37
  ...location,
37
- build: flags.outdir,
38
+ build: "build",
38
39
  }, (_, path) => root.join(path));
40
+ if (flags.outdir.startsWith("/")) {
41
+ this.#path.build = fs.ref(flags.outdir);
42
+ }
43
+ else {
44
+ this.#path.build = root.join(flags.outdir);
45
+ }
39
46
  this.#mode = flags.mode;
40
47
  this.#target = new TargetManager(this);
41
48
  this.#target_name = flags.target;
@@ -50,7 +57,7 @@ export default class App {
50
57
  this.#target.set(this.#target_name);
51
58
  return this;
52
59
  }
53
- register(created) {
60
+ module(created) {
54
61
  if (this.#module_names.has(created.name)) {
55
62
  throw E.app_duplicate_module(created.name);
56
63
  }
@@ -1,27 +1,36 @@
1
1
  import App from "#App";
2
- import type Binder from "#Binder";
2
+ import type { Loader } from "#build/Loader";
3
3
  import type ServeApp from "#serve/App";
4
4
  import type { FileRef } from "@rcompat/fs";
5
5
  import type { Dict } from "@rcompat/type";
6
6
  import type { Plugin } from "esbuild";
7
7
  type PluginType = "server" | "client";
8
+ type ExtensionQuery = "backend" | "frontend" | "bundler";
8
9
  export default class BuildApp extends App {
9
10
  #private;
10
11
  buildInit(): Promise<void>;
11
- get extensions(): string[];
12
12
  get id(): string;
13
13
  get paths(): Dict<string[]>;
14
- get frontendExtensions(): string[];
15
- get frontends(): Map<string, string[]>;
16
14
  get conditions(): Set<string>;
15
+ backends(): Map<string, {
16
+ type: "backend";
17
+ extensions: string[];
18
+ onLoad: import("#build/Loader").OnLoad;
19
+ }>;
20
+ frontends(options?: {
21
+ client?: boolean;
22
+ }): Map<string, Loader<"frontend">>;
23
+ register(name: string, loader: Loader): void;
24
+ extensions(query: ExtensionQuery, options?: {
25
+ client?: boolean;
26
+ }): string[];
17
27
  addRoot(name: string, source: string): void;
18
28
  get roots(): Dict<string>;
19
29
  plugin(type: PluginType, plugin: Plugin): void;
20
30
  plugins(type: PluginType): Plugin[];
21
31
  entrypoint(code: string): void;
22
32
  get entrypoints(): string;
23
- binder(file: FileRef): Binder | undefined;
24
- bind(extension: string, binder: Binder): void;
33
+ load(file: FileRef): Loader | undefined;
25
34
  done(fn: () => void): void;
26
35
  cleanup(): void;
27
36
  basename(file: FileRef, directory: FileRef): string;
@@ -1,44 +1,80 @@
1
1
  import App from "#App";
2
2
  import build from "#build/hook";
3
+ import E from "#errors";
3
4
  import resolve_paths from "#paths";
4
5
  import s_layout_depth from "#symbol/layout-depth";
6
+ function is_frontend(loader) {
7
+ return loader.type === "frontend";
8
+ }
9
+ function is_backend(loader) {
10
+ return loader.type === "backend";
11
+ }
5
12
  export default class BuildApp extends App {
6
- #frontends = new Map();
13
+ #loaders = new Map([
14
+ ["javascript", {
15
+ type: "backend",
16
+ extensions: [".js"],
17
+ onLoad: file => file.text(),
18
+ }],
19
+ ["typescript", {
20
+ type: "backend",
21
+ extensions: [".ts"],
22
+ onLoad: file => file.text(),
23
+ }],
24
+ ]);
7
25
  #conditions = new Set();
8
26
  #postbuild = [];
9
27
  #roots = {};
10
28
  #id = crypto.randomUUID().slice(0, 8);
11
29
  #session_active = false;
12
30
  #paths;
13
- #bindings = [
14
- [".js", file => file.text()],
15
- [".ts", file => file.text()],
16
- ];
17
31
  #plugins = [];
18
32
  #entrypoint_imports = [];
19
33
  async buildInit() {
20
34
  this.#paths = await resolve_paths(this);
21
35
  await build(this);
22
36
  }
23
- get extensions() {
24
- const builtin = [".ts", ".js", ".json"];
25
- return [...builtin, ...this.#bindings.map(([extension]) => extension)];
26
- }
27
37
  get id() {
28
38
  return this.#id;
29
39
  }
30
40
  get paths() {
31
41
  return this.#paths;
32
42
  }
33
- get frontendExtensions() {
34
- return [...this.#frontends.values()].flat();
35
- }
36
- get frontends() {
37
- return this.#frontends;
38
- }
39
43
  get conditions() {
40
44
  return this.#conditions;
41
45
  }
46
+ backends() {
47
+ return new Map([...this.#loaders.entries()]
48
+ .filter((entry) => is_backend(entry[1])));
49
+ }
50
+ frontends(options = {}) {
51
+ return new Map([...this.#loaders.entries()]
52
+ .filter((entry) => is_frontend(entry[1]))
53
+ .filter(([, frontend]) => options.client === undefined ||
54
+ frontend.client === options.client));
55
+ }
56
+ register(name, loader) {
57
+ const extensions = new Set(loader.extensions);
58
+ for (const [existing_name, existing] of this.#loaders) {
59
+ if (existing_name === name)
60
+ continue;
61
+ const duplicate = existing.extensions.find(extension => extensions.has(extension));
62
+ if (duplicate !== undefined)
63
+ throw E.view_duplicate_extension(duplicate);
64
+ }
65
+ this.#loaders.set(name, loader);
66
+ }
67
+ extensions(query, options = {}) {
68
+ if (query === "backend") {
69
+ return [...this.backends().values()].flatMap(backend => backend.extensions);
70
+ }
71
+ if (query === "frontend") {
72
+ return [...this.frontends(options).values()]
73
+ .flatMap(frontend => frontend.extensions);
74
+ }
75
+ return [".json", ...this.#loaders.values()
76
+ .flatMap(loader => loader.extensions)];
77
+ }
42
78
  addRoot(name, source) {
43
79
  this.#roots[name] = source;
44
80
  }
@@ -59,16 +95,15 @@ export default class BuildApp extends App {
59
95
  get entrypoints() {
60
96
  return this.#entrypoint_imports.join("\n");
61
97
  }
62
- binder(file) {
63
- return this.#bindings
64
- .toSorted(([a], [b]) => a.length > b.length ? -1 : 1)
65
- .find(([extension]) => file.path.endsWith(extension))?.[1];
66
- }
67
- bind(extension, binder) {
68
- if (this.extensions.includes(extension)) {
69
- throw new Error(`${extension} already bound`);
70
- }
71
- this.#bindings.push([extension, binder]);
98
+ load(file) {
99
+ return [...this.#loaders.values()]
100
+ .flatMap(loader => loader.extensions.map(extension => ({
101
+ extension,
102
+ loader,
103
+ })))
104
+ .toSorted((a, b) => a.extension.length > b.extension.length ? -1 : 1)
105
+ .find(({ extension }) => file.path.endsWith(extension))
106
+ ?.loader;
72
107
  }
73
108
  done(fn) {
74
109
  this.#postbuild.push(fn);
@@ -78,7 +113,7 @@ export default class BuildApp extends App {
78
113
  }
79
114
  basename(file, directory) {
80
115
  const relative = file.debase(directory);
81
- const extensions = this.extensions
116
+ const extensions = this.extensions("bundler")
82
117
  .toSorted((a, b) => a.length > b.length ? -1 : 1);
83
118
  for (const extension of extensions) {
84
119
  if (relative.path.endsWith(extension)) {
@@ -0,0 +1,17 @@
1
+ import type { FileRef } from "@rcompat/fs";
2
+ import type { MaybePromise } from "@rcompat/type";
3
+ type LoaderType = "frontend" | "backend";
4
+ type OnLoad = (file: FileRef, options: {
5
+ build: {
6
+ id: string;
7
+ };
8
+ }) => MaybePromise<string>;
9
+ type Loader<T extends LoaderType = LoaderType> = {
10
+ type: T;
11
+ extensions: string[];
12
+ onLoad: OnLoad;
13
+ } & (T extends "frontend" ? {
14
+ client: boolean;
15
+ } : {});
16
+ export type { Loader, LoaderType, OnLoad };
17
+ //# sourceMappingURL=Loader.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=Loader.js.map
@@ -16,8 +16,9 @@ const write_bootstrap = async (app) => {
16
16
  const build_start_script = `
17
17
  import serve from "primate/serve";
18
18
  import views from "app:views";
19
- import routes from "app:routes";
20
19
  import pages from "app:pages";
20
+ import routes from "app:routes";
21
+ import templates from "app:templates";
21
22
  import assets from "app:assets";
22
23
  ${migrate_auto ? `import migrations from "app:migrations";` : ""}
23
24
  ${migrate_auto ? `import autoapply from "app:migrations/autoapply";` : ""}
@@ -38,6 +39,7 @@ const write_bootstrap = async (app) => {
38
39
  routes,
39
40
  views,
40
41
  pages,
42
+ templates,
41
43
  session: session_config,
42
44
  mode: "${app.mode}",
43
45
  target: "${app.target.name}",
@@ -80,7 +82,7 @@ export default async function build_client(app) {
80
82
  ...user_entrypoints(app),
81
83
  },
82
84
  conditions: ["style", "browser", "default", "module", ...conditions],
83
- resolveExtensions: [".ts", ".js", ...app.frontendExtensions],
85
+ resolveExtensions: [".ts", ".js", ...app.extensions("frontend", { client: true })],
84
86
  ...await tsconfig.exists() ? { tsconfig: tsconfig.path } : {},
85
87
  bundle: true,
86
88
  format: "esm",
@@ -3,8 +3,8 @@ export default function plugin_client_request(app) {
3
3
  name: "primate/client/app/request",
4
4
  setup(build) {
5
5
  build.onResolve({ filter: /^app:request$/ }, ({ importer }) => {
6
- const frontend = [...app.frontends.entries()]
7
- .find(([, exts]) => exts.some(ext => importer.endsWith(ext)));
6
+ const frontend = [...app.frontends().entries()]
7
+ .find(([, { extensions }]) => extensions.some(ext => importer.endsWith(ext)));
8
8
  return frontend !== undefined
9
9
  ? { namespace: "request", path: frontend[0] }
10
10
  : undefined;
@@ -6,7 +6,8 @@ export default function plugin_client_frontend(app) {
6
6
  return { namespace: "frontends", path };
7
7
  });
8
8
  build.onLoad({ filter: /app:frontends/ }, async () => {
9
- const contents = [...app.frontends.keys()].map(name => `export { app as ${name} } from "@primate/${name}";`).join("\n");
9
+ const contents = [...app.frontends({ client: true }).entries()]
10
+ .map(([name]) => `export { app as ${name} } from "@primate/${name}";`).join("\n");
10
11
  return { contents, resolveDir: app.root.path };
11
12
  });
12
13
  },
@@ -9,12 +9,12 @@ export default function plugin_client_route_module(app) {
9
9
  });
10
10
  build.onLoad({ filter: /.*/, namespace: "primate-route" }, async (args) => {
11
11
  const base = app.path.routes.join(args.path);
12
- for (const ext of app.extensions) {
12
+ for (const ext of app.extensions("bundler")) {
13
13
  const candidate = fs.ref(base.path + ext);
14
14
  if (await candidate.exists()) {
15
- const binder = app.binder(candidate);
16
- const contents = binder
17
- ? await binder(candidate, { build: { id: app.id }, context: "routes" })
15
+ const loader = app.load(candidate);
16
+ const contents = loader
17
+ ? await loader.onLoad(candidate, { build: { id: app.id } })
18
18
  : await candidate.text();
19
19
  return {
20
20
  contents,
@@ -7,6 +7,10 @@ export default function plugin_client_view(app) {
7
7
  const name = args.path.slice("view:".length);
8
8
  return { path: app.root.join(location.views, name).path };
9
9
  });
10
+ build.onResolve({ filter: /^page:/ }, args => {
11
+ const name = args.path.slice("page:".length);
12
+ return { path: app.root.join(location.routes, name).path };
13
+ });
10
14
  },
11
15
  };
12
16
  }
@@ -22,7 +22,7 @@ async function pre(app) {
22
22
  await app.runpath(location.client).create();
23
23
  const stamp = app.runpath("client", "server-stamp.js");
24
24
  await stamp.write("export default 0;\n");
25
- const router = await $router(app.path.routes, app.extensions);
25
+ const router = await $router(app.path.routes, app.extensions("backend"), app.extensions("frontend"));
26
26
  app.set(s_layout_depth, router.depth("layout"));
27
27
  const session_ts = app.path.config.join("session.ts");
28
28
  const session_js = app.path.config.join("session.js");
@@ -11,7 +11,7 @@ import plugin_route from "#build/server/plugin/route";
11
11
  import plugin_route_client from "#build/server/plugin/route-client";
12
12
  import plugin_view from "#build/server/plugin/view";
13
13
  import plugin_views from "#build/server/plugin/views";
14
- import plugin_virtual_pages from "#build/server/plugin/virtual-pages";
14
+ import plugin_virtual_templates from "#build/server/plugin/virtual-templates";
15
15
  import plugin_virtual_routes from "#build/server/plugin/virtual-routes";
16
16
  import plugin_wasm from "#build/server/plugin/wasm";
17
17
  import plugin_app_request from "#build/shared/plugin/app-request";
@@ -22,7 +22,7 @@ export default async function build_server(app) {
22
22
  app.plugin("server", plugin_view(app));
23
23
  if (app.mode === "development")
24
24
  app.plugin("server", plugin_live_reload(app));
25
- app.plugin("server", plugin_virtual_pages(app));
25
+ app.plugin("server", plugin_virtual_templates(app));
26
26
  app.plugin("server", plugin_virtual_routes(app));
27
27
  app.plugin("server", plugin_route_client(app));
28
28
  app.plugin("server", plugin_route(app));
@@ -55,7 +55,7 @@ export default async function build_server(app) {
55
55
  const require = __createRequire(import.meta.url);`,
56
56
  },
57
57
  nodePaths: [app.root.join("node_modules").path],
58
- resolveExtensions: app.extensions,
58
+ resolveExtensions: app.extensions("bundler"),
59
59
  absWorkingDir: app.root.path,
60
60
  ...await tsconfig_json.exists() ? { tsconfig: tsconfig_json.path } : {},
61
61
  conditions: [
@@ -3,8 +3,8 @@ export default function plugin_server_request(app) {
3
3
  name: "primate/server/app/request",
4
4
  setup(build) {
5
5
  build.onResolve({ filter: /^app:request$/ }, ({ importer }) => {
6
- const frontend = [...app.frontends.entries()]
7
- .find(([, exts]) => exts.some(ext => importer.endsWith(ext)));
6
+ const frontend = [...app.frontends().entries()]
7
+ .find(([, { extensions }]) => extensions.some(ext => importer.endsWith(ext)));
8
8
  return frontend !== undefined
9
9
  ? { namespace: "request", path: frontend[0] }
10
10
  : undefined;
@@ -1,19 +1,19 @@
1
1
  import fs from "@rcompat/fs";
2
2
  export default function plugin_server_frontend(app) {
3
- const filter = new RegExp(`(${app.frontendExtensions.map(e => e.replace(".", "\\.")).join("|")})$`);
3
+ const frontend_extensions = app.extensions("frontend");
4
+ const filter = new RegExp(`(${frontend_extensions.map(e => e.replace(".", "\\.")).join("|")})$`);
4
5
  return {
5
6
  name: "primate/server/frontend",
6
7
  setup(build) {
7
- if (app.frontendExtensions.length === 0)
8
+ if (frontend_extensions.length === 0)
8
9
  return;
9
10
  build.onLoad({ filter, namespace: "file" }, async (args) => {
10
11
  const file = fs.ref(args.path);
11
- const binder = app.binder(file);
12
- if (!binder)
12
+ const loader = app.load(file);
13
+ if (!loader)
13
14
  return null;
14
- const contents = await binder(file, {
15
+ const contents = await loader.onLoad(file, {
15
16
  build: { id: app.id },
16
- context: "views",
17
17
  });
18
18
  return { contents, loader: "js", resolveDir: file.directory.path };
19
19
  });
@@ -13,7 +13,7 @@ export default function plugin_server_route(app) {
13
13
  build.onLoad({ filter: /.*/, namespace: "primate-route" }, async (args) => {
14
14
  const relative = args.path;
15
15
  const base = path_routes.join(relative);
16
- const extensions = app.extensions;
16
+ const extensions = app.extensions("bundler");
17
17
  let file;
18
18
  let extension;
19
19
  for (const e of extensions) {
@@ -29,12 +29,11 @@ export default function plugin_server_route(app) {
29
29
  }
30
30
  const resolveDir = file.directory.path;
31
31
  const watchFiles = [file.path];
32
- const binder = app.binder(file);
33
- if (!binder)
32
+ const loader = app.load(file);
33
+ if (!loader)
34
34
  return { contents: empty, loader: "js", resolveDir, watchFiles };
35
- const compiled = await binder(file, {
35
+ const compiled = await loader.onLoad(file, {
36
36
  build: { id: app.id },
37
- context: "routes",
38
37
  });
39
38
  return {
40
39
  contents: compiled,
@@ -39,22 +39,29 @@ export default function plugin_server_view(app) {
39
39
  });
40
40
  build.onResolve({ filter: /^view:/ }, async (args) => {
41
41
  const name = args.path.slice("view:".length);
42
- for (const ext of app.extensions) {
42
+ for (const ext of app.extensions("bundler")) {
43
43
  const file = app.path.views.join(`${name}${ext}`);
44
44
  if (await file.exists()) {
45
- return { path: file.path, namespace: "primate-view-original" };
45
+ return { path: file.path, namespace: "primate-component-source" };
46
46
  }
47
47
  }
48
48
  return null;
49
49
  });
50
- build.onLoad({ filter: /.*/, namespace: "primate-view-original" }, async (args) => {
50
+ build.onResolve({ filter: /^page:/ }, async (args) => {
51
+ const name = args.path.slice("page:".length);
52
+ const file = app.path.routes.join(name);
53
+ if (await file.exists()) {
54
+ return { path: file.path, namespace: "primate-component-source" };
55
+ }
56
+ return null;
57
+ });
58
+ build.onLoad({ filter: /.*/, namespace: "primate-component-source" }, async (args) => {
51
59
  const file = fs.ref(args.path);
52
- const binder = app.binder(file);
53
- if (binder === undefined)
60
+ const loader = app.load(file);
61
+ if (loader === undefined)
54
62
  return null;
55
- const contents = await binder(file, {
63
+ const contents = await loader.onLoad(file, {
56
64
  build: { id: app.id },
57
- context: "views",
58
65
  });
59
66
  return { contents, loader: "js", resolveDir: file.directory.path };
60
67
  });
@@ -1,13 +1,24 @@
1
+ import E from "#errors";
1
2
  import fs from "@rcompat/fs";
2
3
  export default function plugin_server_views(app) {
3
4
  return {
4
5
  name: "primate/server/views",
5
6
  setup(build) {
7
+ const frontend_extensions = app.extensions("frontend");
8
+ const route_pages = async () => app.path.routes.files({
9
+ filter: file => frontend_extensions.some(ext => file.path.endsWith(ext)),
10
+ recursive: true,
11
+ });
6
12
  build.onResolve({ filter: /^app:views/ }, () => {
7
13
  return { path: "views-virtual", namespace: "primate-views" };
8
14
  });
15
+ build.onResolve({ filter: /^app:pages/ }, () => {
16
+ return { path: "pages-virtual", namespace: "primate-pages" };
17
+ });
9
18
  build.onLoad({ filter: /.*/, namespace: "primate-views" }, async () => {
10
19
  const files = await app.path.views.files({ recursive: true });
20
+ const pages = await route_pages();
21
+ const page_keys = new Set();
11
22
  const roots = Object.keys(app.roots);
12
23
  const contents = `
13
24
  const view = [];
@@ -18,6 +29,17 @@ export default function plugin_server_views(app) {
18
29
  view.push(["${fs.webpath(path)}", view${i}]);`;
19
30
  }).join("\n")}
20
31
 
32
+ ${pages.map((file, i) => {
33
+ const key = fs.webpath(app.basename(file, app.path.routes));
34
+ if (page_keys.has(key))
35
+ throw E.build_duplicate_route_page(key);
36
+ page_keys.add(key);
37
+ const path = file.debase(app.path.routes).path.replace(/^[\\/]/, "");
38
+ return `
39
+ import * as page${i} from "${fs.webpath(`page:${path}`)}";
40
+ view.push(["$page/${key}", page${i}]);`;
41
+ }).join("\n")}
42
+
21
43
  ${roots.map((filename, i) => `
22
44
  import * as root${i} from "app:root/${filename}";
23
45
  view.push(["${filename}", root${i}]);
@@ -30,6 +52,28 @@ export default function plugin_server_views(app) {
30
52
  resolveDir: app.root.path,
31
53
  };
32
54
  });
55
+ build.onLoad({ filter: /.*/, namespace: "primate-pages" }, async () => {
56
+ const pages = await route_pages();
57
+ const page_keys = new Set();
58
+ const contents = `
59
+ const pages = [];
60
+ ${pages.map((file, i) => {
61
+ const key = fs.webpath(app.basename(file, app.path.routes));
62
+ if (page_keys.has(key))
63
+ throw E.build_duplicate_route_page(key);
64
+ page_keys.add(key);
65
+ const path = file.debase(app.path.routes).path.replace(/^[\\/]/, "");
66
+ return `
67
+ pages.push(["${key}", "$page/${fs.webpath(path)}"]);`;
68
+ }).join("\n")}
69
+
70
+ export default pages;`;
71
+ return {
72
+ contents,
73
+ loader: "js",
74
+ resolveDir: app.root.path,
75
+ };
76
+ });
33
77
  },
34
78
  };
35
79
  }
@@ -3,11 +3,14 @@ function is_hook_file(p) {
3
3
  return basename === "+hook" || basename.startsWith("+hook.");
4
4
  }
5
5
  function plugin_server_virtual_routes(app) {
6
- const extension_pattern = new RegExp(`(${app.extensions.map(e => e.replace(".", "\\.")).join("|")})$`);
6
+ const route_extensions = app.extensions("backend");
7
+ const frontend_extensions = app.extensions("frontend");
8
+ const extension_pattern = new RegExp(`(${route_extensions.map(e => e.replace(".", "\\.")).join("|")})$`);
7
9
  function is_route_file(f) {
8
10
  return !f.name.endsWith("~") &&
9
11
  !f.name.startsWith(".") &&
10
12
  !f.name.startsWith("-") &&
13
+ !frontend_extensions.some(ext => f.path.endsWith(ext)) &&
11
14
  extension_pattern.test(f.path);
12
15
  }
13
16
  return {
@@ -0,0 +1,4 @@
1
+ import type BuildApp from "#build/App";
2
+ import type { Plugin } from "esbuild";
3
+ export default function plugin_server_virtual_templates(app: BuildApp): Plugin;
4
+ //# sourceMappingURL=virtual-templates.d.ts.map
@@ -1,33 +1,33 @@
1
1
  import runtime from "@rcompat/runtime";
2
2
  const core = await runtime.projectRoot(import.meta.dirname);
3
- export default function plugin_server_virtual_pages(app) {
3
+ export default function plugin_server_virtual_templates(app) {
4
4
  return {
5
- name: "primate/server/virtual/pages",
5
+ name: "primate/server/virtual/templates",
6
6
  setup(build) {
7
- build.onResolve({ filter: /^app:pages$/ }, () => {
8
- return { path: "pages-virtual", namespace: "primate-pages" };
7
+ build.onResolve({ filter: /^app:templates$/ }, () => {
8
+ return { path: "templates-virtual", namespace: "primate-templates" };
9
9
  });
10
- build.onLoad({ filter: /.*/, namespace: "primate-pages" }, async () => {
10
+ build.onLoad({ filter: /.*/, namespace: "primate-templates" }, async () => {
11
11
  const filter = /^.*\.html$/ui;
12
12
  const defaults = core.join("lib", "private", "defaults");
13
- const pages = {};
13
+ const templates = {};
14
14
  for (const file of await defaults.files({ filter })) {
15
- pages[file.name] = file;
15
+ templates[file.name] = file;
16
16
  }
17
- if (await app.path.pages.exists()) {
18
- for (const file of await app.path.pages.files({ filter })) {
19
- pages[file.name] = file;
17
+ if (await app.path.templates.exists()) {
18
+ for (const file of await app.path.templates.files({ filter })) {
19
+ templates[file.name] = file;
20
20
  }
21
21
  }
22
- const entries = await Promise.all(Object.entries(pages).map(async ([name, file]) => {
22
+ const entries = await Promise.all(Object.entries(templates).map(async ([name, file]) => {
23
23
  const text = await file.text();
24
24
  return `"${name}": ${JSON.stringify(text)}`;
25
25
  }));
26
26
  const contents = `
27
- const pages = {
27
+ const templates = {
28
28
  ${entries.join(",\n")}
29
29
  };
30
- export default pages;
30
+ export default templates;
31
31
  `;
32
32
  return {
33
33
  contents,
@@ -38,4 +38,4 @@ export default function plugin_server_virtual_pages(app) {
38
38
  },
39
39
  };
40
40
  }
41
- //# sourceMappingURL=virtual-pages.js.map
41
+ //# sourceMappingURL=virtual-templates.js.map
@@ -4,8 +4,8 @@ export default function plugin_shared_app_request(app) {
4
4
  setup(build) {
5
5
  build.onResolve({ filter: /^app:/ }, ({ path, importer }) => {
6
6
  const framework = path.slice(4);
7
- const exts = app.frontends.get(framework);
8
- return exts?.some(ext => importer.endsWith(ext))
7
+ const frontend = app.frontends().get(framework);
8
+ return frontend?.extensions.some(ext => importer.endsWith(ext))
9
9
  ? { namespace: "app-frontend", path: framework }
10
10
  : undefined;
11
11
  });
@@ -5,7 +5,7 @@ export default interface ViewOptions extends ResponseInit {
5
5
  style_src?: string[];
6
6
  };
7
7
  head?: string;
8
- page?: string;
8
+ template?: string;
9
9
  partial?: boolean;
10
10
  placeholders?: Omit<Dict, "body" | "head">;
11
11
  }