tiendu 0.6.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +106 -42
- package/bin/tiendu.js +130 -55
- package/lib/build.mjs +240 -131
- package/lib/config.mjs +68 -12
- package/lib/dev.mjs +26 -30
- package/lib/init.mjs +116 -106
- package/lib/preview.mjs +63 -42
- package/lib/publish.mjs +14 -16
- package/lib/pull.mjs +9 -6
- package/lib/push.mjs +8 -10
- package/lib/stores.mjs +91 -0
- package/lib/ui.mjs +138 -0
- package/lib/update-check.mjs +8 -4
- package/package.json +1 -1
package/lib/build.mjs
CHANGED
|
@@ -10,8 +10,7 @@ import {
|
|
|
10
10
|
} from "node:fs/promises";
|
|
11
11
|
import path from "node:path";
|
|
12
12
|
import * as esbuild from "esbuild";
|
|
13
|
-
import
|
|
14
|
-
import { readThemeConfig } from "./config.mjs";
|
|
13
|
+
import { getThemePipelineConfig, readThemeConfig } from "./config.mjs";
|
|
15
14
|
import {
|
|
16
15
|
flattenAssetLogicalPath,
|
|
17
16
|
getAssetImportFilter,
|
|
@@ -22,17 +21,26 @@ import {
|
|
|
22
21
|
} from "./assets.mjs";
|
|
23
22
|
import { listFilesRecursive } from "./fs-utils.mjs";
|
|
24
23
|
import { createCssPostCssPlugin } from "./postcss.mjs";
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
import * as ui from "./ui.mjs";
|
|
25
|
+
|
|
26
|
+
const THEME_SOURCE_OUTPUT_DIRS = [
|
|
27
|
+
"layout",
|
|
28
|
+
"templates",
|
|
29
|
+
"sections",
|
|
30
|
+
"blocks",
|
|
31
|
+
"snippets",
|
|
32
|
+
"config",
|
|
33
|
+
];
|
|
27
34
|
const LIQUID_LIKE_EXTENSIONS = new Set([".liquid", ".html", ".htm"]);
|
|
28
35
|
const ENTRY_SOURCE_EXTENSIONS = new Set([".js", ".ts", ".css"]);
|
|
29
|
-
const NESTED_ASSET_PATH_PATTERN =
|
|
36
|
+
const NESTED_ASSET_PATH_PATTERN =
|
|
37
|
+
/\/assets\/([A-Za-z0-9._-]+(?:\/[A-Za-z0-9._/-]+)+)([?#][A-Za-z0-9=&._-]+)?/g;
|
|
30
38
|
|
|
31
39
|
/**
|
|
32
|
-
* Discover JS/TS and CSS entry points from src/layout
|
|
40
|
+
* Discover optional JS/TS and CSS entry points from src/layout/templates or layout/templates.
|
|
33
41
|
* Returns separate maps for JS and CSS to avoid key collisions.
|
|
34
42
|
*/
|
|
35
|
-
const discoverEntryPoints = async (
|
|
43
|
+
const discoverEntryPoints = async (rootDir) => {
|
|
36
44
|
const jsEntries = {};
|
|
37
45
|
const cssEntries = {};
|
|
38
46
|
|
|
@@ -40,24 +48,29 @@ const discoverEntryPoints = async (srcDir) => {
|
|
|
40
48
|
["layout", "layout"],
|
|
41
49
|
["templates", "template"],
|
|
42
50
|
]) {
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
files
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const ext = path.extname(file);
|
|
52
|
-
if (![".js", ".ts", ".css"].includes(ext)) continue;
|
|
53
|
-
const name = path.basename(file, ext);
|
|
54
|
-
const key = `${prefix}-${name}.bundle`;
|
|
55
|
-
const fullPath = path.join(dirPath, file);
|
|
56
|
-
if (ext === ".css") {
|
|
57
|
-
cssEntries[key] = fullPath;
|
|
58
|
-
} else {
|
|
59
|
-
jsEntries[key] = fullPath;
|
|
51
|
+
const sourceCandidates = [path.join(rootDir, "src", dir), path.join(rootDir, dir)];
|
|
52
|
+
|
|
53
|
+
for (const dirPath of sourceCandidates) {
|
|
54
|
+
let files;
|
|
55
|
+
try {
|
|
56
|
+
files = await readdir(dirPath);
|
|
57
|
+
} catch {
|
|
58
|
+
continue;
|
|
60
59
|
}
|
|
60
|
+
for (const file of files) {
|
|
61
|
+
const ext = path.extname(file);
|
|
62
|
+
if (![".js", ".ts", ".css"].includes(ext)) continue;
|
|
63
|
+
const name = path.basename(file, ext);
|
|
64
|
+
const key = `${prefix}-${name}.bundle`;
|
|
65
|
+
const fullPath = path.join(dirPath, file);
|
|
66
|
+
if (ext === ".css") {
|
|
67
|
+
cssEntries[key] = fullPath;
|
|
68
|
+
} else {
|
|
69
|
+
jsEntries[key] = fullPath;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
break;
|
|
61
74
|
}
|
|
62
75
|
}
|
|
63
76
|
|
|
@@ -123,22 +136,37 @@ const copyThemeSourceFile = async (
|
|
|
123
136
|
|
|
124
137
|
if (LIQUID_LIKE_EXTENSIONS.has(path.extname(src).toLowerCase())) {
|
|
125
138
|
const source = await readFile(src, "utf-8");
|
|
126
|
-
await writeFile(
|
|
139
|
+
await writeFile(
|
|
140
|
+
dest,
|
|
141
|
+
rewriteDirectAssetPaths(source, knownAssetLogicalPaths),
|
|
142
|
+
"utf-8",
|
|
143
|
+
);
|
|
127
144
|
return;
|
|
128
145
|
}
|
|
129
146
|
|
|
130
147
|
await copyFile(src, dest);
|
|
131
148
|
};
|
|
132
149
|
|
|
133
|
-
const copyThemeFiles = async (
|
|
150
|
+
const copyThemeFiles = async (
|
|
151
|
+
rootDir,
|
|
152
|
+
distDir,
|
|
153
|
+
themeSourceDirs,
|
|
154
|
+
knownAssetLogicalPaths,
|
|
155
|
+
) => {
|
|
134
156
|
for (const sourceDir of themeSourceDirs) {
|
|
135
157
|
const absoluteSourceDir = path.join(rootDir, sourceDir.sourceRelativeDir);
|
|
136
158
|
const absoluteFiles = await listFilesRecursive(absoluteSourceDir);
|
|
137
159
|
|
|
138
160
|
for (const absolutePath of absoluteFiles) {
|
|
139
161
|
const nestedRelativePath = path.relative(absoluteSourceDir, absolutePath);
|
|
140
|
-
const sourceRelativePath = path.join(
|
|
141
|
-
|
|
162
|
+
const sourceRelativePath = path.join(
|
|
163
|
+
sourceDir.sourceRelativeDir,
|
|
164
|
+
nestedRelativePath,
|
|
165
|
+
);
|
|
166
|
+
const outputRelativePath = path.join(
|
|
167
|
+
sourceDir.outputRelativeDir,
|
|
168
|
+
nestedRelativePath,
|
|
169
|
+
);
|
|
142
170
|
if (!shouldCopyThemeSourceFile(sourceRelativePath)) continue;
|
|
143
171
|
await copyThemeSourceFile(
|
|
144
172
|
rootDir,
|
|
@@ -155,11 +183,17 @@ const shouldTriggerTailwindCssRebuild = (relativePath) => {
|
|
|
155
183
|
const normalizedPath = relativePath.split(path.sep).join("/");
|
|
156
184
|
const extension = path.extname(normalizedPath).toLowerCase();
|
|
157
185
|
|
|
158
|
-
if (
|
|
186
|
+
if (
|
|
187
|
+
!["layout/", "templates/", "sections/", "blocks/", "snippets/"].some(
|
|
188
|
+
(prefix) => normalizedPath.startsWith(prefix),
|
|
189
|
+
)
|
|
190
|
+
) {
|
|
159
191
|
return false;
|
|
160
192
|
}
|
|
161
193
|
|
|
162
|
-
return [".liquid", ".html", ".htm", ".js", ".ts", ".json", ".md"].includes(
|
|
194
|
+
return [".liquid", ".html", ".htm", ".js", ".ts", ".json", ".md"].includes(
|
|
195
|
+
extension,
|
|
196
|
+
);
|
|
163
197
|
};
|
|
164
198
|
|
|
165
199
|
const createSourceAssetImportPlugin = (rootDir, sourceDirs) => ({
|
|
@@ -173,7 +207,11 @@ const createSourceAssetImportPlugin = (rootDir, sourceDirs) => ({
|
|
|
173
207
|
const candidatePath = path.isAbsolute(args.path)
|
|
174
208
|
? args.path
|
|
175
209
|
: path.resolve(args.resolveDir, args.path);
|
|
176
|
-
const assetInfo = await getAssetSourceInfo(
|
|
210
|
+
const assetInfo = await getAssetSourceInfo(
|
|
211
|
+
rootDir,
|
|
212
|
+
candidatePath,
|
|
213
|
+
sourceDirs,
|
|
214
|
+
);
|
|
177
215
|
if (!assetInfo) return null;
|
|
178
216
|
|
|
179
217
|
return {
|
|
@@ -183,14 +221,17 @@ const createSourceAssetImportPlugin = (rootDir, sourceDirs) => ({
|
|
|
183
221
|
};
|
|
184
222
|
});
|
|
185
223
|
|
|
186
|
-
build.onLoad(
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
224
|
+
build.onLoad(
|
|
225
|
+
{ filter: /.*/, namespace: "tiendu-source-asset" },
|
|
226
|
+
async (args) => {
|
|
227
|
+
const assetInfo = args.pluginData;
|
|
228
|
+
return {
|
|
229
|
+
contents: `export default ${JSON.stringify(`/${assetInfo.outputRelativePath}`)};`,
|
|
230
|
+
loader: "js",
|
|
231
|
+
watchFiles: [assetInfo.absolutePath],
|
|
232
|
+
};
|
|
233
|
+
},
|
|
234
|
+
);
|
|
194
235
|
},
|
|
195
236
|
});
|
|
196
237
|
|
|
@@ -208,12 +249,15 @@ const runEntryBuilds = async (jsBuildOptions, cssBuildOptions) => {
|
|
|
208
249
|
*/
|
|
209
250
|
export const build = async ({ watch: watchMode = false } = {}) => {
|
|
210
251
|
const rootDir = process.cwd();
|
|
211
|
-
const srcDir = path.join(rootDir, "src");
|
|
212
252
|
const distDir = path.join(rootDir, "dist");
|
|
213
253
|
|
|
214
254
|
const themeConfig = await readThemeConfig();
|
|
215
|
-
|
|
216
|
-
|
|
255
|
+
const pipeline = getThemePipelineConfig(themeConfig);
|
|
256
|
+
|
|
257
|
+
if (pipeline.postcss && !pipeline.compileStyles) {
|
|
258
|
+
ui.log.error(
|
|
259
|
+
"Invalid tiendu.config.json: pipeline.postcss requires pipeline.compileStyles to be enabled.",
|
|
260
|
+
);
|
|
217
261
|
return { ok: false };
|
|
218
262
|
}
|
|
219
263
|
|
|
@@ -222,10 +266,14 @@ export const build = async ({ watch: watchMode = false } = {}) => {
|
|
|
222
266
|
await mkdir(distDir, { recursive: true });
|
|
223
267
|
|
|
224
268
|
const themeSourceDirs = await getThemeSourceDirs(rootDir);
|
|
225
|
-
const staticAssetSourceDirs = await getStaticAssetSourceDirs(rootDir, {
|
|
269
|
+
const staticAssetSourceDirs = await getStaticAssetSourceDirs(rootDir, {
|
|
270
|
+
refresh: true,
|
|
271
|
+
});
|
|
226
272
|
|
|
227
273
|
// Discover entry points (JS and CSS separately to avoid key collisions)
|
|
228
|
-
const
|
|
274
|
+
const discoveredEntries = await discoverEntryPoints(rootDir);
|
|
275
|
+
const jsEntries = pipeline.compileScripts ? discoveredEntries.jsEntries : {};
|
|
276
|
+
const cssEntries = pipeline.compileStyles ? discoveredEntries.cssEntries : {};
|
|
229
277
|
const jsCount = Object.keys(jsEntries).length;
|
|
230
278
|
const cssCount = Object.keys(cssEntries).length;
|
|
231
279
|
const entryCount = jsCount + cssCount;
|
|
@@ -236,7 +284,9 @@ export const build = async ({ watch: watchMode = false } = {}) => {
|
|
|
236
284
|
let staticAssetOwners = new Map();
|
|
237
285
|
const knownAssetLogicalPaths = new Set();
|
|
238
286
|
const cssPlugins = [];
|
|
239
|
-
const jsPlugins =
|
|
287
|
+
const jsPlugins = pipeline.compileScripts
|
|
288
|
+
? [createSourceAssetImportPlugin(rootDir, staticAssetSourceDirs)]
|
|
289
|
+
: [];
|
|
240
290
|
|
|
241
291
|
try {
|
|
242
292
|
staticAssetOwners = await syncStaticAssets(
|
|
@@ -246,7 +296,7 @@ export const build = async ({ watch: watchMode = false } = {}) => {
|
|
|
246
296
|
staticAssetSourceDirs,
|
|
247
297
|
);
|
|
248
298
|
} catch (error) {
|
|
249
|
-
|
|
299
|
+
ui.log.error(`Static asset build failed: ${error.message}`);
|
|
250
300
|
return { ok: false };
|
|
251
301
|
}
|
|
252
302
|
|
|
@@ -255,42 +305,82 @@ export const build = async ({ watch: watchMode = false } = {}) => {
|
|
|
255
305
|
}
|
|
256
306
|
|
|
257
307
|
// Copy theme files after asset paths are known
|
|
258
|
-
await copyThemeFiles(
|
|
308
|
+
await copyThemeFiles(
|
|
309
|
+
rootDir,
|
|
310
|
+
distDir,
|
|
311
|
+
themeSourceDirs,
|
|
312
|
+
knownAssetLogicalPaths,
|
|
313
|
+
);
|
|
259
314
|
|
|
260
|
-
if (cssCount > 0) {
|
|
315
|
+
if (cssCount > 0 && pipeline.postcss) {
|
|
261
316
|
try {
|
|
262
317
|
cssPlugins.push(
|
|
263
|
-
await createCssPostCssPlugin(rootDir, {
|
|
318
|
+
await createCssPostCssPlugin(rootDir, {
|
|
319
|
+
sourceDirs: staticAssetSourceDirs,
|
|
320
|
+
}),
|
|
264
321
|
);
|
|
265
322
|
} catch (error) {
|
|
266
|
-
|
|
323
|
+
ui.log.error(`CSS pipeline failed to initialize: ${error.message}`);
|
|
267
324
|
return { ok: false };
|
|
268
325
|
}
|
|
269
326
|
}
|
|
270
327
|
|
|
271
328
|
if (entryCount === 0) {
|
|
272
|
-
|
|
273
|
-
|
|
329
|
+
const hasThemeFiles =
|
|
330
|
+
themeSourceDirs.length > 0 || staticAssetSourceDirs.length > 0;
|
|
331
|
+
|
|
332
|
+
if (!hasThemeFiles) {
|
|
333
|
+
ui.log.error("No theme source files or entry points found.");
|
|
334
|
+
return { ok: false };
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (!watchMode) {
|
|
338
|
+
ui.log.success("Prepared theme files to dist/.");
|
|
339
|
+
return { ok: true };
|
|
340
|
+
}
|
|
274
341
|
}
|
|
275
342
|
|
|
276
343
|
const outdir = path.join(distDir, "assets");
|
|
277
|
-
const jsBuildOptions =
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
344
|
+
const jsBuildOptions =
|
|
345
|
+
jsCount > 0
|
|
346
|
+
? {
|
|
347
|
+
entryPoints: jsEntries,
|
|
348
|
+
bundle: true,
|
|
349
|
+
format: "esm",
|
|
350
|
+
target: "es2020",
|
|
351
|
+
outdir,
|
|
352
|
+
logLevel: "warning",
|
|
353
|
+
write: true,
|
|
354
|
+
resolveExtensions: [".ts", ".tsx", ".js", ".jsx", ".json"],
|
|
355
|
+
plugins: jsPlugins,
|
|
356
|
+
}
|
|
357
|
+
: null;
|
|
358
|
+
const cssBuildOptions =
|
|
359
|
+
cssCount > 0
|
|
360
|
+
? {
|
|
361
|
+
entryPoints: cssEntries,
|
|
362
|
+
bundle: true,
|
|
363
|
+
outdir,
|
|
364
|
+
logLevel: "warning",
|
|
365
|
+
write: true,
|
|
366
|
+
plugins: cssPlugins,
|
|
367
|
+
}
|
|
368
|
+
: null;
|
|
283
369
|
|
|
284
370
|
if (!watchMode) {
|
|
285
371
|
// One-shot build
|
|
286
372
|
try {
|
|
287
373
|
await runEntryBuilds(jsBuildOptions, cssBuildOptions);
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
374
|
+
if (entryCount === 0) {
|
|
375
|
+
ui.log.success("Prepared theme files to dist/.");
|
|
376
|
+
} else {
|
|
377
|
+
ui.log.success(
|
|
378
|
+
`Built ${entryCount} entry point${entryCount === 1 ? "" : "s"} to dist/`,
|
|
379
|
+
);
|
|
380
|
+
}
|
|
291
381
|
return { ok: true };
|
|
292
382
|
} catch (error) {
|
|
293
|
-
|
|
383
|
+
ui.log.error(`Build failed: ${error.message}`);
|
|
294
384
|
return { ok: false };
|
|
295
385
|
}
|
|
296
386
|
}
|
|
@@ -312,14 +402,18 @@ export const build = async ({ watch: watchMode = false } = {}) => {
|
|
|
312
402
|
contexts.push(cssCtx);
|
|
313
403
|
}
|
|
314
404
|
} catch (error) {
|
|
315
|
-
|
|
405
|
+
ui.log.error(`Build failed: ${error.message}`);
|
|
316
406
|
for (const ctx of contexts) await ctx.dispose();
|
|
317
407
|
return { ok: false };
|
|
318
408
|
}
|
|
319
409
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
410
|
+
if (entryCount === 0) {
|
|
411
|
+
ui.log.success("Prepared theme files. Watching for changes...");
|
|
412
|
+
} else {
|
|
413
|
+
ui.log.success(
|
|
414
|
+
`Built ${entryCount} entry point${entryCount === 1 ? "" : "s"}. Watching for changes...`,
|
|
415
|
+
);
|
|
416
|
+
}
|
|
323
417
|
|
|
324
418
|
// Watch theme directories for Liquid/static asset changes and copy to dist
|
|
325
419
|
const themeWatchers = [];
|
|
@@ -340,9 +434,9 @@ export const build = async ({ watch: watchMode = false } = {}) => {
|
|
|
340
434
|
|
|
341
435
|
try {
|
|
342
436
|
await cssCtx.rebuild();
|
|
343
|
-
console.log("
|
|
437
|
+
console.log("CSS bundles updated");
|
|
344
438
|
} catch (error) {
|
|
345
|
-
|
|
439
|
+
ui.log.warn(`Error rebuilding CSS: ${error.message}`);
|
|
346
440
|
} finally {
|
|
347
441
|
cssRebuildInFlight = false;
|
|
348
442
|
if (cssRebuildQueued) {
|
|
@@ -380,83 +474,98 @@ export const build = async ({ watch: watchMode = false } = {}) => {
|
|
|
380
474
|
knownAssetLogicalPaths.delete(result.logicalPath);
|
|
381
475
|
}
|
|
382
476
|
|
|
383
|
-
console.log(
|
|
477
|
+
console.log(
|
|
478
|
+
`${result.type === "delete" ? "DELETE" : "UPDATE"} ${result.outputRelativePath}`,
|
|
479
|
+
);
|
|
384
480
|
};
|
|
385
481
|
|
|
386
482
|
for (const sourceDir of themeSourceDirs) {
|
|
387
483
|
const dirPath = path.join(rootDir, sourceDir.sourceRelativeDir);
|
|
388
484
|
|
|
389
|
-
const watcher = watch(
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
485
|
+
const watcher = watch(
|
|
486
|
+
dirPath,
|
|
487
|
+
{ recursive: true },
|
|
488
|
+
(eventType, filename) => {
|
|
489
|
+
if (!filename) return;
|
|
490
|
+
const normalizedFilename = filename.split(path.sep).join("/");
|
|
491
|
+
const sourceRelativePath = `${sourceDir.sourceRelativeDir}/${normalizedFilename}`;
|
|
492
|
+
const outputRelativePath = `${sourceDir.outputRelativeDir}/${normalizedFilename}`;
|
|
493
|
+
|
|
494
|
+
const existing = debounceMap.get(sourceRelativePath);
|
|
495
|
+
if (existing) clearTimeout(existing);
|
|
496
|
+
|
|
497
|
+
const timer = setTimeout(async () => {
|
|
498
|
+
debounceMap.delete(sourceRelativePath);
|
|
499
|
+
try {
|
|
500
|
+
if (!shouldCopyThemeSourceFile(sourceRelativePath)) {
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const fileStat = await stat(
|
|
505
|
+
path.join(rootDir, sourceRelativePath),
|
|
506
|
+
).catch(() => null);
|
|
507
|
+
if (fileStat && fileStat.isFile()) {
|
|
508
|
+
await copyThemeSourceFile(
|
|
509
|
+
rootDir,
|
|
510
|
+
distDir,
|
|
511
|
+
sourceRelativePath,
|
|
512
|
+
outputRelativePath,
|
|
513
|
+
knownAssetLogicalPaths,
|
|
514
|
+
);
|
|
515
|
+
console.log(`UPDATE ${outputRelativePath}`);
|
|
516
|
+
} else if (!fileStat) {
|
|
517
|
+
// File deleted — remove from dist
|
|
518
|
+
const dest = path.join(distDir, outputRelativePath);
|
|
519
|
+
await rm(dest, { force: true });
|
|
520
|
+
console.log(`DELETE ${outputRelativePath}`);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if (
|
|
524
|
+
pipeline.compileStyles &&
|
|
525
|
+
shouldTriggerTailwindCssRebuild(outputRelativePath)
|
|
526
|
+
) {
|
|
527
|
+
queueCssRebuild();
|
|
528
|
+
}
|
|
529
|
+
} catch (error) {
|
|
530
|
+
ui.log.warn(
|
|
531
|
+
`Error copying ${sourceRelativePath}: ${error.message}`,
|
|
415
532
|
);
|
|
416
|
-
console.log(`⟳ ${outputRelativePath}`);
|
|
417
|
-
} else if (!fileStat) {
|
|
418
|
-
// File deleted — remove from dist
|
|
419
|
-
const dest = path.join(distDir, outputRelativePath);
|
|
420
|
-
await rm(dest, { force: true });
|
|
421
|
-
console.log(`✕ ${outputRelativePath}`);
|
|
422
533
|
}
|
|
534
|
+
}, DEBOUNCE_MS);
|
|
423
535
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
} catch (error) {
|
|
428
|
-
p.log.warn(`Error copying ${sourceRelativePath}: ${error.message}`);
|
|
429
|
-
}
|
|
430
|
-
}, DEBOUNCE_MS);
|
|
431
|
-
|
|
432
|
-
debounceMap.set(sourceRelativePath, timer);
|
|
433
|
-
});
|
|
536
|
+
debounceMap.set(sourceRelativePath, timer);
|
|
537
|
+
},
|
|
538
|
+
);
|
|
434
539
|
|
|
435
540
|
themeWatchers.push(watcher);
|
|
436
541
|
}
|
|
437
542
|
|
|
438
543
|
for (const assetDir of await getStaticAssetSourceDirs(rootDir)) {
|
|
439
|
-
const watcher = watch(
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
544
|
+
const watcher = watch(
|
|
545
|
+
assetDir.absoluteDir,
|
|
546
|
+
{ recursive: true },
|
|
547
|
+
(eventType, filename) => {
|
|
548
|
+
if (!filename) return;
|
|
549
|
+
const relativePath = `${assetDir.relativeDir}/${filename.split(path.sep).join("/")}`;
|
|
550
|
+
|
|
551
|
+
const existing = debounceMap.get(relativePath);
|
|
552
|
+
if (existing) clearTimeout(existing);
|
|
553
|
+
|
|
554
|
+
const timer = setTimeout(async () => {
|
|
555
|
+
debounceMap.delete(relativePath);
|
|
556
|
+
try {
|
|
557
|
+
await handleStaticAssetChange(relativePath);
|
|
558
|
+
} catch (error) {
|
|
559
|
+
const errorLabel = error.message.includes("Asset collision")
|
|
560
|
+
? "Asset collision"
|
|
561
|
+
: "Error compiling";
|
|
562
|
+
ui.log.warn(`${errorLabel} ${relativePath}: ${error.message}`);
|
|
563
|
+
}
|
|
564
|
+
}, DEBOUNCE_MS);
|
|
457
565
|
|
|
458
|
-
|
|
459
|
-
|
|
566
|
+
debounceMap.set(relativePath, timer);
|
|
567
|
+
},
|
|
568
|
+
);
|
|
460
569
|
|
|
461
570
|
themeWatchers.push(watcher);
|
|
462
571
|
}
|
package/lib/config.mjs
CHANGED
|
@@ -7,13 +7,42 @@ const CREDENTIALS_FILE = "credentials.json";
|
|
|
7
7
|
const THEME_CONFIG_FILE = "tiendu.config.json";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* @typedef {{ storeId
|
|
10
|
+
* @typedef {{ storeId?: number, apiBaseUrl: string, previewKey?: string }} TienduConfig
|
|
11
11
|
* @typedef {{ apiKey: string }} TienduCredentials
|
|
12
|
+
* @typedef {{ compileScripts: boolean, compileStyles: boolean, postcss: boolean }} TienduPipelineConfig
|
|
13
|
+
* @typedef {{ pipeline?: Partial<TienduPipelineConfig> }} TienduThemeConfig
|
|
12
14
|
*/
|
|
13
15
|
|
|
14
16
|
const getConfigDir = () => path.resolve(process.cwd(), CONFIG_DIR);
|
|
15
17
|
const getConfigPath = () => path.join(getConfigDir(), CONFIG_FILE);
|
|
16
18
|
const getCredentialsPath = () => path.join(getConfigDir(), CREDENTIALS_FILE);
|
|
19
|
+
const getThemeConfigPath = () => path.resolve(process.cwd(), THEME_CONFIG_FILE);
|
|
20
|
+
|
|
21
|
+
const isPlainObject = (value) =>
|
|
22
|
+
value !== null && typeof value === "object" && !Array.isArray(value);
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {TienduThemeConfig} themeConfig
|
|
26
|
+
* @returns {TienduThemeConfig}
|
|
27
|
+
*/
|
|
28
|
+
const validateThemeConfig = (themeConfig) => {
|
|
29
|
+
if (!isPlainObject(themeConfig)) {
|
|
30
|
+
throw new Error("tiendu.config.json must contain a JSON object.");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (themeConfig.pipeline !== undefined && !isPlainObject(themeConfig.pipeline)) {
|
|
34
|
+
throw new Error('tiendu.config.json: "pipeline" must be an object.');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
for (const key of ["compileScripts", "compileStyles", "postcss"]) {
|
|
38
|
+
const value = themeConfig.pipeline?.[key];
|
|
39
|
+
if (value !== undefined && typeof value !== "boolean") {
|
|
40
|
+
throw new Error(`tiendu.config.json: "pipeline.${key}" must be true or false.`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return themeConfig;
|
|
45
|
+
};
|
|
17
46
|
|
|
18
47
|
/** @returns {Promise<TienduConfig | null>} */
|
|
19
48
|
export const readConfig = async () => {
|
|
@@ -55,29 +84,49 @@ export const writeCredentials = async (credentials) => {
|
|
|
55
84
|
);
|
|
56
85
|
};
|
|
57
86
|
|
|
58
|
-
/** @returns {Promise<
|
|
87
|
+
/** @returns {Promise<TienduThemeConfig | null>} */
|
|
59
88
|
export const readThemeConfig = async () => {
|
|
60
89
|
try {
|
|
61
|
-
const raw = await readFile(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
90
|
+
const raw = await readFile(getThemeConfigPath(), "utf-8");
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
return validateThemeConfig(JSON.parse(raw));
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if (error instanceof SyntaxError) {
|
|
96
|
+
throw new Error(`Invalid tiendu.config.json: ${error.message}`);
|
|
97
|
+
}
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
} catch (error) {
|
|
101
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
throw error;
|
|
68
105
|
}
|
|
69
106
|
};
|
|
70
107
|
|
|
71
|
-
/**
|
|
72
|
-
|
|
108
|
+
/**
|
|
109
|
+
* @param {TienduThemeConfig | null} themeConfig
|
|
110
|
+
* @returns {TienduPipelineConfig}
|
|
111
|
+
*/
|
|
112
|
+
export const getThemePipelineConfig = (themeConfig) => ({
|
|
113
|
+
compileScripts: themeConfig?.pipeline?.compileScripts === true,
|
|
114
|
+
compileStyles: themeConfig?.pipeline?.compileStyles === true,
|
|
115
|
+
postcss: themeConfig?.pipeline?.postcss === true,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
/** @returns {Promise<TienduPipelineConfig>} */
|
|
119
|
+
export const readThemePipelineConfig = async () =>
|
|
120
|
+
getThemePipelineConfig(await readThemeConfig());
|
|
73
121
|
|
|
74
122
|
/** @returns {string} */
|
|
75
123
|
export const getDistDir = () => path.resolve(process.cwd(), "dist");
|
|
76
124
|
|
|
77
125
|
/**
|
|
126
|
+
* @param {{ requireStore?: boolean }} [options]
|
|
78
127
|
* @returns {Promise<{ config: TienduConfig, credentials: TienduCredentials }>}
|
|
79
128
|
*/
|
|
80
|
-
export const loadConfigOrFail = async () => {
|
|
129
|
+
export const loadConfigOrFail = async ({ requireStore = true } = {}) => {
|
|
81
130
|
const config = await readConfig();
|
|
82
131
|
if (!config) {
|
|
83
132
|
console.error("Error: no .cli/config.json found. Run tiendu init first.");
|
|
@@ -92,5 +141,12 @@ export const loadConfigOrFail = async () => {
|
|
|
92
141
|
process.exit(1);
|
|
93
142
|
}
|
|
94
143
|
|
|
144
|
+
if (requireStore && !config.storeId) {
|
|
145
|
+
console.error(
|
|
146
|
+
"Error: no store selected. Run tiendu stores list and tiendu stores set <id>.",
|
|
147
|
+
);
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
|
|
95
151
|
return { config, credentials };
|
|
96
152
|
};
|