@antelopejs/dms-frontend 0.3.11 → 0.4.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.
- package/README.md +85 -5
- package/dist/commands/verify-source-action.js +2 -10
- package/dist/commands/verify-source.js +11 -7
- package/dist/config.d.ts +1 -0
- package/dist/config.js +5 -1
- package/dist/derived-outputs.d.ts +2 -0
- package/dist/derived-outputs.js +22 -3
- package/dist/help.js +4 -0
- package/dist/layers.js +1 -0
- package/dist/verify-source-runner.js +6 -42
- package/dist/verify-source-sources.d.ts +13 -0
- package/dist/verify-source-sources.js +189 -0
- package/dist/workspace.d.ts +1 -0
- package/package.json +1 -1
- package/templates/vue/DmsDynamicPage.vue +3 -1
- package/templates/vue/app-runtime.ts +42 -26
- package/templates/vue/frontend-build-loader.ts +78 -0
- package/templates/vue/frontend-build.ts +41 -0
- package/templates/vue/frontend-module.ts +166 -22
- package/templates/vue/icon-hydration.ts +78 -0
- package/templates/vue/server/auth/backend.mjs +20 -6
- package/templates/vue/server/auth/client-ip.mjs +28 -0
- package/templates/vue/server/auth/routes.mjs +1 -0
- package/templates/vue/server/auth/session.mjs +6 -0
- package/templates/vue/server/icons.mjs +43 -1
- package/templates/vue/server.mjs +14 -13
- package/templates/vue/ssr-renderer.ts +18 -1
- package/templates/vue/vite.config.ts +25 -23
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.readSourceOptions = readSourceOptions;
|
|
4
|
+
exports.readSources = readSources;
|
|
5
|
+
const node_fs_1 = require("node:fs");
|
|
6
|
+
const node_path_1 = require("node:path");
|
|
7
|
+
const common_1 = require("./common");
|
|
8
|
+
const output_1 = require("./output");
|
|
9
|
+
const verify_source_result_1 = require("./verify-source-result");
|
|
10
|
+
const DMS_PACKAGE = "@antelopejs/dms";
|
|
11
|
+
const DMS_CORE_LAYER_PACKAGE = "@antelopejs/dms-frontend-vue";
|
|
12
|
+
const CORE_LAYER_DIRECTORY = "frontend-vue";
|
|
13
|
+
const FRONTEND_ENTRY = "dms.frontend.ts";
|
|
14
|
+
const ADD_DEV_DEPENDENCY = {
|
|
15
|
+
pnpm: "pnpm add -D",
|
|
16
|
+
npm: "npm install -D",
|
|
17
|
+
yarn: "yarn add -D",
|
|
18
|
+
bun: "bun add -d",
|
|
19
|
+
};
|
|
20
|
+
const LOCKFILES = [
|
|
21
|
+
["pnpm-lock.yaml", "pnpm"],
|
|
22
|
+
["package-lock.json", "npm"],
|
|
23
|
+
["yarn.lock", "yarn"],
|
|
24
|
+
["bun.lock", "bun"],
|
|
25
|
+
["bun.lockb", "bun"],
|
|
26
|
+
];
|
|
27
|
+
function readSourceOptions() {
|
|
28
|
+
return {
|
|
29
|
+
projectDir: process.cwd(),
|
|
30
|
+
layerSource: process.env.DMS_LAYER_SOURCE || undefined,
|
|
31
|
+
moduleSources: JSON.parse(process.env.DMS_MODULE_SOURCES ?? "[]"),
|
|
32
|
+
localPackages: JSON.parse(process.env.DMS_LOCAL_PACKAGES ?? "{}"),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function readManifest(packagePath) {
|
|
36
|
+
return JSON.parse((0, node_fs_1.readFileSync)(packagePath, "utf8"));
|
|
37
|
+
}
|
|
38
|
+
function readLayer(root) {
|
|
39
|
+
const packagePath = (0, node_path_1.join)(root, "package.json");
|
|
40
|
+
if (!(0, node_fs_1.existsSync)(packagePath))
|
|
41
|
+
throw new common_1.UsageError({
|
|
42
|
+
title: `No package.json in ${(0, output_1.showPath)(root)}`,
|
|
43
|
+
fixes: [verify_source_result_1.LAYER_PATH_FIX],
|
|
44
|
+
});
|
|
45
|
+
const { name } = readManifest(packagePath);
|
|
46
|
+
return { path: root, sourcePath: root, packageName: name };
|
|
47
|
+
}
|
|
48
|
+
function isCoreLayer(layer) {
|
|
49
|
+
return layer.packageName === DMS_CORE_LAYER_PACKAGE;
|
|
50
|
+
}
|
|
51
|
+
function shellArgument(path) {
|
|
52
|
+
return /^[\w@%+=:,./~-]+$/.test(path) ? path : JSON.stringify(path);
|
|
53
|
+
}
|
|
54
|
+
function defaultModuleRoot(projectDir) {
|
|
55
|
+
const root = [projectDir, (0, node_path_1.join)(projectDir, CORE_LAYER_DIRECTORY)].find((candidate) => (0, node_fs_1.existsSync)((0, node_path_1.join)(candidate, FRONTEND_ENTRY)));
|
|
56
|
+
if (root)
|
|
57
|
+
return root;
|
|
58
|
+
throw new common_1.UsageError({
|
|
59
|
+
title: "No frontend module to verify",
|
|
60
|
+
reason: `Neither ${(0, output_1.showPath)(projectDir)} nor its ${CORE_LAYER_DIRECTORY} directory contains ${FRONTEND_ENTRY}.`,
|
|
61
|
+
fixes: [
|
|
62
|
+
`Run the command from the frontend module, or pass its root with -m`,
|
|
63
|
+
],
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
function packageManager(projectDir) {
|
|
67
|
+
for (let directory = projectDir;; directory = (0, node_path_1.dirname)(directory)) {
|
|
68
|
+
const packagePath = (0, node_path_1.join)(directory, "package.json");
|
|
69
|
+
if ((0, node_fs_1.existsSync)(packagePath)) {
|
|
70
|
+
const [name] = (readManifest(packagePath).packageManager ?? "").split("@");
|
|
71
|
+
if (name && ADD_DEV_DEPENDENCY[name])
|
|
72
|
+
return name;
|
|
73
|
+
const lockfile = LOCKFILES.find(([file]) => (0, node_fs_1.existsSync)((0, node_path_1.join)(directory, file)));
|
|
74
|
+
if (lockfile)
|
|
75
|
+
return lockfile[1];
|
|
76
|
+
}
|
|
77
|
+
if ((0, node_path_1.dirname)(directory) === directory)
|
|
78
|
+
return "pnpm";
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function addDmsCommand(projectDir, version = "") {
|
|
82
|
+
return `${ADD_DEV_DEPENDENCY[packageManager(projectDir)]} ${DMS_PACKAGE}${version}`;
|
|
83
|
+
}
|
|
84
|
+
function describeDirectory(directory) {
|
|
85
|
+
return (0, node_path_1.resolve)(directory) === process.cwd()
|
|
86
|
+
? "the current directory"
|
|
87
|
+
: (0, output_1.showPath)(directory);
|
|
88
|
+
}
|
|
89
|
+
function resolveDmsPackage(projectDir) {
|
|
90
|
+
try {
|
|
91
|
+
return require.resolve(`${DMS_PACKAGE}/package.json`, {
|
|
92
|
+
paths: [projectDir],
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function installedCoreLayer(projectDir) {
|
|
100
|
+
const packagePath = resolveDmsPackage(projectDir);
|
|
101
|
+
if (!packagePath)
|
|
102
|
+
throw new common_1.UsageError({
|
|
103
|
+
title: `${DMS_PACKAGE} is not installed in this project`,
|
|
104
|
+
reason: `Frontend modules are verified on top of the DMS core layer that ${DMS_PACKAGE} ships, and Node finds no ${DMS_PACKAGE} from ${describeDirectory(projectDir)}.`,
|
|
105
|
+
fixes: [
|
|
106
|
+
`Add it as a development dependency: ${addDmsCommand(projectDir)}`,
|
|
107
|
+
],
|
|
108
|
+
});
|
|
109
|
+
const { version } = readManifest(packagePath);
|
|
110
|
+
const root = (0, node_path_1.join)((0, node_path_1.dirname)(packagePath), CORE_LAYER_DIRECTORY);
|
|
111
|
+
const layer = (0, node_fs_1.existsSync)((0, node_path_1.join)(root, "package.json"))
|
|
112
|
+
? readLayer(root)
|
|
113
|
+
: undefined;
|
|
114
|
+
if (!layer || !isCoreLayer(layer))
|
|
115
|
+
throw new common_1.UsageError({
|
|
116
|
+
title: `${DMS_PACKAGE} ${version} ships no DMS core layer`,
|
|
117
|
+
reason: `${(0, output_1.showPath)((0, node_path_1.dirname)(packagePath))} has no ${CORE_LAYER_DIRECTORY} directory holding ${DMS_CORE_LAYER_PACKAGE}.`,
|
|
118
|
+
fixes: [`Update it: ${addDmsCommand(projectDir, "@latest")}`],
|
|
119
|
+
});
|
|
120
|
+
return {
|
|
121
|
+
layers: [layer],
|
|
122
|
+
coreLayer: `${DMS_PACKAGE} ${version} (installed in this project)`,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function notCoreLayer(sourceRoot, layers, modules, localPackages) {
|
|
126
|
+
const command = [
|
|
127
|
+
verify_source_result_1.VERIFY_SOURCE_COMMAND,
|
|
128
|
+
...[...layers, ...modules].flatMap((layer) => [
|
|
129
|
+
"-m",
|
|
130
|
+
shellArgument((0, output_1.showPath)(layer.path)),
|
|
131
|
+
]),
|
|
132
|
+
...Object.entries(localPackages).flatMap(([name, path]) => [
|
|
133
|
+
"--local-package",
|
|
134
|
+
shellArgument(`${name}=${(0, output_1.showPath)(path)}`),
|
|
135
|
+
]),
|
|
136
|
+
].join(" ");
|
|
137
|
+
const names = layers
|
|
138
|
+
.map((layer) => layer.packageName ?? "a package without a name")
|
|
139
|
+
.join(", ");
|
|
140
|
+
const shown = (0, output_1.showPath)(sourceRoot);
|
|
141
|
+
return {
|
|
142
|
+
title: `${shown} is not the DMS core layer`,
|
|
143
|
+
reason: `--layer is for verifying an unpublished DMS core layer, ${DMS_CORE_LAYER_PACKAGE}, and ${shown} holds ${names}. ` +
|
|
144
|
+
`Without it, frontend modules are verified on the core layer of the ${DMS_PACKAGE} installed in the project.`,
|
|
145
|
+
fixes: [`Drop -l and pass the folder with -m: ${command}`],
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function readLayerSource(layerSource) {
|
|
149
|
+
const sourceRoot = (0, node_path_1.resolve)(layerSource);
|
|
150
|
+
if (!(0, node_fs_1.statSync)(sourceRoot, { throwIfNoEntry: false })?.isDirectory())
|
|
151
|
+
throw new common_1.UsageError({
|
|
152
|
+
title: `Layer path not found: ${(0, output_1.showPath)(sourceRoot)}`,
|
|
153
|
+
fixes: [verify_source_result_1.LAYER_PATH_FIX],
|
|
154
|
+
});
|
|
155
|
+
const roots = (0, node_fs_1.existsSync)((0, node_path_1.join)(sourceRoot, FRONTEND_ENTRY))
|
|
156
|
+
? [sourceRoot]
|
|
157
|
+
: (0, node_fs_1.readdirSync)(sourceRoot, { withFileTypes: true })
|
|
158
|
+
.filter((entry) => entry.isDirectory())
|
|
159
|
+
.map((entry) => (0, node_path_1.join)(sourceRoot, entry.name));
|
|
160
|
+
if (roots.length === 0)
|
|
161
|
+
throw new common_1.UsageError({
|
|
162
|
+
title: `No frontend package in ${(0, output_1.showPath)(sourceRoot)}`,
|
|
163
|
+
fixes: [verify_source_result_1.LAYER_PATH_FIX],
|
|
164
|
+
});
|
|
165
|
+
return { sourceRoot, layers: roots.map(readLayer) };
|
|
166
|
+
}
|
|
167
|
+
function readSources(options) {
|
|
168
|
+
const { projectDir, layerSource, localPackages } = options;
|
|
169
|
+
const modules = (options.moduleSources.length > 0 || layerSource
|
|
170
|
+
? options.moduleSources.map((root) => (0, node_path_1.resolve)(root))
|
|
171
|
+
: [defaultModuleRoot(projectDir)]).map(readLayer);
|
|
172
|
+
if (layerSource) {
|
|
173
|
+
const { sourceRoot, layers } = readLayerSource(layerSource);
|
|
174
|
+
if (!layers.some(isCoreLayer))
|
|
175
|
+
throw new common_1.UsageError(notCoreLayer(sourceRoot, layers, modules, localPackages));
|
|
176
|
+
return {
|
|
177
|
+
layers: [...layers, ...modules],
|
|
178
|
+
coreLayer: `the DMS core layer in ${(0, output_1.showPath)(sourceRoot)}`,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
const passedCore = modules.find(isCoreLayer);
|
|
182
|
+
if (passedCore)
|
|
183
|
+
return {
|
|
184
|
+
layers: [passedCore, ...modules.filter((layer) => layer !== passedCore)],
|
|
185
|
+
coreLayer: `the DMS core layer in ${(0, output_1.showPath)(passedCore.path)}`,
|
|
186
|
+
};
|
|
187
|
+
const installed = installedCoreLayer(projectDir);
|
|
188
|
+
return { ...installed, layers: [...installed.layers, ...modules] };
|
|
189
|
+
}
|
package/dist/workspace.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -37,6 +37,7 @@ interface PageRoute {
|
|
|
37
37
|
interface PagePayload {
|
|
38
38
|
layout?: PageLayout;
|
|
39
39
|
route?: PageRoute;
|
|
40
|
+
module?: string;
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
interface DynamicPageProps {
|
|
@@ -55,7 +56,8 @@ function resolveDefinition(
|
|
|
55
56
|
return {
|
|
56
57
|
id,
|
|
57
58
|
component: definition.componentName
|
|
58
|
-
? resolveDmsComponent(definition.componentName) ||
|
|
59
|
+
? resolveDmsComponent(definition.componentName, props.page.module) ||
|
|
60
|
+
"div"
|
|
59
61
|
: "div",
|
|
60
62
|
componentName: definition.componentName,
|
|
61
63
|
options: definition.options,
|
|
@@ -6,6 +6,7 @@ import { en as uiEn, fr as uiFr } from "@nuxt/ui/locale";
|
|
|
6
6
|
import ui from "@nuxt/ui/vue-plugin";
|
|
7
7
|
import { useHead } from "@unhead/vue";
|
|
8
8
|
import type { VueHeadClient } from "@unhead/vue/types";
|
|
9
|
+
import type { IconifyIcon } from "@iconify/vue";
|
|
9
10
|
import type { FetchOptions } from "ofetch";
|
|
10
11
|
import {
|
|
11
12
|
type App,
|
|
@@ -23,7 +24,9 @@ import DmsDynamicPage from "./DmsDynamicPage.vue";
|
|
|
23
24
|
import {
|
|
24
25
|
type DmsLocaleLoader,
|
|
25
26
|
type DmsPageProps,
|
|
27
|
+
type DmsUser,
|
|
26
28
|
createDmsFrontendRuntime,
|
|
29
|
+
dmsPageSuspenseProps,
|
|
27
30
|
getDmsErrorPage,
|
|
28
31
|
getDmsLayout,
|
|
29
32
|
getDmsLayoutProps,
|
|
@@ -39,7 +42,13 @@ import {
|
|
|
39
42
|
useDmsAppConfig,
|
|
40
43
|
useDmsState,
|
|
41
44
|
useError,
|
|
45
|
+
useUserSession,
|
|
42
46
|
} from "./frontend-module";
|
|
47
|
+
import {
|
|
48
|
+
SSR_ICONS_ID,
|
|
49
|
+
addDmsIcons,
|
|
50
|
+
drawLoadedIconsAtOnce,
|
|
51
|
+
} from "./icon-hydration";
|
|
43
52
|
import {
|
|
44
53
|
loadLocaleMessages,
|
|
45
54
|
localeMessages,
|
|
@@ -94,30 +103,24 @@ function uiLocale(locale: string): UiLocale {
|
|
|
94
103
|
return UI_LOCALES[locale as keyof typeof UI_LOCALES] ?? UI_LOCALES.en;
|
|
95
104
|
}
|
|
96
105
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
106
|
+
/** The supported locale of a user's language, if it has one. */
|
|
107
|
+
function userLocale(user: DmsUser | null | undefined): string | undefined {
|
|
108
|
+
const language = user?.language;
|
|
109
|
+
if (typeof language !== "string") return undefined;
|
|
100
110
|
const locale = language.slice(0, 2);
|
|
101
|
-
return supportedLocales.includes(locale) ? locale :
|
|
111
|
+
return supportedLocales.includes(locale) ? locale : undefined;
|
|
102
112
|
}
|
|
103
113
|
|
|
104
|
-
function
|
|
105
|
-
if (typeof document === "undefined") return
|
|
106
|
-
const element = document.getElementById(
|
|
107
|
-
if (!element?.textContent) return
|
|
108
|
-
const
|
|
114
|
+
function readServerRendered<T>(id: string, empty: T): T {
|
|
115
|
+
if (typeof document === "undefined") return empty;
|
|
116
|
+
const element = document.getElementById(id);
|
|
117
|
+
if (!element?.textContent) return empty;
|
|
118
|
+
const value = JSON.parse(element.textContent) as T;
|
|
109
119
|
element.remove();
|
|
110
|
-
return
|
|
120
|
+
return value;
|
|
111
121
|
}
|
|
112
122
|
|
|
113
|
-
|
|
114
|
-
if (typeof document === "undefined") return [];
|
|
115
|
-
const element = document.getElementById(SSR_ASYNC_COMPONENTS_ID);
|
|
116
|
-
if (!element?.textContent) return [];
|
|
117
|
-
const names = JSON.parse(element.textContent) as string[];
|
|
118
|
-
element.remove();
|
|
119
|
-
return names;
|
|
120
|
-
}
|
|
123
|
+
drawLoadedIconsAtOnce();
|
|
121
124
|
|
|
122
125
|
const DmsInertiaPage = defineComponent({
|
|
123
126
|
name: "DmsInertiaPage",
|
|
@@ -148,17 +151,25 @@ const DmsPersistentLayout = defineComponent({
|
|
|
148
151
|
setup(_, { attrs, slots }) {
|
|
149
152
|
const props = computed(() => attrs as unknown as DmsPageProps);
|
|
150
153
|
const i18n = useI18n();
|
|
154
|
+
const { user } = useUserSession();
|
|
151
155
|
let localeVersion = 0;
|
|
152
156
|
useHead({
|
|
153
157
|
htmlAttrs: { lang: computed(() => i18n.locale.value) },
|
|
154
158
|
});
|
|
155
159
|
watch(
|
|
156
160
|
() => [attrs.path, attrs.page, attrs.user, attrs.session, attrs.error],
|
|
157
|
-
|
|
161
|
+
() => hydrateDmsPageProps(props.value),
|
|
162
|
+
{ immediate: true, flush: "sync" },
|
|
163
|
+
);
|
|
164
|
+
// The session, not the page props: a page restored from history or from
|
|
165
|
+
// the prefetch cache carries the user as it was when it was fetched, and
|
|
166
|
+
// would switch the interface back to a language the user has left. A
|
|
167
|
+
// session without a language keeps the one on screen.
|
|
168
|
+
watch(
|
|
169
|
+
() => userLocale(user.value),
|
|
170
|
+
async (locale) => {
|
|
158
171
|
const version = ++localeVersion;
|
|
159
|
-
|
|
160
|
-
const locale = pageLocale(props.value);
|
|
161
|
-
if (i18n.locale.value === locale) return;
|
|
172
|
+
if (!locale || i18n.locale.value === locale) return;
|
|
162
173
|
const messages = await loadLocaleMessages(locale);
|
|
163
174
|
if (version !== localeVersion) return;
|
|
164
175
|
i18n.setLocaleMessage(locale, messages);
|
|
@@ -205,7 +216,7 @@ function renderDmsPage(
|
|
|
205
216
|
error,
|
|
206
217
|
key: props.path,
|
|
207
218
|
});
|
|
208
|
-
return h(Suspense,
|
|
219
|
+
return h(Suspense, dmsPageSuspenseProps(), { default: () => pageContent });
|
|
209
220
|
}
|
|
210
221
|
|
|
211
222
|
function renderDmsPersistentLayout(
|
|
@@ -247,9 +258,9 @@ export async function configureDmsApp(
|
|
|
247
258
|
options.runtime ??
|
|
248
259
|
createDmsFrontendRuntime(
|
|
249
260
|
options.serverFetch as typeof import("ofetch").ofetch,
|
|
250
|
-
|
|
261
|
+
readServerRendered<Record<string, unknown>>(SSR_ASYNC_DATA_ID, {}),
|
|
251
262
|
);
|
|
252
|
-
const locale =
|
|
263
|
+
const locale = userLocale(options.initialPageProps.user) ?? DEFAULT_LOCALE;
|
|
253
264
|
const messages = await loadLocaleMessages(locale);
|
|
254
265
|
const i18n = createI18n({
|
|
255
266
|
legacy: false,
|
|
@@ -266,6 +277,9 @@ export async function configureDmsApp(
|
|
|
266
277
|
options.app.runWithContext(() =>
|
|
267
278
|
hydrateDmsPageProps(options.initialPageProps, options.initialPageUrl),
|
|
268
279
|
);
|
|
280
|
+
addDmsIcons(
|
|
281
|
+
readServerRendered<Record<string, IconifyIcon>>(SSR_ICONS_ID, {}),
|
|
282
|
+
);
|
|
269
283
|
if (typeof window !== "undefined") {
|
|
270
284
|
const toast = options.app.runWithContext(() => useToast());
|
|
271
285
|
showNetworkErrors(toast.add, i18n.global);
|
|
@@ -275,7 +289,9 @@ export async function configureDmsApp(
|
|
|
275
289
|
// Hydration then adopts the server-rendered markup instead of discarding it.
|
|
276
290
|
await Promise.all([
|
|
277
291
|
preloadDmsPage(options.initialPageProps),
|
|
278
|
-
resolveDmsAsyncComponents(
|
|
292
|
+
resolveDmsAsyncComponents(
|
|
293
|
+
readServerRendered<string[]>(SSR_ASYNC_COMPONENTS_ID, []),
|
|
294
|
+
),
|
|
279
295
|
]);
|
|
280
296
|
const mounted = await installDmsPlugins(
|
|
281
297
|
options.app,
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { isAbsolute, posix, resolve } from "node:path";
|
|
2
|
+
import { normalizePath } from "vite";
|
|
3
|
+
import type { DmsFrontendBuildSetup } from "./frontend-build";
|
|
4
|
+
|
|
5
|
+
/** A module's `dms.frontend.build.ts`, as the generated loader imports it. */
|
|
6
|
+
export interface FrontendBuildDeclaration {
|
|
7
|
+
/** The module's id in the generated module registry. */
|
|
8
|
+
id: string;
|
|
9
|
+
setup: DmsFrontendBuildSetup;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface FrontendModuleRoot {
|
|
13
|
+
id: string;
|
|
14
|
+
root: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface AutoImportDirectory {
|
|
18
|
+
glob: string;
|
|
19
|
+
types: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** A module's private code: never auto-imported, whatever it declares. */
|
|
23
|
+
const PRIVATE_DIRECTORY = /(?:^|\/)app\/build(?:\/|$)/;
|
|
24
|
+
|
|
25
|
+
function declaredDirectories(setup: DmsFrontendBuildSetup): string[] {
|
|
26
|
+
const directories: string[] = [];
|
|
27
|
+
setup({
|
|
28
|
+
registerAutoImports: (entries) => directories.push(...entries),
|
|
29
|
+
});
|
|
30
|
+
return directories;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function rejection(directory: string): string | undefined {
|
|
34
|
+
const path = posix.normalize(normalizePath(directory));
|
|
35
|
+
if (isAbsolute(directory) || path === ".." || path.startsWith("../"))
|
|
36
|
+
return "it is not inside the module";
|
|
37
|
+
if (PRIVATE_DIRECTORY.test(path))
|
|
38
|
+
return "app/build/ holds the module's private code, which is never auto-imported";
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The directories the modules declared in their `dms.frontend.build.ts`, as
|
|
44
|
+
* the auto-importer's scan globs. Each module's `app/build/` directories are
|
|
45
|
+
* excluded even from a broader declaration; a declaration naming one, or
|
|
46
|
+
* leaving the module, is ignored with a warning.
|
|
47
|
+
*
|
|
48
|
+
* Globs are POSIX on every platform: the scanner reads a backslash as an
|
|
49
|
+
* escape, not as a separator.
|
|
50
|
+
*/
|
|
51
|
+
export function autoImportDirectories(
|
|
52
|
+
modules: readonly FrontendModuleRoot[],
|
|
53
|
+
declarations: readonly FrontendBuildDeclaration[],
|
|
54
|
+
warn: (message: string) => void,
|
|
55
|
+
): AutoImportDirectory[] {
|
|
56
|
+
const roots = new Map(modules.map((module) => [module.id, module.root]));
|
|
57
|
+
return declarations.flatMap(({ id, setup }) => {
|
|
58
|
+
const root = roots.get(id);
|
|
59
|
+
if (root === undefined) return [];
|
|
60
|
+
const scan = (pattern: string) => normalizePath(resolve(root, pattern));
|
|
61
|
+
const accepted = declaredDirectories(setup).filter((directory) => {
|
|
62
|
+
const reason = rejection(directory);
|
|
63
|
+
if (reason)
|
|
64
|
+
warn(
|
|
65
|
+
`Frontend module ${id} declares "${directory}" for auto-import, which is ignored: ${reason}.`,
|
|
66
|
+
);
|
|
67
|
+
return !reason;
|
|
68
|
+
});
|
|
69
|
+
if (accepted.length === 0) return [];
|
|
70
|
+
return [
|
|
71
|
+
...accepted.map((directory) => ({
|
|
72
|
+
glob: scan(`${directory}/**/*`),
|
|
73
|
+
types: true,
|
|
74
|
+
})),
|
|
75
|
+
{ glob: `!${scan("**/app/build/**")}`, types: true },
|
|
76
|
+
];
|
|
77
|
+
});
|
|
78
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a frontend module's `dms.frontend.build.ts` declares about how the
|
|
3
|
+
* application is built from it. Read when Vite starts, by `ajs dms dev` and
|
|
4
|
+
* `ajs dms build` alike.
|
|
5
|
+
*/
|
|
6
|
+
export interface DmsFrontendBuild {
|
|
7
|
+
/**
|
|
8
|
+
* Makes every export of the files under these directories available to
|
|
9
|
+
* the whole application without an import, in scripts and templates.
|
|
10
|
+
*
|
|
11
|
+
* Each entry is a directory relative to the module root, and may hold glob
|
|
12
|
+
* patterns: `layers/*\/app/composables` covers that directory of every
|
|
13
|
+
* layer. Nothing under an `app/build/` directory is ever auto-imported: it
|
|
14
|
+
* holds the module's private code, which its own files import by path.
|
|
15
|
+
*/
|
|
16
|
+
registerAutoImports(directories: readonly string[]): void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type DmsFrontendBuildSetup = (build: DmsFrontendBuild) => void;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Declares how the application is built from a frontend module, as the
|
|
23
|
+
* default export of its `dms.frontend.build.ts`:
|
|
24
|
+
*
|
|
25
|
+
* ```ts
|
|
26
|
+
* import { defineDmsFrontendBuild } from "#dms/frontend-build";
|
|
27
|
+
*
|
|
28
|
+
* export default defineDmsFrontendBuild((build) => {
|
|
29
|
+
* build.registerAutoImports(["layers/*\/app/composables"]);
|
|
30
|
+
* });
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* A module without this file has nothing auto-imported. The file runs in
|
|
34
|
+
* Node.js when Vite starts, not in the application: it may import Node.js
|
|
35
|
+
* built-ins, never the application's own code.
|
|
36
|
+
*/
|
|
37
|
+
export function defineDmsFrontendBuild(
|
|
38
|
+
setup: DmsFrontendBuildSetup,
|
|
39
|
+
): DmsFrontendBuildSetup {
|
|
40
|
+
return setup;
|
|
41
|
+
}
|