@atomicsmash/cli 9.0.1-beta.3 → 10.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,550 +0,0 @@
1
- import crypto from "node:crypto";
2
- import { readFileSync } from "node:fs";
3
- import { readdir, readFile, writeFile } from "node:fs/promises";
4
- import { basename, dirname, extname, join, resolve as resolvePath, sep as pathSeparator, } from "node:path";
5
- import { pathToFileURL } from "node:url";
6
- import { DatePHP } from "@atomicsmash/date-php";
7
- import autoprefixer from "autoprefixer";
8
- import CopyWebpackPlugin from "copy-webpack-plugin";
9
- import { cosmiconfig } from "cosmiconfig";
10
- import cssnano from "cssnano";
11
- import glob from "glob-promise";
12
- import postcss from "postcss";
13
- import postcssLoadConfig from "postcss-load-config";
14
- import { compileStringAsync } from "sass";
15
- import { TsconfigPathsPlugin } from "tsconfig-paths-webpack-plugin";
16
- import { tsImport } from "tsx/esm/api";
17
- import webpack from "webpack";
18
- import defaultConfig from "../external/wordpress.webpack.config.cjs";
19
- import { getSmashConfig, startRunningMessage, toCamelCase } from "../utils.js";
20
- const isProduction = process.env.NODE_ENV === "production";
21
- export const command = "blocks";
22
- export const describe = "A command to generate WordPress blocks from a src folder.";
23
- export const deprecated = "Please migrate to using the @atomicsmash/compiler package. This command will be removed in the next major version.";
24
- export const builder = function (yargs) {
25
- return yargs
26
- .options({
27
- in: {
28
- string: true,
29
- normalize: true,
30
- description: "The directory where the WP blocks can be found. Relative to cwd.",
31
- },
32
- out: {
33
- string: true,
34
- normalize: true,
35
- description: "The directory where the WP blocks will be output. Relative to cwd.",
36
- },
37
- tsConfigPath: {
38
- string: true,
39
- normalize: true,
40
- description: "The directory where the tsconfig file can be found. Relative to cwd. Defaults to the in folder.",
41
- },
42
- postcssConfigPath: {
43
- string: true,
44
- normalize: true,
45
- description: "The directory where the postcss config file can be found. Relative to cwd. Defaults to the in folder and then searches up the directory tree.",
46
- },
47
- watch: {
48
- boolean: true,
49
- default: false,
50
- description: "Watch the blocks in folder for changes and compile.",
51
- },
52
- excludeBlocks: {
53
- array: true,
54
- string: true,
55
- default: ["__TEMPLATE__"],
56
- description: "A list of the folder names of blocks to exclude from compilation.",
57
- },
58
- excludeRootFiles: {
59
- array: true,
60
- string: true,
61
- default: [],
62
- description: "A list of the root file names to exclude from compilation.",
63
- },
64
- alwaysCompileRootFiles: {
65
- boolean: true,
66
- default: false,
67
- description: "By default, we won't compile root files if no blocks are found, this allows you to override that setting.",
68
- },
69
- ignoreWarnings: {
70
- boolean: true,
71
- default: false,
72
- description: "If webpack has warnings, don't output them or change error code.",
73
- },
74
- })
75
- .example("$0 blocks --watch --in src --out build --tsConfigPath tsconfig.json", "");
76
- };
77
- export async function handler(args) {
78
- const smashConfig = await getSmashConfig();
79
- const srcFolder = args.in
80
- ? resolvePath(args.in)
81
- : smashConfig?.themePath
82
- ? resolvePath(join(smashConfig?.themePath, "src", "blocks"))
83
- : null;
84
- const distFolder = args.out
85
- ? resolvePath(args.out)
86
- : smashConfig?.themePath
87
- ? resolvePath(join(smashConfig?.themePath, smashConfig.assetsOutputFolder, "blocks"))
88
- : null;
89
- if (!srcFolder || !distFolder) {
90
- throw new Error("Failed to get the in or out folders for the blocks. Please add a smash.config.ts file to your project with a themeName and a themePath.");
91
- }
92
- const tsConfigLocation = args.tsConfigPath ?? srcFolder;
93
- const postcssConfigLocation = args.postcssConfigPath ?? srcFolder;
94
- const excludeBlocks = args.excludeBlocks;
95
- const excludeRootFiles = args.excludeRootFiles;
96
- const shouldAlwaysCompileRootFiles = args.alwaysCompileRootFiles;
97
- const isWatch = args.watch;
98
- const shouldIgnoreWarnings = args.ignoreWarnings;
99
- await runCommand({
100
- srcFolder,
101
- distFolder,
102
- tsConfigLocation,
103
- postcssConfigLocation,
104
- excludeBlocks,
105
- excludeRootFiles,
106
- shouldAlwaysCompileRootFiles,
107
- isWatch,
108
- shouldIgnoreWarnings,
109
- });
110
- }
111
- async function runCommand({ srcFolder, distFolder, tsConfigLocation, postcssConfigLocation, excludeBlocks, excludeRootFiles, shouldAlwaysCompileRootFiles, isWatch, shouldIgnoreWarnings, }) {
112
- const blocks = await getBlockJsonFiles(srcFolder, excludeBlocks);
113
- const isNoBlocks = Object.keys(blocks).length === 0;
114
- const compiler = webpack({
115
- ...defaultConfig,
116
- entry: async () => {
117
- const entryPoints = {
118
- ...(isNoBlocks && !shouldAlwaysCompileRootFiles
119
- ? {}
120
- : await getRootFileJSEntryPoints({ srcFolder, excludeRootFiles })),
121
- ...getAllBlocksJSEntryPoints({
122
- srcFolder,
123
- blocks,
124
- }),
125
- };
126
- // TODO: improve this console output
127
- console.log({ entryPoints });
128
- console.log(`EntryPoints set at ${new DatePHP().format("jS F Y \\a\\t H:i:s")}`);
129
- return entryPoints;
130
- },
131
- context: srcFolder,
132
- plugins: [
133
- ...(defaultConfig.plugins?.filter((plugin) => {
134
- return ![
135
- "CopyPlugin", // CopyWebpackPlugin
136
- ].includes(plugin?.constructor.name ?? "");
137
- }) ?? []),
138
- ...(isNoBlocks
139
- ? []
140
- : [
141
- new CopyWebpackPlugin({
142
- patterns: [...getCopyPatternsForBlocks(blocks, distFolder)],
143
- }),
144
- ]),
145
- new CustomBlocksCSSHandler({
146
- blocks,
147
- srcFolder,
148
- distFolder,
149
- postcssConfigLocation,
150
- }),
151
- new BlockJsonAssetTransformer({ distFolder }),
152
- ],
153
- output: {
154
- path: distFolder,
155
- clean: true,
156
- },
157
- resolve: {
158
- plugins: [
159
- new TsconfigPathsPlugin({
160
- configFile: tsConfigLocation,
161
- }),
162
- ],
163
- extensions: [".ts", ".js", ".tsx", ".jsx"],
164
- },
165
- mode: isProduction ? "production" : "development",
166
- node: { global: false },
167
- });
168
- if (!compiler) {
169
- throw new Error("Failed to initialise compiler.");
170
- }
171
- if (isWatch) {
172
- console.log("The blocks command is now deprecated. " + deprecated);
173
- const watching = compiler.watch({
174
- // Example
175
- aggregateTimeout: 300,
176
- poll: undefined,
177
- }, (error, stats) => {
178
- if (error) {
179
- console.error(error.stack ?? error);
180
- process.exitCode = 1;
181
- watching?.close((closeError) => {
182
- console.error(closeError);
183
- });
184
- }
185
- else if (stats) {
186
- const info = stats.toJson();
187
- if (stats.hasErrors()) {
188
- console.error(info.errors);
189
- }
190
- if (stats.hasWarnings() && !shouldIgnoreWarnings) {
191
- console.warn(info.warnings);
192
- }
193
- }
194
- });
195
- process.on("SIGINT", function () {
196
- watching?.close((closeError) => {
197
- console.log("The blocks command is now deprecated. " + deprecated);
198
- console.error(closeError);
199
- });
200
- });
201
- }
202
- else {
203
- const stopRunningMessage = startRunningMessage("Building blocks");
204
- compiler.run((error, stats) => {
205
- void stopRunningMessage();
206
- if (error) {
207
- console.error(error);
208
- process.exitCode = 1;
209
- }
210
- else if (stats) {
211
- const info = stats.toJson();
212
- if (stats.hasErrors()) {
213
- console.error(info.errors);
214
- process.exitCode = 1;
215
- }
216
- if (stats.hasWarnings() && !shouldIgnoreWarnings) {
217
- console.warn(info.warnings);
218
- if (!stats.hasErrors()) {
219
- // Only change exit code if there's no errors.
220
- process.exitCode = 2;
221
- }
222
- }
223
- }
224
- compiler.close((closeError) => {
225
- console.log("The blocks command is now deprecated. " + deprecated);
226
- if (closeError) {
227
- process.exitCode = 1;
228
- }
229
- });
230
- });
231
- }
232
- }
233
- export async function getRootFileJSEntryPoints({ srcFolder, excludeRootFiles, }) {
234
- const entryPoints = {};
235
- const rootFiles = await readdir(srcFolder, {
236
- withFileTypes: true,
237
- }).then((srcDirFiles) => {
238
- return srcDirFiles
239
- .filter((dirent) => dirent.isFile())
240
- .map((dirent) => dirent.name);
241
- });
242
- for (const fileNameAndExtension of rootFiles) {
243
- const [entryName, extension] = fileNameAndExtension.split(".", 2);
244
- if (excludeRootFiles.includes(entryName) ||
245
- excludeRootFiles.includes(fileNameAndExtension)) {
246
- continue;
247
- }
248
- if (entryName === undefined || extension === undefined) {
249
- console.log(`Failed to process ${fileNameAndExtension}, continuing anyway.`);
250
- continue;
251
- }
252
- if (!["ts", "js", "tsx", "jsx"].includes(extension)) {
253
- continue;
254
- }
255
- entryPoints[toCamelCase(entryName)] = {
256
- import: [srcFolder, fileNameAndExtension].join(pathSeparator),
257
- filename: `${entryName}${isProduction ? `.[contenthash]` : ""}.js`,
258
- };
259
- }
260
- return entryPoints;
261
- }
262
- export function getAllBlocksJSEntryPoints({ srcFolder, blocks, }) {
263
- const entryPoints = {};
264
- const blocksJsonObjects = Object.values(blocks);
265
- for (const { blockFolder, blockJson } of blocksJsonObjects) {
266
- const fields = getBlockJsonScriptFields(blockJson);
267
- if (!fields) {
268
- continue;
269
- }
270
- for (const value of Object.values(fields).flat()) {
271
- if ("string" !== typeof value || !value.startsWith("file:")) {
272
- continue;
273
- }
274
- // Removes the `file:` prefix.
275
- const filepath = join(blockFolder, value.replace("file:", ""));
276
- const entryName = filepath
277
- .replace(srcFolder, "")
278
- .replace(extname(filepath), "")
279
- .replace(/\\/g, "/")
280
- .split("/")
281
- .filter((string) => string !== "")
282
- .join("-");
283
- entryPoints[toCamelCase(entryName)] = {
284
- import: filepath,
285
- filename: `${filepath.replace(`${srcFolder}${pathSeparator}`, "").replace(extname(filepath), "")}${isProduction ? `.[contenthash]` : ""}.js`,
286
- };
287
- }
288
- }
289
- return entryPoints;
290
- }
291
- export async function getBlockJsonFiles(srcFolder, excludeBlocks) {
292
- const blockJsonFiles = await glob
293
- .promise(`${srcFolder.replaceAll(pathSeparator, "/")}/**/block.json.ts`, {
294
- ignore: excludeBlocks.map((blockName) => `${srcFolder.replaceAll(pathSeparator, "/")}/**/${blockName}/block.json.ts`),
295
- })
296
- .then((paths) => paths.map((path) => path.replaceAll("/", pathSeparator)));
297
- const blocks = {};
298
- for (const blockJsonFile of blockJsonFiles) {
299
- const blockJson = await tsImport(`${blockJsonFile.split(pathSeparator).shift()?.includes(":") ? `file:///` : ""}${blockJsonFile}`, import.meta.url).then((loadedFile) => {
300
- return loadedFile.blockJson;
301
- });
302
- const blockName = blockJsonFile
303
- .replace(`${pathSeparator}block.json.ts`, "")
304
- .split(pathSeparator)
305
- .pop();
306
- blocks[blockName] = { blockFolder: dirname(blockJsonFile), blockJson };
307
- }
308
- return blocks;
309
- }
310
- export function getBlockJsonScriptFields(blockJson) {
311
- const scriptFields = ["viewScript", "script", "editorScript"];
312
- let result = null;
313
- for (const field of scriptFields) {
314
- if (Object.hasOwn(blockJson, field)) {
315
- result ??= {};
316
- result[field] = blockJson[field];
317
- }
318
- }
319
- return result;
320
- }
321
- export function getBlockJsonStyleFields(blockJson) {
322
- const styleFields = ["viewStyle", "style", "editorStyle"];
323
- let result = null;
324
- for (const field of styleFields) {
325
- if (Object.hasOwn(blockJson, field)) {
326
- result ??= {};
327
- result[field] = blockJson[field];
328
- }
329
- }
330
- return result;
331
- }
332
- async function getSassOptions(srcFolder) {
333
- const smashConfig = await getSmashConfig();
334
- if (smashConfig) {
335
- return smashConfig.scssAliases;
336
- }
337
- // Legacy fallback.
338
- const explorer = cosmiconfig("scssAliases");
339
- const config = await explorer
340
- .load(resolvePath(process.cwd(), "scssAliases.config.ts"))
341
- .then((result) => {
342
- if (!result || result.isEmpty) {
343
- throw new Error("Return default config.");
344
- }
345
- const config = result.config;
346
- if (typeof config === "object" &&
347
- config !== null &&
348
- Object.keys(config).length <= 2) {
349
- if (Object.keys(config).length === 2 &&
350
- "importers" in config &&
351
- "loadPaths" in config) {
352
- return config;
353
- }
354
- if (Object.keys(config).length === 1 &&
355
- ("importers" in config || "loadPaths" in config)) {
356
- return config;
357
- }
358
- }
359
- throw new Error("Return default config.");
360
- })
361
- .catch(() => {
362
- const defaultConfig = {
363
- importers: [
364
- {
365
- findFileUrl(url) {
366
- if (!url.startsWith("sitecss:"))
367
- return null;
368
- const pathname = url.substring(8);
369
- return pathToFileURL(`${resolvePath(srcFolder, "../css")}${pathname.startsWith("/") ? pathname : `/${pathname}`}`);
370
- },
371
- },
372
- {
373
- findFileUrl(url) {
374
- if (!url.startsWith("launchpad:"))
375
- return null;
376
- const pathname = url.substring(10);
377
- return pathToFileURL(`${resolvePath(process.cwd(), "public/wp-content/themes/launchpad/src/styles")}${pathname.startsWith("/") ? pathname : `/${pathname}`}`);
378
- },
379
- },
380
- ],
381
- };
382
- return defaultConfig;
383
- });
384
- return config;
385
- }
386
- function getCopyPatternsForBlocks(blocks, distFolder) {
387
- const blocksCopyPatterns = [];
388
- const blocksEntries = Object.entries(blocks);
389
- for (const [blockName, { blockFolder, blockJson }] of blocksEntries) {
390
- // Generate block.json file.
391
- blocksCopyPatterns.push({
392
- from: `${blockFolder}/block.json.ts`,
393
- to: `${distFolder}/${blockName}/block.json`,
394
- noErrorOnMissing: true,
395
- transform: {
396
- transformer() {
397
- return JSON.stringify(blockJson, null, 2);
398
- },
399
- cache: true,
400
- },
401
- });
402
- // Copy render.php file
403
- blocksCopyPatterns.push({
404
- from: `${blockFolder}/render.php`,
405
- to: `${distFolder}/${blockName}/render.php`,
406
- noErrorOnMissing: true,
407
- });
408
- // Copy index.php file
409
- blocksCopyPatterns.push({
410
- from: `${blockFolder}/index.php`,
411
- to: `${distFolder}/${blockName}/index.php`,
412
- noErrorOnMissing: true,
413
- });
414
- }
415
- return blocksCopyPatterns;
416
- }
417
- /**
418
- * This class handles CSS compilation for blocks.
419
- */
420
- class CustomBlocksCSSHandler {
421
- blocks;
422
- distFolder;
423
- srcFolder;
424
- postcssConfigLocation;
425
- constructor({ blocks, srcFolder, distFolder, postcssConfigLocation, }) {
426
- this.blocks = blocks;
427
- this.srcFolder = srcFolder;
428
- this.distFolder = distFolder;
429
- this.postcssConfigLocation = postcssConfigLocation;
430
- }
431
- apply(compiler) {
432
- // RawSource is one of the "sources" classes that should be used
433
- // to represent asset sources in compilation.
434
- const { RawSource } = webpack.sources;
435
- compiler.hooks.thisCompilation.tap({
436
- name: "CustomBlocksCSSHandler",
437
- stage: webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL,
438
- }, (compilation) => {
439
- compilation.hooks.processAssets.tapPromise("CustomBlocksCSSHandler", async () => {
440
- const blocksJsonObjects = Object.values(this.blocks);
441
- for (const { blockFolder, blockJson } of blocksJsonObjects) {
442
- const fields = getBlockJsonStyleFields(blockJson);
443
- if (!fields) {
444
- continue;
445
- }
446
- for (const value of Object.values(fields).flat()) {
447
- if ("string" !== typeof value || !value.startsWith("file:")) {
448
- continue;
449
- }
450
- // Removes the `file:` prefix.
451
- const filepath = join(blockFolder, value.replace("file:", ""));
452
- const fileNameWithExtension = basename(filepath);
453
- let css = readFileSync(filepath, "utf8");
454
- let newFileNameWithExtension = fileNameWithExtension;
455
- if (extname(fileNameWithExtension) === ".scss") {
456
- css = await compileStringAsync(css, await getSassOptions(this.srcFolder)).then((result) => result.css);
457
- newFileNameWithExtension = newFileNameWithExtension.replace(".scss", ".css");
458
- }
459
- let newMatch = filepath.replace(`${this.srcFolder}/`, "");
460
- if (isProduction) {
461
- const fileBuffer = readFileSync(filepath);
462
- const contentHash = crypto.createHash("shake256", {
463
- outputLength: 10,
464
- });
465
- contentHash.update(fileBuffer);
466
- newFileNameWithExtension = `${fileNameWithExtension.split(".")[0]}.${contentHash.digest("hex")}.css`;
467
- }
468
- newMatch = newMatch.replace(fileNameWithExtension, newFileNameWithExtension);
469
- const { plugins, options } = await postcssLoadConfig({}, this.postcssConfigLocation)
470
- .then(({ plugins, options }) => {
471
- options.from = filepath;
472
- options.to = newMatch;
473
- return { plugins, options };
474
- })
475
- .catch((error) => {
476
- if (error &&
477
- error instanceof Error &&
478
- error.message.startsWith("No PostCSS Config found")) {
479
- const postCSSPlugins = [autoprefixer];
480
- if (isProduction) {
481
- postCSSPlugins.push(cssnano({ preset: "default" }));
482
- }
483
- return {
484
- plugins: postCSSPlugins,
485
- options: {
486
- from: filepath,
487
- to: newMatch,
488
- },
489
- };
490
- }
491
- throw error;
492
- });
493
- const result = await postcss(plugins).process(css, options);
494
- compilation.emitAsset(newMatch, new RawSource(result.css));
495
- if (result.map) {
496
- compilation.emitAsset(`${newMatch}.map`, new RawSource(result.map.toString()));
497
- }
498
- }
499
- }
500
- });
501
- });
502
- }
503
- }
504
- class BlockJsonAssetTransformer {
505
- distFolder;
506
- constructor({ distFolder }) {
507
- this.distFolder = distFolder;
508
- }
509
- apply(compiler) {
510
- compiler.hooks.afterEmit.tapPromise("BlockJsonAssetTransformer", async (compilation) => {
511
- const blockJsonFiles = Object.keys(compilation.assets).filter((asset) => asset.endsWith("block.json"));
512
- for (const blockJsonFile of blockJsonFiles) {
513
- await readFile(`${this.distFolder}/${blockJsonFile}`, "utf-8")
514
- .then((data) => {
515
- return JSON.parse(data);
516
- })
517
- .then((json) => {
518
- const blockName = blockJsonFile.replace("/block.json", "");
519
- [
520
- getBlockJsonScriptFields(json),
521
- getBlockJsonStyleFields(json),
522
- ].forEach((fields) => {
523
- if (fields === null) {
524
- return;
525
- }
526
- Object.entries(fields).forEach(([fieldName, value]) => {
527
- const values = [value].flat(1);
528
- json[fieldName] = values.map((srcFilePath) => {
529
- const fileName = srcFilePath
530
- .replace("file:./", "")
531
- .replace(extname(srcFilePath), "");
532
- const hashedAsset = Object.keys(compilation.assets).find((asset) => {
533
- return (asset.startsWith(`${blockName}/${fileName}`) &&
534
- !asset.endsWith(".php"));
535
- });
536
- return hashedAsset
537
- ? `file:./${hashedAsset.replace(`${blockName}/`, "")}`
538
- : srcFilePath;
539
- });
540
- });
541
- });
542
- return json;
543
- })
544
- .then(async (transformedJson) => {
545
- await writeFile(`${this.distFolder}/${blockJsonFile}`, JSON.stringify(transformedJson, undefined, 2), "utf-8");
546
- });
547
- }
548
- });
549
- }
550
- }