@digigov/cli-build 2.0.0-2177f152 → 2.0.0-2271444d

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/copy-files.js CHANGED
@@ -1,14 +1,10 @@
1
- import { logger, resolveProject } from "@digigov/cli/lib";
2
- import fs from "fs-extra";
3
- import path from "path";
4
- import glob from "globby";
1
+ import { logger, resolveProject } from '@digigov/cli/lib';
2
+ import fs from 'fs-extra';
3
+ import path from 'path';
5
4
 
6
5
  const packagePath = process.cwd();
7
-
8
- function getBuildPath() {
9
- const project = resolveProject();
10
- return path.join(project.root, project.distDir);
11
- }
6
+ const project = resolveProject();
7
+ const buildPath = path.join(project.root, project.distDir);
12
8
 
13
9
  /**
14
10
  * Copy a file from the package to the build directory
@@ -17,10 +13,9 @@ function getBuildPath() {
17
13
  */
18
14
  function includeFileInBuild(file) {
19
15
  const sourcePath = path.resolve(packagePath, file);
20
- const buildPath = getBuildPath();
21
16
  const targetPath = path.resolve(buildPath, path.basename(file));
22
17
  fs.copySync(sourcePath, targetPath);
23
- logger.log(`Copied ${sourcePath} to ${targetPath}`);
18
+ logger.debug(`Copied ${sourcePath} to build directory`);
24
19
  }
25
20
 
26
21
  /**
@@ -28,55 +23,42 @@ function includeFileInBuild(file) {
28
23
  */
29
24
  function createRootPackageFile() {
30
25
  const packageData = fs.readFileSync(
31
- path.resolve(packagePath, "./package.json"),
32
- "utf8",
26
+ path.resolve(packagePath, './package.json'),
27
+ 'utf8'
33
28
  );
29
+
30
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
34
31
  const { nyc, scripts, devDependencies, workspaces, ...packageDataOther } =
35
32
  JSON.parse(packageData);
36
33
  const newPackageData = {
37
34
  ...packageDataOther,
35
+ main: undefined,
38
36
  private: false,
39
- main: "./cjs/index.js",
40
- module: "./index.js",
41
- typings: "./index.d.ts",
37
+ type: 'module', // ESM only
38
+ exports: {
39
+ '.': {
40
+ types: './index.d.ts',
41
+ import: './index.js',
42
+ },
43
+ './src/*': './src/*',
44
+ './*': {
45
+ types: ['./*.d.ts', './*/index.d.ts'],
46
+ import: ['./*', './*/index.js'],
47
+ },
48
+ './*.js': {
49
+ types: './*.d.ts',
50
+ import: './*.js',
51
+ },
52
+ },
42
53
  };
43
- const buildPath = getBuildPath();
44
- const targetPath = path.resolve(buildPath, "./package.json");
54
+ const targetPath = path.resolve(buildPath, './package.json');
45
55
 
46
- fs.writeFileSync(targetPath, JSON.stringify(newPackageData, null, 2), "utf8");
47
- logger.log(`Created package.json in ${targetPath}`);
56
+ fs.writeFileSync(targetPath, JSON.stringify(newPackageData, null, 2), 'utf8');
57
+ logger.debug(`Created package.json in build directory`);
48
58
 
49
59
  return newPackageData;
50
60
  }
51
61
 
52
- /**
53
- * Create nested package.json files in the build directory
54
- *
55
- */
56
- function createNestedPackageFiles() {
57
- const buildPath = getBuildPath();
58
- const indexPaths = glob.sync(path.join(buildPath, "**/index.js"), {
59
- ignore: [path.join(buildPath, "cjs/**")],
60
- });
61
-
62
- indexPaths.forEach((indexPath) => {
63
- if (indexPath === path.join(buildPath, "index.js")) return;
64
- const packageData = {
65
- sideEffects: false,
66
- module: "./index.js",
67
- types: "./index.d.ts",
68
- main: path.relative(
69
- path.dirname(indexPath),
70
- indexPath.replace(buildPath, path.join(buildPath, "/cjs")),
71
- ),
72
- };
73
- fs.writeFileSync(
74
- path.join(path.dirname(indexPath), "package.json"),
75
- JSON.stringify(packageData, null, 2),
76
- );
77
- });
78
- }
79
-
80
62
  /**
81
63
  * Prepend a string to a file
82
64
  *
@@ -84,8 +66,9 @@ function createNestedPackageFiles() {
84
66
  * @param {string} string - The string to prepend
85
67
  */
86
68
  function prepend(file, string) {
87
- const data = fs.readFileSync(file, "utf8");
88
- fs.writeFileSync(file, string + data, "utf8");
69
+ const data = fs.readFileSync(file, 'utf8');
70
+ fs.writeFileSync(file, string + data, 'utf8');
71
+ logger.debug(`Prepended license to ${file}`);
89
72
  }
90
73
 
91
74
  /**
@@ -94,24 +77,23 @@ function prepend(file, string) {
94
77
  * @param {object} packageData - The package data
95
78
  */
96
79
  function addLicense(packageData) {
97
- const buildPath = getBuildPath();
98
- const license = `/** @license Digigov v${packageData["version"]}
80
+ const license = `/** @license Digigov v${packageData['version']}
99
81
  *
100
82
  * This source code is licensed under the BSD-2-Clause license found in the
101
83
  * LICENSE file in the root directory of this source tree.
102
84
  */
103
85
  `;
104
- ["./index.js", "./index.mjs"].map(async (file) => {
86
+ ['./index.js', './index.mjs'].map(async (file) => {
105
87
  try {
106
88
  prepend(path.resolve(buildPath, file), license);
107
89
  } catch (err) {
108
90
  if (
109
- typeof err === "object" &&
91
+ typeof err === 'object' &&
110
92
  err &&
111
- "code" in err &&
112
- err.code === "ENOENT"
93
+ 'code' in err &&
94
+ err.code === 'ENOENT'
113
95
  ) {
114
- logger.log(`Skipped license for ${file}`);
96
+ logger.debug(`Skipped license for ${file}`);
115
97
  } else {
116
98
  throw err;
117
99
  }
@@ -119,36 +101,19 @@ function addLicense(packageData) {
119
101
  });
120
102
  }
121
103
 
122
- /**
123
- * Create separate index modules for each directory
124
- */
125
- function createSeparateIndexModules() {
126
- const buildPath = getBuildPath();
127
- const files = glob.sync(path.join(buildPath, "**/*.js"), {
128
- ignore: [path.join(buildPath, "**/index.js")],
129
- });
130
-
131
- files.forEach((file) => {
132
- fs.mkdirSync(file.replace(/\.js$/, ""));
133
- fs.renameSync(file, file.replace(/\.js$/, "/index.js"));
134
- });
135
- }
136
-
137
104
  /**
138
105
  * Run the copy files script
139
106
  */
140
107
  export default function run() {
141
108
  const packageData = createRootPackageFile();
142
- createSeparateIndexModules();
143
- createNestedPackageFiles();
144
109
 
145
110
  [
146
111
  // use enhanced readme from workspace root for `@digigov/ui`
147
112
  // packageData.name === '@digigov/ui' ? '../../README.md' : './README.md',
148
- "./src",
149
- "./README.md",
150
- "./CHANGELOG.md",
151
- "../../LICENSE",
152
- ].map((file) => includeFileInBuild(file)),
153
- addLicense(packageData);
113
+ './src',
114
+ './README.md',
115
+ './CHANGELOG.md',
116
+ '../../LICENSE',
117
+ ].map((file) => includeFileInBuild(file));
118
+ addLicense(packageData);
154
119
  }
@@ -0,0 +1,3 @@
1
+ import config from '@digigov/cli-lint/eslint.config';
2
+
3
+ export default [...config];
@@ -0,0 +1,215 @@
1
+ import { logger } from '@digigov/cli/lib';
2
+ import path from 'path';
3
+ import fs from 'fs-extra';
4
+ import { SyntaxKind, Project as TsMorphProject } from 'ts-morph';
5
+ import assert from 'assert';
6
+
7
+ import { getProjectTsconfig } from './common.js';
8
+
9
+ /** @typedef {Object} Project - Represents the project to be built
10
+ * @property {string} root - The project root directory
11
+ * @property {string} name - The project name as in package.json
12
+ * @property {string} src - The project src directory
13
+ * @property {string} distDir - The project build directory
14
+ */
15
+
16
+ /**
17
+ * Generate registry file for the given project
18
+ *
19
+ * @param {Project} project - The project object
20
+ * @param {string} [registryFilename="registry.js"] - The name of the registry file
21
+ * @param {string[]} absoluteFilePaths - The absolute paths of the files to include in the registry
22
+ * @returns {Promise<string>} - The path to the generated registry file
23
+ */
24
+ export async function generateRegistry(
25
+ project,
26
+ absoluteFilePaths,
27
+ registryFilename = 'registry.js'
28
+ ) {
29
+ const registryPath = ensureRegistryPath(project, registryFilename);
30
+
31
+ const relativePaths = absoluteFilePaths.map((path) => {
32
+ assert(
33
+ path.startsWith(project.root),
34
+ 'Expected path to be in project root'
35
+ );
36
+ return toNodeResolvablePath(
37
+ path.replace(`${project.root}/src/`, `${project.name}/`)
38
+ );
39
+ });
40
+ let registryPaths = relativePaths.map((path) => ({
41
+ path,
42
+ uid: createUid(path),
43
+ }));
44
+
45
+ if (registryPaths.length === 0)
46
+ throw new Error(
47
+ 'Could not generate registry. No exportable modules found.'
48
+ );
49
+
50
+ const importStatements = registryPaths.map(
51
+ (file) => `import * as ${file.uid} from "${file.path}";`
52
+ );
53
+ const componentsToExport = registryPaths.map(
54
+ (file) => ` '${file.path}': lazyImport(${file.uid})`
55
+ );
56
+
57
+ logger.debug(
58
+ `Including ${componentsToExport.length} items in ${registryPath}`
59
+ );
60
+
61
+ let registryFileContent = `
62
+ ${importStatements.join('\n')}
63
+ function lazyImport(pkgImport) {
64
+ return new Proxy(
65
+ {},
66
+ {
67
+ get: (_target, name) => {
68
+ if (name === '__esModule' || name === 'default') {
69
+ return pkgImport.default;
70
+ } else if(
71
+ name === '*'
72
+ ) {
73
+ return pkgImport;
74
+ } else {
75
+ return pkgImport[name];
76
+ }
77
+ },
78
+ }
79
+ )
80
+ }
81
+ export default {
82
+ ${componentsToExport.join(',\n')}
83
+ };
84
+ `;
85
+ await fs.writeFile(registryPath, registryFileContent);
86
+
87
+ return registryPath;
88
+ }
89
+
90
+ /**
91
+ * Generate a lazy registry file for the given project
92
+ *
93
+ * @param {Project} project - The project object
94
+ * @param {string[]} filePaths - The files whose exports will be included in the lazy registry
95
+ * @param {string} [lazyFilename="lazy.js"] - The name of the registry file
96
+ * @returns {Promise<string>} - The path to the generated lazy registry file
97
+ */
98
+ export async function generateLazyRegistry(
99
+ project,
100
+ filePaths,
101
+ lazyFilename = 'lazy.js'
102
+ ) {
103
+ const lazyPath = ensureRegistryPath(project, lazyFilename);
104
+
105
+ const tsMorphProject = new TsMorphProject({
106
+ tsConfigFilePath: getProjectTsconfig(project.root),
107
+ });
108
+
109
+ /** @type {Record<string, string>} */
110
+ let allComponents = {};
111
+
112
+ for (const filePath of filePaths) {
113
+ const sourceFile = tsMorphProject.addSourceFileAtPath(filePath);
114
+ const exports = sourceFile
115
+ .getExportSymbols()
116
+ .filter(isJsExport)
117
+ .map((symbol) => symbol.getName());
118
+
119
+ for (const exportedComponent of exports) {
120
+ if (
121
+ exportedComponent !== 'default' &&
122
+ exportedComponent.match(/^[A-Z][a-z]+/)
123
+ ) {
124
+ if (
125
+ !allComponents[exportedComponent] ||
126
+ allComponents[exportedComponent].length < filePath.length // Make import path more specific
127
+ ) {
128
+ allComponents[exportedComponent] = toNodeResolvablePath(
129
+ filePath.replace(`${project.root}/src/`, `${project.name}/`)
130
+ );
131
+ }
132
+ }
133
+ }
134
+ }
135
+
136
+ const componentCount = Object.keys(allComponents).length;
137
+
138
+ if (componentCount === 0)
139
+ throw new Error(
140
+ 'Could not generate lazy registry. No exportable components found.'
141
+ );
142
+
143
+ logger.debug(`Including ${componentCount} components in ${lazyPath}`);
144
+
145
+ const componentsToExport = Object.entries(allComponents)
146
+ .map(
147
+ ([component, filePath]) =>
148
+ ` '${component}': lazy(() => import('${filePath}').then((module) => ({ default: module['${component}'] })))`
149
+ )
150
+ .join(',\n');
151
+
152
+ const lazyFileContent = `import { lazy } from 'react';
153
+ export default {
154
+ ${componentsToExport}
155
+ };
156
+ `;
157
+
158
+ await fs.writeFile(lazyPath, lazyFileContent);
159
+
160
+ return lazyPath;
161
+ }
162
+
163
+ /**
164
+ * Ensure that the registry file does not already exist at the given path
165
+ *
166
+ * @param {Project} project - The project object
167
+ * @param {string} fileName - The name of the registry file
168
+ */
169
+ function ensureRegistryPath(project, fileName) {
170
+ const registryPath = path.join(project.root, project.src, fileName);
171
+ if (fs.existsSync(registryPath))
172
+ throw new Error(`A "${fileName}" file already exists at ${registryPath}.`);
173
+ return registryPath;
174
+ }
175
+
176
+ /**
177
+ * Extract a node-resolvable path
178
+ *
179
+ * @param {string} inputPath - The file path
180
+ * @returns {string} - The node-resolvable path
181
+ */
182
+ function toNodeResolvablePath(inputPath) {
183
+ const dir = path.dirname(inputPath);
184
+ const base = path.basename(inputPath, path.extname(inputPath));
185
+
186
+ return base === 'index' ? dir : path.join(dir, base);
187
+ }
188
+
189
+ /**
190
+ * Create a UID from a path
191
+ *
192
+ * @param {string} inputPath - The path
193
+ * @returns {string} - The UID
194
+ */
195
+ function createUid(inputPath) {
196
+ return inputPath.replace(/[/@\-.]/g, '_');
197
+ }
198
+
199
+ /**
200
+ * Check if a symbol is a JS export
201
+ *
202
+ * @param {import("ts-morph").Symbol} symbol - The symbol to check
203
+ */
204
+ function isJsExport(symbol) {
205
+ const declarations = symbol.getDeclarations();
206
+ return declarations.some((declaration) => {
207
+ const kind = declaration.getKind();
208
+ return (
209
+ kind === SyntaxKind.FunctionDeclaration ||
210
+ kind === SyntaxKind.ClassDeclaration ||
211
+ kind === SyntaxKind.VariableDeclaration ||
212
+ kind === SyntaxKind.ExportSpecifier
213
+ );
214
+ });
215
+ }
package/index.js CHANGED
@@ -1,85 +1,176 @@
1
- import { DigigovCommand, resolveProject } from "@digigov/cli/lib";
2
- import copyFiles from "./copy-files.js";
1
+ import { DigigovCommand, resolveProject, logger } from '@digigov/cli/lib';
2
+ import { build } from '@rslib/core';
3
+ import copyFiles from './copy-files.js';
3
4
 
4
- import fs from "fs-extra";
5
- import path from "path";
6
- import esbuild from "esbuild";
7
- import glob from "globby";
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';
8
12
 
9
- const command = new DigigovCommand("build", import.meta.url).action(build);
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);
10
24
  export default command;
11
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
+
12
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
13
42
  * @param {DigigovCommand} ctx
14
43
  */
15
- async function build(_, ctx) {
16
- await ctx.exec("rimraf", ["dist"]);
17
- const project = resolveProject();
44
+ async function main(options, ctx) {
45
+ /** @type {string[]} */
46
+ let filesToDelete = [];
47
+ let isCleaningUp = false;
48
+ let signalHandlersRegistered = false;
18
49
 
19
- const distDir = path.resolve(project.root, project.distDir);
20
- const basename = path.basename(project.root);
50
+ const cleanup = async () => {
51
+ if (isCleaningUp) return;
52
+ isCleaningUp = true;
21
53
 
22
- if (project.isTs) {
23
- const tsconfigProduction = path.join(
24
- project.root,
25
- "tsconfig.production.json",
26
- );
27
- /** @type {string[]} */
28
- const tsArgs = [];
29
- if (fs.existsSync(tsconfigProduction)) {
30
- tsArgs.push("--project", tsconfigProduction);
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);
60
+ }
31
61
  }
32
- await ctx.exec("tsc", [
33
- "--emitDeclarationOnly",
34
- "--outDir",
35
- "dist",
36
- ...tsArgs,
37
- ]);
38
- if (fs.existsSync(path.join(distDir, basename))) {
39
- const typesIncluded = fs.readdirSync(path.join(distDir));
40
- const paths = fs.readdirSync(path.join(distDir, basename, project.src));
41
- paths.forEach((p) => {
42
- fs.renameSync(
43
- path.join(distDir, basename, project.src, p),
44
- path.join(distDir, p),
45
- );
46
- });
47
- typesIncluded.forEach((typesDir) => {
48
- fs.rmSync(path.join(distDir, typesDir), { recursive: true });
49
- });
62
+
63
+ // Remove signal handlers after cleanup
64
+ if (signalHandlersRegistered) {
65
+ process.off('SIGINT', handleSignal);
66
+ process.off('SIGTERM', handleSignal);
50
67
  }
51
- }
68
+ };
52
69
 
53
- const files = glob.sync(
54
- path.join(project.root, "src", "**/*.{tsx,ts,js,jsx}"),
55
- {
56
- ignore: ["**/*.{spec,test}.{ts,tsx,js,jsx}"],
57
- },
58
- );
59
- const commonBuildOptions = {
60
- entryPoints: files,
61
- platform: "node",
62
- sourcemap: true,
63
- target: ["esnext"],
64
- logLevel: "error",
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);
65
75
  };
66
- if (fs.existsSync(path.join(project.root, "tsconfig.production.json"))) {
67
- commonBuildOptions["tsconfig"] = "tsconfig.production.json";
68
- } else if (fs.existsSync(path.join(project.root, "tsconfig.json"))) {
69
- commonBuildOptions["tsconfig"] = "tsconfig.json";
70
- }
71
76
 
72
- await Promise.all([
73
- esbuild.build({
74
- ...commonBuildOptions,
75
- format: "esm",
76
- outdir: `dist`,
77
- }),
78
- esbuild.build({
79
- ...commonBuildOptions,
80
- format: "cjs",
81
- outdir: `dist/cjs`,
82
- }),
83
- ]);
84
- copyFiles();
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'))
106
+ );
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
+ );
119
+ }
120
+
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');
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();
175
+ }
85
176
  }