@next-core/build-next-bricks 1.5.3 → 1.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.
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import path from "node:path";
3
3
  import { existsSync } from "node:fs";
4
+ import { mkdir, rm, writeFile } from "node:fs/promises";
4
5
  import build from "../src/build.js";
5
6
  import scanBricks from "../src/scanBricks.js";
6
- import generateMetadata from "../src/generateMetadata.js";
7
+ import generatePkgBuild from "../src/generatePkgBuild.js";
7
8
 
8
9
  /**
9
10
  * @typedef {T | Array<T>} MaybeArray<T>
@@ -12,6 +13,9 @@ import generateMetadata from "../src/generateMetadata.js";
12
13
 
13
14
  /** @typedef {import("@next-core/build-next-bricks").BuildNextBricksConfig} BuildNextBricksConfig */
14
15
 
16
+ const manifestOnly = process.argv.includes("--manifest-only");
17
+ const watch = process.argv.includes("--watch");
18
+
15
19
  try {
16
20
  const startTime = Date.now();
17
21
 
@@ -37,43 +41,63 @@ try {
37
41
  ? `${scanBricksCost}ms`
38
42
  : `${(scanBricksCost / 1000).toFixed(2)}s`
39
43
  );
44
+ if (manifestOnly) {
45
+ const distDir = path.join(packageDir, "dist");
46
+ if (existsSync(distDir)) {
47
+ await rm(distDir, { recursive: true });
48
+ }
49
+ await mkdir(distDir);
50
+ await writeFile(
51
+ path.join(distDir, "manifest.json"),
52
+ JSON.stringify(config.manifest, null, 2)
53
+ );
54
+ }
40
55
  }
41
56
  }
42
57
 
43
- const compiler = await build(
44
- configList.length === 1 ? configList[0] : configList
45
- );
46
-
47
- const watch = process.argv.includes("--watch");
48
-
49
- if (watch) {
50
- compiler.watch({}, (err, stats) => {
51
- if (err || stats.hasErrors()) {
52
- console.error("Failed to build bricks:");
53
- console.error(err || stats.toString());
54
- } else {
55
- console.log("Build bricks done in watch mode");
56
- }
57
- });
58
+ if (manifestOnly) {
59
+ console.log(
60
+ `Build bricks manifest done in ${(
61
+ (Date.now() - startTime) /
62
+ 1000
63
+ ).toFixed(2)}s`
64
+ );
58
65
  } else {
59
- await new Promise((resolve, reject) => {
60
- compiler.run((err, stats) => {
66
+ const compiler = await build(
67
+ configList.length === 1 ? configList[0] : configList
68
+ );
69
+
70
+ if (watch) {
71
+ compiler.watch({}, (err, stats) => {
61
72
  if (err || stats.hasErrors()) {
62
73
  console.error("Failed to build bricks:");
63
- reject(err || stats.toString());
74
+ console.error(err || stats.toString());
64
75
  } else {
65
- resolve();
76
+ console.log("Build bricks done in watch mode");
66
77
  }
67
78
  });
68
- });
79
+ } else {
80
+ await new Promise((resolve, reject) => {
81
+ compiler.run((err, stats) => {
82
+ if (err || stats.hasErrors()) {
83
+ console.error("Failed to build bricks:");
84
+ reject(err || stats.toString());
85
+ } else {
86
+ resolve();
87
+ }
88
+ });
89
+ });
69
90
 
70
- if (configList.some((config) => !config.type || config.type === "bricks")) {
71
- await generateMetadata(packageDir);
72
- }
91
+ if (
92
+ configList.some((config) => !config.type || config.type === "bricks")
93
+ ) {
94
+ await generatePkgBuild(packageDir);
95
+ }
73
96
 
74
- console.log(
75
- `Build bricks done in ${((Date.now() - startTime) / 1000).toFixed(2)}s`
76
- );
97
+ console.log(
98
+ `Build bricks done in ${((Date.now() - startTime) / 1000).toFixed(2)}s`
99
+ );
100
+ }
77
101
  }
78
102
  } catch (e) {
79
103
  console.error(e);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@next-core/build-next-bricks",
3
- "version": "1.5.3",
3
+ "version": "1.7.0",
4
4
  "description": "Build next bricks",
5
5
  "homepage": "https://github.com/easyops-cn/next-core/tree/v3/packages/build-next-bricks",
6
6
  "license": "GPL-3.0",
@@ -38,6 +38,7 @@
38
38
  "css-loader": "^6.7.4",
39
39
  "cssnano": "^6.0.1",
40
40
  "cssnano-preset-lite": "^3.0.0",
41
+ "doctrine": "^3.0.0",
41
42
  "lodash": "^4.17.21",
42
43
  "mini-css-extract-plugin": "^2.7.6",
43
44
  "postcss": "^8.4.23",
@@ -47,5 +48,8 @@
47
48
  "typescript": "^5.0.4",
48
49
  "webpack": "^5.84.1"
49
50
  },
50
- "gitHead": "d8ed3d57b38b9626e70cbb84e61542cc3c269556"
51
+ "devDependencies": {
52
+ "@next-core/brick-manifest": "^0.1.0"
53
+ },
54
+ "gitHead": "aabcaa697f6aae0c4494f0dc19cf5e02e5bcf990"
51
55
  }
@@ -1,10 +1,14 @@
1
1
  import webpack from "webpack";
2
2
 
3
+ /**
4
+ * @typedef {import("@next-core/brick-manifest").PackageManifest} PackageManifest
5
+ */
6
+
3
7
  const pluginName = "EmitBricksJsonPlugin";
4
8
 
5
9
  export default class EmitBricksJsonPlugin {
6
10
  /**
7
- * @param {{ packageName: string; bricks: string[]; processors: string[]; dependencies: Record<string, string[]>; }} options
11
+ * @param {{ packageName: string; bricks: string[]; processors: string[]; dependencies: Record<string, string[]>; manifest: PackageManifest; }} options
8
12
  */
9
13
  constructor(options) {
10
14
  this.packageName = options.packageName;
@@ -12,6 +16,7 @@ export default class EmitBricksJsonPlugin {
12
16
  this.elements = options.elements;
13
17
  this.processors = options.processors;
14
18
  this.dependencies = options.dependencies;
19
+ this.manifest = options.manifest;
15
20
  }
16
21
 
17
22
  /**
@@ -60,6 +65,12 @@ export default class EmitBricksJsonPlugin {
60
65
  new webpack.sources.RawSource(bricksJson, false)
61
66
  );
62
67
 
68
+ const manifestJson = JSON.stringify(this.manifest, null, 2);
69
+ compilation.emitAsset(
70
+ "manifest.json",
71
+ new webpack.sources.RawSource(manifestJson, false)
72
+ );
73
+
63
74
  console.log("Defined bricks:", this.bricks);
64
75
  console.log("Defined elements:", this.elements);
65
76
  console.log("Defined processors:", this.processors);
package/src/build.js CHANGED
@@ -365,6 +365,7 @@ async function getWebpackConfig(config) {
365
365
  elements,
366
366
  processors,
367
367
  dependencies: config.dependencies,
368
+ manifest: config.manifest,
368
369
  }),
369
370
  ]
370
371
  : []),
@@ -9,7 +9,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
9
  /**
10
10
  * @param {string} packageDir
11
11
  */
12
- export default async function generateMetadata(packageDir) {
12
+ export default async function generatePkgBuild(packageDir) {
13
13
  const targetPkgBuildDir = path.join(packageDir, ".pkgbuild");
14
14
  if (!existsSync(targetPkgBuildDir)) {
15
15
  mkdirSync(targetPkgBuildDir);
@@ -0,0 +1,299 @@
1
+ import { parse } from "doctrine";
2
+
3
+ /**
4
+ * @typedef {import("@next-core/brick-manifest").BrickManifest} BrickManifest
5
+ * @typedef {import("@next-core/brick-manifest").PropertyManifest} PropertyManifest
6
+ * @typedef {import("@next-core/brick-manifest").EventManifest} EventManifest
7
+ * @typedef {import("@next-core/brick-manifest").MethodManifest} MethodManifest
8
+ * @typedef {import("@babel/types").Node} Node
9
+ * @typedef {import("@babel/traverse").NodePath} NodePath
10
+ * @typedef {import("@babel/types").ClassDeclaration} ClassDeclaration
11
+ * @typedef {import("doctrine").Tag} Tag
12
+ */
13
+
14
+ /**
15
+ * @param {string} name
16
+ * @param {NodePath} nodePath
17
+ * @param {string} source
18
+ * @returns {BrickManifest}
19
+ */
20
+ export default function makeBrickManifest(name, nodePath, source) {
21
+ /** @type {import("@babel/traverse").NodePath<ClassDeclaration>} */
22
+ const classPath = nodePath.parentPath;
23
+ /** @type {BrickManifest} */
24
+ const manifest = {
25
+ name,
26
+ properties: [],
27
+ events: [],
28
+ slots: [],
29
+ methods: [],
30
+ };
31
+
32
+ const docComment = findDocComment(nodePath, source);
33
+ if (docComment) {
34
+ manifest.description = docComment.description;
35
+ for (const tag of docComment.tags) {
36
+ if (tag.title === "slot") {
37
+ const match = tag.description.match(/^(?:([-\w]+)\s+-\s+)?(.*)$/);
38
+ if (!match) {
39
+ throw new Error(
40
+ `Doc comment for slot is invalid: '${tag.description}'`
41
+ );
42
+ }
43
+ manifest.slots.push({
44
+ name: match[1] ?? null,
45
+ description: match[2],
46
+ });
47
+ }
48
+ }
49
+ }
50
+
51
+ scanFields(manifest, classPath.node.body.body, source);
52
+
53
+ return manifest;
54
+ }
55
+
56
+ /**
57
+ * @param {NodePath} nodePath
58
+ * @param {string} source
59
+ */
60
+ function findDocComment({ node, parentPath }, source) {
61
+ if (node.type !== "Program") {
62
+ const docComment = parseDocComment(node, source);
63
+ if (docComment) {
64
+ return docComment;
65
+ }
66
+ }
67
+ if (parentPath) {
68
+ return findDocComment(parentPath, source);
69
+ }
70
+ }
71
+
72
+ /**
73
+ * @param {BrickManifest} manifest
74
+ * @param {Node[]} nodes
75
+ * @param {string} source
76
+ */
77
+ function scanFields(manifest, nodes, source) {
78
+ for (const node of nodes) {
79
+ if (node.type === "ClassAccessorProperty" && node.decorators?.length) {
80
+ for (const { expression } of node.decorators) {
81
+ if (
82
+ expression.type === "CallExpression" &&
83
+ expression.callee.type === "Identifier"
84
+ ) {
85
+ switch (expression.callee.name) {
86
+ case "property": {
87
+ /** @type {PropertyManifest} */
88
+ const prop = {
89
+ name: node.key.name,
90
+ };
91
+ const docComment = parseDocComment(node, source);
92
+ if (docComment) {
93
+ prop.description = docComment.description;
94
+ prop.required = getBooleanTag(docComment.tags, "required");
95
+ prop.deprecated = getDeprecatedInfo(docComment.tags);
96
+ prop.default = findTag(docComment.tags, "default")?.description;
97
+ }
98
+ // Find out the `attribute` option for the property.
99
+ if (expression.arguments.length > 0) {
100
+ const options = expression.arguments[0];
101
+ if (options.type === "ObjectExpression") {
102
+ for (const opt of options.properties) {
103
+ if (
104
+ opt.type === "ObjectProperty" &&
105
+ !opt.computed &&
106
+ opt.key.type === "Identifier" &&
107
+ opt.key.name === "attribute"
108
+ ) {
109
+ if (
110
+ opt.value.type === "BooleanLiteral" ||
111
+ opt.value.type === "StringLiteral"
112
+ ) {
113
+ prop.attribute = opt.value.value;
114
+ }
115
+ break;
116
+ }
117
+ }
118
+ }
119
+ }
120
+ // Find out the type annotation for the property.
121
+ if (
122
+ node.typeAnnotation &&
123
+ node.typeAnnotation.type === "TSTypeAnnotation"
124
+ ) {
125
+ const { typeAnnotation } = node.typeAnnotation;
126
+ prop.type = getTypeWithoutUndefined(typeAnnotation, source);
127
+ }
128
+ if (node.value && !prop.default) {
129
+ prop.default = source.substring(
130
+ node.value.start,
131
+ node.value.end
132
+ );
133
+ }
134
+ manifest.properties.push(prop);
135
+ break;
136
+ }
137
+
138
+ case "event": {
139
+ /** @type {EventManifest} */
140
+ const event = {};
141
+
142
+ // Find out the `type` option for the event.
143
+ if (expression.arguments.length > 0) {
144
+ const options = expression.arguments[0];
145
+ if (options.type === "ObjectExpression") {
146
+ for (const opt of options.properties) {
147
+ if (
148
+ opt.type === "ObjectProperty" &&
149
+ !opt.computed &&
150
+ opt.key.type === "Identifier" &&
151
+ opt.key.name === "type"
152
+ ) {
153
+ if (opt.value.type === "StringLiteral") {
154
+ event.name = opt.value.value;
155
+ }
156
+ break;
157
+ }
158
+ }
159
+ }
160
+ }
161
+ if (event.name === undefined) {
162
+ throw new Error(
163
+ `Invalid @event() call: no literal type option in event '${node.key.name}'`
164
+ );
165
+ }
166
+ const docComment = parseDocComment(node, source);
167
+ if (docComment) {
168
+ event.description = docComment.description;
169
+ event.deprecated = getDeprecatedInfo(docComment.tags);
170
+ const detailTag = findTag(docComment.tags, "detail");
171
+ if (detailTag) {
172
+ event.detail ??= {};
173
+ event.detail.description = detailTag.description;
174
+ }
175
+ }
176
+ // Find out the type annotation for the event detail.
177
+ if (
178
+ node.typeAnnotation &&
179
+ node.typeAnnotation.type === "TSTypeAnnotation"
180
+ ) {
181
+ const { typeAnnotation } = node.typeAnnotation;
182
+ if (
183
+ typeAnnotation.type === "TSTypeReference" &&
184
+ typeAnnotation.typeName.type === "Identifier" &&
185
+ typeAnnotation.typeName.name === "EventEmitter"
186
+ ) {
187
+ // Extract the parameters from `EventEmitter`
188
+ const param = typeAnnotation.typeParameters.params[0];
189
+ event.detail ??= {};
190
+ event.detail.type = source.substring(param.start, param.end);
191
+ }
192
+ }
193
+ manifest.events.push(event);
194
+ break;
195
+ }
196
+ }
197
+ }
198
+ }
199
+ } else if (node.type === "ClassMethod" && node.decorators?.length) {
200
+ for (const { expression } of node.decorators) {
201
+ if (
202
+ expression.type === "CallExpression" &&
203
+ expression.callee.type === "Identifier" &&
204
+ expression.callee.name === "method"
205
+ ) {
206
+ /** @type {MethodManifest} */
207
+ const method = {
208
+ name: node.key.name,
209
+ params: [],
210
+ };
211
+ const docComment = parseDocComment(node, source);
212
+ if (docComment) {
213
+ method.description = docComment.description;
214
+ method.deprecated = getDeprecatedInfo(docComment.tags);
215
+ }
216
+ for (const param of node.params) {
217
+ method.params.push(source.substring(param.start, param.end));
218
+ }
219
+ if (node.returnType && node.returnType.type === "TSTypeAnnotation") {
220
+ const { typeAnnotation } = node.returnType;
221
+ method.return ??= {};
222
+ method.return.type = source.substring(
223
+ typeAnnotation.start,
224
+ typeAnnotation.end
225
+ );
226
+ }
227
+ manifest.methods.push(method);
228
+ }
229
+ }
230
+ }
231
+ }
232
+ }
233
+
234
+ /**
235
+ * @param {Node[]} nodes
236
+ * @param {string} source
237
+ */
238
+ function parseDocComment(node, source) {
239
+ if (node.leadingComments) {
240
+ const docComment = node.leadingComments.find(
241
+ (comment) => comment.type === "CommentBlock"
242
+ );
243
+ if (docComment) {
244
+ const docSource = source.substring(docComment.start, docComment.end);
245
+ const parsed = parse(docSource, { unwrap: true });
246
+ return parsed;
247
+ }
248
+ }
249
+ }
250
+
251
+ /**
252
+ * @param {Node} node
253
+ * @param {string} source
254
+ */
255
+ function getTypeWithoutUndefined(node, source) {
256
+ if (node.type === "TSUnionType") {
257
+ const filteredTypes = node.types.filter(
258
+ (type) => type.type !== "TSUndefinedKeyword"
259
+ );
260
+ if (filteredTypes.length < node.types.length) {
261
+ return filteredTypes
262
+ .map((type) => source.substring(type.start, type.end))
263
+ .join(" | ");
264
+ }
265
+ }
266
+ return source.substring(node.start, node.end);
267
+ }
268
+
269
+ /**
270
+ * @param {Tag[]} tags
271
+ * @param {string} title
272
+ * @returns {Tag | undefined}
273
+ */
274
+ function findTag(tags, title) {
275
+ for (const tag of tags) {
276
+ if (tag.title === title) {
277
+ return tag;
278
+ }
279
+ }
280
+ }
281
+
282
+ /**
283
+ * @param {Tag[]} tags
284
+ * @param {string} title
285
+ * @returns {boolean | undefined}
286
+ */
287
+ function getBooleanTag(tags, title) {
288
+ const tag = findTag(tags, title);
289
+ return tag ? true : undefined;
290
+ }
291
+
292
+ /**
293
+ * @param {Tag[]} tags
294
+ * @returns {boolean | string | undefined}
295
+ */
296
+ function getDeprecatedInfo(tags) {
297
+ const tag = findTag(tags, "deprecated");
298
+ return tag ? (tag.description === null ? true : tag.description) : undefined;
299
+ }
package/src/scanBricks.js CHANGED
@@ -1,10 +1,17 @@
1
1
  import path from "node:path";
2
2
  import fs, { existsSync, statSync } from "node:fs";
3
- import { readdir, readFile, stat } from "node:fs/promises";
3
+ import { readdir, readFile } from "node:fs/promises";
4
4
  import { parse } from "@babel/parser";
5
5
  import babelTraverse from "@babel/traverse";
6
6
  import _ from "lodash";
7
7
  import getCamelPackageName from "./getCamelPackageName.js";
8
+ import makeBrickManifest from "./makeBrickManifest.js";
9
+
10
+ /**
11
+ * @typedef {import("@next-core/brick-manifest").PackageManifest} PackageManifest
12
+ * @typedef {{import: string; name: string; noNamespace?: boolean;}} Expose
13
+ * @typedef {Record<string, Expose>} Exposes
14
+ */
8
15
 
9
16
  const { default: traverse } = babelTraverse;
10
17
  const { escapeRegExp } = _;
@@ -19,10 +26,10 @@ const validExposeName = /^[-\w]+$/;
19
26
  * Scan defined bricks by AST.
20
27
  *
21
28
  * @param {string} packageDir
22
- * @returns {Promise<{exposes: Record<string, { import: string; name: string; noNamespace?: boolean; }; dependencies: Record<string, string[]>}>>}
29
+ * @returns {Promise<{exposes: Exposes; dependencies: Record<string, string[]>; manifest: PackageManifest}>}
23
30
  */
24
31
  export default async function scanBricks(packageDir) {
25
- /** @type {Map<string, { import: string; name: string; }>} */
32
+ /** @type {Map<string, Expose>} */
26
33
  const exposes = new Map();
27
34
  /** @type {Record<string, string[]>} */
28
35
  const specifiedDeps = {};
@@ -38,6 +45,14 @@ export default async function scanBricks(packageDir) {
38
45
  const packageName = packageJson.name.split("/").pop();
39
46
  const camelPackageName = getCamelPackageName(packageName);
40
47
 
48
+ /** @type {PackageManifest} */
49
+ const manifest = {
50
+ manifest_version: 1,
51
+ package: packageJson.name,
52
+ name: packageName,
53
+ bricks: [],
54
+ };
55
+
41
56
  /** @type {Map<string, Set<string>} */
42
57
  const usingWrappedBricks = new Map();
43
58
 
@@ -302,7 +317,10 @@ export default async function scanBricks(packageDir) {
302
317
  }
303
318
  }
304
319
  },
305
- Decorator({ node: { expression } }) {
320
+ Decorator(nodePath) {
321
+ const {
322
+ node: { expression },
323
+ } = nodePath;
306
324
  // Match `@defineElement(...)`
307
325
  if (
308
326
  expression.type === "CallExpression" &&
@@ -372,6 +390,8 @@ export default async function scanBricks(packageDir) {
372
390
 
373
391
  brickSourceFiles.set(fullName, filePath);
374
392
 
393
+ manifest.bricks.push(makeBrickManifest(fullName, nodePath, content));
394
+
375
395
  exposes.set(`./${brickName}`, {
376
396
  import: `./${path
377
397
  .relative(packageDir, overrideImport || filePath)
@@ -503,5 +523,6 @@ export default async function scanBricks(packageDir) {
503
523
  ...analyzedDeps,
504
524
  ...specifiedDeps,
505
525
  },
526
+ manifest,
506
527
  };
507
528
  }