@orderlyshop/web-components 0.1.0-build.7045

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 (71) hide show
  1. package/AGENTS.md +110 -0
  2. package/README.md +685 -0
  3. package/bin/orderly-build-category-pages.mjs +160 -0
  4. package/bin/orderly-generate-category-pages.mjs +308 -0
  5. package/bin/orderly-hydrate-static-pages.mjs +595 -0
  6. package/bin/orderly-init-navigation.mjs +327 -0
  7. package/bin/orderly-init-shop.mjs +876 -0
  8. package/bin/orderly-init-taxonomy.mjs +2 -0
  9. package/bin/orderly-publish-site.mjs +342 -0
  10. package/custom-elements.json +505 -0
  11. package/dist/browser/orderly-web-components.define.global.js +3551 -0
  12. package/dist/browser/orderly-web-components.define.global.js.map +1 -0
  13. package/dist/browser/orderly-web-components.global.js +3551 -0
  14. package/dist/browser/orderly-web-components.global.js.map +1 -0
  15. package/dist/default-shop-DgX6uy10.d.ts +221 -0
  16. package/dist/default-shop.d.ts +6 -0
  17. package/dist/default-shop.js +762 -0
  18. package/dist/default-shop.js.map +1 -0
  19. package/dist/define-BNMhl19n.d.ts +9 -0
  20. package/dist/define.d.ts +2 -0
  21. package/dist/define.js +11094 -0
  22. package/dist/define.js.map +1 -0
  23. package/dist/index.d.ts +683 -0
  24. package/dist/index.js +11417 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/navigation.d.ts +61 -0
  27. package/dist/navigation.js +1125 -0
  28. package/dist/navigation.js.map +1 -0
  29. package/dist/query.d.ts +31 -0
  30. package/dist/query.js +115 -0
  31. package/dist/query.js.map +1 -0
  32. package/dist/registry-CPDecU3g.d.ts +6 -0
  33. package/dist/shop-BgQhGRzS.d.ts +173 -0
  34. package/dist/shop-query.d.ts +8 -0
  35. package/dist/shop-query.js +100 -0
  36. package/dist/shop-query.js.map +1 -0
  37. package/dist/shop.d.ts +8 -0
  38. package/dist/shop.js +11187 -0
  39. package/dist/shop.js.map +1 -0
  40. package/dist/stores.d.ts +46 -0
  41. package/dist/stores.js +145 -0
  42. package/dist/stores.js.map +1 -0
  43. package/dist/taxonomy.d.ts +35 -0
  44. package/dist/taxonomy.js +247 -0
  45. package/dist/taxonomy.js.map +1 -0
  46. package/dist/types-Bjez59Hr.d.ts +96 -0
  47. package/docs/components/README.md +708 -0
  48. package/docs/components/product-grid.md +182 -0
  49. package/docs/components/product-rail.md +174 -0
  50. package/examples/shop/README.md +72 -0
  51. package/examples/shop/package.json +28 -0
  52. package/examples/shop/src/category.html +20 -0
  53. package/examples/shop/src/checkout.html +21 -0
  54. package/examples/shop/src/forretningsbetingelser.html +81 -0
  55. package/examples/shop/src/includes/body-end.html +1 -0
  56. package/examples/shop/src/includes/body-start.html +3 -0
  57. package/examples/shop/src/includes/head.html +32 -0
  58. package/examples/shop/src/index.html +25 -0
  59. package/examples/shop/src/navigation.ts +154 -0
  60. package/examples/shop/src/product.html +24 -0
  61. package/examples/shop/src/templates/page-layouts.html +162 -0
  62. package/examples/shop/src/templates/shop-footer.html +76 -0
  63. package/examples/shop/tsconfig.json +32 -0
  64. package/examples/shop/vite.config.mjs +190 -0
  65. package/html-custom-data.json +279 -0
  66. package/package.json +118 -0
  67. package/server/README.md +111 -0
  68. package/server/apache/.htaccess +18 -0
  69. package/server/nginx/orderly-products.conf +24 -0
  70. package/server/node/product-snapshot-server.mjs +133 -0
  71. package/server/php/orderly-product.php +204 -0
@@ -0,0 +1,160 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from "node:child_process";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const args = parseArgs(process.argv.slice(2));
6
+
7
+ if (args.help || args.h) {
8
+ printHelp();
9
+ process.exit(0);
10
+ }
11
+
12
+ const generateScript = fileURLToPath(new URL("./orderly-generate-category-pages.mjs", import.meta.url));
13
+ const hydrateScript = fileURLToPath(new URL("./orderly-hydrate-static-pages.mjs", import.meta.url));
14
+ const generatorArgs = passthroughGeneratorArgs(process.argv.slice(2));
15
+ const hydrateArgs = passthroughHydrateArgs(process.argv.slice(2));
16
+
17
+ if (args["dry-run"]) {
18
+ await runNode([generateScript, ...generatorArgs]);
19
+ process.exit(0);
20
+ }
21
+
22
+ await runNode([generateScript, ...generatorArgs]);
23
+ try {
24
+ await runCommand(args["build-command"] ?? "npm run build", {
25
+ VITE_ORDERLY_CATEGORY_URL_MODE: "path",
26
+ ORDERLY_CATEGORY_URL_MODE: "path"
27
+ });
28
+ if (!args["no-hydrate"]) {
29
+ await runNode([hydrateScript, ...hydrateArgs]);
30
+ }
31
+ } finally {
32
+ await runNode([generateScript, "--clean", ...cleanGeneratorArgs(generatorArgs)]);
33
+ }
34
+
35
+ function runNode(args) {
36
+ return run(process.execPath, args);
37
+ }
38
+
39
+ function runCommand(command, env = {}) {
40
+ return run(command, [], { shell: true, env });
41
+ }
42
+
43
+ function run(command, args, options = {}) {
44
+ return new Promise((resolve, reject) => {
45
+ const child = spawn(command, args, {
46
+ env: { ...process.env, ...options.env },
47
+ shell: options.shell ?? false,
48
+ stdio: "inherit"
49
+ });
50
+ child.on("error", reject);
51
+ child.on("exit", (code) => {
52
+ if (code === 0) {
53
+ resolve();
54
+ } else {
55
+ reject(new Error(`${command} ${args.join(" ")} exited with code ${code}.`));
56
+ }
57
+ });
58
+ });
59
+ }
60
+
61
+ function passthroughGeneratorArgs(values) {
62
+ const result = [];
63
+ for (let index = 0; index < values.length; index += 1) {
64
+ const value = values[index];
65
+ const name = value.startsWith("--") ? value.slice(2).split("=", 1)[0] : "";
66
+ if (buildOnlyArgs().has(name) || hydrateOnlyArgs().has(name)) {
67
+ if (!value.includes("=") && values[index + 1] && !values[index + 1].startsWith("--")) {
68
+ index += 1;
69
+ }
70
+ continue;
71
+ }
72
+ result.push(value);
73
+ }
74
+ return result;
75
+ }
76
+
77
+ function passthroughHydrateArgs(values) {
78
+ const result = [];
79
+ for (let index = 0; index < values.length; index += 1) {
80
+ const value = values[index];
81
+ const name = value.startsWith("--") ? value.slice(2).split("=", 1)[0] : "";
82
+ if (buildOnlyArgs().has(name) || generatorOnlyArgs().has(name) || name === "dry-run") {
83
+ if (!value.includes("=") && values[index + 1] && !values[index + 1].startsWith("--")) {
84
+ index += 1;
85
+ }
86
+ continue;
87
+ }
88
+ if (name === "hydrate") {
89
+ continue;
90
+ }
91
+ result.push(value);
92
+ }
93
+ return result;
94
+ }
95
+
96
+ function cleanGeneratorArgs(values) {
97
+ const blocked = new Set(["--dry-run", "--clean"]);
98
+ return values.filter((value) => !blocked.has(value));
99
+ }
100
+
101
+ function buildOnlyArgs() {
102
+ return new Set(["build-command", "hydrate", "no-hydrate"]);
103
+ }
104
+
105
+ function generatorOnlyArgs() {
106
+ return new Set(["template", "categories-dir", "component-tag", "clean"]);
107
+ }
108
+
109
+ function hydrateOnlyArgs() {
110
+ return new Set([
111
+ "dist",
112
+ "dist-categories-dir",
113
+ "shop-query",
114
+ "shop-query-export",
115
+ "base-url",
116
+ "protocol",
117
+ "products-per-category",
118
+ "home-products-per-category",
119
+ "product-page",
120
+ "product-root",
121
+ "image-base-url",
122
+ "skip-products",
123
+ "strict-products"
124
+ ]);
125
+ }
126
+
127
+ function parseArgs(values) {
128
+ const parsed = {};
129
+ for (let index = 0; index < values.length; index += 1) {
130
+ const value = values[index];
131
+ if (!value.startsWith("--")) {
132
+ continue;
133
+ }
134
+ const [name, inlineValue] = value.slice(2).split("=", 2);
135
+ if (inlineValue !== undefined) {
136
+ parsed[name] = inlineValue;
137
+ } else if (values[index + 1] && !values[index + 1].startsWith("--")) {
138
+ parsed[name] = values[index + 1];
139
+ index += 1;
140
+ } else {
141
+ parsed[name] = true;
142
+ }
143
+ }
144
+ return parsed;
145
+ }
146
+
147
+ function printHelp() {
148
+ console.log(`Usage: orderly-build-category-pages [options]
149
+
150
+ Generate real URL category pages, run the current project's build, then remove generated source pages.
151
+
152
+ Options:
153
+ --build-command <cmd> Build command. Defaults to "npm run build".
154
+ --no-hydrate Skip static homepage/category fallback hydration after build.
155
+ --dry-run Forward to orderly-generate-category-pages without building.
156
+
157
+ Category options are forwarded to orderly-generate-category-pages. Hydration options are forwarded
158
+ to orderly-hydrate-static-pages.
159
+ `);
160
+ }
@@ -0,0 +1,308 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
3
+ import { createRequire } from "node:module";
4
+ import { basename, dirname, extname, resolve } from "node:path";
5
+ import { pathToFileURL } from "node:url";
6
+ import {
7
+ createCategoryNavigation,
8
+ flattenCategoryNavigation
9
+ } from "../dist/taxonomy.js";
10
+
11
+ const root = process.cwd();
12
+ const args = parseArgs(process.argv.slice(2));
13
+
14
+ if (args.help || args.h) {
15
+ printHelp();
16
+ process.exit(0);
17
+ }
18
+
19
+ const navigationFile = resolve(root, args.navigation ?? args.taxonomy ?? defaultNavigationPath());
20
+ const categoriesDir = resolve(root, args["categories-dir"] ?? defaultCategoriesDir());
21
+ const templateFile = resolve(root, args.template ?? defaultTemplatePath());
22
+ const exportName = args.export;
23
+ const pageRoot = args["page-root"] ?? "/categories/";
24
+ const componentTag = args["component-tag"] ?? "orderly-category-page";
25
+ const siteTitle = args["site-title"] ?? defaultSiteTitle();
26
+ const generatedMarker = `<!-- Generated by orderly-generate-category-pages from ${relativePath(navigationFile)}. Do not edit directly. -->`;
27
+ const dryRun = Boolean(args["dry-run"]);
28
+ const cleanOnly = Boolean(args.clean);
29
+
30
+ if (!existsSync(navigationFile)) {
31
+ throw new Error(`Navigation file not found: ${relativePath(navigationFile)}`);
32
+ }
33
+
34
+ const navigationModule = await loadNavigationModule(navigationFile);
35
+ const definitions = navigationDefinitions(navigationModule, exportName);
36
+ if (!Array.isArray(definitions)) {
37
+ const expected = exportName ? `an array named ${exportName}` : "navigationDefinitions";
38
+ throw new Error(`${relativePath(navigationFile)} must export ${expected}.`);
39
+ }
40
+
41
+ const navigation = createCategoryNavigation(definitions, {
42
+ pageRoot,
43
+ urlMode: "path"
44
+ });
45
+ const categories = flattenCategoryNavigation(navigation);
46
+
47
+ if (cleanOnly) {
48
+ const removed = cleanGeneratedPages();
49
+ console.log(`Removed ${removed} generated category page${removed === 1 ? "" : "s"}.`);
50
+ process.exit(0);
51
+ }
52
+
53
+ if (!existsSync(templateFile)) {
54
+ throw new Error(`Category page template not found: ${relativePath(templateFile)}`);
55
+ }
56
+
57
+ const template = readFileSync(templateFile, "utf8");
58
+ const pages = categories.map((category) => {
59
+ const file = resolve(categoriesDir, ...category.metadata.pathSegments, "index.html");
60
+ return {
61
+ category,
62
+ file,
63
+ html: renderCategoryPage(template, category)
64
+ };
65
+ });
66
+
67
+ if (dryRun) {
68
+ for (const page of pages) {
69
+ console.log(`${page.category.id} -> ${relativePath(page.file)}`);
70
+ }
71
+ console.log(`Would generate ${pages.length} category page${pages.length === 1 ? "" : "s"}.`);
72
+ process.exit(0);
73
+ }
74
+
75
+ cleanGeneratedPages();
76
+ for (const page of pages) {
77
+ mkdirSync(dirname(page.file), { recursive: true });
78
+ writeFileSync(page.file, page.html, "utf8");
79
+ }
80
+ console.log(`Generated ${pages.length} category page${pages.length === 1 ? "" : "s"}.`);
81
+
82
+ function renderCategoryPage(templateHtml, category) {
83
+ let html = templateHtml.replace("<!doctype html>", `<!doctype html>\n${generatedMarker}`);
84
+ html = html.replace(/<title>.*?<\/title>/, `<title>${escapeHtml(category.label)} | ${escapeHtml(siteTitle)}</title>`);
85
+ html = replaceCategoryElement(html, category);
86
+ html = injectDescription(html, category.metadata.description);
87
+ return html;
88
+ }
89
+
90
+ function replaceCategoryElement(html, category) {
91
+ const emptyElement = `<${componentTag}></${componentTag}>`;
92
+ if (html.includes(emptyElement)) {
93
+ return html.replace(emptyElement, `<${componentTag} slug="${escapeAttribute(category.id)}"></${componentTag}>`);
94
+ }
95
+ const selfClosingPattern = new RegExp(`<${escapeRegExp(componentTag)}\\b([^>]*)/>`);
96
+ if (selfClosingPattern.test(html)) {
97
+ return html.replace(selfClosingPattern, (_match, attributes) => `<${componentTag}${setAttribute(attributes, "slug", category.id)}></${componentTag}>`);
98
+ }
99
+ const openingPattern = new RegExp(`<${escapeRegExp(componentTag)}\\b([^>]*)>`);
100
+ if (openingPattern.test(html)) {
101
+ return html.replace(openingPattern, (_match, attributes) => `<${componentTag}${setAttribute(attributes, "slug", category.id)}>`);
102
+ }
103
+ throw new Error(`${relativePath(templateFile)} must contain <${componentTag}></${componentTag}>.`);
104
+ }
105
+
106
+ function injectDescription(html, description) {
107
+ const meta = `<meta name="description" content="${escapeAttribute(description)}">`;
108
+ if (html.includes("<!-- orderly-head-includes -->")) {
109
+ return html.replace("<!-- orderly-head-includes -->", `<!-- orderly-head-includes -->\n ${meta}`);
110
+ }
111
+ if (html.includes("</head>")) {
112
+ return html.replace("</head>", ` ${meta}\n </head>`);
113
+ }
114
+ return html;
115
+ }
116
+
117
+ function cleanGeneratedPages() {
118
+ let removed = 0;
119
+ for (const file of generatedCategoryFiles(categoriesDir)) {
120
+ if (readFileSync(file, "utf8").includes("Generated by orderly-generate-category-pages")) {
121
+ rmSync(file);
122
+ removed += 1;
123
+ }
124
+ }
125
+ if (existsSync(categoriesDir)) {
126
+ removeEmptyDirectories(categoriesDir);
127
+ if (readdirSync(categoriesDir).length === 0) {
128
+ rmSync(categoriesDir, { recursive: true });
129
+ }
130
+ }
131
+ return removed;
132
+ }
133
+
134
+ function generatedCategoryFiles(dir) {
135
+ if (!existsSync(dir)) {
136
+ return [];
137
+ }
138
+ const files = [];
139
+ for (const entry of readdirSync(dir)) {
140
+ const fullPath = resolve(dir, entry);
141
+ const stat = statSync(fullPath);
142
+ if (stat.isDirectory()) {
143
+ files.push(...generatedCategoryFiles(fullPath));
144
+ } else if (entry === "index.html" && fullPath !== templateFile) {
145
+ files.push(fullPath);
146
+ }
147
+ }
148
+ return files;
149
+ }
150
+
151
+ function removeEmptyDirectories(dir) {
152
+ for (const entry of readdirSync(dir)) {
153
+ const fullPath = resolve(dir, entry);
154
+ if (statSync(fullPath).isDirectory()) {
155
+ removeEmptyDirectories(fullPath);
156
+ if (readdirSync(fullPath).length === 0) {
157
+ rmSync(fullPath, { recursive: true });
158
+ }
159
+ }
160
+ }
161
+ }
162
+
163
+ async function loadNavigationModule(file) {
164
+ const extension = extname(file).toLowerCase();
165
+ if (extension === ".ts" || extension === ".tsx") {
166
+ return loadTypeScriptNavigationModule(file);
167
+ }
168
+ return import(pathToFileURL(file).href);
169
+ }
170
+
171
+ async function loadTypeScriptNavigationModule(file) {
172
+ const require = createRequire(resolve(root, "package.json"));
173
+ let typescript;
174
+ try {
175
+ typescript = require("typescript");
176
+ } catch (error) {
177
+ throw new Error(`TypeScript navigation files require the current shop project to install typescript. Could not load typescript while reading ${relativePath(file)}.`, { cause: error });
178
+ }
179
+
180
+ const output = typescript.transpileModule(readFileSync(file, "utf8"), {
181
+ fileName: file,
182
+ compilerOptions: {
183
+ module: typescript.ModuleKind.ES2022,
184
+ target: typescript.ScriptTarget.ES2022,
185
+ esModuleInterop: true,
186
+ importsNotUsedAsValues: typescript.ImportsNotUsedAsValues?.Remove
187
+ }
188
+ }).outputText;
189
+ const temporaryFile = resolve(dirname(file), `.orderly-${basename(file, extname(file))}-${process.pid}-${Date.now()}.mjs`);
190
+ writeFileSync(temporaryFile, output, "utf8");
191
+ try {
192
+ return await import(pathToFileURL(temporaryFile).href);
193
+ } finally {
194
+ rmSync(temporaryFile, { force: true });
195
+ }
196
+ }
197
+
198
+ function defaultNavigationPath() {
199
+ for (const candidate of ["src/navigation.ts", "src/navigation.js", "src/navigation.mjs", "navigation.ts", "navigation.js", "navigation.mjs", "src/taxonomy.ts", "src/taxonomy.js", "src/taxonomy.mjs", "taxonomy.ts", "taxonomy.js", "taxonomy.mjs"]) {
200
+ if (existsSync(resolve(root, candidate))) {
201
+ return candidate;
202
+ }
203
+ }
204
+ return "src/navigation.ts";
205
+ }
206
+
207
+ function defaultTemplatePath() {
208
+ if (existsSync(resolve(root, "src/category.html"))) {
209
+ return "src/category.html";
210
+ }
211
+ return "category.html";
212
+ }
213
+
214
+ function defaultCategoriesDir() {
215
+ if (existsSync(resolve(root, "src/category.html"))) {
216
+ return "src/categories";
217
+ }
218
+ return "categories";
219
+ }
220
+
221
+ function navigationDefinitions(module, exportName) {
222
+ if (exportName) {
223
+ return module[exportName];
224
+ }
225
+ return module.navigationDefinitions ?? module.categoryDefinitions;
226
+ }
227
+
228
+ function defaultSiteTitle() {
229
+ const packageFile = resolve(root, "package.json");
230
+ if (!existsSync(packageFile)) {
231
+ return "Shop";
232
+ }
233
+ try {
234
+ const packageJson = JSON.parse(readFileSync(packageFile, "utf8"));
235
+ return packageJson.displayName ?? packageJson.description ?? packageJson.name ?? "Shop";
236
+ } catch {
237
+ return "Shop";
238
+ }
239
+ }
240
+
241
+ function parseArgs(values) {
242
+ const parsed = {};
243
+ for (let index = 0; index < values.length; index += 1) {
244
+ const value = values[index];
245
+ if (!value.startsWith("--")) {
246
+ continue;
247
+ }
248
+ const [name, inlineValue] = value.slice(2).split("=", 2);
249
+ if (inlineValue !== undefined) {
250
+ parsed[name] = inlineValue;
251
+ } else if (values[index + 1] && !values[index + 1].startsWith("--")) {
252
+ parsed[name] = values[index + 1];
253
+ index += 1;
254
+ } else {
255
+ parsed[name] = true;
256
+ }
257
+ }
258
+ return parsed;
259
+ }
260
+
261
+ function printHelp() {
262
+ console.log(`Usage: orderly-generate-category-pages [options]
263
+
264
+ Generate real URL category pages for the current shop from NavigationDefinition data.
265
+
266
+ Options:
267
+ --navigation <file> Navigation module. Defaults to src/navigation.ts or src/navigation.js when present.
268
+ --taxonomy <file> Backward-compatible alias for --navigation.
269
+ --export <name> Export name for navigation definitions. Defaults to navigationDefinitions.
270
+ --template <file> Category page template. Defaults to src/category.html when present, otherwise category.html.
271
+ --categories-dir <dir> Output directory. Defaults to src/categories when src/category.html is present, otherwise categories.
272
+ --page-root <path> Category page root used for navigation. Defaults to /categories/.
273
+ --component-tag <tag> Category page custom element tag. Defaults to orderly-category-page.
274
+ --site-title <title> Site title suffix used in generated <title> tags.
275
+ --dry-run Print generated pages without writing files.
276
+ --clean Remove generated category pages.
277
+ --help, -h Show this help.
278
+ `);
279
+ }
280
+
281
+ function relativePath(file) {
282
+ return file.replace(`${root}/`, "");
283
+ }
284
+
285
+ function escapeHtml(value) {
286
+ return String(value)
287
+ .replace(/&/g, "&amp;")
288
+ .replace(/</g, "&lt;")
289
+ .replace(/>/g, "&gt;");
290
+ }
291
+
292
+ function escapeAttribute(value) {
293
+ return escapeHtml(value).replace(/"/g, "&quot;");
294
+ }
295
+
296
+ function escapeRegExp(value) {
297
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
298
+ }
299
+
300
+ function setAttribute(attributes, name, value) {
301
+ const cleanAttributes = attributes.replace(/\s*\/\s*$/g, "");
302
+ const escapedValue = escapeAttribute(value);
303
+ const pattern = new RegExp(`\\s${escapeRegExp(name)}=["'][^"']*["']`, "i");
304
+ if (pattern.test(cleanAttributes)) {
305
+ return cleanAttributes.replace(pattern, ` ${name}="${escapedValue}"`);
306
+ }
307
+ return `${cleanAttributes} ${name}="${escapedValue}"`;
308
+ }