@nebula.js/cli-build 7.8.0 → 8.0.0-alpha.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.
package/README.md CHANGED
@@ -20,18 +20,15 @@ Options:
20
20
  --config, -c Path to a JavaScript config file
21
21
  [string] [default: "nebula.config.js"]
22
22
  --watch, -w Watch source files
23
- [choices: "umd", "systemjs"] [default: "umd"]
23
+ [choices: "esm", "systemjs"] [default: "esm"]
24
24
  --sourcemap, -m Generate source map [boolean] [default: true]
25
25
  --mode Explicitly set mode
26
26
  [string] [choices: "production", "development"]
27
27
  --core Set a core build target [string] [default: "core"]
28
28
  --typescript Enable typescript bundling [boolean] [default: false]
29
- --preferBuiltins Specify whether to use Node.js built-in modules.
29
+ --browser Use the 'browser' field and export condition of the packages
30
+ in the bundle so browser conditionals in exports are applied.
30
31
  [boolean] [default: true]
31
- --browser Use the browser module resolutions in package.json and adds
32
- 'browser' to exportConditions if it is not present so browser
33
- conditionals in exports are applied.
34
- [boolean] [default: false]
35
32
  ```
36
33
 
37
34
  ## Example
@@ -145,9 +142,9 @@ nebula build --core minimal/target
145
142
  The code is exported into `/minimal/target` directory
146
143
 
147
144
  Tips:
148
- In the package.json file, the main field makes sure that Node users using require
149
- can be served the umd version. The module field is a common convention to designate
150
- how to import an esm version of your code.
145
+ The `module` field of package.json is the file of the es module that is built, and the
146
+ `systemjs` field the file of the SystemJS module. A build needs at least one of them. There is
147
+ no UMD bundle, the `main` field is not used by the build.
151
148
 
152
149
  ### SystemJS build
153
150
 
@@ -158,6 +155,15 @@ package.json which specifies the output file from the build:
158
155
  "systemjs": "dist/hello.systemjs.js"
159
156
  ```
160
157
 
158
+ ### Styles
159
+
160
+ Stylesheets that the source imports are added to the bundle and injected in a `<style>` element
161
+ in the document head when the bundle is loaded, nothing is injected when there is no `document`.
162
+
163
+ - `import './style.css'` adds the stylesheet. PostCSS is applied, using the `postcss.config.js` of your project if there is one.
164
+ - `import styles from './style.module.css'` is a CSS module, `styles` maps the class names of the file to the generated ones.
165
+ - `import './style.scss'` is compiled when `sass` is installed in your project, `import './style.less'` when `less` is.
166
+
161
167
  ### Typescript
162
168
 
163
169
  With this option you can enable typescript bundling of your code. Add a `tsconfig.json`
@@ -166,3 +172,23 @@ file to configure typescript to your own preferences.
166
172
  ```sh
167
173
  nebula build --typescript
168
174
  ```
175
+
176
+ ## Building with Rslib
177
+
178
+ The bundles are built with [Rslib](https://rslib.rs), on [Rspack](https://rspack.rs). The output has the same formats and behaves the
179
+ same way as it did when the bundler was Rollup, with these differences to be aware of when you upgrade:
180
+
181
+ - The generated class names of CSS modules have another hash (`name_class__hash`).
182
+ - `replacementStrings` are applied as definitions, a key has to be an identifier or a member expression
183
+ such as `__VERSION__` or `process.env.VERSION`, not any piece of text, and the value is the code that replaces it.
184
+ - With `--reactNative`, a `.native` file replaces the file it sits next to when it is imported without a file extension.
185
+ - An import that cannot be resolved is an error, it used to be left as an external dependency with a warning. Add the package to the dependencies, or to the peer dependencies when the host provides it.
186
+ - Node.js built-in modules are not bundled and not dependencies, importing one gives an empty module unless a package of that name is installed. `--preferBuiltins` is removed.
187
+ - The `bundle-analysis.html` that development builds used to write is no longer created.
188
+ - `@rollup/plugin-typescript` is not needed for `--typescript`, TypeScript is compiled with SWC. The code, and the packages in `node_modules`, are compiled for the browsers of `@qlik/browserslist-config`.
189
+ - Stylus (`.styl`) files are not supported.
190
+ - With `--codeSplit` a SystemJS build is the entry `index.js` and a chunk per dynamic import. The chunks are loaded from the directory that SystemJS loaded the entry from, also when the entry file is renamed.
191
+ - **The UMD bundle is gone.** `nebula build` writes the es module (`module`) and the SystemJS module (`systemjs`), and `--watch` watches `esm` or `systemjs`. The entry of a bundle is an es module: an entry that is a CommonJS module (`module.exports = …`) has no exports in the es module, its properties are the named exports of the SystemJS module.
192
+ - A bundle contains a small module runtime, so the output of a very small visualization is a few kilobytes larger.
193
+ - A flag or a key in the `build` section of the config file that `nebula build` does not know of is an error (`Unknown argument: foo`), and no longer ignored. A misspelled option used to do nothing.
194
+ - The notices of the licenses (`/*!`, `@license`, `@preserve`) of the code in a production bundle are written to `<bundle>.LICENSE.txt` next to it, and no longer kept in the bundle. Publish that file with the bundle.
package/command.js CHANGED
@@ -1,4 +1,3 @@
1
- /* eslint-disable import/extensions */
2
1
  import build from './lib/build.js';
3
2
  import init from './lib/init-config.js';
4
3
 
@@ -6,7 +5,8 @@ export default {
6
5
  command: 'build',
7
6
  desc: 'Build visualization',
8
7
  builder(yargs) {
9
- init(yargs).argv;
8
+ // a flag or a key of the config file that it does not know of is an error, a misspelled one would otherwise do nothing
9
+ init(yargs).strictOptions().argv;
10
10
  },
11
11
  handler(argv) {
12
12
  build(argv);
package/lib/build.js CHANGED
@@ -1,23 +1,15 @@
1
- /* eslint-disable no-console, import/extensions */
2
1
  import path from 'path';
2
+ import { createRequire } from 'module';
3
3
  import extend from 'extend';
4
4
  import yargs from 'yargs';
5
- import * as rollup from 'rollup';
6
5
 
7
6
  import initConfig from './init-config.js';
8
7
  import config from './config.js';
9
8
  import watch from './watch.js';
9
+ import { run } from './compile.js';
10
10
  import systemjsBehaviours from './systemjs.js';
11
11
 
12
- const umd = async (argv) => {
13
- const c = config({
14
- mode: argv.mode || 'production',
15
- format: 'umd',
16
- argv,
17
- });
18
- const bundle = await rollup.rollup(c.input);
19
- await bundle.write(c.output);
20
- };
12
+ const require = createRequire(import.meta.url);
21
13
 
22
14
  const esm = async (argv, core) => {
23
15
  const c = config({
@@ -33,8 +25,7 @@ const esm = async (argv, core) => {
33
25
  if (!c) {
34
26
  return undefined;
35
27
  }
36
- const bundle = await rollup.rollup(c.input);
37
- return bundle.write(c.output);
28
+ return run(c);
38
29
  };
39
30
 
40
31
  const systemjs = async (argv) => {
@@ -47,8 +38,7 @@ const systemjs = async (argv) => {
47
38
  if (!c) {
48
39
  return undefined;
49
40
  }
50
- const bundle = await rollup.rollup(c.input);
51
- return bundle.write(c.output);
41
+ return run(c);
52
42
  };
53
43
 
54
44
  async function build(argv = {}) {
@@ -64,10 +54,11 @@ async function build(argv = {}) {
64
54
  if (buildConfig.watch) {
65
55
  return watch(buildConfig);
66
56
  }
57
+ const pkg = require(path.resolve(buildConfig.cwd || process.cwd(), 'package.json'));
58
+ if (!pkg.module && !(buildConfig.systemjs && pkg.systemjs)) {
59
+ throw new Error('Nothing to build: package.json has no "module" field for the esm bundle or "systemjs" field');
60
+ }
67
61
  console.time('Total build time');
68
- console.time('Generate UMD bundle');
69
- await umd(buildConfig);
70
- console.timeEnd('Generate UMD bundle');
71
62
  console.time('Create ESM bundle');
72
63
  await esm(buildConfig);
73
64
  console.timeEnd('Create ESM bundle');
@@ -0,0 +1,13 @@
1
+ import { builtinModules } from 'module';
2
+
3
+ /**
4
+ * The `resolve` options that make an empty module of every node builtin.
5
+ *
6
+ * A builtin is never a dependency of a bundle. Library code that reaches for one in a path of its own for node
7
+ * (random-js and @qlik/api do, with require('crypto') and import('fs')) would otherwise make a host that loads the
8
+ * bundle look for it. A package of the same name that is installed is used instead.
9
+ */
10
+ export default {
11
+ fallback: Object.fromEntries(builtinModules.map((name) => [name, false])),
12
+ alias: Object.fromEntries(builtinModules.map((name) => [`node:${name}`, false])),
13
+ };
package/lib/compile.js ADDED
@@ -0,0 +1,44 @@
1
+ import chalk from 'chalk';
2
+ import { createRslib } from '@rslib/core';
3
+
4
+ const formatWarnings = (stats) => stats.toString({ all: false, warnings: true, colors: false });
5
+
6
+ const formatErrors = (stats) => stats.toString({ all: false, errors: true, errorDetails: true, colors: false });
7
+
8
+ /**
9
+ * Rslib with what a build of nebula needs: it prints nothing itself, the stats of the compilations are
10
+ * handed to `onCompile` and the build is started with the plugin of `hooks`.
11
+ */
12
+ const create = async ({ cwd, config }, hooks = () => {}) =>
13
+ createRslib({
14
+ cwd,
15
+ config: {
16
+ ...config,
17
+ logLevel: 'silent',
18
+ plugins: [{ name: 'nebula-build', setup: hooks }],
19
+ },
20
+ });
21
+
22
+ /**
23
+ * Run a build once and resolve when it is written to disk.
24
+ * Rejects with the compilation errors, the way a failed rollup bundle did.
25
+ */
26
+ export const run = async (build) => {
27
+ let stats;
28
+ const rslib = await create(build, (api) => {
29
+ api.onAfterBuild((result) => {
30
+ stats = result.stats;
31
+ });
32
+ });
33
+ try {
34
+ await rslib.build();
35
+ } catch (e) {
36
+ throw stats && stats.hasErrors() ? new Error(formatErrors(stats)) : e;
37
+ }
38
+ if (stats && stats.hasWarnings()) {
39
+ console.log(`${chalk.black.bgYellow(' WARN ')} ${chalk.yellow(formatWarnings(stats))}`);
40
+ }
41
+ return stats;
42
+ };
43
+
44
+ export { create, formatWarnings, formatErrors };
package/lib/config.js CHANGED
@@ -1,23 +1,16 @@
1
- /* eslint-disable import/extensions */
2
1
  import fs from 'fs';
3
2
  import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
4
  import { createRequire } from 'module';
5
- import { visualizer } from 'rollup-plugin-visualizer';
5
+ import { rspack } from '@rslib/core';
6
6
 
7
- import resolveNative from './resolveNative.js';
7
+ import builtins from './builtins.js';
8
+ import matchPeers from './peers.js';
8
9
 
9
10
  const require = createRequire(import.meta.url);
10
- const babel = require('@rollup/plugin-babel');
11
- const postcss = require('rollup-plugin-postcss');
12
- const replace = require('@rollup/plugin-replace');
13
- const json = require('@rollup/plugin-json');
14
- const { nodeResolve } = require('@rollup/plugin-node-resolve');
15
- const commonjs = require('@rollup/plugin-commonjs');
16
- const terser = require('@rollup/plugin-terser');
17
- const babelPreset = require('@babel/preset-env');
11
+ const dirname = path.dirname(fileURLToPath(import.meta.url));
12
+
18
13
  const browsersList = require('@qlik/browserslist-config');
19
- const babelPresetReact = require('@babel/preset-react');
20
- const babelPresetTypescript = require('@babel/preset-typescript');
21
14
 
22
15
  const resolveReplacementStrings = (replacementStrings) => {
23
16
  if (typeof replacementStrings !== 'object') {
@@ -32,7 +25,6 @@ const setupReactNative = (argv) => {
32
25
  if (reactNative) {
33
26
  reactNativePath = argv.reactNativePath || './react-native';
34
27
  if (!fs.existsSync(`${reactNativePath}/package.json`)) {
35
- // eslint-disable-next-line no-console
36
28
  console.warn(
37
29
  `WARNING: No ${reactNativePath}/package.json was found. If you really intended to build a react-native version of this package, please provide one.\nOther wise, to suppress this warning, omit the --reactNative flag.`
38
30
  );
@@ -65,33 +57,38 @@ const getExternalCore = ({ pkg }) => {
65
57
  return [...defaultExternal, ...peers];
66
58
  };
67
59
 
68
- const getOutputFileDefault = ({ pkg }) => pkg.main;
60
+ const getOutputFileDefault = ({ pkg }) => pkg.module;
69
61
 
70
- const getOutputNameDefault = ({ pkg }) => pkg.name.split('/').reverse()[0];
62
+ // the project's own sass and less, the way rollup-plugin-postcss picked them up
63
+ const resolveFromProject = (name, cwd) => {
64
+ try {
65
+ return require(require.resolve(name, { paths: [cwd] }));
66
+ } catch (e) {
67
+ return null;
68
+ }
69
+ };
71
70
 
72
71
  const config = ({
73
72
  mode = 'production',
74
- format = 'umd',
73
+ format = 'esm',
75
74
  cwd = process.cwd(),
76
75
  argv = { sourcemap: true, codeSplit: false, inlineDynamic: false },
77
76
  core,
78
77
  behaviours: {
79
78
  getExternal = getExternalDefault,
80
79
  getOutputFile = getOutputFileDefault,
81
- getOutputName = getOutputNameDefault,
82
80
  // Return false if no build should be done, otherwise true
83
81
  enabled = () => true,
84
82
  } = {},
85
83
  } = {}) => {
86
- const CWD = argv.cwd || cwd;
84
+ const CWD = path.resolve(argv.cwd || cwd);
87
85
  const { reactNative, reactNativePath } = setupReactNative(argv);
88
86
  let dir = CWD;
89
87
  let pkg = require(path.resolve(CWD, 'package.json'));
90
88
  const corePkg = core ? require(path.resolve(core, 'package.json')) : null;
91
89
  pkg = reactNative ? require(path.resolve(reactNativePath, 'package.json')) : pkg;
92
- const { sourcemap, replacementStrings = {}, typescript, preferBuiltins, browser } = argv;
90
+ const { sourcemap, replacementStrings = {}, typescript, browser } = argv;
93
91
  const banner = getBanner({ pkg });
94
- const outputName = getOutputName({ pkg, config: argv });
95
92
 
96
93
  if (reactNative) {
97
94
  dir = `${dir}/${reactNativePath}`;
@@ -105,144 +102,188 @@ const config = ({
105
102
  }
106
103
  const outputFile = getOutputFile({ pkg, config: argv });
107
104
 
108
- const extensions = ['.mjs', '.js', '.jsx', '.json', '.node'];
109
-
110
- let typescriptPlugin;
105
+ const extensions = ['.mjs', '.js', '.jsx', '.json'];
111
106
  if (typescript) {
112
107
  extensions.push('.tsx', '.ts');
113
- try {
114
- typescriptPlugin = require('@rollup/plugin-typescript'); // eslint-disable-line
115
- } catch (e) {
116
- throw new Error(`${e}\n '@rollup/plugin-typescript' is required to build using typescript.`);
117
- }
118
108
  }
119
109
 
120
110
  const external = core ? getExternalCore({ pkg }) : getExternal({ pkg, config: argv });
121
111
  // stardust should always be external
122
112
  if (external.indexOf('@nebula.js/stardust') === -1) {
123
- // eslint-disable-next-line no-console
124
113
  console.warn('@nebula.js/stardust should be specified as a peer dependency');
125
114
  external.push('@nebula.js/stardust');
126
115
  }
127
116
 
128
- const output = () => {
129
- const outputConfig = {
130
- banner,
131
- format,
132
- name: outputName,
133
- sourcemap,
134
- globals: {
135
- '@nebula.js/stardust': 'stardust',
136
- },
137
- };
138
- if (!argv.codeSplit || format === 'umd') {
139
- outputConfig.file = path.resolve(dir, outputFile);
140
- } else {
141
- outputConfig.dir = path.resolve(dir, outputFile.split('/')[0]);
142
- }
143
- if (format === 'umd' || argv.inlineDynamic) {
144
- outputConfig.inlineDynamicImports = true;
145
- }
146
- return outputConfig;
117
+ const isEsm = format === 'esm';
118
+ const development = mode === 'development';
119
+ // a bundle ends up in one file unless code splitting is asked for
120
+ const singleFile = argv.inlineDynamic || !argv.codeSplit;
121
+ const outputDir = singleFile
122
+ ? path.dirname(path.resolve(dir, outputFile))
123
+ : path.resolve(dir, outputFile.split('/')[0]);
124
+ // rslib wants the entry as a file, the extension of it is the first that exists
125
+ const entry =
126
+ ['.js', '.jsx', '.mjs', ...(typescript ? ['.ts', '.tsx'] : [])]
127
+ .map((extension) => path.resolve(CWD, `src/index${extension}`))
128
+ .find((file) => fs.existsSync(file)) || path.resolve(CWD, 'src/index.js');
129
+ // the chunks of a code split systemjs bundle are loaded relative to the url that SystemJS loaded the entry from
130
+ const systemChunks = format === 'systemjs' && !singleFile;
131
+
132
+ // the class names of a css module are the default export of the module, and are named like they were
133
+ const cssModules = {
134
+ namedExport: false,
135
+ exportLocalsConvention: 'as-is',
136
+ localIdentName: '[name]_[local]__[hash:base64:5]',
147
137
  };
148
138
 
149
- const babelPresets = [
150
- [
151
- babelPreset,
152
- {
153
- modules: false,
154
- targets: {
155
- browsers: browsersList,
156
- },
157
- },
158
- ],
139
+ // `url()` is left as it is written, the stylesheet is not a place to bundle assets, as it was before
140
+ const cssLoaders = (cssOptions = {}) => [
141
+ require.resolve('./inject-style-loader.cjs'),
142
+ {
143
+ loader: require.resolve('css-loader'),
144
+ // the style is a string in the bundle, a source map of it in the text would only make the bundle larger
145
+ options: { importLoaders: 1, url: false, sourceMap: false, ...cssOptions },
146
+ },
147
+ require.resolve('postcss-loader'),
159
148
  ];
160
149
 
161
- const babelOverrides = [
150
+ const sass = resolveFromProject('sass', CWD);
151
+ const less = resolveFromProject('less', CWD);
152
+
153
+ const styleRules = [
162
154
  {
163
- test: /\.[jt]sx$/,
164
- presets: [[babelPresetReact, { runtime: 'automatic', development: mode === 'development' }]],
155
+ test: /\.module\.css$/,
156
+ use: cssLoaders({ modules: cssModules }),
165
157
  },
158
+ { test: /\.css$/, exclude: /\.module\.css$/, use: cssLoaders() },
159
+ ...(sass
160
+ ? [
161
+ {
162
+ test: /\.s[ac]ss$/,
163
+ use: [
164
+ ...cssLoaders({ importLoaders: 2, modules: { auto: true, ...cssModules } }),
165
+ { loader: require.resolve('sass-loader'), options: { implementation: sass } },
166
+ ],
167
+ },
168
+ ]
169
+ : []),
170
+ ...(less
171
+ ? [
172
+ {
173
+ test: /\.less$/,
174
+ use: [
175
+ ...cssLoaders({ importLoaders: 2, modules: { auto: true, ...cssModules } }),
176
+ { loader: require.resolve('less-loader'), options: { implementation: less } },
177
+ ],
178
+ },
179
+ ]
180
+ : []),
166
181
  ];
167
182
 
168
- if (typescript) {
169
- babelPresets.push([
170
- babelPresetTypescript,
171
- {
172
- allowNamespaces: true,
173
- onlyRemoveTypeImports: true,
174
- },
175
- ]);
176
- }
183
+ const name = path.basename(outputFile).replace(/\.js$/, '');
177
184
 
178
- return {
179
- input: {
180
- onwarn(warning, warn) {
181
- // Supress "use client" warnings coming from MUI bundling
182
- if (warning.code === 'MODULE_LEVEL_DIRECTIVE' && warning.message.includes(`"use client"`)) {
183
- return;
184
- }
185
- // https://github.com/d3/d3-interpolate/issues/58
186
- if (/Circular dependency.*d3-*/.test(warning.message)) {
187
- return;
188
- }
189
- warn(warning);
185
+ const lib = {
186
+ // rslib has no system format, the iife of it is the System.register of rspack below
187
+ format: isEsm ? 'esm' : 'iife',
188
+ bundle: true,
189
+ syntax: browsersList,
190
+ banner: { js: banner },
191
+ source: {
192
+ entry: {
193
+ [singleFile ? name : 'index']: systemChunks ? [path.resolve(dirname, 'system-public-path.js'), entry] : entry,
190
194
  },
191
- input: path.resolve(CWD, 'src/index'),
192
- external,
193
- plugins: [
194
- resolveNative({ reactNative }),
195
- replace({
196
- 'process.env.NODE_ENV': JSON.stringify(mode === 'development' ? 'development' : 'production'),
197
- preventAssignment: true,
198
- ...resolveReplacementStrings(replacementStrings),
199
- }),
200
- nodeResolve({
201
- extensions,
202
- browser,
203
- preferBuiltins,
204
- }),
205
- postcss({
206
- exclude: /\.module\.css$/,
207
- }),
208
- // Handle all CSS with conditional modules processing
209
- postcss({
210
- include: /\.module\.css$/,
211
- modules: true,
212
- extract: false,
213
- }),
214
- commonjs({
215
- ignoreTryCatch: false, // Avoids problems with require() inside try catch (https://github.com/rollup/plugins/issues/1004)
216
- }),
217
- json(),
218
-
219
- babel({
220
- babelHelpers: 'bundled',
221
- babelrc: false,
222
- inputSourceMap: sourcemap,
223
- extensions,
224
- overrides: babelOverrides,
225
- presets: babelPresets,
226
- }),
227
- [
228
- mode === 'production'
229
- ? terser({
230
- output: {
231
- preamble: banner,
232
- },
233
- })
234
- : false,
235
- ],
236
- [
237
- mode === 'development'
238
- ? visualizer({
239
- filename: 'bundle-analysis.html',
240
- })
241
- : false,
195
+ define: {
196
+ 'process.env.NODE_ENV': JSON.stringify(development ? 'development' : 'production'),
197
+ ...resolveReplacementStrings(replacementStrings),
198
+ },
199
+ // the dependencies are transpiled for the browsers as the code of the visualization is
200
+ include: [/node_modules/],
201
+ },
202
+ output: {
203
+ target: 'web',
204
+ distPath: { root: outputDir },
205
+ filename: { js: singleFile ? path.basename(outputFile) : '[name].js' },
206
+ minify: !development,
207
+ sourceMap: sourcemap ? { js: 'source-map' } : false,
208
+ externals: matchPeers(external),
209
+ // the notices of the licenses go to <bundle>.LICENSE.txt
210
+ legalComments: 'linked',
211
+ cleanDistPath: false,
212
+ },
213
+ tools: {
214
+ swc: {
215
+ jsc: { transform: { react: { runtime: 'automatic', development } } },
216
+ },
217
+ // the stylesheets are handled below, with what they were handled with
218
+ bundlerChain: (chain) => {
219
+ ['css', 'sass', 'less'].forEach((rule) => chain.module.rules.delete(rule));
220
+ },
221
+ rspack: (base) => ({
222
+ ...base,
223
+ context: CWD,
224
+ output: {
225
+ ...base.output,
226
+ chunkFilename: '[name]-[contenthash].js',
227
+ // the url of a chunk of a code split systemjs bundle is relative to the module, see system-public-path.js
228
+ ...(systemChunks ? { publicPath: '' } : {}),
229
+ ...(isEsm ? {} : { library: { type: 'system' } }),
230
+ // an iife has no chunks in rslib, a code split systemjs bundle loads its chunks with script tags
231
+ ...(systemChunks ? { asyncChunks: true, chunkLoading: 'jsonp', chunkFormat: 'array-push' } : {}),
232
+ },
233
+ // the modules of the host are System.register dependencies, and `import()` of one stays a dynamic import
234
+ externalsType: isEsm ? 'module-import' : 'system',
235
+ resolve: {
236
+ ...base.resolve,
237
+ extensions: reactNative ? extensions.flatMap((e) => [`.native${e}`, e]) : extensions,
238
+ alias: { ...base.resolve?.alias, ...builtins.alias },
239
+ mainFields: browser ? ['browser', 'module', 'main'] : ['module', 'main'],
240
+ fallback: builtins.fallback,
241
+ ...(browser ? {} : { conditionNames: ['module', 'import', 'require', 'default'] }),
242
+ },
243
+ module: {
244
+ ...base.module,
245
+ rules: [
246
+ ...(base.module?.rules || []),
247
+ // packages that ship esm without file extensions in their imports
248
+ { test: /\.m?js$/, resolve: { fullySpecified: false } },
249
+ ...styleRules,
250
+ ],
251
+ },
252
+ optimization: {
253
+ ...base.optimization,
254
+ // rollup tree-shakes in development too, rspack's development mode would not
255
+ usedExports: true,
256
+ sideEffects: true,
257
+ providedExports: true,
258
+ innerGraph: true,
259
+ concatenateModules: true,
260
+ },
261
+ plugins: [
262
+ ...(base.plugins || []),
263
+ // @qlik/api imports the runtime modules for node (qix, invoke-fetch) behind `if (isNode)`, a browser gets
264
+ // them from the host. Rollup left those out because it dropped the function that imports them when nothing
265
+ // calls it, rspack drops the function too but has already bundled what it imports: about 450kb with
266
+ // enigma.js and lodash.
267
+ ...(browser
268
+ ? [
269
+ new rspack.IgnorePlugin({
270
+ resourceRegExp: /^\.\/(?:qix|invoke-fetch)-[\w-]+\.js$/,
271
+ contextRegExp: /@qlik[\\/]api[\\/]chunks$/,
272
+ }),
273
+ ]
274
+ : []),
275
+ ...(singleFile ? [new rspack.optimize.LimitChunkCountPlugin({ maxChunks: 1 })] : []),
242
276
  ],
243
- ].filter(Boolean),
277
+ }),
278
+ },
279
+ };
280
+
281
+ return {
282
+ cwd: CWD,
283
+ config: {
284
+ mode: development ? 'development' : 'production',
285
+ lib: [lib],
244
286
  },
245
- output: output(),
246
287
  };
247
288
  };
248
289
 
@@ -17,7 +17,7 @@ const options = {
17
17
  watch: {
18
18
  description: 'Rebuild the bundle when its source files change on disk',
19
19
  alias: 'w',
20
- choices: ['umd', 'systemjs', true],
20
+ choices: ['esm', 'systemjs', true],
21
21
  },
22
22
  sourcemap: {
23
23
  description: 'Generate source maps',
@@ -45,40 +45,39 @@ const options = {
45
45
  type: 'boolean',
46
46
  default: false,
47
47
  },
48
- preferBuiltins: {
49
- description:
50
- 'In Rollup, preferBuiltins is a configuration option that specifies whether to use Node.js built-in modules (such as fs or path) when bundling for Node.js environment.',
51
- type: 'boolean',
52
- default: true,
53
- },
54
48
  browser: {
55
49
  description:
56
- 'In Rollup, the browser option is a configuration option that specifies whether the bundle is intended to run in a browser environment.',
50
+ "Use the 'browser' field and export condition of the packages in the bundle, so that browser specific files and conditionals in exports are applied.",
57
51
  type: 'boolean',
58
52
  default: true,
59
53
  },
60
54
  codeSplit: {
61
55
  description:
62
- 'Sets output.dir instead of output.file for code-splitting builds. Sets inlineDynamicImports for umd builds to avoid throwing errors.',
56
+ 'Write the esm and systemjs builds to a directory with the entry and a chunk for every dynamic import, instead of one file.',
63
57
  type: 'boolean',
64
58
  default: false,
65
59
  },
60
+ // not in the help, they are for the other commands, for nebula.config.js and for react native builds
61
+ cwd: { type: 'string', hidden: true },
62
+ replacementStrings: { hidden: true },
63
+ reactNative: { type: 'boolean', hidden: true },
64
+ reactNativePath: { type: 'string', hidden: true },
66
65
  inlineDynamic: {
67
- description: 'Sets inlineDynamicImports to true for all builds to help with certain dependencies',
66
+ description: 'Inline dynamic imports into the bundle in all builds, also when codeSplit is set',
68
67
  type: 'boolean',
69
68
  default: false,
70
69
  },
71
70
  };
72
71
 
73
- // nebula build --watch - watch umd bundle
74
- // nebula build --watch umd - watch umd bundle
72
+ // nebula build --watch - watch esm bundle
73
+ // nebula build --watch esm - watch esm bundle
75
74
  // nebula build --watch systemjs - watch systemjs bundle
76
75
  // nebula build - watch disabled
77
76
  const watchMiddleware = (argv) => {
78
77
  let value = false;
79
78
 
80
79
  if (argv.watch === true) {
81
- value = 'umd';
80
+ value = 'esm';
82
81
  } else if (typeof argv.watch === 'string') {
83
82
  value = argv.watch;
84
83
  }
@@ -0,0 +1,25 @@
1
+ // Puts the css that css-loader produced into a <style> element in the document head when the bundle is loaded.
2
+ //
3
+ // This is what rollup-plugin-postcss did (it inlined the css into the bundle with a tiny helper), kept on
4
+ // purpose instead of style-loader:
5
+ // - nothing happens when there is no document, so a bundle can still be required in node (tests, ssr)
6
+ // - no extra runtime modules in the bundle
7
+ //
8
+ // The module's default export is the class name map for css modules (`import styles from './a.module.css'`)
9
+ // and the css text for plain css, as it was with rollup-plugin-postcss.
10
+
11
+ module.exports = function injectStyleLoader() {};
12
+
13
+ module.exports.pitch = function pitch(remainingRequest) {
14
+ const request = JSON.stringify(this.utils.contextify(this.context, `!!${remainingRequest}`));
15
+ return `
16
+ import content from ${request};
17
+ var css = String(content);
18
+ if (css && typeof document !== 'undefined') {
19
+ var style = document.createElement('style');
20
+ style.appendChild(document.createTextNode(css));
21
+ document.head.appendChild(style);
22
+ }
23
+ export default content && content.locals ? content.locals : css;
24
+ `;
25
+ };
package/lib/peers.js ADDED
@@ -0,0 +1,8 @@
1
+ const escape = (name) => name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
2
+
3
+ /**
4
+ * What a bundle leaves out of itself: a package, and the modules of it, as `react` and `react-dom/client`. A
5
+ * regular expression is kept as it is.
6
+ */
7
+ export default (externals) =>
8
+ externals.map((external) => (typeof external === 'string' ? new RegExp(`^${escape(external)}(?:/.*)?$`) : external));
@@ -0,0 +1,9 @@
1
+ // First module of a code split systemjs bundle: where the chunks are loaded from.
2
+ //
3
+ // rspack loads a chunk with a script tag and takes the url of the directory from document.currentScript, which is
4
+ // gone by the time a System.register module executes, so the chunks would be requested from the wrong place. A
5
+ // System.register module is told its own url, rollup's systemjs output imported its chunks relative to it, and so
6
+ // do we.
7
+ if (typeof __system_context__ !== 'undefined' && __system_context__ && __system_context__.meta) {
8
+ __webpack_public_path__ = __system_context__.meta.url.replace(/[^/]*$/, '');
9
+ }
package/lib/systemjs.js CHANGED
@@ -13,7 +13,6 @@ const systemjsBehaviours = {
13
13
  return Array.isArray(external) ? external : defaultExternal;
14
14
  },
15
15
  getOutputFile: ({ pkg }) => pkg.systemjs,
16
- getOutputName: () => undefined,
17
16
  enabled: ({ pkg }) => !!pkg.systemjs,
18
17
  };
19
18
 
package/lib/watch.js CHANGED
@@ -1,28 +1,27 @@
1
- /* eslint-disable no-console, import/extensions */
2
1
  import path from 'path';
3
2
  import readline from 'readline';
3
+ import { EventEmitter } from 'events';
4
4
  import { createRequire } from 'module';
5
5
  import chalk from 'chalk';
6
- import * as rollup from 'rollup';
7
6
 
8
7
  import config from './config.js';
9
8
  import systemjsBehaviours from './systemjs.js';
9
+ import { create, formatErrors, formatWarnings } from './compile.js';
10
10
 
11
11
  const require = createRequire(import.meta.url);
12
12
 
13
13
  const getPackage = (argv, cwd = process.cwd()) => require(path.resolve(argv.cwd || cwd, 'package.json'));
14
14
 
15
+ // the field of package.json that has the output file of a format
16
+ const OUTPUT_FIELDS = { systemjs: 'systemjs', esm: 'module' };
17
+
15
18
  const validateWatchInput = (argv) => {
16
- if (argv.watch === 'systemjs') {
17
- const pkg = getPackage(argv);
18
- if (!pkg.systemjs) {
19
- console.log(
20
- `${chalk.white.bgRed(' ERROR ')} ${chalk.red(
21
- 'No "systemjs" field specifying output file found in package.json'
22
- )}`
23
- );
24
- return false;
25
- }
19
+ const field = OUTPUT_FIELDS[argv.watch];
20
+ if (field && !getPackage(argv)[field]) {
21
+ console.log(
22
+ `${chalk.white.bgRed(' ERROR ')} ${chalk.red(`No "${field}" field specifying output file found in package.json`)}`
23
+ );
24
+ return false;
26
25
  }
27
26
  return true;
28
27
  };
@@ -39,10 +38,19 @@ const getWatchConfig = (argv) => {
39
38
  case 'systemjs':
40
39
  opts = { ...base, format: 'systemjs', behaviours: systemjsBehaviours };
41
40
  break;
42
- case 'umd':
43
- default:
44
- opts = { ...base, format: 'umd' };
41
+ case 'esm':
42
+ opts = {
43
+ ...base,
44
+ format: 'esm',
45
+ behaviours: {
46
+ getOutputFile: ({ pkg }) => pkg.module,
47
+ // the modules that the host of the bundle has, the peer dependencies of the package when it is not told
48
+ getExternal: ({ pkg, config: cfg }) => cfg.external || Object.keys(pkg.peerDependencies || {}),
49
+ },
50
+ };
45
51
  break;
52
+ default:
53
+ throw new Error(`Cannot watch '${argv.watch}', watch esm or systemjs`);
46
54
  }
47
55
  return config(opts);
48
56
  };
@@ -60,65 +68,85 @@ function clearScreen(msg) {
60
68
  }
61
69
  }
62
70
 
71
+ /**
72
+ * Watch the sources and rebuild on change.
73
+ *
74
+ * Resolves with a watcher after the first successful build. The watcher has the surface the rollup
75
+ * watcher had, which `nebula serve` depends on: `close()` and `on('event', fn)` with events shaped
76
+ * `{ code: 'BUNDLE_START' | 'BUNDLE_END' | 'ERROR', ... }`.
77
+ */
63
78
  const watch = async (argv) => {
64
79
  if (!validateWatchInput(argv)) {
65
80
  return undefined;
66
81
  }
67
82
  const c = getWatchConfig(argv);
68
83
 
69
- let hasWarnings = false;
84
+ const events = new EventEmitter();
85
+ let closeWatch;
70
86
 
71
- const watcher = rollup.watch({
72
- ...c.input,
73
- onwarn({ loc, message, code }) {
74
- if (!hasWarnings) {
75
- clearScreen();
87
+ const watcher = {
88
+ on: (name, listener) => {
89
+ events.on(name, listener);
90
+ return watcher;
91
+ },
92
+ close: async () => {
93
+ if (closeWatch) {
94
+ await closeWatch();
76
95
  }
96
+ },
97
+ };
77
98
 
78
- // Supress "use client" warnings coming from MUI bundling
79
- if (code === 'MODULE_LEVEL_DIRECTIVE' && message.includes(`"use client"`)) {
80
- return;
99
+ return new Promise((resolve, reject) => {
100
+ let settled = false;
101
+ const settle = (fn, value) => {
102
+ if (!settled) {
103
+ settled = true;
104
+ fn(value);
81
105
  }
106
+ };
82
107
 
83
- console.log(
84
- `${chalk.black.bgYellow(' WARN ')} ${chalk.yellow(
85
- loc ? `${loc.file} (${loc.line}:${loc.column}) ${message}` : message
86
- )}`
87
- );
88
- hasWarnings = true;
89
- },
90
- output: c.output,
91
- });
108
+ const rslibPromise = create(c, (api) => {
109
+ api.onBeforeBuild(() => {
110
+ clearScreen();
111
+ console.log(`${chalk.black.bgBlue(' INFO ')} Compiling...\n`);
112
+ events.emit('event', { code: 'BUNDLE_START' });
113
+ });
92
114
 
93
- return new Promise((resolve, reject) => {
94
- watcher.on('event', (event) => {
95
- switch (event.code) {
96
- case 'BUNDLE_START':
97
- hasWarnings = false;
98
- clearScreen();
99
- console.log(`${chalk.black.bgBlue(' INFO ')} Compiling...\n`);
100
- break;
101
- case 'FATAL':
102
- case 'ERROR':
115
+ api.onAfterBuild(({ stats }) => {
116
+ if (stats.hasErrors()) {
117
+ const failure = new Error(formatErrors(stats));
103
118
  clearScreen();
104
119
  console.log(`${chalk.white.bgRed(' ERROR ')} ${chalk.red('Failed to compile\n\n')}`);
105
- console.error(event.error.stack);
106
- reject(watcher);
107
- break;
108
- case 'BUNDLE_END':
109
- if (!hasWarnings) {
110
- clearScreen();
111
- } else {
112
- console.log();
113
- }
114
- console.log(
115
- `${chalk.black.bgGreen(' DONE ')} ${chalk.green(`Compiled successfully in ${event.duration}ms\n`)}`
116
- );
117
- resolve(watcher);
118
- break;
119
- default:
120
- }
120
+ console.error(failure.stack);
121
+ events.emit('event', { code: 'ERROR', error: failure });
122
+ settle(reject, watcher);
123
+ return;
124
+ }
125
+
126
+ if (stats.hasWarnings()) {
127
+ console.log(`${chalk.black.bgYellow(' WARN ')} ${chalk.yellow(formatWarnings(stats))}`);
128
+ console.log();
129
+ } else {
130
+ clearScreen();
131
+ }
132
+ const duration = stats.endTime - stats.startTime;
133
+ console.log(`${chalk.black.bgGreen(' DONE ')} ${chalk.green(`Compiled successfully in ${duration}ms\n`)}`);
134
+ events.emit('event', { code: 'BUNDLE_END', duration });
135
+ settle(resolve, watcher);
136
+ });
121
137
  });
138
+
139
+ rslibPromise
140
+ .then((rslib) => rslib.build({ watch: true }))
141
+ .then((result) => {
142
+ closeWatch = result && result.close;
143
+ })
144
+ .catch((e) => {
145
+ // a failed compile is reported by the hook above
146
+ if (!settled) {
147
+ settle(reject, e);
148
+ }
149
+ });
122
150
  });
123
151
  };
124
152
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nebula.js/cli-build",
3
- "version": "7.8.0",
3
+ "version": "8.0.0-alpha.0",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -18,38 +18,26 @@
18
18
  "lib",
19
19
  "command.js"
20
20
  ],
21
- "scripts": {
22
- "lint": "eslint src"
23
- },
24
21
  "dependencies": {
25
- "@babel/cli": "8.0.6",
26
- "@babel/core": "8.0.7",
27
- "@babel/helper-plugin-utils": "8.0.1",
28
- "@babel/plugin-proposal-object-rest-spread": "7.20.7",
29
- "@babel/plugin-transform-react-jsx": "8.0.1",
30
- "@babel/preset-env": "8.0.7",
31
- "@babel/preset-react": "8.0.1",
32
- "@babel/preset-typescript": "8.0.7",
33
22
  "@qlik/browserslist-config": "^3.1.0",
34
- "@rollup/plugin-babel": "7.1.0",
35
- "@rollup/plugin-commonjs": "29.0.3",
36
- "@rollup/plugin-json": "6.1.0",
37
- "@rollup/plugin-node-resolve": "16.0.3",
38
- "@rollup/plugin-replace": "6.0.3",
39
- "@rollup/plugin-terser": "1.0.0",
23
+ "@rslib/core": "1.0.5",
40
24
  "chalk": "6.0.1",
25
+ "css-loader": "7.1.5",
41
26
  "extend": "3.0.2",
42
- "postcss": "^8.5.29",
43
- "rollup": "4.64.5",
44
- "rollup-plugin-postcss": "4.0.2",
45
- "rollup-plugin-sass": "1.15.3",
46
- "rollup-plugin-visualizer": "7.1.1",
27
+ "less-loader": "13.0.0",
28
+ "postcss": "^8.5.32",
29
+ "postcss-loader": "8.2.1",
30
+ "sass-loader": "17.0.1",
47
31
  "yargs": "18.2.0"
48
32
  },
49
33
  "devDependencies": {
50
- "@rollup/plugin-typescript": "12.3.0",
34
+ "less": "4.9.1",
35
+ "sass": "1.106.1",
51
36
  "tslib": "*",
52
37
  "typescript": "^5.9.3"
53
38
  },
54
- "gitHead": "870f985afe1849f7a695b34d6f9aebc8e811682d"
55
- }
39
+ "scripts": {
40
+ "lint": "eslint src",
41
+ "test": "node --test"
42
+ }
43
+ }
@@ -1,29 +0,0 @@
1
- /* eslint-disable no-console */
2
- import path from 'path';
3
- import fs from 'fs';
4
- // this plugin will check if there is a corresponding .native file for the input file and output the contents of the native file
5
- // This plugin should be loaded first.
6
-
7
- export default ({ reactNative }) => ({
8
- async load(id) {
9
- if (!reactNative) {
10
- return null;
11
- }
12
- if (id) {
13
- const parsed = path.parse(id);
14
- if (parsed) {
15
- if (parsed.name.length > 0) {
16
- const nativeTest = `${parsed.dir}/${parsed.name}.native${parsed.ext}`;
17
- try {
18
- if (fs.existsSync(nativeTest)) {
19
- return fs.readFileSync(nativeTest, { encoding: 'utf8', flag: 'r' });
20
- }
21
- } catch (err) {
22
- console.log('there was a problem reading the file', nativeTest, err);
23
- }
24
- }
25
- }
26
- }
27
- return null;
28
- },
29
- });