primo-cli 0.1.3 → 0.1.5
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 +111 -39
- package/dist/commands/build.js +488 -272
- package/dist/commands/deploy.d.ts +1 -1
- package/dist/commands/deploy.js +293 -141
- package/dist/commands/dev.d.ts +2 -0
- package/dist/commands/dev.js +2007 -150
- package/dist/commands/init.d.ts +2 -2
- package/dist/commands/init.js +65 -43
- package/dist/commands/login.d.ts +1 -2
- package/dist/commands/login.js +24 -6
- package/dist/commands/new.js +161 -274
- package/dist/commands/pull-library.d.ts +7 -0
- package/dist/commands/pull-library.js +92 -0
- package/dist/commands/pull.d.ts +0 -1
- package/dist/commands/pull.js +160 -165
- package/dist/commands/push-library.d.ts +7 -0
- package/dist/commands/push-library.js +88 -0
- package/dist/commands/push.d.ts +2 -0
- package/dist/commands/push.js +358 -51
- package/dist/commands/validate.d.ts +1 -1
- package/dist/commands/validate.js +379 -161
- package/dist/index.js +110 -20
- package/dist/utils/binary.js +1 -1
- package/dist/utils/format.d.ts +12 -0
- package/dist/utils/format.js +98 -0
- package/dist/utils/head-svelte.d.ts +2 -0
- package/dist/utils/head-svelte.js +53 -0
- package/dist/utils/server-config.d.ts +19 -0
- package/dist/utils/server-config.js +49 -0
- package/dist/utils/site-config.d.ts +11 -0
- package/dist/utils/site-config.js +14 -0
- package/package.json +8 -4
- package/dist/commands/export.d.ts +0 -8
- package/dist/commands/export.js +0 -163
- package/dist/commands/import.d.ts +0 -9
- package/dist/commands/import.js +0 -118
- package/dist/commands/publish.d.ts +0 -6
- package/dist/commands/publish.js +0 -239
package/dist/commands/build.js
CHANGED
|
@@ -3,81 +3,91 @@ import path from 'path';
|
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import ora from 'ora';
|
|
5
5
|
import { load as load_yaml } from 'js-yaml';
|
|
6
|
+
import { compile } from 'svelte/compiler';
|
|
7
|
+
import * as esbuild from 'esbuild';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
import { read_site_config, SITE_CONFIG_FILE } from '../utils/site-config.js';
|
|
10
|
+
import { validate_head_svelte_content } from '../utils/head-svelte.js';
|
|
11
|
+
// CSS reset applied to all sites by default
|
|
12
|
+
const CSS_RESET = `*, *::before, *::after { box-sizing: border-box; }
|
|
13
|
+
* { margin: 0; }
|
|
14
|
+
body { line-height: 1.5; -webkit-font-smoothing: antialiased; }
|
|
15
|
+
img, picture, video, canvas, svg { display: block; max-width: 100%; }
|
|
16
|
+
input, button, textarea, select { font: inherit; }
|
|
17
|
+
p, h1, h2, h3, h4, h5, h6 { overflow-wrap: break-word; }`;
|
|
6
18
|
export async function build_site(options) {
|
|
7
19
|
const spinner = ora('Building site...').start();
|
|
8
20
|
try {
|
|
9
21
|
const site_dir = path.resolve(options.dir);
|
|
10
22
|
const output_dir = path.resolve(options.output);
|
|
23
|
+
const temp_dir = path.join(site_dir, '.primo', 'build-temp');
|
|
11
24
|
// Read site config
|
|
12
|
-
const config_path = path.join(site_dir, 'primo.json');
|
|
13
25
|
let config;
|
|
14
26
|
try {
|
|
15
|
-
|
|
16
|
-
config = JSON.parse(config_data);
|
|
27
|
+
config = await read_site_config(site_dir);
|
|
17
28
|
}
|
|
18
29
|
catch {
|
|
19
|
-
spinner.fail(
|
|
30
|
+
spinner.fail(`No ${SITE_CONFIG_FILE} found. Run \`primo new\` first.`);
|
|
20
31
|
process.exit(1);
|
|
21
32
|
}
|
|
22
|
-
// Clean and create
|
|
33
|
+
// Clean and create directories
|
|
23
34
|
await fs.rm(output_dir, { recursive: true, force: true });
|
|
24
35
|
await fs.mkdir(output_dir, { recursive: true });
|
|
25
|
-
|
|
36
|
+
await fs.rm(temp_dir, { recursive: true, force: true });
|
|
37
|
+
await fs.mkdir(temp_dir, { recursive: true });
|
|
38
|
+
// Read head.svelte for global head content
|
|
26
39
|
let head_content = '';
|
|
27
40
|
try {
|
|
28
41
|
const head_path = path.join(site_dir, 'site', 'head.svelte');
|
|
29
42
|
head_content = await fs.readFile(head_path, 'utf-8');
|
|
43
|
+
validate_head_svelte_content(head_content, 'site/head.svelte');
|
|
30
44
|
}
|
|
31
|
-
catch {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
// Extract CSS from head.svelte
|
|
35
|
-
let global_css = '';
|
|
36
|
-
if (head_content) {
|
|
37
|
-
const style_match = head_content.match(/<style[^>]*>([\s\S]*?)<\/style>/i);
|
|
38
|
-
if (style_match) {
|
|
39
|
-
global_css = style_match[1];
|
|
45
|
+
catch (error) {
|
|
46
|
+
if (error?.code !== 'ENOENT') {
|
|
47
|
+
throw error;
|
|
40
48
|
}
|
|
49
|
+
// No head.svelte, that's fine
|
|
41
50
|
}
|
|
42
51
|
// Find all pages
|
|
43
52
|
const pages_dir = path.join(site_dir, 'pages');
|
|
44
|
-
const
|
|
45
|
-
spinner.text = `Building ${
|
|
46
|
-
//
|
|
47
|
-
const
|
|
48
|
-
|
|
53
|
+
const page_files = await find_pages(pages_dir);
|
|
54
|
+
spinner.text = `Building ${page_files.length} page${page_files.length !== 1 ? 's' : ''}...`;
|
|
55
|
+
// Compile all blocks once and cache them
|
|
56
|
+
const block_cache = new Map();
|
|
57
|
+
// Cache layouts per page type
|
|
58
|
+
const layout_cache = new Map();
|
|
59
|
+
// Cache per-page-type head fragments (mirrors site head behavior; empty
|
|
60
|
+
// string = no head.svelte for that page type).
|
|
61
|
+
const page_type_head_cache = new Map();
|
|
62
|
+
// Load site data (fields and content)
|
|
63
|
+
const site_data = await load_site_data(site_dir);
|
|
49
64
|
// Build each page
|
|
50
|
-
for (const page_file of
|
|
65
|
+
for (const page_file of page_files) {
|
|
51
66
|
const page_content = await fs.readFile(page_file, 'utf-8');
|
|
52
67
|
const page = load_yaml(page_content);
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const block_html = await render_block(site_dir, block_name, content);
|
|
70
|
-
page_html += block_html;
|
|
68
|
+
const page_path = get_page_path_from_file(site_dir, page_file);
|
|
69
|
+
spinner.text = `Building ${page.name || page_path || 'home'}...`;
|
|
70
|
+
const result = await build_page({
|
|
71
|
+
page,
|
|
72
|
+
page_path,
|
|
73
|
+
site_dir,
|
|
74
|
+
temp_dir,
|
|
75
|
+
head_content,
|
|
76
|
+
site_name: config.name,
|
|
77
|
+
block_cache,
|
|
78
|
+
layout_cache,
|
|
79
|
+
page_type_head_cache,
|
|
80
|
+
site_data
|
|
81
|
+
});
|
|
82
|
+
if (result.error) {
|
|
83
|
+
console.log(chalk.yellow(` Warning: ${page.name}: ${result.error}`));
|
|
71
84
|
}
|
|
72
|
-
//
|
|
73
|
-
const
|
|
74
|
-
// Determine output path
|
|
75
|
-
const slug = page.slug || path.basename(page_file, '.yaml');
|
|
76
|
-
const out_path = slug === '' || slug === 'index'
|
|
85
|
+
// Determine output path from the page file path.
|
|
86
|
+
const out_path = page_path === ''
|
|
77
87
|
? path.join(output_dir, 'index.html')
|
|
78
|
-
: path.join(output_dir,
|
|
88
|
+
: path.join(output_dir, page_path, 'index.html');
|
|
79
89
|
await fs.mkdir(path.dirname(out_path), { recursive: true });
|
|
80
|
-
await fs.writeFile(out_path, html);
|
|
90
|
+
await fs.writeFile(out_path, result.html);
|
|
81
91
|
}
|
|
82
92
|
// Copy uploads directory
|
|
83
93
|
const uploads_src = path.join(site_dir, 'uploads');
|
|
@@ -88,14 +98,17 @@ export async function build_site(options) {
|
|
|
88
98
|
catch {
|
|
89
99
|
// No uploads directory, that's fine
|
|
90
100
|
}
|
|
91
|
-
|
|
101
|
+
// Clean up temp directory
|
|
102
|
+
await fs.rm(temp_dir, { recursive: true, force: true });
|
|
103
|
+
spinner.succeed(`Built ${page_files.length} page${page_files.length !== 1 ? 's' : ''} to ${chalk.cyan(output_dir)}`);
|
|
92
104
|
console.log('');
|
|
93
105
|
console.log(chalk.dim(' Preview locally:'));
|
|
94
106
|
console.log(chalk.dim(` npx serve ${options.output}`));
|
|
95
107
|
console.log('');
|
|
96
108
|
console.log(chalk.dim(' Deploy:'));
|
|
97
|
-
console.log(chalk.dim(
|
|
98
|
-
console.log(chalk.dim(
|
|
109
|
+
console.log(chalk.dim(` npx vercel deploy ${options.output} --prod`));
|
|
110
|
+
console.log(chalk.dim(` npx netlify deploy --prod --dir=${options.output}`));
|
|
111
|
+
console.log(chalk.dim(` npx wrangler pages deploy ${options.output}`));
|
|
99
112
|
console.log('');
|
|
100
113
|
}
|
|
101
114
|
catch (error) {
|
|
@@ -106,224 +119,298 @@ export async function build_site(options) {
|
|
|
106
119
|
process.exit(1);
|
|
107
120
|
}
|
|
108
121
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
for (const entry of entries) {
|
|
120
|
-
const full_path = path.join(dir, entry.name);
|
|
121
|
-
if (entry.isDirectory()) {
|
|
122
|
-
await scan(full_path);
|
|
123
|
-
}
|
|
124
|
-
else if (entry.name.endsWith('.yaml') || entry.name.endsWith('.yml')) {
|
|
125
|
-
pages.push(full_path);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
await scan(pages_dir);
|
|
130
|
-
return pages;
|
|
131
|
-
}
|
|
132
|
-
async function compile_block(site_dir, block_name) {
|
|
133
|
-
const component_path = path.join(site_dir, 'blocks', block_name, 'component.svelte');
|
|
122
|
+
// Lazily load and validate a page type's head.svelte. Cached value is the raw
|
|
123
|
+
// fragment (may include <style>); empty string means "no head file present".
|
|
124
|
+
async function load_page_type_head(site_dir, page_type, cache) {
|
|
125
|
+
const cached = cache.get(page_type);
|
|
126
|
+
if (cached !== undefined)
|
|
127
|
+
return cached;
|
|
128
|
+
const head_path = path.join(site_dir, 'page-types', page_type, 'head.svelte');
|
|
129
|
+
let content = '';
|
|
134
130
|
try {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const style_match = source.match(/<style[^>]*>([\s\S]*?)<\/style>/i);
|
|
138
|
-
const css = style_match ? style_match[1] : '';
|
|
139
|
-
return { css };
|
|
131
|
+
content = await fs.readFile(head_path, 'utf-8');
|
|
132
|
+
validate_head_svelte_content(content, `page-types/${page_type}/head.svelte`);
|
|
140
133
|
}
|
|
141
|
-
catch {
|
|
142
|
-
|
|
134
|
+
catch (error) {
|
|
135
|
+
if (error?.code !== 'ENOENT')
|
|
136
|
+
throw error;
|
|
137
|
+
content = '';
|
|
143
138
|
}
|
|
139
|
+
cache.set(page_type, content);
|
|
140
|
+
return content;
|
|
144
141
|
}
|
|
145
|
-
async function
|
|
146
|
-
const
|
|
142
|
+
async function build_page(options) {
|
|
143
|
+
const { page, page_path, site_dir, temp_dir, head_content, site_name, block_cache, layout_cache, page_type_head_cache, site_data } = options;
|
|
147
144
|
try {
|
|
148
|
-
|
|
149
|
-
//
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
//
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const value = obj[prop_match[2]];
|
|
184
|
-
if (value !== undefined && value !== null) {
|
|
185
|
-
return escape_html(String(value));
|
|
186
|
-
}
|
|
187
|
-
}
|
|
145
|
+
const page_build_id = safe_temp_id(page._id || page.id || page_path || page.name || 'page');
|
|
146
|
+
// Load layout for this page type
|
|
147
|
+
const page_type = page.page_type || 'default';
|
|
148
|
+
let layout = layout_cache.get(page_type);
|
|
149
|
+
if (layout === undefined) {
|
|
150
|
+
layout = await load_layout(site_dir, page_type);
|
|
151
|
+
layout_cache.set(page_type, layout);
|
|
152
|
+
}
|
|
153
|
+
// Page-type head is concatenated after the site head, mirroring server
|
|
154
|
+
// publish behavior (site.head + page_type.head).
|
|
155
|
+
const page_type_head = await load_page_type_head(site_dir, page_type, page_type_head_cache);
|
|
156
|
+
const combined_head_content = page_type_head ? `${head_content}\n${page_type_head}` : head_content;
|
|
157
|
+
// Combine header + page sections + footer
|
|
158
|
+
const header_sections = await resolve_layout_sections(layout.header || [], site_dir, site_data);
|
|
159
|
+
const footer_sections = await resolve_layout_sections(layout.footer || [], site_dir, site_data);
|
|
160
|
+
const page_sections = await resolve_page_sections(page.sections || [], site_dir, site_data);
|
|
161
|
+
const all_sections = [...header_sections, ...page_sections, ...footer_sections];
|
|
162
|
+
if (all_sections.length === 0) {
|
|
163
|
+
return { html: generate_empty_page(site_name, page.name, combined_head_content) };
|
|
164
|
+
}
|
|
165
|
+
const sections = all_sections;
|
|
166
|
+
// Compile each block and collect CSS
|
|
167
|
+
const all_css = [];
|
|
168
|
+
const section_components = [];
|
|
169
|
+
for (let i = 0; i < sections.length; i++) {
|
|
170
|
+
const section = sections[i];
|
|
171
|
+
const block_name = section.block;
|
|
172
|
+
// Check cache first
|
|
173
|
+
let compiled = block_cache.get(block_name);
|
|
174
|
+
if (!compiled) {
|
|
175
|
+
compiled = await compile_block(site_dir, block_name, temp_dir);
|
|
176
|
+
block_cache.set(block_name, compiled);
|
|
177
|
+
}
|
|
178
|
+
if (compiled.css) {
|
|
179
|
+
all_css.push(compiled.css);
|
|
188
180
|
}
|
|
189
|
-
|
|
181
|
+
// Create import and usage for this section
|
|
182
|
+
const component_name = `Section_${i}_${block_name.replace(/-/g, '_')}`;
|
|
183
|
+
section_components.push({
|
|
184
|
+
name: component_name,
|
|
185
|
+
block_name,
|
|
186
|
+
props: section.content || {}
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
// Create a page component that renders all sections
|
|
190
|
+
const page_component = generate_page_component(section_components, sections);
|
|
191
|
+
const page_component_path = path.join(temp_dir, `page_${page_build_id}.svelte`);
|
|
192
|
+
await fs.writeFile(page_component_path, page_component);
|
|
193
|
+
// Compile the page component
|
|
194
|
+
const page_source = await fs.readFile(page_component_path, 'utf-8');
|
|
195
|
+
const page_compiled = compile(page_source, {
|
|
196
|
+
generate: 'server',
|
|
197
|
+
filename: page_component_path,
|
|
198
|
+
css: 'external'
|
|
190
199
|
});
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
if (var_match) {
|
|
196
|
-
const value = content[var_match[1]];
|
|
197
|
-
if (value !== undefined && value !== null && value !== '') {
|
|
198
|
-
return escape_html(String(value));
|
|
200
|
+
if (page_compiled.warnings.length > 0) {
|
|
201
|
+
for (const warning of page_compiled.warnings) {
|
|
202
|
+
if (!warning.message.includes('unused')) {
|
|
203
|
+
console.log(chalk.yellow(` Svelte warning: ${warning.message}`));
|
|
199
204
|
}
|
|
200
205
|
}
|
|
201
|
-
|
|
206
|
+
}
|
|
207
|
+
// Write compiled JS and bundle with esbuild
|
|
208
|
+
const compiled_path = path.join(temp_dir, `page_${page_build_id}.js`);
|
|
209
|
+
await fs.writeFile(compiled_path, page_compiled.js.code);
|
|
210
|
+
// Copy all block compiled JS files to temp for bundling
|
|
211
|
+
for (const section of sections) {
|
|
212
|
+
const block_js_path = path.join(temp_dir, `${section.block}.compiled.js`);
|
|
213
|
+
const cached = block_cache.get(section.block);
|
|
214
|
+
if (cached?.js) {
|
|
215
|
+
await fs.writeFile(block_js_path, cached.js);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
// Bundle with esbuild - include svelte runtime
|
|
219
|
+
const bundle_path = path.join(temp_dir, `bundle_${page_build_id}.mjs`);
|
|
220
|
+
// Find where svelte is installed (could be in primo-cli's node_modules or globally)
|
|
221
|
+
const svelte_base = await find_svelte_path();
|
|
222
|
+
await esbuild.build({
|
|
223
|
+
entryPoints: [compiled_path],
|
|
224
|
+
bundle: true,
|
|
225
|
+
format: 'esm',
|
|
226
|
+
platform: 'node',
|
|
227
|
+
outfile: bundle_path,
|
|
228
|
+
logLevel: 'silent',
|
|
229
|
+
alias: {
|
|
230
|
+
'svelte/internal/server': path.join(svelte_base, 'src/internal/server/index.js'),
|
|
231
|
+
'svelte/internal/shared': path.join(svelte_base, 'src/internal/shared/index.js'),
|
|
232
|
+
'svelte/internal/client': path.join(svelte_base, 'src/internal/client/index.js'),
|
|
233
|
+
'svelte': path.join(svelte_base, 'src/index.js')
|
|
234
|
+
}
|
|
202
235
|
});
|
|
203
|
-
//
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
236
|
+
// Import and render
|
|
237
|
+
const bundle_url = `file://${bundle_path}`;
|
|
238
|
+
const { default: PageComponent } = await import(bundle_url);
|
|
239
|
+
// Import render from svelte/server
|
|
240
|
+
const { render } = await import('svelte/server');
|
|
241
|
+
// Build props for all sections
|
|
242
|
+
const props = {};
|
|
243
|
+
sections.forEach((section, i) => {
|
|
244
|
+
props[`section_${i}_props`] = section.content || {};
|
|
210
245
|
});
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
246
|
+
const rendered = render(PageComponent, { props });
|
|
247
|
+
// Extract CSS from combined head fragments (site + page-type).
|
|
248
|
+
let head_css = '';
|
|
249
|
+
let head_html = combined_head_content;
|
|
250
|
+
const style_matches = combined_head_content.matchAll(/<style[^>]*>([\s\S]*?)<\/style>/gi);
|
|
251
|
+
const head_css_parts = [];
|
|
252
|
+
for (const m of style_matches) {
|
|
253
|
+
head_css_parts.push(m[1]);
|
|
254
|
+
}
|
|
255
|
+
if (head_css_parts.length > 0) {
|
|
256
|
+
head_css = head_css_parts.join('\n');
|
|
257
|
+
head_html = combined_head_content.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '');
|
|
258
|
+
}
|
|
259
|
+
// Combine all CSS (reset first, then head, then blocks)
|
|
260
|
+
const combined_css = [CSS_RESET, head_css, ...all_css].filter(Boolean).join('\n');
|
|
261
|
+
// Generate final HTML
|
|
262
|
+
const title = page.name === 'Home' ? site_name : `${page.name} | ${site_name}`;
|
|
263
|
+
const html = `<!DOCTYPE html>
|
|
264
|
+
<html lang="en">
|
|
265
|
+
<head>
|
|
266
|
+
<meta charset="UTF-8">
|
|
267
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
268
|
+
<title>${escape_html(title)}</title>
|
|
269
|
+
${head_html}
|
|
270
|
+
<style>
|
|
271
|
+
${combined_css}
|
|
272
|
+
</style>
|
|
273
|
+
${rendered.head || ''}
|
|
274
|
+
</head>
|
|
275
|
+
<body>
|
|
276
|
+
${rendered.body || ''}
|
|
277
|
+
</body>
|
|
278
|
+
</html>`;
|
|
279
|
+
return { html };
|
|
214
280
|
}
|
|
215
281
|
catch (error) {
|
|
216
|
-
|
|
217
|
-
return
|
|
282
|
+
const error_msg = error instanceof Error ? error.message : String(error);
|
|
283
|
+
return {
|
|
284
|
+
html: generate_error_page(site_name, page.name, error_msg, head_content),
|
|
285
|
+
error: error_msg,
|
|
286
|
+
};
|
|
218
287
|
}
|
|
219
288
|
}
|
|
220
|
-
function
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
//
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
const obj = content[optional_match[1]];
|
|
238
|
-
if (!obj || typeof obj !== 'object')
|
|
239
|
-
return false;
|
|
240
|
-
return !!obj[optional_match[2]];
|
|
241
|
-
}
|
|
242
|
-
// Handle simple variable check
|
|
243
|
-
const simple_match = condition.match(/^([a-zA-Z_][a-zA-Z0-9_]*)$/);
|
|
244
|
-
if (simple_match) {
|
|
245
|
-
const value = content[simple_match[1]];
|
|
246
|
-
return !!value;
|
|
247
|
-
}
|
|
248
|
-
// Handle obj.prop patterns
|
|
249
|
-
const prop_match = condition.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\.(\w+)$/);
|
|
250
|
-
if (prop_match) {
|
|
251
|
-
const obj = content[prop_match[1]];
|
|
252
|
-
if (!obj || typeof obj !== 'object')
|
|
253
|
-
return false;
|
|
254
|
-
return !!obj[prop_match[2]];
|
|
289
|
+
async function compile_block(site_dir, block_name, temp_dir) {
|
|
290
|
+
const component_path = path.join(site_dir, 'blocks', block_name, 'component.svelte');
|
|
291
|
+
try {
|
|
292
|
+
const source = await fs.readFile(component_path, 'utf-8');
|
|
293
|
+
// Compile with Svelte
|
|
294
|
+
const compiled = compile(source, {
|
|
295
|
+
generate: 'server',
|
|
296
|
+
filename: component_path,
|
|
297
|
+
css: 'external',
|
|
298
|
+
name: block_name.replace(/-/g, '_')
|
|
299
|
+
});
|
|
300
|
+
// Extract CSS
|
|
301
|
+
const css = compiled.css?.code || '';
|
|
302
|
+
// Write compiled JS for importing
|
|
303
|
+
const output_path = path.join(temp_dir, `${block_name}.compiled.js`);
|
|
304
|
+
await fs.writeFile(output_path, compiled.js.code);
|
|
305
|
+
return { js: compiled.js.code, css };
|
|
255
306
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
return !!content[var_match[1]];
|
|
307
|
+
catch (error) {
|
|
308
|
+
console.log(chalk.yellow(` Warning: Could not compile block "${block_name}": ${error}`));
|
|
309
|
+
return { js: '', css: '' };
|
|
260
310
|
}
|
|
261
|
-
return false;
|
|
262
311
|
}
|
|
263
|
-
function
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
return
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
return String(value);
|
|
288
|
-
});
|
|
289
|
-
}
|
|
290
|
-
// Replace {item} if item is a primitive
|
|
291
|
-
if (typeof item !== 'object') {
|
|
292
|
-
result = result.replace(new RegExp(`\\{${item_var}\\}`, 'g'), escape_html(String(item)));
|
|
293
|
-
}
|
|
294
|
-
// Replace index variable if present
|
|
295
|
-
if (index_var) {
|
|
296
|
-
result = result.replace(new RegExp(`\\{${index_var}\\}`, 'g'), String(index));
|
|
297
|
-
}
|
|
298
|
-
return result;
|
|
299
|
-
}).join('');
|
|
300
|
-
});
|
|
312
|
+
function generate_page_component(components, sections) {
|
|
313
|
+
const imports = sections.map((section, i) => {
|
|
314
|
+
const safe_name = section.block.replace(/-/g, '_');
|
|
315
|
+
return `import Section_${i} from './${section.block}.compiled.js'`;
|
|
316
|
+
}).join('\n');
|
|
317
|
+
const props_declarations = sections.map((_, i) => {
|
|
318
|
+
return `section_${i}_props = {}`;
|
|
319
|
+
}).join(',\n\t');
|
|
320
|
+
const section_renders = sections.map((_, i) => {
|
|
321
|
+
return `<Section_${i} {...section_${i}_props} />`;
|
|
322
|
+
}).join('\n\t\t');
|
|
323
|
+
return `<script module>
|
|
324
|
+
${imports}
|
|
325
|
+
</script>
|
|
326
|
+
|
|
327
|
+
<script>
|
|
328
|
+
let {
|
|
329
|
+
${props_declarations}
|
|
330
|
+
} = $props()
|
|
331
|
+
</script>
|
|
332
|
+
|
|
333
|
+
<main>
|
|
334
|
+
${section_renders}
|
|
335
|
+
</main>`;
|
|
301
336
|
}
|
|
302
|
-
function
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
337
|
+
function generate_empty_page(site_name, page_name, head_content) {
|
|
338
|
+
const title = page_name === 'Home' ? site_name : `${page_name} | ${site_name}`;
|
|
339
|
+
return `<!DOCTYPE html>
|
|
340
|
+
<html lang="en">
|
|
341
|
+
<head>
|
|
342
|
+
<meta charset="UTF-8">
|
|
343
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
344
|
+
<title>${escape_html(title)}</title>
|
|
345
|
+
<style>${CSS_RESET}</style>
|
|
346
|
+
${head_content}
|
|
347
|
+
</head>
|
|
348
|
+
<body>
|
|
349
|
+
</body>
|
|
350
|
+
</html>`;
|
|
351
|
+
}
|
|
352
|
+
function generate_error_page(site_name, page_name, error, head_content) {
|
|
353
|
+
const title = page_name === 'Home' ? site_name : `${page_name} | ${site_name}`;
|
|
354
|
+
// Extract just the CSS from head_content
|
|
355
|
+
let head_css = '';
|
|
356
|
+
const style_match = head_content.match(/<style[^>]*>([\s\S]*?)<\/style>/i);
|
|
357
|
+
if (style_match) {
|
|
358
|
+
head_css = style_match[1];
|
|
311
359
|
}
|
|
312
|
-
return
|
|
360
|
+
return `<!DOCTYPE html>
|
|
361
|
+
<html lang="en">
|
|
362
|
+
<head>
|
|
363
|
+
<meta charset="UTF-8">
|
|
364
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
365
|
+
<title>${escape_html(title)}</title>
|
|
366
|
+
<style>
|
|
367
|
+
${CSS_RESET}
|
|
368
|
+
${head_css}
|
|
369
|
+
</style>
|
|
370
|
+
</head>
|
|
371
|
+
<body>
|
|
372
|
+
<div style="padding: 2rem; text-align: center;">
|
|
373
|
+
<h1>Build Error</h1>
|
|
374
|
+
<p style="color: #888;">${escape_html(error)}</p>
|
|
375
|
+
</div>
|
|
376
|
+
</body>
|
|
377
|
+
</html>`;
|
|
313
378
|
}
|
|
314
|
-
function
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
normalized[key] = value[0];
|
|
379
|
+
async function find_pages(pages_dir) {
|
|
380
|
+
const pages = [];
|
|
381
|
+
async function scan(dir) {
|
|
382
|
+
let entries;
|
|
383
|
+
try {
|
|
384
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
321
385
|
}
|
|
322
|
-
|
|
323
|
-
|
|
386
|
+
catch {
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
for (const entry of entries) {
|
|
390
|
+
const full_path = path.join(dir, entry.name);
|
|
391
|
+
if (entry.isDirectory()) {
|
|
392
|
+
await scan(full_path);
|
|
393
|
+
}
|
|
394
|
+
else if (entry.name.endsWith('.yaml')) {
|
|
395
|
+
pages.push(full_path);
|
|
396
|
+
}
|
|
324
397
|
}
|
|
325
398
|
}
|
|
326
|
-
|
|
399
|
+
await scan(pages_dir);
|
|
400
|
+
return pages;
|
|
401
|
+
}
|
|
402
|
+
function get_page_path_from_file(site_dir, page_file) {
|
|
403
|
+
const pages_dir = path.join(site_dir, 'pages');
|
|
404
|
+
let relative_path = path.relative(pages_dir, page_file);
|
|
405
|
+
relative_path = relative_path.replaceAll('\\', '/');
|
|
406
|
+
relative_path = relative_path.replace(/\.yaml$/i, '');
|
|
407
|
+
if (relative_path === 'index') {
|
|
408
|
+
return '';
|
|
409
|
+
}
|
|
410
|
+
if (relative_path.endsWith('/index')) {
|
|
411
|
+
return relative_path.slice(0, -'/index'.length);
|
|
412
|
+
}
|
|
413
|
+
return relative_path;
|
|
327
414
|
}
|
|
328
415
|
function escape_html(str) {
|
|
329
416
|
return str
|
|
@@ -333,35 +420,8 @@ function escape_html(str) {
|
|
|
333
420
|
.replace(/"/g, '"')
|
|
334
421
|
.replace(/'/g, ''');
|
|
335
422
|
}
|
|
336
|
-
function
|
|
337
|
-
|
|
338
|
-
return `<!DOCTYPE html>
|
|
339
|
-
<html lang="en">
|
|
340
|
-
<head>
|
|
341
|
-
<meta charset="UTF-8">
|
|
342
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
343
|
-
<title>${escape_html(title)}</title>
|
|
344
|
-
<style>
|
|
345
|
-
*, *::before, *::after {
|
|
346
|
-
box-sizing: border-box;
|
|
347
|
-
}
|
|
348
|
-
body {
|
|
349
|
-
margin: 0;
|
|
350
|
-
font-family: system-ui, -apple-system, sans-serif;
|
|
351
|
-
line-height: 1.5;
|
|
352
|
-
}
|
|
353
|
-
img {
|
|
354
|
-
max-width: 100%;
|
|
355
|
-
height: auto;
|
|
356
|
-
}
|
|
357
|
-
${css}
|
|
358
|
-
</style>
|
|
359
|
-
</head>
|
|
360
|
-
<body>
|
|
361
|
-
${body}
|
|
362
|
-
</body>
|
|
363
|
-
</html>
|
|
364
|
-
`;
|
|
423
|
+
function safe_temp_id(value) {
|
|
424
|
+
return value.replace(/[^a-zA-Z0-9_-]/g, '_') || 'page';
|
|
365
425
|
}
|
|
366
426
|
async function copy_dir(src, dest) {
|
|
367
427
|
await fs.mkdir(dest, { recursive: true });
|
|
@@ -377,3 +437,159 @@ async function copy_dir(src, dest) {
|
|
|
377
437
|
}
|
|
378
438
|
}
|
|
379
439
|
}
|
|
440
|
+
async function load_layout(site_dir, page_type) {
|
|
441
|
+
const layout_path = path.join(site_dir, 'page-types', page_type, 'layout.yaml');
|
|
442
|
+
try {
|
|
443
|
+
const content = await fs.readFile(layout_path, 'utf-8');
|
|
444
|
+
return load_yaml(content);
|
|
445
|
+
}
|
|
446
|
+
catch {
|
|
447
|
+
// No layout file, return empty layout
|
|
448
|
+
return {};
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
async function resolve_layout_sections(sections, site_dir, site_data) {
|
|
452
|
+
// For layout sections without content, load from block's content.yaml
|
|
453
|
+
const resolved = [];
|
|
454
|
+
for (const section of sections) {
|
|
455
|
+
let content;
|
|
456
|
+
if (section.content && Object.keys(section.content).length > 0) {
|
|
457
|
+
content = section.content;
|
|
458
|
+
}
|
|
459
|
+
else {
|
|
460
|
+
// Try to load default content from block's content.yaml
|
|
461
|
+
content = await load_block_defaults(site_dir, section.block);
|
|
462
|
+
}
|
|
463
|
+
// Resolve any site-field references in the content
|
|
464
|
+
const resolved_content = await resolve_site_fields(site_dir, section.block, content, site_data);
|
|
465
|
+
resolved.push({ ...section, content: resolved_content });
|
|
466
|
+
}
|
|
467
|
+
return resolved;
|
|
468
|
+
}
|
|
469
|
+
async function resolve_page_sections(sections, site_dir, site_data) {
|
|
470
|
+
// Resolve site-field references in page sections
|
|
471
|
+
const resolved = [];
|
|
472
|
+
for (const section of sections) {
|
|
473
|
+
const content = section.content || {};
|
|
474
|
+
const resolved_content = await resolve_site_fields(site_dir, section.block, content, site_data);
|
|
475
|
+
resolved.push({ ...section, content: resolved_content });
|
|
476
|
+
}
|
|
477
|
+
return resolved;
|
|
478
|
+
}
|
|
479
|
+
async function load_block_defaults(site_dir, block_name) {
|
|
480
|
+
const content_path = path.join(site_dir, 'blocks', block_name, 'content.yaml');
|
|
481
|
+
try {
|
|
482
|
+
const content = await fs.readFile(content_path, 'utf-8');
|
|
483
|
+
return load_yaml(content) || {};
|
|
484
|
+
}
|
|
485
|
+
catch {
|
|
486
|
+
return {};
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
async function load_fields_file(file_path) {
|
|
490
|
+
const data = await fs.readFile(file_path, 'utf-8');
|
|
491
|
+
return load_yaml(data);
|
|
492
|
+
}
|
|
493
|
+
function extract_fields_array(data) {
|
|
494
|
+
if (Array.isArray(data)) {
|
|
495
|
+
return data;
|
|
496
|
+
}
|
|
497
|
+
if (data && typeof data === 'object' && Array.isArray(data.fields)) {
|
|
498
|
+
return data.fields;
|
|
499
|
+
}
|
|
500
|
+
return [];
|
|
501
|
+
}
|
|
502
|
+
function get_field_id(field) {
|
|
503
|
+
return field._id || field.id;
|
|
504
|
+
}
|
|
505
|
+
async function load_site_data(site_dir) {
|
|
506
|
+
const fields_path = path.join(site_dir, 'site', 'fields.yaml');
|
|
507
|
+
const content_path = path.join(site_dir, 'site', 'content.yaml');
|
|
508
|
+
let fields = [];
|
|
509
|
+
let content = {};
|
|
510
|
+
try {
|
|
511
|
+
fields = extract_fields_array(await load_fields_file(fields_path));
|
|
512
|
+
}
|
|
513
|
+
catch {
|
|
514
|
+
// No site fields defined
|
|
515
|
+
}
|
|
516
|
+
try {
|
|
517
|
+
const content_data = await fs.readFile(content_path, 'utf-8');
|
|
518
|
+
content = load_yaml(content_data) || {};
|
|
519
|
+
}
|
|
520
|
+
catch {
|
|
521
|
+
// No site content defined
|
|
522
|
+
}
|
|
523
|
+
return { fields, content };
|
|
524
|
+
}
|
|
525
|
+
async function load_block_fields(site_dir, block_name) {
|
|
526
|
+
const fields_path = path.join(site_dir, 'blocks', block_name, 'fields.yaml');
|
|
527
|
+
try {
|
|
528
|
+
return extract_fields_array(await load_fields_file(fields_path));
|
|
529
|
+
}
|
|
530
|
+
catch {
|
|
531
|
+
return [];
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
async function resolve_site_fields(site_dir, block_name, content, site_data) {
|
|
535
|
+
// Load block field definitions to check for site-field types
|
|
536
|
+
const block_fields = await load_block_fields(site_dir, block_name);
|
|
537
|
+
// Create maps for site field lookup
|
|
538
|
+
const site_field_id_map = new Map(); // ID -> name
|
|
539
|
+
const site_field_name_set = new Set(); // names that exist
|
|
540
|
+
for (const site_field of site_data.fields) {
|
|
541
|
+
const site_field_id = get_field_id(site_field);
|
|
542
|
+
if (site_field_id) {
|
|
543
|
+
site_field_id_map.set(site_field_id, site_field.name);
|
|
544
|
+
}
|
|
545
|
+
site_field_name_set.add(site_field.name);
|
|
546
|
+
}
|
|
547
|
+
// Resolve site-field references in content
|
|
548
|
+
const resolved = { ...content };
|
|
549
|
+
// For each block field that is type site-field, resolve its value
|
|
550
|
+
for (const field of block_fields) {
|
|
551
|
+
if (field.type !== 'site-field')
|
|
552
|
+
continue;
|
|
553
|
+
const field_ref = field.config?.field;
|
|
554
|
+
if (field_ref) {
|
|
555
|
+
// Field reference can be either a name or an ID
|
|
556
|
+
// Try as name first (new format)
|
|
557
|
+
if (site_field_name_set.has(field_ref) && site_data.content[field_ref] !== undefined) {
|
|
558
|
+
resolved[field.name] = site_data.content[field_ref];
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
// Try as ID (old format)
|
|
562
|
+
const site_field_name = site_field_id_map.get(field_ref);
|
|
563
|
+
if (site_field_name && site_data.content[site_field_name] !== undefined) {
|
|
564
|
+
resolved[field.name] = site_data.content[site_field_name];
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
// Fallback: if block field name matches a site field name, use that value
|
|
569
|
+
if (site_field_name_set.has(field.name) && site_data.content[field.name] !== undefined) {
|
|
570
|
+
resolved[field.name] = site_data.content[field.name];
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
return resolved;
|
|
574
|
+
}
|
|
575
|
+
async function find_svelte_path() {
|
|
576
|
+
// Try to find svelte in various locations
|
|
577
|
+
const possible_paths = [
|
|
578
|
+
// In primo-cli's node_modules (when npm linked or installed globally)
|
|
579
|
+
path.join(path.dirname(fileURLToPath(import.meta.url)), '../../node_modules/svelte'),
|
|
580
|
+
// In the current project's node_modules
|
|
581
|
+
path.join(process.cwd(), 'node_modules/svelte'),
|
|
582
|
+
// Try resolving from this file's location
|
|
583
|
+
path.join(path.dirname(fileURLToPath(import.meta.url)), '../../../node_modules/svelte')
|
|
584
|
+
];
|
|
585
|
+
for (const p of possible_paths) {
|
|
586
|
+
try {
|
|
587
|
+
await fs.access(p);
|
|
588
|
+
return p;
|
|
589
|
+
}
|
|
590
|
+
catch {
|
|
591
|
+
// Continue to next path
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
throw new Error('Could not find svelte package. Make sure svelte is installed.');
|
|
595
|
+
}
|