-
-
Notifications
You must be signed in to change notification settings - Fork 108
/
npm_translate_lock.bzl
834 lines (651 loc) · 36.5 KB
/
npm_translate_lock.bzl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
"""Repository rule to fetch npm packages for a lockfile.
Load this with,
```starlark
load("@aspect_rules_js//npm:repositories.bzl", "npm_translate_lock")
```
These use Bazel's downloader to fetch the packages.
You can use this to redirect all fetches through a store like Artifactory.
See <https://blog.aspect.build/configuring-bazels-downloader> for more info about how it works
and how to configure it.
[`npm_translate_lock`](#npm_translate_lock) is the primary user-facing API.
It uses the lockfile format from [pnpm](https://pnpm.io/motivation) because it gives us reliable
semantics for how to dynamically lay out `node_modules` trees on disk in bazel-out.
To create `pnpm-lock.yaml`, consider using [`pnpm import`](https://pnpm.io/cli/import)
to preserve the versions pinned by your existing `package-lock.json` or `yarn.lock` file.
If you don't have an existing lock file, you can run `npx pnpm install --lockfile-only`.
Advanced users may want to directly fetch a package from npm rather than start from a lockfile,
[`npm_import`](./npm_import) does this.
"""
load("@aspect_bazel_lib//lib:utils.bzl", bazel_lib_utils = "utils")
load("@aspect_bazel_lib//lib:write_source_files.bzl", "write_source_file")
load("@bazel_skylib//lib:paths.bzl", "paths")
load(":list_sources.bzl", "list_sources")
load(":npm_translate_lock_generate.bzl", "generate_repository_files")
load(":npm_translate_lock_helpers.bzl", "helpers")
load(":npm_translate_lock_macro_helpers.bzl", macro_helpers = "helpers")
load(":npm_translate_lock_state.bzl", "DEFAULT_ROOT_PACKAGE", "npm_translate_lock_state")
load(":pnpm_repository.bzl", "DEFAULT_PNPM_VERSION", _pnpm_repository = "pnpm_repository")
load(":transitive_closure.bzl", "translate_to_transitive_closure")
load(":utils.bzl", "utils")
RULES_JS_FROZEN_PNPM_LOCK_ENV = "ASPECT_RULES_JS_FROZEN_PNPM_LOCK"
################################################################################
DEFAULT_REPOSITORIES_BZL_FILENAME = "repositories.bzl"
DEFAULT_DEFS_BZL_FILENAME = "defs.bzl"
_ATTRS = {
"additional_file_contents": attr.string_list_dict(),
"bins": attr.string_list_dict(),
"bzlmod": attr.bool(),
"custom_postinstalls": attr.string_dict(),
"data": attr.label_list(),
"defs_bzl_filename": attr.string(default = DEFAULT_DEFS_BZL_FILENAME),
"dev": attr.bool(),
"external_repository_action_cache": attr.string(default = utils.default_external_repository_action_cache()),
"generate_bzl_library_targets": attr.bool(),
"replace_packages": attr.string_dict(),
"lifecycle_hooks_envs": attr.string_list_dict(),
"lifecycle_hooks_execution_requirements": attr.string_list_dict(),
"lifecycle_hooks_use_default_shell_env": attr.string_dict(),
"lifecycle_hooks": attr.string_list_dict(),
"link_workspace": attr.string(),
"no_optional": attr.bool(),
"node_toolchain_prefix": attr.string(default = "nodejs"),
"npm_package_lock": attr.label(),
"npm_package_target_name": attr.string(),
"npmrc": attr.label(),
"package_visibility": attr.string_list_dict(),
"patch_args": attr.string_list_dict(),
"patches": attr.string_list_dict(),
"use_pnpm": attr.label(default = "@pnpm//:package/bin/pnpm.cjs"), # bzlmod pnpm extension
"pnpm_lock": attr.label(),
"preupdate": attr.label_list(),
"prod": attr.bool(),
"public_hoist_packages": attr.string_list_dict(),
"quiet": attr.bool(default = True),
"repositories_bzl_filename": attr.string(default = DEFAULT_REPOSITORIES_BZL_FILENAME),
"root_package": attr.string(default = DEFAULT_ROOT_PACKAGE),
"update_pnpm_lock": attr.bool(),
"use_home_npmrc": attr.bool(),
"verify_node_modules_ignored": attr.label(),
"verify_patches": attr.label(),
"yarn_lock": attr.label(),
"yq_toolchain_prefix": attr.string(default = "yq"),
}
npm_translate_lock_lib = struct(
attrs = _ATTRS,
)
################################################################################
def _npm_translate_lock_impl(rctx):
rctx.report_progress("Initializing")
state = npm_translate_lock_state.new(rctx.name, rctx, rctx.attr, rctx.attr.bzlmod)
# If a pnpm lock file has not been specified then we need to bootstrap by running `pnpm
# import` in the user's repository
if not rctx.attr.pnpm_lock:
_bootstrap_import(rctx, state)
if state.should_update_pnpm_lock():
# Run `pnpm install --lockfile-only` or `pnpm import` if its inputs have changed since last update
if state.action_cache_miss():
_fail_if_frozen_pnpm_lock(rctx, state)
if _update_pnpm_lock(rctx, state):
msg = """
INFO: {} file updated. Please run your build again.
See https://github.com/aspect-build/rules_js/issues/1445
""".format(state.label_store.relative_path("pnpm_lock"))
fail(msg)
helpers.verify_node_modules_ignored(rctx, state.importers(), state.root_package())
helpers.verify_patches(rctx, state)
helpers.verify_lifecycle_hooks_specified(rctx, state)
rctx.report_progress("Translating {}".format(state.label_store.relative_path("pnpm_lock")))
importers, packages = translate_to_transitive_closure(
state.importers(),
state.packages(),
rctx.attr.prod,
rctx.attr.dev,
rctx.attr.no_optional,
)
rctx.report_progress("Generating starlark for npm dependencies")
generate_repository_files(
rctx,
state.label_store.label("pnpm_lock"),
importers,
packages,
state.patched_dependencies(),
state.only_built_dependencies(),
state.root_package(),
state.default_registry(),
state.npm_registries(),
state.npm_auth(),
state.link_workspace(),
)
npm_translate_lock_rule = repository_rule(
implementation = _npm_translate_lock_impl,
attrs = _ATTRS,
)
def npm_translate_lock(
name,
pnpm_lock = None,
npm_package_lock = None,
yarn_lock = None,
update_pnpm_lock = False,
node_toolchain_prefix = "nodejs",
yq_toolchain_prefix = "yq",
preupdate = [],
npmrc = None,
use_home_npmrc = None,
data = [],
patches = {},
patch_args = {"*": ["-p0"]},
custom_postinstalls = {},
package_visibility = {},
prod = False,
public_hoist_packages = {},
dev = False,
no_optional = False,
run_lifecycle_hooks = True,
lifecycle_hooks = {},
lifecycle_hooks_envs = {},
lifecycle_hooks_exclude = [],
lifecycle_hooks_execution_requirements = {},
lifecycle_hooks_no_sandbox = True,
lifecycle_hooks_use_default_shell_env = {},
replace_packages = {},
bins = {},
verify_node_modules_ignored = None,
verify_patches = None,
quiet = True,
external_repository_action_cache = utils.default_external_repository_action_cache(),
link_workspace = None,
pnpm_version = DEFAULT_PNPM_VERSION,
use_pnpm = None,
npm_package_target_name = "pkg",
**kwargs):
"""Repository macro to generate starlark code from a lock file.
In most repositories, it would be an impossible maintenance burden to manually declare all
of the [`npm_import`](./npm_import) rules. This helper generates an external repository
containing a helper starlark module `repositories.bzl`, which supplies a loadable macro
`npm_repositories`. That macro creates an `npm_import` for each package.
The generated repository also contains:
- A `defs.bzl` file containing some rules such as `npm_link_all_packages`, which are [documented here](./npm_link_all_packages.md).
- `BUILD` files declaring targets for the packages listed as `dependencies` or `devDependencies` in `package.json`,
so you can declare dependencies on those packages without having to repeat version information.
This macro creates a `pnpm` external repository, if the user didn't create a repository named
"pnpm" prior to calling `npm_translate_lock`.
`rules_js` currently only uses this repository when `npm_package_lock` or `yarn_lock` are used.
Set `pnpm_version` to `None` to inhibit this repository creation.
For more about how to use npm_translate_lock, read [pnpm and rules_js](/docs/pnpm.md).
Args:
name: The repository rule name
pnpm_lock: The `pnpm-lock.yaml` file.
npm_package_lock: The `package-lock.json` file written by `npm install`.
Only one of `npm_package_lock` or `yarn_lock` may be set.
yarn_lock: The `yarn.lock` file written by `yarn install`.
Only one of `npm_package_lock` or `yarn_lock` may be set.
update_pnpm_lock: When True, the pnpm lock file will be updated automatically when any of its inputs
have changed since the last update.
Defaults to True when one of `npm_package_lock` or `yarn_lock` are set.
Otherwise it defaults to False.
Read more: [using update_pnpm_lock](/docs/pnpm.md#update_pnpm_lock)
node_toolchain_prefix: the prefix of the node toolchain to use when generating the pnpm lockfile.
yq_toolchain_prefix: the prefix of the yq toolchain to use for parsing the pnpm lockfile.
preupdate: Node.js scripts to run in this repository rule before auto-updating the pnpm lock file.
Scripts are run sequentially in the order they are listed. The working directory is set to the root of the
external repository. Make sure all files required by preupdate scripts are added to the `data` attribute.
A preupdate script could, for example, transform `resolutions` in the root `package.json` file from a format
that yarn understands such as `@foo/**/bar` to the equivalent `@foo/*>bar` that pnpm understands so that
`resolutions` are compatible with pnpm when running `pnpm import` to update the pnpm lock file.
Only needed when `update_pnpm_lock` is True.
Read more: [using update_pnpm_lock](/docs/pnpm.md#update_pnpm_lock)
npmrc: The `.npmrc` file, if any, to use.
When set, the `.npmrc` file specified is parsed and npm auth tokens and basic authentication configuration
specified in the file are passed to the Bazel downloader for authentication with private npm registries.
In a future release, pnpm settings such as public-hoist-patterns will be used.
use_home_npmrc: Use the `$HOME/.npmrc` file (or `$USERPROFILE/.npmrc` when on Windows) if it exists.
Settings from home `.npmrc` are merged with settings loaded from the `.npmrc` file specified
in the `npmrc` attribute, if any. Where there are conflicting settings, the home `.npmrc` values
will take precedence.
WARNING: The repository rule will not be invalidated by changes to the home `.npmrc` file since there
is no way to specify this file as an input to the repository rule. If changes are made to the home
`.npmrc` you can force the repository rule to re-run and pick up the changes by running:
`bazel run @{name}//:sync` where `name` is the name of the `npm_translate_lock` you want to re-run.
Because of the repository rule invalidation issue, using the home `.npmrc` is not recommended.
`.npmrc` settings should generally go in the `npmrc` in your repository so they are shared by all
developers. The home `.npmrc` should be reserved for authentication settings for private npm repositories.
data: Data files required by this repository rule when auto-updating the pnpm lock file.
Only needed when `update_pnpm_lock` is True.
Read more: [using update_pnpm_lock](/docs/pnpm.md#update_pnpm_lock)
patches: A map of package names or package names with their version (e.g., "my-package" or "my-package@v1.2.3")
to a label list of patches to apply to the downloaded npm package. Multiple matches are additive.
These patches are applied after any patches in [pnpm.patchedDependencies](https://pnpm.io/next/package_json#pnpmpatcheddependencies).
Read more: [patching](/docs/pnpm.md#patching)
patch_args: A map of package names or package names with their version (e.g., "my-package" or "my-package@v1.2.3")
to a label list arguments to pass to the patch tool. The most specific match wins.
Read more: [patching](/docs/pnpm.md#patching)
custom_postinstalls: A map of package names or package names with their version (e.g., "my-package" or "my-package@v1.2.3")
to a custom postinstall script to apply to the downloaded npm package after its lifecycle scripts runs.
If the version is left out of the package name, the script will run on every version of the npm package. If
a custom postinstall scripts exists for a package as well as for a specific version, the script for the versioned package
will be appended with `&&` to the non-versioned package script.
For example,
```
custom_postinstalls = {
"@foo/bar": "echo something > somewhere.txt",
"fum@0.0.1": "echo something_else > somewhere_else.txt",
},
```
Custom postinstalls are additive and joined with ` && ` when there are multiple matches for a package.
More specific matches are appended to previous matches.
package_visibility: A map of package names or package names with their version (e.g., "my-package" or "my-package@v1.2.3")
to a visibility list to use for the package's generated node_modules link targets. Multiple matches are additive.
If there are no matches then the package's generated node_modules link targets default to public visibility
(`["//visibility:public"]`).
prod: If True, only install `dependencies` but not `devDependencies`.
public_hoist_packages: A map of package names or package names with their version (e.g., "my-package" or "my-package@v1.2.3")
to a list of Bazel packages in which to hoist the package to the top-level of the node_modules tree when linking.
This is similar to setting https://pnpm.io/npmrc#public-hoist-pattern in an .npmrc file outside of Bazel, however,
wild-cards are not yet supported and npm_translate_lock will fail if there are multiple versions of a package that
are to be hoisted.
```
public_hoist_packages = {
"@foo/bar": [""] # link to the root package in the WORKSPACE
"fum@0.0.1": ["some/sub/package"]
},
```
List of public hoist packages are additive when there are multiple matches for a package. More specific matches
are appended to previous matches.
dev: If True, only install `devDependencies`
no_optional: If True, `optionalDependencies` are not installed.
Currently `npm_translate_lock` behaves differently from pnpm in that is downloads all `optionaDependencies`
while pnpm doesn't download `optionalDependencies` that are not needed for the platform pnpm is run on.
See https://github.com/pnpm/pnpm/pull/3672 for more context.
run_lifecycle_hooks: Sets a default value for `lifecycle_hooks` if `*` not already set.
Set this to `False` to disable lifecycle hooks.
lifecycle_hooks: A dict of package names to list of lifecycle hooks to run for that package.
By default the `preinstall`, `install` and `postinstall` hooks are run if they exist. This attribute allows
the default to be overridden for packages to run `prepare`.
List of hooks are not additive. The most specific match wins.
Read more: [lifecycles](/docs/pnpm.md#lifecycles)
lifecycle_hooks_exclude: A list of package names or package names with their version (e.g., "my-package" or "my-package@v1.2.3")
to not run any lifecycle hooks on.
Equivalent to adding `<value>: []` to `lifecycle_hooks`.
Read more: [lifecycles](/docs/pnpm.md#lifecycles)
lifecycle_hooks_envs: Environment variables set for the lifecycle hooks actions on npm packages.
The environment variables can be defined per package by package name or globally using "*".
Variables are declared as key/value pairs of the form "key=value".
Multiple matches are additive.
Read more: [lifecycles](/docs/pnpm.md#lifecycles)
lifecycle_hooks_execution_requirements: Execution requirements applied to the preinstall, install and postinstall
lifecycle hooks on npm packages.
The execution requirements can be defined per package by package name or globally using "*".
Execution requirements are not additive. The most specific match wins.
Read more: [lifecycles](/docs/pnpm.md#lifecycles)
lifecycle_hooks_no_sandbox: If True, a "no-sandbox" execution requirement is added to all lifecycle hooks
unless overridden by `lifecycle_hooks_execution_requirements`.
Equivalent to adding `"*": ["no-sandbox"]` to `lifecycle_hooks_execution_requirements`.
This defaults to True to limit the overhead of sandbox creation and copying the output
TreeArtifacts out of the sandbox.
Read more: [lifecycles](/docs/pnpm.md#lifecycles)
lifecycle_hooks_use_default_shell_env: The `use_default_shell_env` attribute of the lifecycle hooks
actions on npm packages.
See [use_default_shell_env](https://bazel.build/rules/lib/builtins/actions#run.use_default_shell_env)
Note: [--incompatible_merge_fixed_and_default_shell_env](https://bazel.build/reference/command-line-reference#flag--incompatible_merge_fixed_and_default_shell_env)
is often required and not enabled by default in Bazel < 7.0.0.
This defaults to False reduce the negative effects of `use_default_shell_env`. Requires bazel-lib >= 2.4.2.
Read more: [lifecycles](/docs/pnpm.md#lifecycles)
replace_packages: A dict of package names to npm_package targets to link instead of the sources specified in the pnpm lock file for the corresponding packages.
The injected npm_package targets may optionally contribute transitive npm package dependencies on top
of the transitive dependencies specified in the pnpm lock file for their respective packages, however, these
transitive dependencies must not collide with pnpm lock specified transitive dependencies.
Any patches specified for the packages will be not applied to the injected npm_package targets. They
will be applied, however, to the fetches sources for their respecitve packages so they can still be useful
for patching the fetched `package.json` files, which are used to determine the generated bin entries for packages.
NB: lifecycle hooks and custom_postinstall scripts, if implicitly or explicitly enabled, will be run on
the injected npm_package targets. These may be disabled explicitly using the `lifecycle_hooks` attribute.
bins: Binary files to create in `node_modules/.bin` for packages in this lock file.
For a given package, this is typically derived from the "bin" attribute in
the package.json file of that package.
For example:
```
bins = {
"@foo/bar": {
"foo": "./foo.js",
"bar": "./bar.js"
},
}
```
Dicts of bins not additive. The most specific match wins.
In the future, this field may be automatically populated from information in the pnpm lock
file. That feature is currently blocked on https://github.com/pnpm/pnpm/issues/5131.
Note: Bzlmod users must use an alternative syntax due to module extensions not supporting
dict-of-dict attributes:
```
bins = {
"@foo/bar": [
"foo=./foo.js",
"bar=./bar.js"
],
}
```
verify_node_modules_ignored: node_modules folders in the source tree should be ignored by Bazel.
This points to a `.bazelignore` file to verify that all nested node_modules directories
pnpm will create are listed.
See https://github.com/bazelbuild/bazel/issues/8106
verify_patches: Label to a patch list file.
Use this in together with the `list_patches` macro to guarantee that all patches in a patch folder
are included in the `patches` attribute.
For example:
```
verify_patches = "//patches:patches.list",
```
In your patches folder add a BUILD.bazel file containing.
```
load("@aspect_rules_js//npm:repositories.bzl", "list_patches")
list_patches(
name = "patches",
out = "patches.list",
)
```
Once you have created this file, you need to create an empty `patches.list` file before generating the first list. You can do this by running
```
touch patches/patches.list
```
Finally, write the patches file at least once to make sure all patches are listed. This can be done by running `bazel run //patches:patches_update`.
See the `list_patches` documentation for further info.
NOTE: if you would like to customize the patches directory location, you can set a flag in the `.npmrc`. Here is an example of what this might look like
```
# Set the directory for pnpm when patching
# https://github.com/pnpm/pnpm/issues/6508#issuecomment-1537242124
patches-dir=bazel/js/patches
```
If you do this, you will have to update the `verify_patches` path to be this path instead of `//patches` like above.
quiet: Set to False to print info logs and output stdout & stderr of pnpm lock update actions to the console.
external_repository_action_cache: The location of the external repository action cache to write to when `update_pnpm_lock` = True.
link_workspace: The workspace name where links will be created for the packages in this lock file.
This is typically set in rule sets and libraries that vendor the starlark generated by npm_translate_lock
so the link_workspace passed to npm_import is set correctly so that links are created in the external
repository and not the user workspace.
Can be left unspecified if the link workspace is the user workspace.
pnpm_version: pnpm version to use when generating the @pnpm repository. Set to None to not create this repository.
Can be left unspecified and the rules_js default `DEFAULT_PNPM_VERSION` will be used.
use_pnpm: label of the pnpm entry point to use.
npm_package_target_name: The name of linked `npm_package`, `js_library` or `JsInfo` producing targets.
When targets are linked as pnpm workspace packages, the name of the target must align with this value.
The `{dirname}` placeholder is replaced with the directory name of the target.
**kwargs: Internal use only
"""
if not bazel_lib_utils.is_bazel_6_or_greater():
# ctx.actions.declare_symlink was added in Bazel 6
fail("A minimum version of Bazel 6 required to use rules_js")
# Gather undocumented attributes
root_package = kwargs.pop("root_package", None)
additional_file_contents = kwargs.pop("additional_file_contents", {})
repositories_bzl_filename = kwargs.pop("repositories_bzl_filename", None)
defs_bzl_filename = kwargs.pop("defs_bzl_filename", None)
generate_bzl_library_targets = kwargs.pop("generate_bzl_library_targets", None)
if len(kwargs):
msg = "Invalid npm_translate_lock parameter '{}'".format(kwargs.keys()[0])
fail(msg)
if pnpm_version != None:
_pnpm_repository(name = "pnpm", pnpm_version = pnpm_version)
if yarn_lock:
data = data + [yarn_lock]
if npm_package_lock:
data = data + [npm_package_lock]
# convert bins to a string_list_dict to satisfy attr type in repository rule
bins_string_list_dict = {}
if type(bins) != "dict":
fail("Expected bins to be a dict")
for key, value in bins.items():
if type(value) == "list":
# The passed 'bins' value is already in the dict-of-string-list
# form needed by the rule. This is undocumented but necessary for
# the bzlmod interface to use this macro since dict-of-dicts attributes
# cannot be passed into module extension attrs.
bins_string_list_dict = bins
break
if type(value) != "dict":
fail("Expected values in bins to be a dicts")
if key not in bins_string_list_dict:
bins_string_list_dict[key] = []
for value_key, value_value in value.items():
bins_string_list_dict[key].append("{}={}".format(value_key, value_value))
if not update_pnpm_lock and preupdate:
fail("expected update_pnpm_lock to be True when preupdate are specified")
lifecycle_hooks, lifecycle_hooks_execution_requirements, lifecycle_hooks_use_default_shell_env = macro_helpers.macro_lifecycle_args_to_rule_attrs(
lifecycle_hooks,
lifecycle_hooks_exclude,
run_lifecycle_hooks,
lifecycle_hooks_no_sandbox,
lifecycle_hooks_execution_requirements,
lifecycle_hooks_use_default_shell_env,
)
npm_translate_lock_rule(
name = name,
pnpm_lock = pnpm_lock,
npm_package_lock = npm_package_lock,
yarn_lock = yarn_lock,
update_pnpm_lock = update_pnpm_lock,
npmrc = npmrc,
use_home_npmrc = use_home_npmrc,
patches = patches,
patch_args = patch_args,
custom_postinstalls = custom_postinstalls,
package_visibility = package_visibility,
prod = prod,
public_hoist_packages = public_hoist_packages,
dev = dev,
no_optional = no_optional,
lifecycle_hooks = lifecycle_hooks,
lifecycle_hooks_envs = lifecycle_hooks_envs,
lifecycle_hooks_execution_requirements = lifecycle_hooks_execution_requirements,
lifecycle_hooks_use_default_shell_env = lifecycle_hooks_use_default_shell_env,
replace_packages = replace_packages,
bins = bins_string_list_dict,
verify_node_modules_ignored = verify_node_modules_ignored,
verify_patches = verify_patches,
external_repository_action_cache = external_repository_action_cache,
link_workspace = link_workspace,
root_package = root_package,
additional_file_contents = additional_file_contents,
repositories_bzl_filename = repositories_bzl_filename,
defs_bzl_filename = defs_bzl_filename,
generate_bzl_library_targets = generate_bzl_library_targets,
data = data,
preupdate = preupdate,
quiet = quiet,
node_toolchain_prefix = node_toolchain_prefix,
use_pnpm = use_pnpm,
yq_toolchain_prefix = yq_toolchain_prefix,
npm_package_target_name = npm_package_target_name,
bzlmod = False,
)
def list_patches(name, out = None, include_patterns = ["*.diff", "*.patch"], exclude_patterns = []):
"""Write a file containing a list of all patches in the current folder to the source tree.
Use this together with the `verify_patches` attribute of `npm_translate_lock` to verify
that all patches in a patch folder are included. This macro stamps a test to ensure the
file stays up to date.
Args:
name: Name of the target
out: Name of file to write to the source tree. If unspecified, `name` is used
include_patterns: Patterns to pass to a glob of patch files
exclude_patterns: Patterns to ignore in a glob of patch files
"""
outfile = out if out else name
# Ignore the patch list file we generate
exclude_patterns = exclude_patterns[:]
exclude_patterns.append(outfile)
list_sources(
name = "%s_list" % name,
srcs = native.glob(include_patterns, exclude = exclude_patterns),
)
write_source_file(
name = "%s_update" % name,
in_file = ":%s_list" % name,
out_file = outfile,
)
################################################################################
def _bootstrap_import(rctx, state):
pnpm_lock_label = state.label_store.label("pnpm_lock")
pnpm_lock_path = state.label_store.path("pnpm_lock")
# Check if the pnpm lock file already exists and copy it over if it does.
# When we do this, warn the user that we do.
if utils.exists(rctx, pnpm_lock_path):
# buildifier: disable=print
print("""
WARNING: Implicitly using pnpm-lock.yaml file `{pnpm_lock}` that is expected to be the result of running `pnpm import` on the `{lock}` lock file.
Set the `pnpm_lock` attribute of `npm_translate_lock(name = "{rctx_name}")` to `{pnpm_lock}` suppress this warning.
""".format(pnpm_lock = pnpm_lock_label, lock = state.label_store.label("lock"), rctx_name = rctx.name))
return
# No pnpm lock file exists and the user has specified a yarn or npm lock file. Bootstrap
# the pnpm lock file by running `pnpm import` in the source tree. We run in the source tree
# because at this point the user has likely not added all package.json and data files that
# pnpm import depends on to `npm_translate_lock`. In order to get a complete initial pnpm lock
# file with all workspace package imports listed we likely need to run in the source tree.
bootstrap_working_directory = paths.dirname(pnpm_lock_path)
if not rctx.attr.quiet:
# buildifier: disable=print
print("""
INFO: Running initial `pnpm import` in `{wd}` to bootstrap the pnpm-lock.yaml file required by rules_js.
It is recommended that you check the generated pnpm-lock.yaml file into source control and add it to the pnpm_lock
attribute of `npm_translate_lock(name = "{rctx_name}")` so subsequent invocations of the repository
rule do not need to run `pnpm import` unless an input has changed.""".format(wd = bootstrap_working_directory, rctx_name = rctx.name))
rctx.report_progress("Bootstrapping pnpm-lock.yaml file with `pnpm import`")
result = rctx.execute(
[
state.label_store.path("host_node"),
state.label_store.path("pnpm_entry"),
"import",
],
working_directory = bootstrap_working_directory,
quiet = rctx.attr.quiet,
)
if result.return_code:
msg = """ERROR: 'pnpm import' exited with status {status}:
STDOUT:
{stdout}
STDERR:
{stderr}
""".format(status = result.return_code, stdout = result.stdout, stderr = result.stderr)
fail(msg)
if not utils.exists(rctx, pnpm_lock_path):
msg = """
ERROR: Running `pnpm import` did not generate the {path} file.
Try installing pnpm (https://pnpm.io/installation) and running `pnpm import` manually
to generate the pnpm-lock.yaml file.""".format(path = pnpm_lock_path)
fail(msg)
msg = """
INFO: Initial pnpm-lock.yaml file generated. Please add the generated pnpm-lock.yaml file into
source control and set the `pnpm_lock` attribute in `npm_translate_lock(name = "{rctx_name}")` to `{pnpm_lock}`
and then run your build again.""".format(
rctx_name = rctx.name,
pnpm_lock = pnpm_lock_label,
)
fail(msg)
################################################################################
def _execute_preupdate_scripts(rctx, state):
for i in range(len(rctx.attr.preupdate)):
script_key = "preupdate_{}".format(i)
rctx.report_progress("Executing preupdate Node.js script `{script}`".format(
script = state.label_store.relative_path(script_key),
))
result = rctx.execute(
[
state.label_store.path("host_node"),
state.label_store.path(script_key),
],
# To keep things simple, run at the root of the external repository
working_directory = state.label_store.repo_root,
quiet = rctx.attr.quiet,
)
if result.return_code:
msg = """
ERROR: `node {script}` exited with status {status}.
Make sure all package.json and other data files required for the running `node {script}` are added to
the data attribute of `npm_translate_lock(name = "{rctx_name}")`.
STDOUT:
{stdout}
STDERR:
{stderr}
""".format(
script = state.label_store.relative_path(script_key),
rctx_name = rctx.name,
status = result.return_code,
stderr = result.stderr,
stdout = result.stdout,
)
fail(msg)
################################################################################
def _update_pnpm_lock(rctx, state):
_execute_preupdate_scripts(rctx, state)
pnpm_lock_label = state.label_store.label("pnpm_lock")
pnpm_lock_relative_path = state.label_store.relative_path("pnpm_lock")
update_cmd = ["import"] if rctx.attr.npm_package_lock or rctx.attr.yarn_lock else ["install", "--lockfile-only"]
update_working_directory = paths.dirname(state.label_store.repository_path("pnpm_lock"))
pnpm_cmd = " ".join(update_cmd)
if not rctx.attr.quiet:
# buildifier: disable=print
print("""
INFO: Updating `{pnpm_lock}` file as its inputs have changed since the last update.
Running `pnpm {pnpm_cmd}` in `{wd}`.
To disable this feature set `update_pnpm_lock` to False in `npm_translate_lock(name = "{rctx_name}")`.""".format(
pnpm_lock = pnpm_lock_relative_path,
pnpm_cmd = pnpm_cmd,
wd = update_working_directory,
rctx_name = rctx.name,
))
rctx.report_progress("Updating pnpm-lock.yaml with `pnpm {pnpm_cmd}`".format(pnpm_cmd = pnpm_cmd))
result = rctx.execute(
[
state.label_store.path("host_node"),
state.label_store.path("pnpm_entry"),
] + update_cmd,
# Run pnpm in the external repository so that we are hermetic and all data files that are required need
# to be specified. This requirement means that if any data file changes then the update command will be
# re-run. For cases where all data files cannot be specified a user can simply turn off auto-updates
# by setting update_pnpm_lock to False and update their pnpm-lock.yaml file manually.
working_directory = update_working_directory,
quiet = rctx.attr.quiet,
)
if result.return_code:
msg = """
ERROR: `pnpm {cmd}` exited with status {status}.
Make sure all package.json and other data files required for the running `pnpm {cmd}` are added to
the data attribute of `npm_translate_lock(name = "{rctx_name}")`.
If the problem persists, install pnpm (https://pnpm.io/installation) and run `pnpm {cmd}`
manually to update the pnpm-lock.yaml file. If you have specified `preupdate` scripts in
`npm_translate_lock(name = "{rctx_name}")` you may have to run these manually as well.
STDOUT:
{stdout}
STDERR:
{stderr}
""".format(
cmd = " ".join(update_cmd),
rctx_name = rctx.name,
status = result.return_code,
stderr = result.stderr,
stdout = result.stdout,
)
fail(msg)
lockfile_changed = False
if state.set_input_hash(
state.label_store.relative_path("pnpm_lock"),
utils.hash(rctx.read(state.label_store.repository_path("pnpm_lock"))),
):
# The lock file has changed
if not rctx.attr.quiet:
# buildifier: disable=print
print("""
INFO: {} file has changed""".format(pnpm_lock_relative_path))
utils.reverse_force_copy(rctx, pnpm_lock_label)
lockfile_changed = True
state.write_action_cache()
return lockfile_changed
################################################################################
def _fail_if_frozen_pnpm_lock(rctx, state):
if RULES_JS_FROZEN_PNPM_LOCK_ENV in rctx.os.environ.keys() and rctx.os.environ[RULES_JS_FROZEN_PNPM_LOCK_ENV]:
fail("""
ERROR: `{action_cache}` is out of date. `{pnpm_lock}` may require an update. To update run,
bazel run @{rctx_name}//:sync
""".format(
action_cache = state.label_store.relative_path("action_cache"),
pnpm_lock = state.label_store.relative_path("pnpm_lock"),
rctx_name = rctx.name,
))