@lukoweb/apitogo 0.2.42 → 0.2.44

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,188 +1,190 @@
1
- import path from "node:path";
2
- import type { Plugin } from "vite";
3
- import { normalizePath } from "vite";
4
- import { getCurrentConfig } from "../config/loader.js";
5
- import { writePluginDebugCode } from "./debug.js";
6
-
7
- const getLandingModuleImport = (moduleDir: string, mode: string) =>
8
- mode === "internal"
9
- ? normalizePath(path.join(moduleDir, "../module-landing/src/index.tsx"))
10
- : "@lukoweb/apitogo-module-landing";
11
-
12
- const getUserPanelModuleImport = (moduleDir: string, mode: string) =>
13
- mode === "internal"
14
- ? normalizePath(path.join(moduleDir, "../module-user-panel/src/index.tsx"))
15
- : "@lukoweb/apitogo-module-user-panel";
16
-
17
- const getBillingPluginImport = (moduleDir: string, mode: string) =>
18
- mode === "internal"
19
- ? normalizePath(
20
- path.join(moduleDir, "../plugin-dev-portal-billing/src/index.ts"),
21
- )
22
- : "@lukoweb/apitogo-plugin-dev-portal-billing";
23
-
24
- const resolveProjectPageImport = (rootDir: string, pagePath: string) =>
25
- normalizePath(path.resolve(rootDir, pagePath));
26
-
27
- const viteModulesPlugin = (): Plugin => {
28
- const virtualModuleId = "virtual:zudoku-modules-plugin";
29
- const resolvedVirtualModuleId = `\0${virtualModuleId}`;
30
-
31
- return {
32
- name: "zudoku-modules-plugin",
33
- resolveId(id) {
34
- if (id === virtualModuleId) {
35
- return resolvedVirtualModuleId;
36
- }
37
- },
38
- async load(id) {
39
- if (id !== resolvedVirtualModuleId) return;
40
-
41
- const config = getCurrentConfig();
42
- const { landing, userPanel } = config.__resolvedModules;
43
-
44
- if (config.__meta.mode === "standalone") {
45
- return [
46
- `export const configuredModulesPlugins = [];`,
47
- `export const configuredModulesSlots = {};`,
48
- ].join("\n");
49
- }
50
-
51
- const landingImport = getLandingModuleImport(
52
- config.__meta.moduleDir,
53
- config.__meta.mode,
54
- );
55
- const userPanelImport = getUserPanelModuleImport(
56
- config.__meta.moduleDir,
57
- config.__meta.mode,
58
- );
59
-
60
- const landingConfig = {
61
- enabled: landing.enabled,
62
- path: landing.path,
63
- layout: landing.layout,
64
- content: landing.content,
65
- };
66
-
67
- const userPanelConfig = {
68
- enabled: userPanel.enabled,
69
- basePath: userPanel.basePath,
70
- dashboard: {
71
- enabled: userPanel.dashboard.enabled,
72
- path: userPanel.dashboard.path,
73
- content: userPanel.dashboard.content,
74
- },
75
- userManagement: {
76
- enabled: userPanel.userManagement.enabled,
77
- routes: userPanel.userManagement.routes,
78
- content: userPanel.userManagement.content,
79
- },
80
- apiKeys: userPanel.apiKeys,
81
- plans: {
82
- enabled: userPanel.plans.enabled,
83
- path: userPanel.plans.path,
84
- content: userPanel.plans.content,
85
- },
86
- };
87
-
88
- const landingPagePath = config.modules?.landing?.page;
89
- const dashboardPagePath = config.modules?.userPanel?.dashboard?.page;
90
- const profilePagePath = config.modules?.userPanel?.userManagement?.page;
91
- const plansPagePath = config.modules?.userPanel?.plans?.page;
92
-
93
- const code: string[] = [`import config from "virtual:zudoku-config";`];
94
-
95
- if (landingPagePath) {
96
- code.push(
97
- `import LandingPage from "${resolveProjectPageImport(config.__meta.rootDir, landingPagePath)}";`,
98
- );
99
- }
100
-
101
- const useBillingDashboard = !dashboardPagePath && userPanel.plans.enabled;
102
-
103
- if (dashboardPagePath) {
104
- code.push(
105
- `import UserPanelDashboardPage from "${resolveProjectPageImport(config.__meta.rootDir, dashboardPagePath)}";`,
106
- );
107
- } else if (useBillingDashboard) {
108
- code.push(
109
- `import { OverviewDashboardPage as UserPanelDashboardPage } from "${getBillingPluginImport(config.__meta.moduleDir, config.__meta.mode)}";`,
110
- );
111
- }
112
-
113
- if (profilePagePath) {
114
- code.push(
115
- `import UserPanelProfilePage from "${resolveProjectPageImport(config.__meta.rootDir, profilePagePath)}";`,
116
- );
117
- }
118
-
119
- if (plansPagePath) {
120
- code.push(
121
- `import UserPanelPlansPage from "${resolveProjectPageImport(config.__meta.rootDir, plansPagePath)}";`,
122
- );
123
- }
124
-
125
- const configuredPlugins: string[] = [];
126
- let hasLandingFooterSlot = false;
127
-
128
- if (landing.enabled) {
129
- code.push(`import { landingModule } from "${landingImport}";`);
130
- code.push(
131
- `const landing = landingModule({`,
132
- ` ...${JSON.stringify(landingConfig)},`,
133
- ` component: config.modules?.landing?.component${landingPagePath ? " ?? LandingPage" : ""},`,
134
- `});`,
135
- );
136
- configuredPlugins.push("...(landing ? [landing] : [])");
137
-
138
- // Render the configured landing footer on every page via the
139
- // layout "footer" slot (home, docs, API reference, pricing, auth).
140
- if (landing.content.footer) {
141
- code.push(
142
- `import { createLandingFooterSlot } from "${landingImport}";`,
143
- `const landingFooterSlot = createLandingFooterSlot(${JSON.stringify(landing.content.footer)});`,
144
- );
145
- hasLandingFooterSlot = true;
146
- }
147
- }
148
-
149
- code.push(
150
- hasLandingFooterSlot
151
- ? `export const configuredModulesSlots = landingFooterSlot ? { footer: landingFooterSlot } : {};`
152
- : `export const configuredModulesSlots = {};`,
153
- );
154
-
155
- if (userPanel.enabled) {
156
- code.push(`import { userPanelModule } from "${userPanelImport}";`);
157
- code.push(
158
- `const userPanel = userPanelModule({`,
159
- ` ...${JSON.stringify(userPanelConfig)},`,
160
- ` dashboard: {`,
161
- ` ...${JSON.stringify(userPanelConfig.dashboard)},`,
162
- ` component: config.modules?.userPanel?.dashboard?.component${dashboardPagePath || useBillingDashboard ? " ?? UserPanelDashboardPage" : ""},`,
163
- ` },`,
164
- ` userManagement: {`,
165
- ` ...${JSON.stringify(userPanelConfig.userManagement)},`,
166
- ` component: config.modules?.userPanel?.userManagement?.component${profilePagePath ? " ?? UserPanelProfilePage" : ""},`,
167
- ` },`,
168
- ` plans: {`,
169
- ` ...${JSON.stringify(userPanelConfig.plans)},`,
170
- ` component: config.modules?.userPanel?.plans?.component${plansPagePath ? " ?? UserPanelPlansPage" : ""},`,
171
- ` },`,
172
- `});`,
173
- );
174
- configuredPlugins.push("...(userPanel ? [userPanel] : [])");
175
- }
176
-
177
- code.push(
178
- `export const configuredModulesPlugins = [${configuredPlugins.join(", ")}];`,
179
- );
180
-
181
- await writePluginDebugCode(config.__meta.rootDir, "modules-plugin", code);
182
-
183
- return code.join("\n");
184
- },
185
- };
186
- };
187
-
188
- export default viteModulesPlugin;
1
+ import path from "node:path";
2
+ import type { Plugin } from "vite";
3
+ import { normalizePath } from "vite";
4
+ import { getCurrentConfig } from "../config/loader.js";
5
+ import { writePluginDebugCode } from "./debug.js";
6
+
7
+ const getLandingModuleImport = (moduleDir: string, mode: string) =>
8
+ mode === "internal"
9
+ ? normalizePath(path.join(moduleDir, "../module-landing/src/index.tsx"))
10
+ : "@lukoweb/apitogo-module-landing";
11
+
12
+ const getUserPanelModuleImport = (moduleDir: string, mode: string) =>
13
+ mode === "internal"
14
+ ? normalizePath(path.join(moduleDir, "../module-user-panel/src/index.tsx"))
15
+ : "@lukoweb/apitogo-module-user-panel";
16
+
17
+ const getBillingPluginImport = (moduleDir: string, mode: string) =>
18
+ mode === "internal"
19
+ ? normalizePath(
20
+ path.join(moduleDir, "../plugin-dev-portal-billing/src/index.ts"),
21
+ )
22
+ : "@lukoweb/apitogo-plugin-dev-portal-billing";
23
+
24
+ const resolveProjectPageImport = (rootDir: string, pagePath: string) =>
25
+ normalizePath(path.resolve(rootDir, pagePath));
26
+
27
+ const viteModulesPlugin = (): Plugin => {
28
+ const virtualModuleId = "virtual:zudoku-modules-plugin";
29
+ const resolvedVirtualModuleId = `\0${virtualModuleId}`;
30
+
31
+ return {
32
+ name: "zudoku-modules-plugin",
33
+ resolveId(id) {
34
+ if (id === virtualModuleId) {
35
+ return resolvedVirtualModuleId;
36
+ }
37
+ },
38
+ async load(id) {
39
+ if (id !== resolvedVirtualModuleId) return;
40
+
41
+ const config = getCurrentConfig();
42
+ const { landing, userPanel } = config.__resolvedModules;
43
+
44
+ if (config.__meta.mode === "standalone") {
45
+ return [
46
+ `export const configuredModulesPlugins = [];`,
47
+ `export const configuredModulesSlots = {};`,
48
+ ].join("\n");
49
+ }
50
+
51
+ const landingImport = getLandingModuleImport(
52
+ config.__meta.moduleDir,
53
+ config.__meta.mode,
54
+ );
55
+ const userPanelImport = getUserPanelModuleImport(
56
+ config.__meta.moduleDir,
57
+ config.__meta.mode,
58
+ );
59
+
60
+ const landingConfig = {
61
+ enabled: landing.enabled,
62
+ path: landing.path,
63
+ layout: landing.layout,
64
+ content: landing.content,
65
+ };
66
+
67
+ const userPanelConfig = {
68
+ enabled: userPanel.enabled,
69
+ basePath: userPanel.basePath,
70
+ dashboard: {
71
+ enabled: userPanel.dashboard.enabled,
72
+ path: userPanel.dashboard.path,
73
+ content: userPanel.dashboard.content,
74
+ },
75
+ userManagement: {
76
+ enabled: userPanel.userManagement.enabled,
77
+ routes: userPanel.userManagement.routes,
78
+ content: userPanel.userManagement.content,
79
+ },
80
+ apiKeys: userPanel.apiKeys,
81
+ plans: {
82
+ enabled: userPanel.plans.enabled,
83
+ path: userPanel.plans.path,
84
+ content: userPanel.plans.content,
85
+ },
86
+ };
87
+
88
+ const landingPagePath = config.modules?.landing?.page;
89
+ const dashboardPagePath = config.modules?.userPanel?.dashboard?.page;
90
+ const profilePagePath = config.modules?.userPanel?.userManagement?.page;
91
+ const plansPagePath = config.modules?.userPanel?.plans?.page;
92
+
93
+ const code: string[] = [`import config from "virtual:zudoku-config";`];
94
+
95
+ if (landingPagePath) {
96
+ code.push(
97
+ `import LandingPage from "${resolveProjectPageImport(config.__meta.rootDir, landingPagePath)}";`,
98
+ );
99
+ }
100
+
101
+ const useBillingDashboard = !dashboardPagePath && userPanel.plans.enabled;
102
+
103
+ if (dashboardPagePath) {
104
+ code.push(
105
+ `import UserPanelDashboardPage from "${resolveProjectPageImport(config.__meta.rootDir, dashboardPagePath)}";`,
106
+ );
107
+ } else if (useBillingDashboard) {
108
+ code.push(
109
+ `import { OverviewDashboardPage as UserPanelDashboardPage } from "${getBillingPluginImport(config.__meta.moduleDir, config.__meta.mode)}";`,
110
+ );
111
+ }
112
+
113
+ if (profilePagePath) {
114
+ code.push(
115
+ `import UserPanelProfilePage from "${resolveProjectPageImport(config.__meta.rootDir, profilePagePath)}";`,
116
+ );
117
+ }
118
+
119
+ if (plansPagePath) {
120
+ code.push(
121
+ `import UserPanelPlansPage from "${resolveProjectPageImport(config.__meta.rootDir, plansPagePath)}";`,
122
+ );
123
+ }
124
+
125
+ const configuredPlugins: string[] = [];
126
+ let hasLandingFooterSlot = false;
127
+
128
+ if (landing.enabled) {
129
+ code.push(`import { landingModule } from "${landingImport}";`);
130
+ code.push(
131
+ `const landing = landingModule({`,
132
+ ` ...${JSON.stringify(landingConfig)},`,
133
+ ` component: config.modules?.landing?.component${landingPagePath ? " ?? LandingPage" : ""},`,
134
+ `});`,
135
+ );
136
+ configuredPlugins.push("...(landing ? [landing] : [])");
137
+
138
+ // Render the configured landing footer on every page via the
139
+ // layout "footer" slot (home, docs, API reference, pricing, auth).
140
+ if (landing.content.footer) {
141
+ code.push(
142
+ `import { createLandingFooterSlot } from "${landingImport}";`,
143
+ `const landingFooterSlot = createLandingFooterSlot(${JSON.stringify(landing.content.footer)});`,
144
+ );
145
+ hasLandingFooterSlot = true;
146
+ }
147
+ }
148
+
149
+ code.push(
150
+ hasLandingFooterSlot
151
+ ? `export const configuredModulesSlots = landingFooterSlot ? { footer: landingFooterSlot } : {};`
152
+ : `export const configuredModulesSlots = {};`,
153
+ );
154
+
155
+ if (userPanel.enabled) {
156
+ code.push(`import { userPanelModule } from "${userPanelImport}";`);
157
+ code.push(
158
+ `const userPanel = userPanelModule({`,
159
+ ` ...${JSON.stringify(userPanelConfig)},`,
160
+ // With billing on, the billing plugin owns the plans page and the account side menu.
161
+ ` billingNav: ${config.__meta.pricingEnabled === true},`,
162
+ ` dashboard: {`,
163
+ ` ...${JSON.stringify(userPanelConfig.dashboard)},`,
164
+ ` component: config.modules?.userPanel?.dashboard?.component${dashboardPagePath || useBillingDashboard ? " ?? UserPanelDashboardPage" : ""},`,
165
+ ` },`,
166
+ ` userManagement: {`,
167
+ ` ...${JSON.stringify(userPanelConfig.userManagement)},`,
168
+ ` component: config.modules?.userPanel?.userManagement?.component${profilePagePath ? " ?? UserPanelProfilePage" : ""},`,
169
+ ` },`,
170
+ ` plans: {`,
171
+ ` ...${JSON.stringify(userPanelConfig.plans)},`,
172
+ ` component: config.modules?.userPanel?.plans?.component${plansPagePath ? " ?? UserPanelPlansPage" : ""},`,
173
+ ` },`,
174
+ `});`,
175
+ );
176
+ configuredPlugins.push("...(userPanel ? [userPanel] : [])");
177
+ }
178
+
179
+ code.push(
180
+ `export const configuredModulesPlugins = [${configuredPlugins.join(", ")}];`,
181
+ );
182
+
183
+ await writePluginDebugCode(config.__meta.rootDir, "modules-plugin", code);
184
+
185
+ return code.join("\n");
186
+ },
187
+ };
188
+ };
189
+
190
+ export default viteModulesPlugin;
@@ -1,70 +1,80 @@
1
- import { lstatSync, realpathSync } from "node:fs";
2
- import path from "node:path";
3
-
4
- /**
5
- * A config version 2 portal builds from `<state>/generated/`, where `content` is a link to the project folder.
6
- * That folder holds the state folder itself (`.apitogo/` or `apitogo/.state/`), which holds `generated/content`
7
- * again, so a file watcher that follows the link goes round forever (ENAMETOOLONG).
8
- */
9
- const STATE_INSIDE_CONTENT =
10
- /[\\/]content[\\/](?:\.apitogo|apitogo[\\/]\.state)(?:[\\/]|$)/;
11
-
12
- const within = (base: string, file: string) =>
13
- file === base || file.startsWith(base + path.sep);
14
-
15
- const unique = (paths: string[]) => [...new Set(paths)];
16
-
17
- /** Whether `root` is the site root (`<state>/generated`) of a config version 2 workspace: it has the `content` link. */
18
- function isWorkspace(root: string): boolean {
19
- try {
20
- return (
21
- path.basename(root) === "generated" &&
22
- lstatSync(path.join(root, "content")).isSymbolicLink()
23
- );
24
- } catch {
25
- return false;
26
- }
27
- }
28
-
29
- /**
30
- * The tenant's own folder of a config version 2 workspace (what `content` links to), or `undefined` for any other
31
- * project. The site root sits inside the state folder, so the folder that holds `node_modules` and the pages is
32
- * outside it and the dev server has to be told it may serve files from there.
33
- */
34
- export function tenantDir(root: string): string | undefined {
35
- return isWorkspace(root)
36
- ? realpathSync(path.join(root, "content"))
37
- : undefined;
38
- }
39
-
40
- /**
41
- * What the dev server's watcher must leave alone in a config version 2 workspace, or `undefined` for any other
42
- * project. The watcher meets the state folder twice: below the `content` link and, after resolving the link,
43
- * by its real path. So the whole state folder is skipped, except `generated/` itself (the site root) and the
44
- * tenant's own files.
45
- */
46
- export function stateIgnore(
47
- root: string,
48
- ): ((file: string) => boolean) | undefined {
49
- if (!isWorkspace(root)) return undefined;
50
-
51
- const state = path.dirname(root);
52
- const states = unique([state, realpathSync(state)]);
53
- const roots = unique([root, realpathSync(root)]);
54
-
55
- return (file) => {
56
- if (!states.some((base) => within(base, file))) return false;
57
- if (!roots.some((base) => within(base, file))) return true;
58
- return STATE_INSIDE_CONTENT.test(file);
59
- };
60
- }
61
-
62
- /**
63
- * The folders the dev server may serve files from. The site root of a config version 2 workspace sits inside the
64
- * state folder, so the package (`node_modules`) and the tenant's pages are outside it. Without them the page's main
65
- * script fails with "Failed to load url ... Does the file exist?" and the server stops.
66
- */
67
- export function devServerAllow(root: string, moduleDir: string): string[] {
68
- const tenant = tenantDir(root);
69
- return [root, path.dirname(root), moduleDir, ...(tenant ? [tenant] : [])];
70
- }
1
+ import { lstatSync, realpathSync } from "node:fs";
2
+ import path from "node:path";
3
+
4
+ /**
5
+ * A config version 2 portal builds from `<state>/generated/`, where `content` is a link to the project folder.
6
+ * That folder holds the state folder itself (`.apitogo/` or `apitogo/.state/`), which holds `generated/content`
7
+ * again, so a file watcher that follows the link goes round forever (ENAMETOOLONG).
8
+ */
9
+ const STATE_INSIDE_CONTENT =
10
+ /[\\/]content[\\/](?:\.apitogo|apitogo[\\/]\.state)(?:[\\/]|$)/;
11
+
12
+ /**
13
+ * On Windows the site root can arrive as `C:/...` while the watcher reports `C:\...`, so both sides are resolved to
14
+ * the platform's form first; otherwise nothing matched and the watcher followed `content` until ELOOP.
15
+ */
16
+ const within = (base: string, file: string) => {
17
+ const resolvedBase = path.resolve(base);
18
+ const resolvedFile = path.resolve(file);
19
+ return (
20
+ resolvedFile === resolvedBase ||
21
+ resolvedFile.startsWith(resolvedBase + path.sep)
22
+ );
23
+ };
24
+
25
+ const unique = (paths: string[]) => [...new Set(paths)];
26
+
27
+ /** Whether `root` is the site root (`<state>/generated`) of a config version 2 workspace: it has the `content` link. */
28
+ function isWorkspace(root: string): boolean {
29
+ try {
30
+ return (
31
+ path.basename(root) === "generated" &&
32
+ lstatSync(path.join(root, "content")).isSymbolicLink()
33
+ );
34
+ } catch {
35
+ return false;
36
+ }
37
+ }
38
+
39
+ /**
40
+ * The tenant's own folder of a config version 2 workspace (what `content` links to), or `undefined` for any other
41
+ * project. The site root sits inside the state folder, so the folder that holds `node_modules` and the pages is
42
+ * outside it and the dev server has to be told it may serve files from there.
43
+ */
44
+ export function tenantDir(root: string): string | undefined {
45
+ return isWorkspace(root)
46
+ ? realpathSync(path.join(root, "content"))
47
+ : undefined;
48
+ }
49
+
50
+ /**
51
+ * What the dev server's watcher must leave alone in a config version 2 workspace, or `undefined` for any other
52
+ * project. The watcher meets the state folder twice: below the `content` link and, after resolving the link,
53
+ * by its real path. So the whole state folder is skipped, except `generated/` itself (the site root) and the
54
+ * tenant's own files.
55
+ */
56
+ export function stateIgnore(
57
+ root: string,
58
+ ): ((file: string) => boolean) | undefined {
59
+ if (!isWorkspace(root)) return undefined;
60
+
61
+ const state = path.dirname(root);
62
+ const states = unique([state, realpathSync(state)]);
63
+ const roots = unique([root, realpathSync(root)]);
64
+
65
+ return (file) => {
66
+ if (!states.some((base) => within(base, file))) return false;
67
+ if (!roots.some((base) => within(base, file))) return true;
68
+ return STATE_INSIDE_CONTENT.test(file);
69
+ };
70
+ }
71
+
72
+ /**
73
+ * The folders the dev server may serve files from. The site root of a config version 2 workspace sits inside the
74
+ * state folder, so the package (`node_modules`) and the tenant's pages are outside it. Without them the page's main
75
+ * script fails with "Failed to load url ... Does the file exist?" and the server stops.
76
+ */
77
+ export function devServerAllow(root: string, moduleDir: string): string[] {
78
+ const tenant = tenantDir(root);
79
+ return [root, path.dirname(root), moduleDir, ...(tenant ? [tenant] : [])];
80
+ }