@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.
- package/dist/cli/cli.js +291 -36
- package/dist/declarations/lib/plugins/openapi/playground/result-panel/ResultPanel.d.ts +3 -1
- package/dist/declarations/lib/plugins/openapi/playground/warmUpRetry.d.ts +5 -0
- package/package.json +1 -1
- package/src/config/v2/full-server.ts +192 -0
- package/src/config/v2/manifest.ts +472 -452
- package/src/config/v2/services.ts +59 -20
- package/src/config/v2/wrapper.ts +60 -4
- package/src/lib/plugins/openapi/playground/Playground.tsx +619 -602
- package/src/lib/plugins/openapi/playground/result-panel/ResultPanel.tsx +179 -106
- package/src/lib/plugins/openapi/playground/warmUpRetry.ts +43 -0
- package/src/vite/plugin-modules.ts +190 -188
- package/src/vite/watch-ignore.ts +80 -70
|
@@ -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
|
-
|
|
161
|
-
`
|
|
162
|
-
`
|
|
163
|
-
`
|
|
164
|
-
`
|
|
165
|
-
`
|
|
166
|
-
`
|
|
167
|
-
`
|
|
168
|
-
`
|
|
169
|
-
`
|
|
170
|
-
`
|
|
171
|
-
`
|
|
172
|
-
`}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
|
|
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;
|
package/src/vite/watch-ignore.ts
CHANGED
|
@@ -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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
+
}
|