zotero-plugin-scaffold 0.0.4

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.
@@ -0,0 +1,194 @@
1
+ import { generateHashSync } from "../utils/crypto.js";
2
+ import { LibBase } from "../utils/libBase.js";
3
+ import { dateFormat } from "../utils/string.js";
4
+ import chalk from "chalk";
5
+ import { zip } from "compressing";
6
+ import { buildSync } from "esbuild";
7
+ import glob from "fast-glob";
8
+ import fs from "fs-extra";
9
+ import path from "path";
10
+ import replaceInFile from "replace-in-file";
11
+ const { replaceInFileSync } = replaceInFile;
12
+ export default class Build extends LibBase {
13
+ buildTime;
14
+ isPreRelease;
15
+ constructor(config) {
16
+ super(config);
17
+ this.buildTime = "";
18
+ this.isPreRelease = this.version.includes("-");
19
+ }
20
+ /**
21
+ * Default build runner
22
+ */
23
+ async run() {
24
+ const t = new Date();
25
+ this.buildTime = dateFormat("YYYY-mm-dd HH:MM:SS", t);
26
+ this.logger.info(`Building version ${chalk.blue(this.version)} to ${chalk.blue(this.config.dist)} at ${chalk.blue(this.buildTime)} in ${chalk.blue(process.env.NODE_ENV)} mode.`);
27
+ fs.emptyDirSync(this.config.dist);
28
+ this.copyAssets();
29
+ this.logger.debug("Preparing manifest...");
30
+ this.makeManifest();
31
+ this.makebootstrap();
32
+ this.logger.debug("Preparing locale files...");
33
+ this.prepareLocaleFiles();
34
+ this.logger.debug("Replacing...");
35
+ this.replaceString();
36
+ this.logger.debug("Running esbuild...");
37
+ this.esbuild();
38
+ this.logger.debug("Running extra builder...");
39
+ await this.config.extraBuilder(this.config);
40
+ this.logger.debug("Addon prepare OK.");
41
+ /**======== build resolved ===========*/
42
+ if (process.env.NODE_ENV === "production") {
43
+ this.logger.debug("Packing Addon...");
44
+ await this.pack();
45
+ this.logger.debug("Preparing update.json...");
46
+ this.makeUpdateJson();
47
+ }
48
+ this.logger.info(`Build finished in ${(new Date().getTime() - t.getTime()) / 1000} s.`);
49
+ }
50
+ /**
51
+ * Copys files in `Config.assets` to `Config.dist`
52
+ */
53
+ copyAssets() {
54
+ const files = glob.sync(this.config.assets);
55
+ files.forEach((file) => {
56
+ const newPath = `${this.config.dist}/addon/${file.replace(new RegExp(this.config.source.join("|")), "")}`;
57
+ this.logger.trace(`Copy ${file} to ${newPath}`);
58
+ fs.copySync(file, newPath);
59
+ });
60
+ }
61
+ makeManifest() {
62
+ if (!this.config.makeManifest.enable)
63
+ return;
64
+ fs.outputJSONSync(`${this.config.dist}/addon/manifest.json`, this.config.makeManifest.template, { spaces: 2 });
65
+ }
66
+ makebootstrap() {
67
+ if (!this.config.makeBootstrap)
68
+ return;
69
+ fs.copySync(path.join(this.config.pkgAbsolute, "template/default/bootstrap.js"), `${this.config.dist}/addon/bootstrap.js`);
70
+ }
71
+ /**
72
+ * Replace all `placeholder.key` to `placeholder.value` for all files in `dist`
73
+ */
74
+ replaceString() {
75
+ const replaceFrom = [], replaceTo = [];
76
+ // Config.placeholders has the highest priority
77
+ replaceFrom.push(...Object.keys(this.config.define).map((k) => new RegExp(`__${k}__`, "g")));
78
+ replaceTo.push(...Object.values(this.config.define));
79
+ replaceFrom.push(/__buildTime__/g);
80
+ replaceTo.push(this.buildTime);
81
+ const replaceResult = replaceInFileSync({
82
+ files: this.config.assets.map((asset) => `${this.config.dist}/${asset}`),
83
+ from: replaceFrom,
84
+ to: replaceTo,
85
+ countMatches: true,
86
+ });
87
+ this.logger.debug("Run replace in ", replaceResult
88
+ .filter((f) => f.hasChanged)
89
+ .map((f) => `${f.file} : ${f.numReplacements} / ${f.numMatches}`));
90
+ }
91
+ prepareLocaleFiles() {
92
+ // Prefix Fluent messages in xhtml
93
+ const MessagesInHTML = new Set();
94
+ replaceInFileSync({
95
+ files: [
96
+ `${this.config.dist}/addon/**/*.xhtml`,
97
+ `${this.config.dist}/addon/**/*.html`,
98
+ ],
99
+ // @ts-expect-error ReplaceInFileConfig has processor
100
+ processor: (input) => {
101
+ const matchs = [...input.matchAll(/(data-l10n-id)="(\S*)"/g)];
102
+ matchs.map((match) => {
103
+ input = input.replace(match[0], this.config.fluent.prefixFluentMessages == true
104
+ ? `${match[1]}="${this.addonRef}-${match[2]}"`
105
+ : match[0]);
106
+ MessagesInHTML.add(match[2]);
107
+ });
108
+ return input;
109
+ },
110
+ });
111
+ // Walk the sub folders of `build/addon/locale`
112
+ const localeNames = glob
113
+ .sync(`${this.config.dist}/addon/locale/*/`, {})
114
+ .map((locale) => path.basename(locale));
115
+ for (const localeName of localeNames) {
116
+ // rename *.ftl to addonRef-*.ftl
117
+ if (this.config.fluent.prefixLocaleFiles == true) {
118
+ glob
119
+ .sync(`${this.config.dist}/addon/locale/${localeName}/**/*.ftl`, {})
120
+ .forEach((f) => {
121
+ fs.moveSync(f, `${path.dirname(f)}/${this.addonRef}-${path.basename(f)}`);
122
+ });
123
+ }
124
+ // Prefix Fluent messages in each ftl
125
+ const MessageInThisLang = new Set();
126
+ replaceInFileSync({
127
+ files: [`${this.config.dist}/addon/locale/${localeName}/**/*.ftl`],
128
+ // @ts-expect-error ReplaceInFileConfig has processor
129
+ processor: (fltContent) => {
130
+ const lines = fltContent.split("\n");
131
+ const prefixedLines = lines.map((line) => {
132
+ // https://regex101.com/r/lQ9x5p/1
133
+ const match = line.match(/^(?<message>[a-zA-Z]\S*)([ ]*=[ ]*)(?<pattern>.*)$/m);
134
+ if (match && match.groups) {
135
+ MessageInThisLang.add(match.groups.message);
136
+ return this.config.fluent.prefixFluentMessages
137
+ ? `${this.addonRef}-${line}`
138
+ : line;
139
+ }
140
+ else {
141
+ return line;
142
+ }
143
+ });
144
+ return prefixedLines.join("\n");
145
+ },
146
+ });
147
+ // If a message in xhtml but not in ftl of current language, log it
148
+ MessagesInHTML.forEach((message) => {
149
+ if (!MessageInThisLang.has(message)) {
150
+ this.logger.error(`${message} don't exist in ${localeName}`);
151
+ }
152
+ });
153
+ }
154
+ }
155
+ esbuild() {
156
+ this.config.esbuildOptions.forEach(async (esbuildOption) => {
157
+ buildSync(esbuildOption);
158
+ });
159
+ }
160
+ makeUpdateJson() {
161
+ fs.writeJsonSync(`${this.config.dist}/update-beta.json`, this.config.makeUpdateJson.template, { spaces: 2 });
162
+ fs.writeJsonSync(`${this.config.dist}/update.json`, this.config.makeUpdateJson.template, { spaces: 2 });
163
+ const updateHash = generateHashSync(path.join(this.config.dist, `${this.xpiName}.xpi`), "sha512");
164
+ const replaceResult = replaceInFileSync({
165
+ files: [
166
+ `${this.config.dist}/update-beta.json`,
167
+ `${this.config.dist}/${this.isPreRelease ? "pass" : "update.json"}`,
168
+ ],
169
+ from: [
170
+ /__addonID__/g,
171
+ /__version__/g,
172
+ /__updateLink__/g,
173
+ /__updateHash__/g,
174
+ ],
175
+ to: [this.addonID, this.version, this.updateLink, updateHash],
176
+ countMatches: true,
177
+ });
178
+ this.logger.debug(`Prepare Update.json for ${this.isPreRelease
179
+ ? "\u001b[31m Prerelease \u001b[0m"
180
+ : "\u001b[32m Release \u001b[0m"}`, replaceResult
181
+ .filter((f) => f.hasChanged)
182
+ .map((f) => `${f.file} : ${f.numReplacements} / ${f.numMatches}`));
183
+ }
184
+ async pack() {
185
+ await zip.compressDir(path.join(this.config.dist, "addon"), path.join(this.config.dist, `${this.xpiName}.xpi`), {
186
+ ignoreBase: true,
187
+ });
188
+ // await webext.cmd.build({
189
+ // sourceDir: `${this.config.dist}/addon`,
190
+ // artifactsDir: this.config.dist,
191
+ // filename: `${this.pkg.name ?? "name"}.xpi`,
192
+ // });
193
+ }
194
+ }
@@ -0,0 +1,19 @@
1
+ export default class Create {
2
+ run(): Promise<void>;
3
+ prompting(): Promise<{
4
+ plugin: {
5
+ name: string;
6
+ description: string;
7
+ id: string;
8
+ };
9
+ code: {
10
+ namespace: string;
11
+ };
12
+ user: {
13
+ name: string;
14
+ email: string;
15
+ };
16
+ }>;
17
+ downloadTemplate(): void;
18
+ parseTemplate(): void;
19
+ }
@@ -0,0 +1,40 @@
1
+ // npx 创建模板
2
+ import { input } from "@inquirer/prompts";
3
+ export default class Create {
4
+ async run() {
5
+ const answers = await this.prompting();
6
+ console.log(answers);
7
+ }
8
+ async prompting() {
9
+ const answers = {
10
+ plugin: {
11
+ name: await input({ message: "What's the name of plugin?" }),
12
+ description: await input({
13
+ message: "What's description of plugin?",
14
+ }),
15
+ id: await input({ message: "What's the ID of plugin?" }),
16
+ },
17
+ code: {
18
+ namespace: await input({
19
+ message: "What's the namespace of plugin?",
20
+ default: "",
21
+ }),
22
+ // package manager select: npm yarn pnpm
23
+ // use typescript?
24
+ // use prettier and lint?
25
+ },
26
+ user: {
27
+ name: await input({ message: "What's your GitHub user ID?" }),
28
+ email: await input({ message: "What's your email?" }),
29
+ },
30
+ // install deps?
31
+ };
32
+ return answers;
33
+ }
34
+ downloadTemplate() {
35
+ //
36
+ }
37
+ parseTemplate() {
38
+ //
39
+ }
40
+ }
@@ -0,0 +1,6 @@
1
+ import { Config } from "../types.js";
2
+ export default class Lint {
3
+ options: Config;
4
+ constructor(options: Config);
5
+ lint(): void;
6
+ }
@@ -0,0 +1,9 @@
1
+ export default class Lint {
2
+ options;
3
+ constructor(options) {
4
+ this.options = options;
5
+ }
6
+ lint() {
7
+ // webext.cmd.lint({});
8
+ }
9
+ }
@@ -0,0 +1,255 @@
1
+ import { Config } from "../types.js";
2
+ import { LibBase } from "../utils/libBase.js";
3
+ import { Octokit } from "@octokit/rest";
4
+ export default class Release extends LibBase {
5
+ isCI: boolean;
6
+ client: Octokit;
7
+ constructor(config: Config);
8
+ /**
9
+ * Runs release
10
+ *
11
+ * if is not CI,bump version, git add (package.json), git commit, git tag, git push;
12
+ * if is CI, do not bump version, do not run git, create release (tag is `v${version}`) and upload xpi,
13
+ * then, create or update release (tag is "release"), update `update.json`.
14
+ */
15
+ run(): Promise<void>;
16
+ /**
17
+ * Bumps release
18
+ *
19
+ * release: bump version, run build, git add, git commit, git tag, git push
20
+ */
21
+ bump(): void;
22
+ /**
23
+ * Create new release and upload XPI to asset
24
+ */
25
+ uploadXPI(): void;
26
+ getReleaseByTag(tag: string): Promise<{
27
+ url: string;
28
+ html_url: string;
29
+ assets_url: string;
30
+ upload_url: string;
31
+ tarball_url: string | null;
32
+ zipball_url: string | null;
33
+ id: number;
34
+ node_id: string;
35
+ tag_name: string;
36
+ target_commitish: string;
37
+ name: string | null;
38
+ body?: string | null | undefined;
39
+ draft: boolean;
40
+ prerelease: boolean;
41
+ created_at: string;
42
+ published_at: string | null;
43
+ author: {
44
+ name?: string | null | undefined;
45
+ email?: string | null | undefined;
46
+ login: string;
47
+ id: number;
48
+ node_id: string;
49
+ avatar_url: string;
50
+ gravatar_id: string | null;
51
+ url: string;
52
+ html_url: string;
53
+ followers_url: string;
54
+ following_url: string;
55
+ gists_url: string;
56
+ starred_url: string;
57
+ subscriptions_url: string;
58
+ organizations_url: string;
59
+ repos_url: string;
60
+ events_url: string;
61
+ received_events_url: string;
62
+ type: string;
63
+ site_admin: boolean;
64
+ starred_at?: string | undefined;
65
+ };
66
+ assets: {
67
+ url: string;
68
+ browser_download_url: string;
69
+ id: number;
70
+ node_id: string;
71
+ name: string;
72
+ label: string | null;
73
+ state: "uploaded" | "open";
74
+ content_type: string;
75
+ size: number;
76
+ download_count: number;
77
+ created_at: string;
78
+ updated_at: string;
79
+ uploader: {
80
+ name?: string | null | undefined;
81
+ email?: string | null | undefined;
82
+ login: string;
83
+ id: number;
84
+ node_id: string;
85
+ avatar_url: string;
86
+ gravatar_id: string | null;
87
+ url: string;
88
+ html_url: string;
89
+ followers_url: string;
90
+ following_url: string;
91
+ gists_url: string;
92
+ starred_url: string;
93
+ subscriptions_url: string;
94
+ organizations_url: string;
95
+ repos_url: string;
96
+ events_url: string;
97
+ received_events_url: string;
98
+ type: string;
99
+ site_admin: boolean;
100
+ starred_at?: string | undefined;
101
+ } | null;
102
+ }[];
103
+ body_html?: string | undefined;
104
+ body_text?: string | undefined;
105
+ mentions_count?: number | undefined;
106
+ discussion_url?: string | undefined;
107
+ reactions?: {
108
+ url: string;
109
+ total_count: number;
110
+ "+1": number;
111
+ "-1": number;
112
+ laugh: number;
113
+ confused: number;
114
+ heart: number;
115
+ hooray: number;
116
+ eyes: number;
117
+ rocket: number;
118
+ } | undefined;
119
+ } | undefined>;
120
+ creatRelease(options: Parameters<Octokit["repos"]["createRelease"]>[0]): Promise<{
121
+ url: string;
122
+ html_url: string;
123
+ assets_url: string;
124
+ upload_url: string;
125
+ tarball_url: string | null;
126
+ zipball_url: string | null;
127
+ id: number;
128
+ node_id: string;
129
+ tag_name: string;
130
+ target_commitish: string;
131
+ name: string | null;
132
+ body?: string | null | undefined;
133
+ draft: boolean;
134
+ prerelease: boolean;
135
+ created_at: string;
136
+ published_at: string | null;
137
+ author: {
138
+ name?: string | null | undefined;
139
+ email?: string | null | undefined;
140
+ login: string;
141
+ id: number;
142
+ node_id: string;
143
+ avatar_url: string;
144
+ gravatar_id: string | null;
145
+ url: string;
146
+ html_url: string;
147
+ followers_url: string;
148
+ following_url: string;
149
+ gists_url: string;
150
+ starred_url: string;
151
+ subscriptions_url: string;
152
+ organizations_url: string;
153
+ repos_url: string;
154
+ events_url: string;
155
+ received_events_url: string;
156
+ type: string;
157
+ site_admin: boolean;
158
+ starred_at?: string | undefined;
159
+ };
160
+ assets: {
161
+ url: string;
162
+ browser_download_url: string;
163
+ id: number;
164
+ node_id: string;
165
+ name: string;
166
+ label: string | null;
167
+ state: "uploaded" | "open";
168
+ content_type: string;
169
+ size: number;
170
+ download_count: number;
171
+ created_at: string;
172
+ updated_at: string;
173
+ uploader: {
174
+ name?: string | null | undefined;
175
+ email?: string | null | undefined;
176
+ login: string;
177
+ id: number;
178
+ node_id: string;
179
+ avatar_url: string;
180
+ gravatar_id: string | null;
181
+ url: string;
182
+ html_url: string;
183
+ followers_url: string;
184
+ following_url: string;
185
+ gists_url: string;
186
+ starred_url: string;
187
+ subscriptions_url: string;
188
+ organizations_url: string;
189
+ repos_url: string;
190
+ events_url: string;
191
+ received_events_url: string;
192
+ type: string;
193
+ site_admin: boolean;
194
+ starred_at?: string | undefined;
195
+ } | null;
196
+ }[];
197
+ body_html?: string | undefined;
198
+ body_text?: string | undefined;
199
+ mentions_count?: number | undefined;
200
+ discussion_url?: string | undefined;
201
+ reactions?: {
202
+ url: string;
203
+ total_count: number;
204
+ "+1": number;
205
+ "-1": number;
206
+ laugh: number;
207
+ confused: number;
208
+ heart: number;
209
+ hooray: number;
210
+ eyes: number;
211
+ rocket: number;
212
+ } | undefined;
213
+ } | undefined>;
214
+ uploadAsset(releaseID: number, asset: string): Promise<{
215
+ url: string;
216
+ browser_download_url: string;
217
+ id: number;
218
+ node_id: string;
219
+ name: string;
220
+ label: string | null;
221
+ state: "uploaded" | "open";
222
+ content_type: string;
223
+ size: number;
224
+ download_count: number;
225
+ created_at: string;
226
+ updated_at: string;
227
+ uploader: {
228
+ name?: string | null | undefined;
229
+ email?: string | null | undefined;
230
+ login: string;
231
+ id: number;
232
+ node_id: string;
233
+ avatar_url: string;
234
+ gravatar_id: string | null;
235
+ url: string;
236
+ html_url: string;
237
+ followers_url: string;
238
+ following_url: string;
239
+ gists_url: string;
240
+ starred_url: string;
241
+ subscriptions_url: string;
242
+ organizations_url: string;
243
+ repos_url: string;
244
+ events_url: string;
245
+ received_events_url: string;
246
+ type: string;
247
+ site_admin: boolean;
248
+ starred_at?: string | undefined;
249
+ } | null;
250
+ }>;
251
+ uploadUpdateJSON(): Promise<void>;
252
+ getClient(): Octokit;
253
+ get owner(): string;
254
+ get repo(): string;
255
+ }
@@ -0,0 +1,165 @@
1
+ import { LibBase } from "../utils/libBase.js";
2
+ import { Octokit } from "@octokit/rest";
3
+ import ci from "ci-info";
4
+ import { default as glob } from "fast-glob";
5
+ import fs from "fs-extra";
6
+ import _ from "lodash";
7
+ import mime from "mime";
8
+ import path from "path";
9
+ import releaseIt from "release-it";
10
+ export default class Release extends LibBase {
11
+ isCI;
12
+ client;
13
+ constructor(config) {
14
+ super(config);
15
+ this.isCI = ci.isCI;
16
+ this.client = this.getClient();
17
+ }
18
+ /**
19
+ * Runs release
20
+ *
21
+ * if is not CI,bump version, git add (package.json), git commit, git tag, git push;
22
+ * if is CI, do not bump version, do not run git, create release (tag is `v${version}`) and upload xpi,
23
+ * then, create or update release (tag is "release"), update `update.json`.
24
+ */
25
+ async run() {
26
+ if (!this.isCI) {
27
+ this.bump();
28
+ }
29
+ else {
30
+ if (glob.globSync(`${this.config.dist}/*.xpi`).length == 0) {
31
+ this.logger.error("No xpi file found, are you sure you have run the build?");
32
+ }
33
+ this.uploadXPI();
34
+ this.uploadUpdateJSON();
35
+ }
36
+ }
37
+ /**
38
+ * Bumps release
39
+ *
40
+ * release: bump version, run build, git add, git commit, git tag, git push
41
+ */
42
+ bump() {
43
+ // versionBump(this.config.release.bumpp);
44
+ const releaseItConfig = {
45
+ "only-version": true,
46
+ };
47
+ releaseIt(_.defaultsDeep(releaseItConfig, this.config.release.releaseIt));
48
+ }
49
+ /**
50
+ * Create new release and upload XPI to asset
51
+ */
52
+ uploadXPI() {
53
+ const releaseItConfig = {
54
+ increment: false,
55
+ git: {
56
+ commit: false,
57
+ tag: false,
58
+ push: false,
59
+ },
60
+ github: {
61
+ release: true,
62
+ },
63
+ verbose: 2,
64
+ ci: true,
65
+ };
66
+ releaseIt(_.defaultsDeep(releaseItConfig, this.config.release.releaseIt));
67
+ }
68
+ async getReleaseByTag(tag) {
69
+ return await this.client.repos
70
+ .getReleaseByTag({
71
+ owner: this.owner,
72
+ repo: this.repo,
73
+ tag: tag,
74
+ })
75
+ .then((res) => {
76
+ if (res.status == 200) {
77
+ return res.data;
78
+ }
79
+ })
80
+ .catch((err) => {
81
+ this.logger.debug(err);
82
+ return undefined;
83
+ });
84
+ }
85
+ async creatRelease(options) {
86
+ return await this.client.repos
87
+ .createRelease(options)
88
+ .then((res) => {
89
+ if (res.status == 201) {
90
+ return res.data;
91
+ }
92
+ })
93
+ .catch((err) => {
94
+ this.logger.debug(err);
95
+ return undefined;
96
+ });
97
+ }
98
+ async uploadAsset(releaseID, asset) {
99
+ return await this.client.repos
100
+ .uploadReleaseAsset({
101
+ owner: this.owner,
102
+ repo: this.repo,
103
+ release_id: releaseID,
104
+ data: fs.readFileSync(asset),
105
+ headers: {
106
+ "content-type": mime.getType(asset) || "application/octet-stream",
107
+ "content-length": fs.statSync(asset).size,
108
+ },
109
+ name: path.basename(asset),
110
+ })
111
+ .then((res) => {
112
+ return res.data;
113
+ });
114
+ }
115
+ async uploadUpdateJSON() {
116
+ const assets = ["update.json", "update-beta.json"];
117
+ const release = (await this.getReleaseByTag("release")) ??
118
+ (await this.creatRelease({
119
+ owner: this.owner,
120
+ repo: this.repo,
121
+ tag_name: "release",
122
+ name: "Release Manifest",
123
+ body: `This release is used to host \`update.json\`, please do not delete or modify it! \n Updated in UTC ${new Date().toISOString()} for version ${this.version}`,
124
+ make_latest: "false",
125
+ }));
126
+ if (!release)
127
+ throw new Error("Get or create 'release' failed.");
128
+ const existAssets = await this.client.repos
129
+ .listReleaseAssets({
130
+ owner: this.owner,
131
+ repo: this.repo,
132
+ release_id: release.id,
133
+ })
134
+ .then((res) => {
135
+ return res.data.filter((asset) => assets.includes(asset.name));
136
+ });
137
+ if (existAssets) {
138
+ for (const existAsset of existAssets) {
139
+ await this.client.repos.deleteReleaseAsset({
140
+ owner: this.owner,
141
+ repo: this.repo,
142
+ asset_id: existAsset.id,
143
+ });
144
+ }
145
+ }
146
+ for (const asset of assets) {
147
+ await this.uploadAsset(release.id, path.join(this.config.dist, asset));
148
+ }
149
+ }
150
+ getClient() {
151
+ if (!process.env.GITHUB_TOKEN)
152
+ throw new Error("No GITHUB_TOKEN.");
153
+ const client = new Octokit({
154
+ auth: process.env.GITHUB_TOKEN,
155
+ userAgent: `zotero-plugin-scaffold/${this.version}`,
156
+ });
157
+ return client;
158
+ }
159
+ get owner() {
160
+ return this.config.define.ghOwner;
161
+ }
162
+ get repo() {
163
+ return this.config.define.ghRepo;
164
+ }
165
+ }