@paroicms/site-generator-plugin 0.1.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 (59) hide show
  1. package/README.md +9 -0
  2. package/gen-backend/dist/context.js +2 -0
  3. package/gen-backend/dist/data-format.js +37 -0
  4. package/gen-backend/dist/generator/actions.js +35 -0
  5. package/gen-backend/dist/generator/fake-content-generator.ts/create-database-with-fake-content.js +227 -0
  6. package/gen-backend/dist/generator/fake-content-generator.ts/create-node-contents.js +156 -0
  7. package/gen-backend/dist/generator/fake-content-generator.ts/fake-content-types.js +1 -0
  8. package/gen-backend/dist/generator/fake-content-generator.ts/generate-fake-content.js +127 -0
  9. package/gen-backend/dist/generator/fake-content-generator.ts/invoke-generate-fake-content.js +49 -0
  10. package/gen-backend/dist/generator/generator-types.js +1 -0
  11. package/gen-backend/dist/generator/helpers/esm-module.helper.js +6 -0
  12. package/gen-backend/dist/generator/helpers/js-utils.js +14 -0
  13. package/gen-backend/dist/generator/lib/common-types.js +1 -0
  14. package/gen-backend/dist/generator/lib/create-prompt.js +44 -0
  15. package/gen-backend/dist/generator/lib/debug-utils.js +118 -0
  16. package/gen-backend/dist/generator/lib/images-lib.js +16 -0
  17. package/gen-backend/dist/generator/lib/llm-invoke-types.js +1 -0
  18. package/gen-backend/dist/generator/lib/llm-tokens.js +10 -0
  19. package/gen-backend/dist/generator/lib/markdown-bulleted-list-parser.js +147 -0
  20. package/gen-backend/dist/generator/lib/parse-llm-response.js +160 -0
  21. package/gen-backend/dist/generator/lib/tasks.js +112 -0
  22. package/gen-backend/dist/generator/lib/utils.js +13 -0
  23. package/gen-backend/dist/generator/llm-queries/invoke-message-guard.js +86 -0
  24. package/gen-backend/dist/generator/llm-queries/invoke-new-site-analysis.js +169 -0
  25. package/gen-backend/dist/generator/llm-queries/invoke-update-site-schema.js +94 -0
  26. package/gen-backend/dist/generator/site-generator/common-template-creator.js +108 -0
  27. package/gen-backend/dist/generator/site-generator/document-template-creator.js +329 -0
  28. package/gen-backend/dist/generator/site-generator/id-key-provider.js +14 -0
  29. package/gen-backend/dist/generator/site-generator/jt-site-schema-helpers.js +55 -0
  30. package/gen-backend/dist/generator/site-generator/site-generator.js +75 -0
  31. package/gen-backend/dist/generator/site-generator/template-creator-types.js +1 -0
  32. package/gen-backend/dist/generator/site-generator/template-helpers.js +26 -0
  33. package/gen-backend/dist/generator/site-generator/theme-creator.js +180 -0
  34. package/gen-backend/dist/generator/site-generator/theme-css.js +323 -0
  35. package/gen-backend/dist/generator/site-schema-generator/analysis-types.js +1 -0
  36. package/gen-backend/dist/generator/site-schema-generator/create-l10n.js +42 -0
  37. package/gen-backend/dist/generator/site-schema-generator/create-site-schema.js +240 -0
  38. package/gen-backend/dist/generator/site-schema-generator/default-pages.js +38 -0
  39. package/gen-backend/dist/plugin.js +86 -0
  40. package/gen-backend/prompts/0-context.md +9 -0
  41. package/gen-backend/prompts/generate-fake-content-multiple.md +22 -0
  42. package/gen-backend/prompts/generate-fake-content-single.md +16 -0
  43. package/gen-backend/prompts/message-guard.md +89 -0
  44. package/gen-backend/prompts/new-site-1-analysis.md +214 -0
  45. package/gen-backend/prompts/new-site-2-fields.md +50 -0
  46. package/gen-backend/prompts/predefined-fields.json +110 -0
  47. package/gen-backend/prompts/test-message1.txt +1 -0
  48. package/gen-backend/prompts/update-site-schema-1-write-details.md +57 -0
  49. package/gen-backend/prompts/update-site-schema-2-execute.md +77 -0
  50. package/gen-front/dist/gen-front.css +1 -0
  51. package/gen-front/dist/gen-front.eot +0 -0
  52. package/gen-front/dist/gen-front.mjs +998 -0
  53. package/gen-front/dist/gen-front.svg +345 -0
  54. package/gen-front/dist/gen-front.ttf +0 -0
  55. package/gen-front/dist/gen-front.woff +0 -0
  56. package/gen-front/dist/gen-front.woff2 +0 -0
  57. package/gen-front/dist/gen-front2.woff2 +0 -0
  58. package/gen-front/dist/gen-front3.woff2 +0 -0
  59. package/package.json +79 -0
@@ -0,0 +1,329 @@
1
+ import { camelToKebabCase } from "../lib/utils.js";
2
+ import { templateOfDocumentBreadcrumb } from "./common-template-creator.js";
3
+ import { createIdKeyProvider } from "./id-key-provider.js";
4
+ import { getJtPartType, getJtRoutingDocumentType, hasTemporalChildren, } from "./jt-site-schema-helpers.js";
5
+ import { getPredefinedDataType, indent, localizedLabelTemplate } from "./template-helpers.js";
6
+ export function templateOfDocumentType(ctx, documentType) {
7
+ const childrenTemplate = templateOfDocumentChildren(ctx, documentType, createIdKeyProvider());
8
+ const featuredImageTemplate = documentType.withFeaturedImage
9
+ ? templateOfPicture({
10
+ imageKey: "doc.featuredImage",
11
+ })
12
+ : undefined;
13
+ const titleFieldsTemplate = templateOfTitleAndFields(ctx, documentType);
14
+ const listTemplates = documentType.lists?.map((list) => templateOfList(ctx, list, { listKey: `doc.list.${list.listName}`, nested: false })) ?? [];
15
+ const specialTemplate = templateOfSpecialDocument(ctx, documentType);
16
+ ctx.addLiquidFile("partials", "breadcrumb.liquid", templateOfDocumentBreadcrumb(), {
17
+ skipIfExists: true,
18
+ });
19
+ const blocks = [
20
+ `{% render "partials/breadcrumb" doc: doc site: site %}`,
21
+ featuredImageTemplate,
22
+ titleFieldsTemplate,
23
+ childrenTemplate,
24
+ specialTemplate,
25
+ ...listTemplates,
26
+ ].filter(Boolean);
27
+ return `{% layout "layouts/main-layout.liquid" doc: doc site: site %}
28
+ {% block %}
29
+ ${blocks.map((block) => indent(block, 1)).join("\n\n")}
30
+ {% endblock %}`;
31
+ }
32
+ function templateOfTitleAndFields(ctx, documentType) {
33
+ const blocks1 = ["<h1>{{ doc.title }}</h1>"];
34
+ const fieldsTemplate = templateOfFields(ctx, documentType.fields, { parentKey: "doc.field" });
35
+ if (fieldsTemplate) {
36
+ blocks1.push(fieldsTemplate);
37
+ }
38
+ const textBlock = `<div class="TextWidth">
39
+ ${indent(blocks1.join("\n"), 1, { skipFirst: true })}
40
+ </div>`;
41
+ const siblingsTemplate = documentType.documentKind === "regular" && templateOfSiblingLinks(ctx);
42
+ const blocks2 = [textBlock, siblingsTemplate].filter(Boolean);
43
+ return `<div class="_bg2">
44
+ <div class="Container">
45
+ <div class="Page">
46
+ ${indent(blocks2.join("\n"), 3, { skipFirst: true })}
47
+ </div>
48
+ </div>
49
+ </div>`;
50
+ }
51
+ function templateOfSpecialDocument(ctx, documentType) {
52
+ const { addLiquidFile } = ctx;
53
+ if (documentType.typeName === "search" || documentType.typeName === "searchPage") {
54
+ addLiquidFile("partials", "result-item.public.liquid", templateOfDocumentTile("doc"));
55
+ return `<div
56
+ class="Container"
57
+ data-effect="searchForm"
58
+ data-template="result-item"
59
+ data-limit="10"></div>`;
60
+ }
61
+ if (documentType.typeName === "contact" || documentType.typeName === "contactPage") {
62
+ return `<div class="Container">
63
+ <div class="TextWidth Pt">
64
+ <div
65
+ data-effect="contactForm"
66
+ data-recaptcha-key="{{ site.recaptchaKey }}"
67
+ data-home-url="{{ site.home.url }}"></div>
68
+ </div>
69
+ </div>`;
70
+ }
71
+ }
72
+ function templateOfDocumentChildren(ctx, parentType, parentIdKeyProvider) {
73
+ const routingBlocks = templateOfRoutingChildren(ctx, parentType, parentIdKeyProvider);
74
+ const regularBlocks = parentType.documentKind === "routing" &&
75
+ parentType.regularChildren &&
76
+ templateOfRegularDocumentTiles(ctx, parentType, parentIdKeyProvider, {
77
+ mode: "all",
78
+ });
79
+ return [routingBlocks, regularBlocks].filter(Boolean).join("\n\n") || undefined;
80
+ }
81
+ function templateOfRoutingChildren(ctx, parentType, parentIdKeyProvider) {
82
+ const { siteSchema } = ctx;
83
+ const routingBlocks = (parentType.routingChildren ?? [])
84
+ .map((childName) => {
85
+ const child = getJtRoutingDocumentType(siteSchema, childName);
86
+ if (child.redirectTo) {
87
+ return templateOfDocumentChildren(ctx, child, parentIdKeyProvider.createForRoutingChild(childName));
88
+ }
89
+ return templateOfRoutingChild(ctx, child, parentIdKeyProvider);
90
+ })
91
+ .filter(Boolean);
92
+ if (routingBlocks.length === 0)
93
+ return;
94
+ return `<div class="Container">
95
+ ${indent(routingBlocks.join("\n\n"), 1, { skipFirst: true })}
96
+ </div>`;
97
+ }
98
+ function templateOfRoutingChild(ctx, child, parentIdKeyProvider) {
99
+ const { siteSchema } = ctx;
100
+ const { typeName, regularChildrenSorting } = child;
101
+ const variableName = `${typeName}Doc`;
102
+ const idKeyProvider = parentIdKeyProvider.createForRoutingChild(typeName);
103
+ const idKey = idKeyProvider.idKey;
104
+ if (!hasTemporalChildren(siteSchema, child) || regularChildrenSorting !== "publishDate desc") {
105
+ const buttonTemplate = `{% getDoc ${variableName} id: ${idKey} %}
106
+ <a class="Button" href="{{ ${variableName}.url }}">{{ ${variableName}.title }}</a>`;
107
+ return buttonTemplate;
108
+ }
109
+ const tilesTemplate = templateOfRegularDocumentTiles(ctx, child, idKeyProvider, {
110
+ mode: "sampleOnly",
111
+ });
112
+ return `{% getDoc ${variableName} id: ${idKey} %}
113
+ <div class="Pt">
114
+ <h2>
115
+ <a class="TextLink" href="{{ ${variableName}.url }}">{{ ${variableName}.title }}</a>
116
+ </h2>
117
+ ${indent(tilesTemplate, 1, { skipFirst: true })}
118
+ </div>`;
119
+ }
120
+ function templateOfRegularDocumentTiles(ctx, parentType, parentIdKeyProvider, { mode }) {
121
+ const { siteSchema, addLiquidFile } = ctx;
122
+ const { typeName: parentTypeName } = parentType;
123
+ const childrenVariableName = `${parentTypeName}Children`;
124
+ const childVariableName = `${parentTypeName}Child`;
125
+ const idKey = parentIdKeyProvider.idKey;
126
+ if (mode === "sampleOnly") {
127
+ return `{% getDocs ${childrenVariableName} parentId: ${idKey} limit: 4 %}
128
+ <div class="List">
129
+ {% for ${childVariableName} in ${childrenVariableName} %}
130
+ ${indent(templateOfDocumentTile(childVariableName), 2, { skipFirst: true })}
131
+ {% endfor %}
132
+ </div>`;
133
+ }
134
+ if (!hasTemporalChildren(siteSchema, parentType)) {
135
+ return `{% getDocs ${childrenVariableName} parentId: ${idKey} %}
136
+ <div class="Container List Pt Pb">
137
+ {% for ${childVariableName} in ${childrenVariableName} %}
138
+ ${indent(templateOfDocumentTile(childVariableName), 2, { skipFirst: true })}
139
+ {% endfor %}
140
+ </div>`;
141
+ }
142
+ const tileTemplateName = `${camelToKebabCase(parentTypeName)}-tile`;
143
+ addLiquidFile("partials", `${tileTemplateName}.public.liquid`, templateOfDocumentTile("doc"));
144
+ return `{% getPaginatedDocs ${childrenVariableName} parentId: ${idKey} pageSize: 10 %}
145
+ <div class="Container">
146
+ <div
147
+ class="Page List"
148
+ data-effect="infiniteLoading"
149
+ data-parent-id="{{ ${idKey} }}"
150
+ data-start="{{ ${childrenVariableName}.pageSize }}"
151
+ data-limit="{{ ${childrenVariableName}.pageSize }}"
152
+ data-total="{{ ${childrenVariableName}.total }}"
153
+ data-template="${tileTemplateName}">
154
+ {% for ${childrenVariableName} in ${childrenVariableName}.items %}
155
+ {% render "partials/${tileTemplateName}.public.liquid" doc: ${childrenVariableName} %}
156
+ {% endfor %}
157
+ </div>
158
+ </div>`;
159
+ }
160
+ function templateOfFields(ctx, fields, { parentKey }) {
161
+ if (!fields || fields.length === 0)
162
+ return;
163
+ const fieldTemplates = fields.map((fieldOrName) => templateOfField(ctx, fieldOrName, parentKey));
164
+ return fieldTemplates.join("\n");
165
+ }
166
+ function templateOfField(ctx, fieldOrName, parentKey) {
167
+ const fieldName = typeof fieldOrName === "string" ? fieldOrName : fieldOrName.name;
168
+ const dataType = typeof fieldOrName === "string" ? getPredefinedDataType(ctx, fieldName) : fieldOrName.dataType;
169
+ if (dataType === "html" || dataType === "quillDelta") {
170
+ return `<div class="Field Text">{{ ${parentKey}.${fieldName} | raw }}</div>`;
171
+ }
172
+ if (dataType === "date") {
173
+ return `<div class="Field">{{ ${parentKey}.${fieldName} | formatDate: "short" }}</div>`;
174
+ }
175
+ if (dataType === "dateTime") {
176
+ return `<div class="Field">{{ ${parentKey}.${fieldName} | formatDate: "long" }}</div>`;
177
+ }
178
+ if (dataType === "json") {
179
+ return `<div class="Field">{{ ${parentKey}.${fieldName} | json }}</div>`;
180
+ }
181
+ if (dataType === "gallery") {
182
+ return `<div class="Field">
183
+ {% for media in ${parentKey}.${fieldName} %}
184
+ {% useImage im uid: media.uid size: "150x150" %}
185
+ <img
186
+ class="Field-img"
187
+ src="{{ im.url }}"
188
+ width="{{ im.width }}"
189
+ height="{{ im.height }}"
190
+ loading="lazy"
191
+ alt=""
192
+ data-zoom-src="{{ media.uid | imageZoomUrl }}">
193
+ {% endfor %}
194
+ </div>`;
195
+ }
196
+ if (dataType === "media") {
197
+ const mediaUidKey = `${parentKey}.${fieldName}.uid`;
198
+ return `{% useImage im uid:${mediaUidKey} size: "x250x" %}
199
+ <div class="Field">
200
+ <img
201
+ class="Field-img"
202
+ src="{{ im.url }}"
203
+ width="{{ im.width }}"
204
+ height="{{ im.height }}"
205
+ loading="lazy"
206
+ alt=""
207
+ data-zoom-src="{{ ${mediaUidKey} | imageZoomUrl }}">>
208
+ </div>`;
209
+ }
210
+ if (fieldName === "title") {
211
+ return `<h2>{{ ${parentKey}.${fieldName} }}</h2>`;
212
+ }
213
+ return `<div class="Field">{{ ${parentKey}.${fieldName} }}</div>`;
214
+ }
215
+ function templateOfPicture({ imageKey }) {
216
+ return `{% if ${imageKey} %}
217
+ {% useImage smallIm uid: ${imageKey}.uid size: "360x48" %}
218
+ {% useImage largeIm uid: ${imageKey}.uid size: "1200x160" %}
219
+ <div class="Container">
220
+ <picture class="Hero">
221
+ <source
222
+ srcset="{{ largeIm.url }}"
223
+ width="{{ largeIm.width }}"
224
+ height="{{ largeIm.height }}"
225
+ media="(min-width: 360px)">
226
+ <img
227
+ src="{{ smallIm.url }}"
228
+ width="{{ smallIm.width }}"
229
+ height="{{ smallIm.height }}"
230
+ loading="lazy"
231
+ alt="">
232
+ </picture>
233
+ </div>
234
+ {% endif %}`;
235
+ }
236
+ function templateOfList(ctx, list, { listKey, nested }) {
237
+ const { siteSchema, addLiquidFile, hasLiquidFile } = ctx;
238
+ if (list.parts.length === 0)
239
+ return;
240
+ const partTemplates = list.parts
241
+ .map((partName, index) => {
242
+ const partType = getJtPartType(siteSchema, partName);
243
+ const partTemplateName = `${camelToKebabCase(partType.typeName)}-part`;
244
+ if (!hasLiquidFile("partials", `${partTemplateName}.liquid`)) {
245
+ const partTemplate = templateOfPart(ctx, partType, "part");
246
+ addLiquidFile("partials", `${partTemplateName}.liquid`, partTemplate, {
247
+ skipIfExists: true,
248
+ });
249
+ }
250
+ const ifOrElsif = index === 0 ? "if" : "elsif";
251
+ return `{% ${ifOrElsif} part.type == "${partType.typeName}" %}
252
+ {% render "partials/${partTemplateName}.liquid" part: part %}`;
253
+ })
254
+ .filter(Boolean);
255
+ partTemplates.push("{% endif %}");
256
+ if (nested) {
257
+ return `{% if ${listKey} %}
258
+ <div class="Indent">
259
+ {% for part in ${listKey} %}
260
+ ${indent(partTemplates.join("\n"), 3, { skipFirst: true })}
261
+ {% endfor %}
262
+ </div>
263
+ {% endif %}`;
264
+ }
265
+ return `{% if ${listKey} %}
266
+ <div class="_bg2">
267
+ <div class="Container Pb">
268
+ {% for part in ${listKey} %}
269
+ ${indent(partTemplates.join("\n"), 4, { skipFirst: true })}
270
+ {% endfor %}
271
+ </div>
272
+ </div>
273
+ {% endif %}`;
274
+ }
275
+ function templateOfPart(ctx, part, partKey) {
276
+ const templates = [
277
+ templateOfFields(ctx, part.fields, { parentKey: `${partKey}.field` }),
278
+ part.list
279
+ ? templateOfList(ctx, part.list, {
280
+ listKey: `${partKey}.parts`,
281
+ nested: true,
282
+ })
283
+ : undefined,
284
+ ].filter(Boolean);
285
+ return `<section class="TextWidth">
286
+ ${indent(templates.join("\n\n"), 1, { skipFirst: true })}
287
+ </section>`;
288
+ }
289
+ function templateOfSiblingLinks(ctx) {
290
+ const previousLabelTemplate = localizedLabelTemplate(ctx, {
291
+ en: "Previous",
292
+ fr: "Précédent",
293
+ });
294
+ const nextLabelTemplate = localizedLabelTemplate(ctx, {
295
+ en: "Next",
296
+ fr: "Suivant",
297
+ });
298
+ return `<div class="Row spaceBetween">
299
+ {% if doc.siblings.previous %}
300
+ <a href="{{ doc.siblings.previous.url }}" title="{{ doc.siblings.previous.title }}">← ${previousLabelTemplate}</a>
301
+ {% else %}
302
+ <span></span>
303
+ {% endif %}
304
+ {% if doc.siblings.next %}
305
+ <a href="{{ doc.siblings.next.url }}" title="{{ doc.siblings.next.title }}">${nextLabelTemplate} →</a>
306
+ {% endif %}
307
+ </div>`;
308
+ }
309
+ function templateOfDocumentTile(docVariableName) {
310
+ return `<a href="{{ ${docVariableName}.url }}">
311
+ <article>
312
+ {% if ${docVariableName}.defaultImage %}
313
+ <div>
314
+ {% useImage im uid: ${docVariableName}.defaultImage.uid size: "120x120" %}
315
+ <img
316
+ src="{{ im.url }}"
317
+ width="{{ im.width }}"
318
+ height="{{ im.height }}"
319
+ loading="lazy"
320
+ alt="">
321
+ </div>
322
+ {% endif %}
323
+ <div>
324
+ <h3>{{ ${docVariableName}.title }}</h3>
325
+ <p>{{ ${docVariableName}.excerpt | makeExcerpt: 40 }}</p>
326
+ </div>
327
+ </article>
328
+ </a>`;
329
+ }
@@ -0,0 +1,14 @@
1
+ export function createIdKeyProvider() {
2
+ return {
3
+ idKey: "doc.id",
4
+ getChildRoutingIdKey: (typeName) => `doc.routingIds.${typeName}.id`,
5
+ createForRoutingChild: (routingTypeName) => createChildIdKeyProvider(`doc.routingIds.${routingTypeName}`),
6
+ };
7
+ }
8
+ function createChildIdKeyProvider(key) {
9
+ return {
10
+ idKey: `${key}.id`,
11
+ getChildRoutingIdKey: (typeName) => `${key}.${typeName}.id`,
12
+ createForRoutingChild: (typeName) => createChildIdKeyProvider(`${key}.${typeName}`),
13
+ };
14
+ }
@@ -0,0 +1,55 @@
1
+ export function getJtRoutingDocumentType(siteSchema, typeName) {
2
+ const nodeType = siteSchema.nodeTypes?.find((nodeType) => nodeType.typeName === typeName);
3
+ if (!nodeType)
4
+ throw new Error(`Node type not found: "${typeName}"`);
5
+ if (nodeType.kind !== "document" || nodeType.documentKind !== "routing") {
6
+ throw new Error(`Invalid routing document: "${typeName}"`);
7
+ }
8
+ return nodeType;
9
+ }
10
+ export function getJtRegularDocumentType(siteSchema, typeName) {
11
+ const nodeType = siteSchema.nodeTypes?.find((nodeType) => nodeType.typeName === typeName);
12
+ if (!nodeType)
13
+ throw new Error(`Node type not found: "${typeName}"`);
14
+ if (nodeType.kind !== "document" || nodeType.documentKind !== "regular") {
15
+ throw new Error(`Invalid regular document: "${typeName}"`);
16
+ }
17
+ return nodeType;
18
+ }
19
+ export function getJtPartType(siteSchema, typeName) {
20
+ const nodeType = siteSchema.nodeTypes?.find((nodeType) => nodeType.typeName === typeName);
21
+ if (!nodeType)
22
+ throw new Error(`Node type not found: "${typeName}"`);
23
+ if (nodeType.kind !== "part")
24
+ throw new Error(`Invalid part type: "${typeName}"`);
25
+ return nodeType;
26
+ }
27
+ export function hasTemporalChildren(siteSchema, documentType) {
28
+ return (documentType.regularChildren?.some((childName) => {
29
+ const child = getJtRegularDocumentType(siteSchema, childName);
30
+ return child.route === ":yyyy/:mm/:dd/:relativeId-:slug";
31
+ }) ?? false);
32
+ }
33
+ export function getJtHomeType(siteSchema) {
34
+ const homeType = siteSchema.nodeTypes?.find((type) => type.typeName === "home");
35
+ if (!homeType)
36
+ throw new Error("Home type not found in site schema");
37
+ if (homeType.kind !== "document" || homeType.documentKind !== "routing") {
38
+ throw new Error("Invalid home type in site schema");
39
+ }
40
+ return homeType;
41
+ }
42
+ export function getJtSiteType(siteSchema) {
43
+ const siteType = siteSchema.nodeTypes?.find((type) => type.kind === "site");
44
+ if (!siteType)
45
+ throw new Error("Site type not found");
46
+ return siteType;
47
+ }
48
+ export function isMultilingual(siteSchema) {
49
+ return siteSchema.languages.length > 1;
50
+ }
51
+ export function getFirstSiteLanguage(siteSchema) {
52
+ if (siteSchema.languages.length === 0)
53
+ throw new Error("Missing language in site schema");
54
+ return siteSchema.languages[0];
55
+ }
@@ -0,0 +1,75 @@
1
+ import { generateSlug } from "@paroicms/public-anywhere-lib";
2
+ import { randomUUID } from "node:crypto";
3
+ import { mkdir, writeFile } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ import { fillSiteWithFakeContent } from "../fake-content-generator.ts/create-database-with-fake-content.js";
6
+ import { createTheme } from "./theme-creator.js";
7
+ export async function generateSite(ctx, input) {
8
+ const { service, logger, pluginConf: { packName }, } = ctx;
9
+ const { generatedSchema: { l10n, siteSchema, siteTitle }, withFakeContent, } = input;
10
+ const siteId = randomUUID();
11
+ logger.info(`Generating site: ${siteId}…`);
12
+ const packConf = service.connector.getSitePackConf(packName);
13
+ const { sitesDir } = packConf;
14
+ if (!sitesDir) {
15
+ throw new Error(`Site-generator plugin can generate sites only for pack with "sitesDir", but pack "${packName}" doesn't have it`);
16
+ }
17
+ const siteDir = join(sitesDir, siteId);
18
+ await mkdir(siteDir);
19
+ await writeFile(join(siteDir, "site-schema.json"), JSON.stringify(siteSchema, null, 2), "utf-8");
20
+ for (const [language, l10nData] of Object.entries(l10n)) {
21
+ await writeFile(join(siteDir, `site-schema.l10n.${language}.json`), JSON.stringify(l10nData, null, 2), "utf-8");
22
+ }
23
+ await writeFile(join(siteDir, "package.json"), JSON.stringify(getPackageJsonContent({
24
+ siteTitle: siteTitle.en ?? Object.values(siteTitle)[0] ?? "new-website",
25
+ }), null, 2), "utf-8");
26
+ await createTheme(ctx, siteDir, siteSchema);
27
+ const siteConf = await service.registerNewSite({
28
+ packName,
29
+ siteDir,
30
+ domain: siteId,
31
+ version: "0.0.0",
32
+ });
33
+ if (withFakeContent) {
34
+ await fillSiteWithFakeContent(ctx, { siteConf, siteId, siteTitle });
35
+ }
36
+ const { siteUrl } = siteConf;
37
+ return {
38
+ siteId,
39
+ url: siteUrl,
40
+ boUrl: `${siteUrl}/adm`,
41
+ };
42
+ }
43
+ function getPackageJsonContent(options) {
44
+ return {
45
+ name: generateSlug(options.siteTitle),
46
+ version: "0.0.0",
47
+ private: true,
48
+ type: "module",
49
+ scripts: {
50
+ start: "paroicms | npm run _pino-pretty",
51
+ "start:dev": "nodemon --watch 'site-schema*.json' --watch config.json",
52
+ clear: "rimraf theme/assets/css/*",
53
+ build: "npm run scss",
54
+ dev: "concurrently -n 'node,scss' -c 'yellow.bold,magenta.bold' 'npm run start:dev' 'npm run scss:watch'",
55
+ scss: "npm run _scss -- --no-source-map --style=compressed",
56
+ "scss:watch": "npm run _scss && npm run _scss -- --watch",
57
+ _scss: "sass theme/assets/scss/theme.scss theme/assets/css/theme.css",
58
+ "_pino-pretty": "pino-pretty -U false -x 'stats:25' -X 'stats:grey' -t 'yyyy-mm-dd HH:MM:ss.l' -i 'hostname,pid,fqdn'",
59
+ },
60
+ dependencies: {
61
+ "@paroicms/contact-form-plugin": "0.18.0",
62
+ "@paroicms/content-loading-plugin": "0.11.0",
63
+ "@paroicms/public-menu-plugin": "0.8.0",
64
+ "@paroicms/server": "*",
65
+ "@paroicms/quill-editor-plugin": "1.27.0",
66
+ },
67
+ devDependencies: {
68
+ concurrently: "~9.1.2",
69
+ nodemon: "~3.1.9",
70
+ "pino-pretty": "~13.0.0",
71
+ rimraf: "~6.0.1",
72
+ sass: "~1.83.4",
73
+ },
74
+ };
75
+ }
@@ -0,0 +1,26 @@
1
+ import { getFirstSiteLanguage, isMultilingual } from "./jt-site-schema-helpers.js";
2
+ export function indent(template, level, { skipFirst = false } = {}) {
3
+ const indentStr = " ".repeat(level);
4
+ if (skipFirst) {
5
+ const lines = template.split("\n");
6
+ return lines.map((line, index) => (index === 0 ? line : indentStr + line)).join("\n");
7
+ }
8
+ // For firstLine true, indent all lines.
9
+ return template.replace(/^/gm, indentStr);
10
+ }
11
+ export function getPredefinedDataType(ctx, fieldName) {
12
+ const predefinedField = ctx.predefinedFields.get(fieldName);
13
+ if (!predefinedField)
14
+ return "string";
15
+ return predefinedField.dataType;
16
+ }
17
+ export function localizedLabelTemplate(ctx, label) {
18
+ const { siteSchema, setLocalizedLabel: appendLocalizedLabel } = ctx;
19
+ const firstLanguage = getFirstSiteLanguage(siteSchema);
20
+ const defaultLabel = firstLanguage in label ? label[firstLanguage] : label.en;
21
+ if (!isMultilingual(siteSchema)) {
22
+ return defaultLabel;
23
+ }
24
+ appendLocalizedLabel(label);
25
+ return `{{ "${defaultLabel.replaceAll('"', '\\"')}" | t }}`;
26
+ }
@@ -0,0 +1,180 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ import { getPredefinedFields } from "../lib/create-prompt.js";
4
+ import { camelToKebabCase } from "../lib/utils.js";
5
+ import { templateOfSiteFooter, templateOfSiteHeader } from "./common-template-creator.js";
6
+ import { templateOfDocumentType } from "./document-template-creator.js";
7
+ import { isMultilingual } from "./jt-site-schema-helpers.js";
8
+ import { getThemeCssContent } from "./theme-css.js";
9
+ export async function createTheme(ctx, siteDir, siteSchema) {
10
+ const themeContext = createThemeCreatorContext(siteSchema);
11
+ for (const nodeType of siteSchema.nodeTypes ?? []) {
12
+ if (nodeType.kind === "site")
13
+ continue;
14
+ if (nodeType.kind === "part")
15
+ continue;
16
+ themeContext.addLiquidFile("root", `${camelToKebabCase(nodeType.typeName)}.liquid`, templateOfDocumentType(themeContext, nodeType));
17
+ }
18
+ themeContext.addFile("theme.json", getThemeJsonContent());
19
+ themeContext.addFile("assets/scss/theme.scss", getThemeCssContent());
20
+ themeContext.addFile("assets/css/theme.css", getThemeCssContent());
21
+ themeContext.addLiquidFile("layouts", "main-layout.liquid", templateOfLayout(themeContext));
22
+ themeContext.addLiquidFile("root", "404.liquid", templateOf404(themeContext));
23
+ if (isMultilingual(siteSchema)) {
24
+ themeContext.addLiquidFile("root", "index.liquid", templateOfIndex());
25
+ }
26
+ const themeDir = join(siteDir, "theme");
27
+ await mkdir(themeDir);
28
+ const { files, issues } = themeContext.toFiles();
29
+ for (const file of files) {
30
+ const filePath = join(themeDir, file.path);
31
+ const dirPath = dirname(filePath);
32
+ await ensureDirectory(dirPath, { recursive: true });
33
+ await writeFile(filePath, file.content, "utf-8");
34
+ }
35
+ if (issues) {
36
+ ctx.logger.warn(`Issues in "${siteDir}":`, issues);
37
+ }
38
+ }
39
+ function createThemeCreatorContext(siteSchema) {
40
+ const languages = siteSchema.languages ?? [];
41
+ const liquidFiles = new Map();
42
+ const localeFiles = new Map();
43
+ const otherFiles = new Map();
44
+ const issues = [];
45
+ return {
46
+ siteSchema,
47
+ predefinedFields: new Map(getPredefinedFields().map((f) => [f.fieldName, f])),
48
+ setLocalizedLabel(label) {
49
+ for (const language of languages) {
50
+ const value = label[language];
51
+ if (!value)
52
+ continue;
53
+ let f = localeFiles.get(language);
54
+ if (!f) {
55
+ f = {};
56
+ localeFiles.set(language, f);
57
+ }
58
+ f[language] = value;
59
+ }
60
+ },
61
+ hasLiquidFile(directory, fileName) {
62
+ const path = directory === "root" ? `templates/${fileName}` : `templates/${directory}/${fileName}`;
63
+ return liquidFiles.has(path);
64
+ },
65
+ addLiquidFile(directory, fileName, content, { skipIfExists = false } = {}) {
66
+ const path = directory === "root" ? `templates/${fileName}` : `templates/${directory}/${fileName}`;
67
+ if (liquidFiles.has(path)) {
68
+ if (skipIfExists)
69
+ return;
70
+ issues.push(`Liquid file already exists, overwrite: "${path}"`);
71
+ }
72
+ liquidFiles.set(path, content);
73
+ },
74
+ addFile(path, content) {
75
+ if (otherFiles.has(path))
76
+ throw new Error(`File already exists: "${path}"`);
77
+ otherFiles.set(path, content);
78
+ },
79
+ toFiles() {
80
+ const files = [
81
+ ...Array.from(liquidFiles.entries()).map(([path, content]) => ({ path, content })),
82
+ ...Array.from(localeFiles.entries()).map(([language, content]) => ({
83
+ path: `locales/${language}.json`,
84
+ content: JSON.stringify(content, null, 2),
85
+ })),
86
+ ...Array.from(otherFiles.entries()).map(([path, content]) => ({ path, content })),
87
+ ];
88
+ files.sort((a, b) => a.path.localeCompare(b.path));
89
+ return { files, issues: issues.length > 0 ? issues : undefined };
90
+ },
91
+ };
92
+ }
93
+ // function getDefaultLiquidContent() {
94
+ // return `{% layout "layouts/main-layout.liquid" doc: doc site: site %}
95
+ // {% block %}
96
+ // <div class="Container">
97
+ // <div class="Page">
98
+ // {{ doc | info }}
99
+ // </div>
100
+ // </div>
101
+ // {% endblock %}`;
102
+ // }
103
+ function templateOf404(ctx) {
104
+ const { siteSchema } = ctx;
105
+ const rawParam = isMultilingual(siteSchema) ? " raw: true" : "";
106
+ return `{% layout "layouts/main-layout.liquid" doc: doc site: site${rawParam} %}
107
+ {% block %}
108
+ <div class="Container">
109
+ <div class="Page">
110
+ <h1>404</h1>
111
+ <p>This page doesn't exist. Please <a href='/'>return to home page</a>.</p>
112
+ </div>
113
+ </div>
114
+ {% endblock %}`;
115
+ }
116
+ function templateOfIndex() {
117
+ return `{% layout "layouts/main-layout.liquid" doc: doc site: site raw: true %}
118
+ {% block %}
119
+ <div class="Container">
120
+ <div class="Page">
121
+ {% for translation in doc.translations %}
122
+ <a href="{{ translation.url }}">{{ translation.languageLabel }}</a>
123
+ {% endfor %}
124
+ </div>
125
+ </div>
126
+ {% endblock %}`;
127
+ }
128
+ function templateOfLayout(ctx) {
129
+ const { siteSchema } = ctx;
130
+ const multilingual = isMultilingual(siteSchema);
131
+ ctx.addLiquidFile("partials", "site-header.liquid", templateOfSiteHeader(ctx));
132
+ ctx.addLiquidFile("partials", "site-footer.liquid", templateOfSiteFooter(ctx));
133
+ const renderHeader = `{% render "partials/site-header" doc: doc site: site %}`;
134
+ const headerTemplate = multilingual
135
+ ? `{% unless raw %}
136
+ ${renderHeader}
137
+ {% endunless %}`
138
+ : renderHeader;
139
+ const renderFooter = `{% render "partials/site-footer" doc: doc site: site %}`;
140
+ const footerTemplate = multilingual
141
+ ? `{% unless raw %}
142
+ ${renderFooter}
143
+ {% endunless %}`
144
+ : renderFooter;
145
+ return `<!doctype html>
146
+ <html lang="{{ doc.language }}">
147
+ <head>
148
+ <meta name="viewport" content="width=device-width,initial-scale=1">
149
+ {{ doc | headTags }}
150
+ <title>
151
+ {% if doc.type != "home" and doc.title %}
152
+ {{ doc.title }} -{% endif %}
153
+ {{ site.field.title }}</title>
154
+ <link rel="stylesheet" href="{{ site.assetsUrl }}/css/theme.css">
155
+ </head>
156
+ <body>
157
+ ${headerTemplate}
158
+
159
+ {% block -%}
160
+ {%- endblock %}
161
+
162
+ ${footerTemplate}
163
+ </body>
164
+ </html>`;
165
+ }
166
+ function getThemeJsonContent() {
167
+ return JSON.stringify({
168
+ fTextImages: ["700x", "x400x", "700x350"],
169
+ pixelRatio: 1.5,
170
+ }, null, 2);
171
+ }
172
+ async function ensureDirectory(dirPath, { recursive = false } = {}) {
173
+ try {
174
+ await mkdir(dirPath, { recursive });
175
+ }
176
+ catch (e) {
177
+ if (e.code !== "EEXIST")
178
+ throw e;
179
+ }
180
+ }