keycloakify 11.1.0 → 11.2.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.
@@ -1,16 +1,56 @@
1
1
  import type { BuildContext } from "../../shared/buildContext";
2
- import { assert } from "tsafe/assert";
3
- import {
4
- generateResourcesForMainTheme,
5
- type BuildContextLike as BuildContextLike_generateResourcesForMainTheme
6
- } from "./generateResourcesForMainTheme";
7
- import { generateResourcesForThemeVariant } from "./generateResourcesForThemeVariant";
8
2
  import fs from "fs";
9
3
  import { rmSync } from "../../tools/fs.rmSync";
4
+ import { transformCodebase } from "../../tools/transformCodebase";
5
+ import {
6
+ join as pathJoin,
7
+ relative as pathRelative,
8
+ dirname as pathDirname,
9
+ extname as pathExtname,
10
+ sep as pathSep
11
+ } from "path";
12
+ import { replaceImportsInJsCode } from "../replacers/replaceImportsInJsCode";
13
+ import { replaceImportsInCssCode } from "../replacers/replaceImportsInCssCode";
14
+ import {
15
+ generateFtlFilesCodeFactory,
16
+ type BuildContextLike as BuildContextLike_kcContextExclusionsFtlCode
17
+ } from "../generateFtl";
18
+ import {
19
+ type ThemeType,
20
+ LOGIN_THEME_PAGE_IDS,
21
+ ACCOUNT_THEME_PAGE_IDS,
22
+ WELL_KNOWN_DIRECTORY_BASE_NAME
23
+ } from "../../shared/constants";
24
+ import { assert, type Equals } from "tsafe/assert";
25
+ import { readFieldNameUsage } from "./readFieldNameUsage";
26
+ import { readExtraPagesNames } from "./readExtraPageNames";
27
+ import {
28
+ generateMessageProperties,
29
+ type BuildContextLike as BuildContextLike_generateMessageProperties
30
+ } from "./generateMessageProperties";
31
+ import { readThisNpmPackageVersion } from "../../tools/readThisNpmPackageVersion";
32
+ import {
33
+ writeMetaInfKeycloakThemes,
34
+ type MetaInfKeycloakTheme
35
+ } from "../../shared/metaInfKeycloakThemes";
36
+ import { objectEntries } from "tsafe/objectEntries";
37
+ import { escapeStringForPropertiesFile } from "../../tools/escapeStringForPropertiesFile";
38
+ import * as child_process from "child_process";
39
+ import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath";
40
+ import propertiesParser from "properties-parser";
10
41
 
11
- export type BuildContextLike = BuildContextLike_generateResourcesForMainTheme & {
12
- themeNames: string[];
13
- };
42
+ export type BuildContextLike = BuildContextLike_kcContextExclusionsFtlCode &
43
+ BuildContextLike_generateMessageProperties & {
44
+ themeNames: string[];
45
+ extraThemeProperties: string[] | undefined;
46
+ projectDirPath: string;
47
+ projectBuildDirPath: string;
48
+ environmentVariables: { name: string; default: string }[];
49
+ implementedThemeTypes: BuildContext["implementedThemeTypes"];
50
+ themeSrcDirPath: string;
51
+ bundler: "vite" | "webpack";
52
+ packageJsonFilePath: string;
53
+ };
14
54
 
15
55
  assert<BuildContext extends BuildContextLike ? true : false>();
16
56
 
@@ -20,25 +60,443 @@ export async function generateResources(params: {
20
60
  }): Promise<void> {
21
61
  const { resourcesDirPath, buildContext } = params;
22
62
 
23
- const [themeName, ...themeVariantNames] = buildContext.themeNames;
63
+ const [themeName] = buildContext.themeNames;
24
64
 
25
65
  if (fs.existsSync(resourcesDirPath)) {
26
66
  rmSync(resourcesDirPath, { recursive: true });
27
67
  }
28
68
 
29
- const { writeMessagePropertiesFilesForThemeVariant } =
30
- await generateResourcesForMainTheme({
31
- resourcesDirPath,
69
+ const getThemeTypeDirPath = (params: {
70
+ themeType: ThemeType | "email";
71
+ themeName: string;
72
+ }) => {
73
+ const { themeType, themeName } = params;
74
+ return pathJoin(resourcesDirPath, "theme", themeName, themeType);
75
+ };
76
+
77
+ const writeMessagePropertiesFilesByThemeType: Partial<
78
+ Record<ThemeType, (params: { messageDirPath: string; themeName: string }) => void>
79
+ > = {};
80
+
81
+ for (const themeType of ["login", "account"] as const) {
82
+ if (!buildContext.implementedThemeTypes[themeType].isImplemented) {
83
+ continue;
84
+ }
85
+
86
+ const isForAccountSpa =
87
+ themeType === "account" &&
88
+ (assert(buildContext.implementedThemeTypes.account.isImplemented),
89
+ buildContext.implementedThemeTypes.account.type === "Single-Page");
90
+
91
+ const themeTypeDirPath = getThemeTypeDirPath({ themeName, themeType });
92
+
93
+ apply_replacers_and_move_to_theme_resources: {
94
+ const destDirPath = pathJoin(
95
+ themeTypeDirPath,
96
+ "resources",
97
+ WELL_KNOWN_DIRECTORY_BASE_NAME.DIST
98
+ );
99
+
100
+ // NOTE: Prevent accumulation of files in the assets dir, as names are hashed they pile up.
101
+ rmSync(destDirPath, { recursive: true, force: true });
102
+
103
+ if (
104
+ themeType === "account" &&
105
+ buildContext.implementedThemeTypes.login.isImplemented
106
+ ) {
107
+ // NOTE: We prevent doing it twice, it has been done for the login theme.
108
+
109
+ transformCodebase({
110
+ srcDirPath: pathJoin(
111
+ getThemeTypeDirPath({
112
+ themeName,
113
+ themeType: "login"
114
+ }),
115
+ "resources",
116
+ WELL_KNOWN_DIRECTORY_BASE_NAME.DIST
117
+ ),
118
+ destDirPath
119
+ });
120
+
121
+ break apply_replacers_and_move_to_theme_resources;
122
+ }
123
+
124
+ {
125
+ const dirPath = pathJoin(
126
+ buildContext.projectBuildDirPath,
127
+ WELL_KNOWN_DIRECTORY_BASE_NAME.KEYCLOAKIFY_DEV_RESOURCES
128
+ );
129
+
130
+ if (fs.existsSync(dirPath)) {
131
+ assert(buildContext.bundler === "webpack");
132
+
133
+ throw new Error(
134
+ [
135
+ `Keycloakify build error: The ${WELL_KNOWN_DIRECTORY_BASE_NAME.KEYCLOAKIFY_DEV_RESOURCES} directory shouldn't exist in your build directory.`,
136
+ `(${pathRelative(process.cwd(), dirPath)}).\n`,
137
+ `Theses assets are only required for local development with Storybook.",
138
+ "Please remove this directory as an additional step of your command.\n`,
139
+ `For example: \`"build": "... && rimraf ${pathRelative(buildContext.projectDirPath, dirPath)}"\``
140
+ ].join(" ")
141
+ );
142
+ }
143
+ }
144
+
145
+ transformCodebase({
146
+ srcDirPath: buildContext.projectBuildDirPath,
147
+ destDirPath,
148
+ transformSourceCode: ({ filePath, fileRelativePath, sourceCode }) => {
149
+ if (filePath.endsWith(".css")) {
150
+ const { fixedCssCode } = replaceImportsInCssCode({
151
+ cssCode: sourceCode.toString("utf8"),
152
+ cssFileRelativeDirPath: pathDirname(fileRelativePath),
153
+ buildContext
154
+ });
155
+
156
+ return {
157
+ modifiedSourceCode: Buffer.from(fixedCssCode, "utf8")
158
+ };
159
+ }
160
+
161
+ if (filePath.endsWith(".js")) {
162
+ const { fixedJsCode } = replaceImportsInJsCode({
163
+ jsCode: sourceCode.toString("utf8"),
164
+ buildContext
165
+ });
166
+
167
+ return {
168
+ modifiedSourceCode: Buffer.from(fixedJsCode, "utf8")
169
+ };
170
+ }
171
+
172
+ return { modifiedSourceCode: sourceCode };
173
+ }
174
+ });
175
+ }
176
+
177
+ const { generateFtlFilesCode } = generateFtlFilesCodeFactory({
32
178
  themeName,
33
- buildContext
179
+ indexHtmlCode: fs
180
+ .readFileSync(pathJoin(buildContext.projectBuildDirPath, "index.html"))
181
+ .toString("utf8"),
182
+ buildContext,
183
+ keycloakifyVersion: readThisNpmPackageVersion(),
184
+ themeType,
185
+ fieldNames: readFieldNameUsage({
186
+ themeSrcDirPath: buildContext.themeSrcDirPath,
187
+ themeType
188
+ })
189
+ });
190
+
191
+ [
192
+ ...(() => {
193
+ switch (themeType) {
194
+ case "login":
195
+ return LOGIN_THEME_PAGE_IDS;
196
+ case "account":
197
+ return isForAccountSpa ? ["index.ftl"] : ACCOUNT_THEME_PAGE_IDS;
198
+ }
199
+ })(),
200
+ ...(isForAccountSpa
201
+ ? []
202
+ : readExtraPagesNames({
203
+ themeType,
204
+ themeSrcDirPath: buildContext.themeSrcDirPath
205
+ }))
206
+ ].forEach(pageId => {
207
+ const { ftlCode } = generateFtlFilesCode({ pageId });
208
+
209
+ fs.writeFileSync(
210
+ pathJoin(themeTypeDirPath, pageId),
211
+ Buffer.from(ftlCode, "utf8")
212
+ );
34
213
  });
35
214
 
36
- for (const themeVariantName of themeVariantNames) {
37
- generateResourcesForThemeVariant({
215
+ let languageTags: string[] | undefined = undefined;
216
+
217
+ i18n_messages_generation: {
218
+ if (isForAccountSpa) {
219
+ break i18n_messages_generation;
220
+ }
221
+
222
+ const wrap = generateMessageProperties({
223
+ buildContext,
224
+ themeType
225
+ });
226
+
227
+ languageTags = wrap.languageTags;
228
+ const { writeMessagePropertiesFiles } = wrap;
229
+
230
+ writeMessagePropertiesFilesByThemeType[themeType] =
231
+ writeMessagePropertiesFiles;
232
+ }
233
+
234
+ bring_in_account_v3_i18n_messages: {
235
+ if (!buildContext.implementedThemeTypes.account.isImplemented) {
236
+ break bring_in_account_v3_i18n_messages;
237
+ }
238
+ if (buildContext.implementedThemeTypes.account.type !== "Single-Page") {
239
+ break bring_in_account_v3_i18n_messages;
240
+ }
241
+
242
+ const accountUiDirPath = child_process
243
+ .execSync("npm list @keycloakify/keycloak-account-ui --parseable", {
244
+ cwd: pathDirname(buildContext.packageJsonFilePath)
245
+ })
246
+ .toString("utf8")
247
+ .trim();
248
+
249
+ const messageDirPath_defaults = pathJoin(accountUiDirPath, "messages");
250
+
251
+ if (!fs.existsSync(messageDirPath_defaults)) {
252
+ throw new Error(
253
+ `Please update @keycloakify/keycloak-account-ui to 25.0.4-rc.5 or later.`
254
+ );
255
+ }
256
+
257
+ const messagesDirPath_dest = pathJoin(
258
+ getThemeTypeDirPath({ themeName, themeType: "account" }),
259
+ "messages"
260
+ );
261
+
262
+ transformCodebase({
263
+ srcDirPath: messageDirPath_defaults,
264
+ destDirPath: messagesDirPath_dest
265
+ });
266
+
267
+ apply_theme_changes: {
268
+ const messagesDirPath_theme = pathJoin(
269
+ buildContext.themeSrcDirPath,
270
+ "account",
271
+ "messages"
272
+ );
273
+
274
+ if (!fs.existsSync(messagesDirPath_theme)) {
275
+ break apply_theme_changes;
276
+ }
277
+
278
+ fs.readdirSync(messagesDirPath_theme).forEach(basename => {
279
+ const filePath_src = pathJoin(messagesDirPath_theme, basename);
280
+ const filePath_dest = pathJoin(messagesDirPath_dest, basename);
281
+
282
+ if (!fs.existsSync(filePath_dest)) {
283
+ fs.cpSync(filePath_src, filePath_dest);
284
+ }
285
+
286
+ const messages_src = propertiesParser.parse(
287
+ fs.readFileSync(filePath_src).toString("utf8")
288
+ );
289
+ const messages_dest = propertiesParser.parse(
290
+ fs.readFileSync(filePath_dest).toString("utf8")
291
+ );
292
+
293
+ const messages = {
294
+ ...messages_dest,
295
+ ...messages_src
296
+ };
297
+
298
+ const editor = propertiesParser.createEditor();
299
+
300
+ Object.entries(messages).forEach(([key, value]) => {
301
+ editor.set(key, value);
302
+ });
303
+
304
+ fs.writeFileSync(
305
+ filePath_dest,
306
+ Buffer.from(editor.toString(), "utf8")
307
+ );
308
+ });
309
+ }
310
+
311
+ languageTags = fs
312
+ .readdirSync(messagesDirPath_dest)
313
+ .map(basename =>
314
+ basename.replace(/^messages_/, "").replace(/\.properties$/, "")
315
+ );
316
+ }
317
+
318
+ keycloak_static_resources: {
319
+ if (isForAccountSpa) {
320
+ break keycloak_static_resources;
321
+ }
322
+
323
+ transformCodebase({
324
+ srcDirPath: pathJoin(
325
+ getThisCodebaseRootDirPath(),
326
+ "res",
327
+ "public",
328
+ WELL_KNOWN_DIRECTORY_BASE_NAME.KEYCLOAKIFY_DEV_RESOURCES,
329
+ themeType
330
+ ),
331
+ destDirPath: pathJoin(themeTypeDirPath, "resources")
332
+ });
333
+ }
334
+
335
+ fs.writeFileSync(
336
+ pathJoin(themeTypeDirPath, "theme.properties"),
337
+ Buffer.from(
338
+ [
339
+ `parent=${(() => {
340
+ switch (themeType) {
341
+ case "account":
342
+ return isForAccountSpa ? "base" : "account-v1";
343
+ case "login":
344
+ return "keycloak";
345
+ }
346
+ assert<Equals<typeof themeType, never>>(false);
347
+ })()}`,
348
+ ...(isForAccountSpa ? ["deprecatedMode=false"] : []),
349
+ ...(buildContext.extraThemeProperties ?? []),
350
+ ...buildContext.environmentVariables.map(
351
+ ({ name, default: defaultValue }) =>
352
+ `${name}=\${env.${name}:${escapeStringForPropertiesFile(defaultValue)}}`
353
+ ),
354
+ ...(languageTags === undefined
355
+ ? []
356
+ : [`locales=${languageTags.join(",")}`])
357
+ ].join("\n\n"),
358
+ "utf8"
359
+ )
360
+ );
361
+ }
362
+
363
+ email: {
364
+ if (!buildContext.implementedThemeTypes.email.isImplemented) {
365
+ break email;
366
+ }
367
+
368
+ const emailThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "email");
369
+
370
+ transformCodebase({
371
+ srcDirPath: emailThemeSrcDirPath,
372
+ destDirPath: getThemeTypeDirPath({ themeName, themeType: "email" })
373
+ });
374
+ }
375
+
376
+ bring_in_account_v1: {
377
+ if (!buildContext.implementedThemeTypes.account.isImplemented) {
378
+ break bring_in_account_v1;
379
+ }
380
+
381
+ if (buildContext.implementedThemeTypes.account.type !== "Multi-Page") {
382
+ break bring_in_account_v1;
383
+ }
384
+
385
+ transformCodebase({
386
+ srcDirPath: pathJoin(getThisCodebaseRootDirPath(), "res", "account-v1"),
387
+ destDirPath: getThemeTypeDirPath({
388
+ themeName: "account-v1",
389
+ themeType: "account"
390
+ })
391
+ });
392
+ }
393
+
394
+ {
395
+ const metaInfKeycloakThemes: MetaInfKeycloakTheme = { themes: [] };
396
+
397
+ for (const themeName of buildContext.themeNames) {
398
+ metaInfKeycloakThemes.themes.push({
399
+ name: themeName,
400
+ types: objectEntries(buildContext.implementedThemeTypes)
401
+ .filter(([, { isImplemented }]) => isImplemented)
402
+ .map(([themeType]) => themeType)
403
+ });
404
+ }
405
+
406
+ if (buildContext.implementedThemeTypes.account.isImplemented) {
407
+ metaInfKeycloakThemes.themes.push({
408
+ name: "account-v1",
409
+ types: ["account"]
410
+ });
411
+ }
412
+
413
+ writeMetaInfKeycloakThemes({
38
414
  resourcesDirPath,
39
- themeName,
40
- themeVariantName,
41
- writeMessagePropertiesFiles: writeMessagePropertiesFilesForThemeVariant
415
+ getNewMetaInfKeycloakTheme: () => metaInfKeycloakThemes
416
+ });
417
+ }
418
+
419
+ for (const themeVariantName of buildContext.themeNames) {
420
+ if (themeVariantName === themeName) {
421
+ continue;
422
+ }
423
+
424
+ transformCodebase({
425
+ srcDirPath: pathJoin(resourcesDirPath, "theme", themeName),
426
+ destDirPath: pathJoin(resourcesDirPath, "theme", themeVariantName),
427
+ transformSourceCode: ({ fileRelativePath, sourceCode }) => {
428
+ if (
429
+ pathExtname(fileRelativePath) === ".ftl" &&
430
+ fileRelativePath.split(pathSep).length === 2
431
+ ) {
432
+ const modifiedSourceCode = Buffer.from(
433
+ Buffer.from(sourceCode)
434
+ .toString("utf-8")
435
+ .replace(
436
+ `"themeName": "${themeName}"`,
437
+ `"themeName": "${themeVariantName}"`
438
+ ),
439
+ "utf8"
440
+ );
441
+
442
+ assert(Buffer.compare(modifiedSourceCode, sourceCode) !== 0);
443
+
444
+ return { modifiedSourceCode };
445
+ }
446
+
447
+ return { modifiedSourceCode: sourceCode };
448
+ }
42
449
  });
43
450
  }
451
+
452
+ for (const themeName of buildContext.themeNames) {
453
+ for (const [themeType, writeMessagePropertiesFiles] of objectEntries(
454
+ writeMessagePropertiesFilesByThemeType
455
+ )) {
456
+ // NOTE: This is just a quirk of the type system: We can't really differentiate in a record
457
+ // between the case where the key isn't present and the case where the value is `undefined`.
458
+ if (writeMessagePropertiesFiles === undefined) {
459
+ return;
460
+ }
461
+ writeMessagePropertiesFiles({
462
+ messageDirPath: pathJoin(
463
+ getThemeTypeDirPath({ themeName, themeType }),
464
+ "messages"
465
+ ),
466
+ themeName
467
+ });
468
+ }
469
+ }
470
+
471
+ modify_email_theme_per_variant: {
472
+ if (!buildContext.implementedThemeTypes.email.isImplemented) {
473
+ break modify_email_theme_per_variant;
474
+ }
475
+
476
+ for (const themeName of buildContext.themeNames) {
477
+ const emailThemeDirPath = getThemeTypeDirPath({
478
+ themeName,
479
+ themeType: "email"
480
+ });
481
+
482
+ transformCodebase({
483
+ srcDirPath: emailThemeDirPath,
484
+ destDirPath: emailThemeDirPath,
485
+ transformSourceCode: ({ filePath, sourceCode }) => {
486
+ if (!filePath.endsWith(".ftl")) {
487
+ return { modifiedSourceCode: sourceCode };
488
+ }
489
+
490
+ return {
491
+ modifiedSourceCode: Buffer.from(
492
+ sourceCode
493
+ .toString("utf8")
494
+ .replace(/xKeycloakify\.themeName/g, `"${themeName}"`),
495
+ "utf8"
496
+ )
497
+ };
498
+ }
499
+ });
500
+ }
501
+ }
44
502
  }
@@ -240,8 +240,7 @@ export function getBuildContext(params: {
240
240
 
241
241
  if (
242
242
  parsedPackageJson.dependencies?.keycloakify === undefined &&
243
- parsedPackageJson.devDependencies?.keycloakify === undefined &&
244
- parsedPackageJson.name !== "keycloakify" // NOTE: For local storybook build
243
+ parsedPackageJson.devDependencies?.keycloakify === undefined
245
244
  ) {
246
245
  break success;
247
246
  }
@@ -470,26 +469,44 @@ export function getBuildContext(params: {
470
469
  }
471
470
 
472
471
  const themeNames = ((): [string, ...string[]] => {
473
- if (buildOptions.themeName === undefined) {
474
- return parsedPackageJson.name === undefined
475
- ? ["keycloakify"]
476
- : [
477
- parsedPackageJson.name
478
- .replace(/^@(.*)/, "$1")
479
- .split("/")
480
- .join("-")
481
- ];
482
- }
472
+ const themeNames = ((): [string, ...string[]] => {
473
+ if (buildOptions.themeName === undefined) {
474
+ return parsedPackageJson.name === undefined
475
+ ? ["keycloakify"]
476
+ : [
477
+ parsedPackageJson.name
478
+ .replace(/^@(.*)/, "$1")
479
+ .split("/")
480
+ .join("-")
481
+ ];
482
+ }
483
483
 
484
- if (typeof buildOptions.themeName === "string") {
485
- return [buildOptions.themeName];
486
- }
484
+ if (typeof buildOptions.themeName === "string") {
485
+ return [buildOptions.themeName];
486
+ }
487
+
488
+ const [mainThemeName, ...themeVariantNames] = buildOptions.themeName;
489
+
490
+ assert(mainThemeName !== undefined);
487
491
 
488
- const [mainThemeName, ...themeVariantNames] = buildOptions.themeName;
492
+ return [mainThemeName, ...themeVariantNames];
493
+ })();
489
494
 
490
- assert(mainThemeName !== undefined);
495
+ for (const themeName of themeNames) {
496
+ if (!/^[a-zA-Z0-9_-]+$/.test(themeName)) {
497
+ console.error(
498
+ chalk.red(
499
+ [
500
+ `Invalid theme name: ${themeName}`,
501
+ `Theme names should only contain letters, numbers, and "_" or "-"`
502
+ ].join(" ")
503
+ )
504
+ );
505
+ process.exit(-1);
506
+ }
507
+ }
491
508
 
492
- return [mainThemeName, ...themeVariantNames];
509
+ return themeNames;
493
510
  })();
494
511
 
495
512
  const projectBuildDirPath = (() => {
@@ -235,9 +235,7 @@ function getBuildContext(params) {
235
235
  })
236
236
  .parse(JSON.parse(external_fs_.readFileSync(packageJsonFilePath).toString("utf8")));
237
237
  if (((_a = parsedPackageJson.dependencies) === null || _a === void 0 ? void 0 : _a.keycloakify) === undefined &&
238
- ((_b = parsedPackageJson.devDependencies) === null || _b === void 0 ? void 0 : _b.keycloakify) === undefined &&
239
- parsedPackageJson.name !== "keycloakify" // NOTE: For local storybook build
240
- ) {
238
+ ((_b = parsedPackageJson.devDependencies) === null || _b === void 0 ? void 0 : _b.keycloakify) === undefined) {
241
239
  break success;
242
240
  }
243
241
  return packageJsonFilePath;
@@ -380,22 +378,34 @@ function getBuildContext(params) {
380
378
  process.exit(-1);
381
379
  }
382
380
  const themeNames = (() => {
383
- if (buildOptions.themeName === undefined) {
384
- return parsedPackageJson.name === undefined
385
- ? ["keycloakify"]
386
- : [
387
- parsedPackageJson.name
388
- .replace(/^@(.*)/, "$1")
389
- .split("/")
390
- .join("-")
391
- ];
392
- }
393
- if (typeof buildOptions.themeName === "string") {
394
- return [buildOptions.themeName];
381
+ const themeNames = (() => {
382
+ if (buildOptions.themeName === undefined) {
383
+ return parsedPackageJson.name === undefined
384
+ ? ["keycloakify"]
385
+ : [
386
+ parsedPackageJson.name
387
+ .replace(/^@(.*)/, "$1")
388
+ .split("/")
389
+ .join("-")
390
+ ];
391
+ }
392
+ if (typeof buildOptions.themeName === "string") {
393
+ return [buildOptions.themeName];
394
+ }
395
+ const [mainThemeName, ...themeVariantNames] = buildOptions.themeName;
396
+ (0,assert.assert)(mainThemeName !== undefined);
397
+ return [mainThemeName, ...themeVariantNames];
398
+ })();
399
+ for (const themeName of themeNames) {
400
+ if (!/^[a-zA-Z0-9_-]+$/.test(themeName)) {
401
+ console.error(source_default().red([
402
+ `Invalid theme name: ${themeName}`,
403
+ `Theme names should only contain letters, numbers, and "_" or "-"`
404
+ ].join(" ")));
405
+ process.exit(-1);
406
+ }
395
407
  }
396
- const [mainThemeName, ...themeVariantNames] = buildOptions.themeName;
397
- (0,assert.assert)(mainThemeName !== undefined);
398
- return [mainThemeName, ...themeVariantNames];
408
+ return themeNames;
399
409
  })();
400
410
  const projectBuildDirPath = (() => {
401
411
  webpack: {