@digigov/cli-build 2.0.0-36b707c1 → 2.0.0-385c7994

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/index.js CHANGED
@@ -1,107 +1,176 @@
1
- const { Command } = require("@oclif/command");
2
- const execa = require("execa");
3
- const { resolveProject, DigigovCommand } = require("@digigov/cli/lib");
4
- const path = require("path");
5
- const fs = require("fs");
6
- const esbuild = require("esbuild");
7
- const glob = require("glob");
8
-
9
- module.exports = class Build extends DigigovCommand {
10
- static description = "build digigov libraries";
11
- static id = "build";
12
- dirname = __dirname;
13
- static examples = [`$ digigov build`];
14
- static strict = false;
15
- static load() {
16
- return Build;
17
- }
18
- script = "babel";
19
- async run() {
20
- const { argv } = this.parse(Build);
21
- try {
22
- await this.exec("rimraf", ["dist"]);
23
- const project = resolveProject();
24
- const babelArgs = [
25
- "--config-file",
26
- path.join(__dirname, "babel.config.js"),
27
- project.src,
28
- "--extensions",
29
- ".tsx,.ts,.js,.jsx",
30
- "--copy-files",
31
- "--out-dir",
32
- ];
33
- const distDir = path.resolve(project.root, project.distDir);
34
- const basename = path.basename(project.root);
35
-
36
- if (project.isTs) {
37
- const tsconfigProduction = path.join(
38
- project.root,
39
- "tsconfig.production.json",
40
- );
41
- const tsArgs = [];
42
- if (fs.existsSync(tsconfigProduction)) {
43
- tsArgs.push("--project", tsconfigProduction);
44
- }
45
- await this.exec("tsc", [
46
- "--emitDeclarationOnly",
47
- "--outDir",
48
- "dist",
49
- ...tsArgs,
50
- ]);
51
- if (fs.existsSync(path.join(distDir, basename))) {
52
- const typesIncluded = fs.readdirSync(path.join(distDir));
53
- const paths = fs.readdirSync(
54
- path.join(distDir, basename, project.src),
55
- );
56
- paths.forEach((p) => {
57
- fs.renameSync(
58
- path.join(distDir, basename, project.src, p),
59
- path.join(distDir, p),
60
- );
61
- });
62
- typesIncluded.forEach((typesDir) => {
63
- fs.rmSync(path.join(distDir, typesDir), { recursive: true });
64
- });
65
- }
1
+ import { DigigovCommand, resolveProject, logger } from '@digigov/cli/lib';
2
+ import { build } from '@rslib/core';
3
+ import copyFiles from './copy-files.js';
4
+
5
+ import { Option } from 'commander';
6
+ import path from 'path';
7
+ import glob from 'globby';
8
+ import assert from 'assert';
9
+ import { getProjectTsconfig } from './common.js';
10
+ import { generateLazyRegistry, generateRegistry } from './generate-registry.js';
11
+ import transformImportsPlugin from './transform-imports-plugin.js';
12
+
13
+ const command = new DigigovCommand('build', import.meta.url)
14
+ .option(
15
+ '--generate-registry',
16
+ 'Generate a registry file for the build output'
17
+ )
18
+ .addOption(
19
+ new Option('--include-stories', 'Include stories in the output').implies({
20
+ generateRegistry: true,
21
+ })
22
+ )
23
+ .action(main);
24
+ export default command;
25
+
26
+ const SRC_GLOB = 'src/**/*.{tsx,ts,js,jsx}';
27
+ const TEST_GLOBS = [
28
+ '**/*.test.{js,jsx,ts,tsx}',
29
+ '**/*.spec.{js,jsx,ts,tsx}',
30
+ '**/__tests__/**/*.{js,jsx,ts,tsx}',
31
+ ];
32
+ const STORIES_GLOBS = [
33
+ '**/*.stories.{js,jsx,ts,tsx}',
34
+ '**/__stories__/**/*.{js,jsx,ts,tsx}',
35
+ '**/__stories__/*.{js,jsx,ts,tsx}',
36
+ ];
37
+
38
+ /**
39
+ * @param {object} options - The command options
40
+ * @param {boolean} options.generateRegistry - Generate a registry file for the build output
41
+ * @param {boolean} options.includeStories - Include stories in the generated registry file
42
+ * @param {DigigovCommand} ctx
43
+ */
44
+ async function main(options, ctx) {
45
+ /** @type {string[]} */
46
+ let filesToDelete = [];
47
+ let isCleaningUp = false;
48
+ let signalHandlersRegistered = false;
49
+
50
+ const cleanup = async () => {
51
+ if (isCleaningUp) return;
52
+ isCleaningUp = true;
53
+
54
+ if (options.generateRegistry && filesToDelete.length > 0) {
55
+ logger.debug('Deleting temporary registry files...');
56
+ try {
57
+ await ctx.exec('rimraf', filesToDelete, {}, true);
58
+ } catch (error) {
59
+ logger.error('Error during cleanup:', error);
66
60
  }
61
+ }
67
62
 
68
- const files = glob.sync(
69
- path.join(project.root, "src", "**/*.{tsx,ts,js,jsx}"),
70
- {
71
- ignore: "**/*.{spec,test}.{ts,tsx,js,jsx}",
72
- },
63
+ // Remove signal handlers after cleanup
64
+ if (signalHandlersRegistered) {
65
+ process.off('SIGINT', handleSignal);
66
+ process.off('SIGTERM', handleSignal);
67
+ }
68
+ };
69
+
70
+ /** @param {string} signal */
71
+ const handleSignal = async (signal) => {
72
+ logger.log(`\nReceived ${signal}, cleaning up...`);
73
+ await cleanup();
74
+ process.exit(signal === 'SIGINT' ? 130 : 143);
75
+ };
76
+
77
+ // Register signal handlers
78
+ process.on('SIGINT', handleSignal);
79
+ process.on('SIGTERM', handleSignal);
80
+ signalHandlersRegistered = true;
81
+
82
+ try {
83
+ const project = resolveProject();
84
+
85
+ await ctx.exec('rimraf', [project.distDir]);
86
+
87
+ /**
88
+ * The project tsconfig, or undefined if the project is not using TypeScript
89
+ * @type {string | undefined}
90
+ */
91
+ let tsconfig;
92
+ if (project.isTs) {
93
+ tsconfig = getProjectTsconfig(project.root);
94
+ assert(tsconfig, 'Expected tsconfig to be in project');
95
+ }
96
+
97
+ if (options.generateRegistry) {
98
+ logger.debug('Generating registry files...');
99
+
100
+ const initialFiles = await glob(path.join(project.root, SRC_GLOB), {
101
+ ignore: [...TEST_GLOBS, ...STORIES_GLOBS],
102
+ });
103
+
104
+ const filesToIncludeInRegistry = initialFiles.filter(
105
+ (file) => !(file.includes('native') || file.endsWith('.d.ts'))
73
106
  );
74
- const commonBuildOptions = {
75
- entryPoints: files,
76
- platform: "node",
77
- sourcemap: true,
78
- target: ["esnext"],
79
- logLevel: "error",
80
- };
81
- if (fs.existsSync(path.join(project.root, "tsconfig.production.json"))) {
82
- commonBuildOptions["tsconfig"] = "tsconfig.production.json";
83
- } else if (fs.existsSync(path.join(project.root, "tsconfig.json"))) {
84
- commonBuildOptions["tsconfig"] = "tsconfig.json";
107
+ let storiesFiles = null;
108
+ if (options.includeStories) {
109
+ logger.debug('Including stories in the registry...');
110
+
111
+ storiesFiles = await glob(
112
+ STORIES_GLOBS.map((glob) =>
113
+ path.join(project.root, project.src, glob)
114
+ ),
115
+ {
116
+ ignore: ['**/*.native.*, **/*.d.ts'],
117
+ }
118
+ );
85
119
  }
86
120
 
87
- await Promise.all([
88
- esbuild.build({
89
- ...commonBuildOptions,
90
- format: "esm",
91
- outdir: `dist`,
92
- }),
93
- esbuild.build({
94
- ...commonBuildOptions,
95
- format: "cjs",
96
- outdir: `dist/cjs`,
97
- }),
98
- ]);
99
- await execa("node", [path.join(__dirname, "copy-files.js")], {
100
- env: {},
101
- stdio: "inherit",
102
- });
103
- } catch (err) {
104
- console.error(err);
121
+ filesToDelete = await Promise.all([
122
+ storiesFiles
123
+ ? generateRegistry(project, storiesFiles, 'stories-registry.ts')
124
+ : null,
125
+ generateRegistry(project, filesToIncludeInRegistry, 'registry.ts'),
126
+ generateLazyRegistry(project, filesToIncludeInRegistry, 'lazy.ts'),
127
+ ]).then((paths) => paths.filter((p) => p !== null));
128
+
129
+ logger.log('Generated registry files');
105
130
  }
131
+
132
+ const IGNORE_BLOBS = [...TEST_GLOBS, ...STORIES_GLOBS].map(
133
+ (path) => `!${project.root}/${path}`
134
+ );
135
+
136
+ logger.debug('Building...');
137
+ await build({
138
+ source: {
139
+ tsconfigPath: tsconfig,
140
+ entry: {
141
+ index: [`${project.root}/${SRC_GLOB}`, ...IGNORE_BLOBS],
142
+ },
143
+ },
144
+ output: {
145
+ distPath: {
146
+ root: project.distDir,
147
+ },
148
+ },
149
+ lib: [
150
+ {
151
+ redirect: {
152
+ dts: {
153
+ extension: true,
154
+ },
155
+ },
156
+ bundle: false,
157
+ dts: {
158
+ bundle: false,
159
+ autoExtension: true,
160
+ },
161
+ format: 'esm',
162
+ plugins: [
163
+ transformImportsPlugin(project, ['@uides/react-qr-reader']),
164
+ ],
165
+ },
166
+ ],
167
+ });
168
+ logger.debug('Building done.');
169
+
170
+ logger.debug('Copying files to build directory...');
171
+ copyFiles();
172
+ logger.debug('Files copied.');
173
+ } finally {
174
+ await cleanup();
106
175
  }
107
- };
176
+ }
package/package.json CHANGED
@@ -1,48 +1,45 @@
1
1
  {
2
2
  "name": "@digigov/cli-build",
3
- "version": "2.0.0-36b707c1",
3
+ "version": "2.0.0-385c7994",
4
4
  "description": "Build plugin for Digigov CLI",
5
5
  "main": "./index.js",
6
+ "type": "module",
6
7
  "author": "GRNET Devs <devs@lists.grnet.gr>",
7
8
  "license": "BSD-2-Clause",
8
9
  "private": false,
9
10
  "dependencies": {
10
- "@babel/cli": "7.12.1",
11
- "@babel/compat-data": "7.12.5",
12
- "@babel/core": "7.12.13",
13
- "@babel/helper-validator-identifier": "7.9.5",
14
- "@babel/plugin-proposal-class-properties": "7.12.1",
15
- "@babel/plugin-proposal-object-rest-spread": "7.12.1",
16
- "@babel/plugin-transform-object-assign": "7.12.1",
17
- "@babel/plugin-transform-runtime": "7.12.1",
18
- "@babel/preset-env": "7.12.13",
19
- "@babel/preset-react": "7.12.13",
20
- "@babel/preset-typescript": "7.12.1",
21
- "babel-plugin-inline-import-data-uri": "1.0.1",
22
- "babel-plugin-module-resolver": "4.0.0",
23
- "babel-plugin-optimize-clsx": "1.1.1",
24
- "babel-plugin-react-remove-properties": "0.3.0",
25
- "babel-plugin-transform-dev-warning": "0.1.1",
26
- "babel-plugin-transform-react-constant-elements": "6.23.0",
27
- "babel-plugin-transform-react-remove-prop-types": "0.4.24",
28
11
  "fs-extra": "11.2.0",
29
- "glob": "7.1.6",
30
- "typescript": "5.6.2",
31
- "@types/node": "18.19.0",
32
- "babel-plugin-istanbul": "7.0.0",
12
+ "globby": "11.0.0",
33
13
  "publint": "0.1.8",
34
- "esbuild": "0.23.0",
35
- "@digigov/babel-ts-to-proptypes": "1.1.0"
14
+ "rimraf": "3.0.2",
15
+ "commander": "12.1.0",
16
+ "ts-morph": "25.0.0",
17
+ "@rslib/core": "0.15.1"
36
18
  },
37
19
  "devDependencies": {
38
- "publint": "0.1.8"
20
+ "@digigov/cli": "2.0.0-385c7994",
21
+ "@digigov/cli-lint": "2.0.0-385c7994",
22
+ "publint": "0.1.8",
23
+ "vitest": "2.1.3",
24
+ "@digigov/cli-test": "2.0.0-385c7994",
25
+ "@types/fs-extra": "11.0.4",
26
+ "@types/node": "20.17.24",
27
+ "typescript": "5.6.2",
28
+ "eslint": "9.16.0",
29
+ "prettier": "3.4.2"
39
30
  },
40
31
  "peerDependencies": {
41
- "@digigov/cli": "1.1.2-36b707c1",
42
- "rimraf": "3.0.2",
32
+ "@digigov/cli": "2.0.0-385c7994",
43
33
  "next": "13.1.1"
44
34
  },
35
+ "peerDependenciesMeta": {
36
+ "next": {
37
+ "optional": true
38
+ }
39
+ },
45
40
  "scripts": {
46
- "publint": "publint"
41
+ "publint": "publint",
42
+ "lint": "digigov lint",
43
+ "typecheck": "tsc"
47
44
  }
48
45
  }
@@ -0,0 +1,263 @@
1
+ import { logger } from '@digigov/cli/lib';
2
+ import path from 'path';
3
+ import fs from 'fs';
4
+
5
+ /**
6
+ * @param {Record<string, any>} project
7
+ * @param {string[]} [pathsToIgnore=[]]
8
+ * @returns {import('@rslib/core').rsbuild.RsbuildPlugin}
9
+ */
10
+ export default function transformImportsPlugin(project, pathsToIgnore = []) {
11
+ const projectName = project['name'];
12
+ const projectRoot = project['root'];
13
+ const workspace = project['workspace'];
14
+
15
+ if (Object.keys(workspace).length === 0) {
16
+ throw new Error(
17
+ `transform-imports-plugin can only be used in workspace projects. ${projectName} is not a workspace project.`
18
+ );
19
+ }
20
+
21
+ const projectScopes =
22
+ workspace.config.projects.map((p) => p.packageName) ?? [];
23
+
24
+ if (projectScopes.length === 0) {
25
+ logger.warn(
26
+ `No project scopes found for ${projectName}. Plugin may not transform any imports.`
27
+ );
28
+ }
29
+
30
+ const importRegex =
31
+ /(?:^|[^'"\\])(?:from|import)\s*\(\s*['"]([^'"]+)['"]\s*\)|(?:from|import)\s+['"]([^'"]+)['"]/gm;
32
+
33
+ const packageJsonCache = new Map();
34
+ const fileExistsCache = new Map();
35
+ const resolvedImportsCache = new Map();
36
+
37
+ /**
38
+ * Check if a package uses exports field
39
+ * @param {string} packageName
40
+ * @returns {boolean}
41
+ */
42
+ function hasExportsField(packageName) {
43
+ if (packageJsonCache.has(packageName)) {
44
+ return packageJsonCache.get(packageName);
45
+ }
46
+
47
+ try {
48
+ const pkgJsonPath = path.join(
49
+ projectRoot,
50
+ 'node_modules',
51
+ packageName,
52
+ 'package.json'
53
+ );
54
+
55
+ if (fs.existsSync(pkgJsonPath)) {
56
+ const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
57
+ const hasExports = !!pkgJson.exports;
58
+ packageJsonCache.set(packageName, hasExports);
59
+ return hasExports;
60
+ }
61
+ } catch (error) {
62
+ logger.warn(`Failed to read package.json for ${packageName}:`, error);
63
+ }
64
+
65
+ packageJsonCache.set(packageName, false);
66
+ return false;
67
+ }
68
+
69
+ /**
70
+ * Check if a file exists with any of the given suffixes (with caching)
71
+ * @param {string} basePath
72
+ * @param {string[]} suffixes
73
+ * @returns {string | null} The matching suffix or null
74
+ */
75
+ function findFileWithSuffix(basePath, suffixes) {
76
+ const cacheKey = `${basePath}:${suffixes.join(',')}`;
77
+
78
+ if (fileExistsCache.has(cacheKey)) {
79
+ return fileExistsCache.get(cacheKey);
80
+ }
81
+
82
+ for (const suffix of suffixes) {
83
+ const fullPath = basePath + suffix;
84
+ if (fs.existsSync(fullPath)) {
85
+ fileExistsCache.set(cacheKey, suffix);
86
+ return suffix;
87
+ }
88
+ }
89
+
90
+ fileExistsCache.set(cacheKey, null);
91
+ return null;
92
+ }
93
+
94
+ /**
95
+ * Extract the package name from an import path
96
+ * @param {string} importPath
97
+ * @returns {string} The package name
98
+ */
99
+ function getPackageName(importPath) {
100
+ if (importPath.startsWith('@')) {
101
+ const parts = importPath.split('/');
102
+ return parts.slice(0, 2).join('/');
103
+ }
104
+ // @ts-expect-error - assured string
105
+ return importPath.split('/')[0];
106
+ }
107
+
108
+ /**
109
+ * Check if a package is internal based on project scopes
110
+ * @param {string} packageName
111
+ * @returns {boolean}
112
+ */
113
+ function isInternalPackage(packageName) {
114
+ return projectScopes.some((scope) => packageName.startsWith(scope));
115
+ }
116
+
117
+ /**
118
+ * Determine if an import should be skipped
119
+ * @param {string} importPath
120
+ * @returns {boolean}
121
+ */
122
+ function shouldSkipImport(importPath) {
123
+ return (
124
+ !importPath ||
125
+ importPath.includes(projectName) ||
126
+ pathsToIgnore.some((ignorePath) => importPath.startsWith(ignorePath)) ||
127
+ importPath.endsWith('.js') ||
128
+ importPath.endsWith('.mjs') ||
129
+ importPath.endsWith('.cjs') ||
130
+ importPath.startsWith('.') ||
131
+ importPath.startsWith('/') ||
132
+ importPath.startsWith('node:') ||
133
+ !importPath.includes('/')
134
+ );
135
+ }
136
+
137
+ /**
138
+ * Transform an internal package import
139
+ * @param {string} importPath
140
+ * @returns {string | null}
141
+ */
142
+ function transformInternalPackageImport(importPath) {
143
+ const parts = importPath.split('/');
144
+ const dependencyName = parts[1];
145
+
146
+ if (!dependencyName) return null;
147
+
148
+ // Replace the dependency name to point to /src
149
+ const srcPath = [...parts];
150
+ srcPath[1] = `${dependencyName}/src`;
151
+ const srcImportPath = srcPath.join('/');
152
+
153
+ const resolvedBase = path.join(projectRoot, 'node_modules', srcImportPath);
154
+
155
+ // Check for direct file match
156
+ const directMatch = findFileWithSuffix(resolvedBase, [
157
+ '.js',
158
+ '.jsx',
159
+ '.ts',
160
+ '.tsx',
161
+ ]);
162
+ if (directMatch) {
163
+ return importPath + '.js';
164
+ }
165
+
166
+ // Check for index file
167
+ const indexMatch = findFileWithSuffix(resolvedBase, [
168
+ '/index.js',
169
+ '/index.jsx',
170
+ '/index.ts',
171
+ '/index.tsx',
172
+ ]);
173
+ if (indexMatch) {
174
+ return importPath + '/index.js';
175
+ }
176
+
177
+ return null;
178
+ }
179
+
180
+ /**
181
+ * Transform a third-party package import
182
+ * @param {string} importPath
183
+ * @returns {string | null}
184
+ */
185
+ function transformThirdPartyImport(importPath) {
186
+ const resolvedBase = path.join(projectRoot, 'node_modules', importPath);
187
+
188
+ // Check for direct .js file
189
+ if (findFileWithSuffix(resolvedBase, ['.js'])) {
190
+ return importPath + '.js';
191
+ }
192
+
193
+ // Check for index.js
194
+ if (findFileWithSuffix(resolvedBase, ['/index.js'])) {
195
+ return importPath + '/index.js';
196
+ }
197
+
198
+ return null;
199
+ }
200
+
201
+ return {
202
+ name: 'transform-imports-plugin',
203
+ setup(api) {
204
+ api.transform(
205
+ { test: /\.[jt]sx?$/, order: 'post' },
206
+ ({ code, resourcePath }) => {
207
+ const transformedCode = code.replace(
208
+ importRegex,
209
+ (fullMatch, dynamicImport, staticImport) => {
210
+ const importPath = dynamicImport || staticImport;
211
+
212
+ // Early return for skippable imports
213
+ if (shouldSkipImport(importPath)) {
214
+ return fullMatch;
215
+ }
216
+
217
+ // Check cache first
218
+ if (resolvedImportsCache.has(importPath)) {
219
+ const cached = resolvedImportsCache.get(importPath);
220
+ return cached
221
+ ? fullMatch.replace(importPath, cached)
222
+ : fullMatch;
223
+ }
224
+
225
+ const packageName = getPackageName(importPath);
226
+
227
+ // Skip third-party packages with exports field
228
+ if (
229
+ !isInternalPackage(packageName) &&
230
+ hasExportsField(packageName)
231
+ ) {
232
+ logger.debug(`Skipping ${importPath} - uses exports field`);
233
+ resolvedImportsCache.set(importPath, null);
234
+ return fullMatch;
235
+ }
236
+
237
+ // Transform based on package type
238
+ let newImportPath = null;
239
+ if (isInternalPackage(packageName)) {
240
+ newImportPath = transformInternalPackageImport(importPath);
241
+ } else {
242
+ newImportPath = transformThirdPartyImport(importPath);
243
+ }
244
+
245
+ // Cache the result
246
+ resolvedImportsCache.set(importPath, newImportPath);
247
+
248
+ if (newImportPath) {
249
+ logger.debug(`Transformed import in ${resourcePath}`);
250
+ logger.debug(` ${importPath} => ${newImportPath}\n`);
251
+ return fullMatch.replace(importPath, newImportPath);
252
+ }
253
+
254
+ return fullMatch;
255
+ }
256
+ );
257
+
258
+ return { code: transformedCode };
259
+ }
260
+ );
261
+ },
262
+ };
263
+ }