Skip to content

Commit 5a7c1a7

Browse files
committed
fix(builtin): fix for pkg_npm single directory artifact dep case
Also fixes the dependency on bazel-bin for the .pack & .publish scripts. This fixes an issue where pkg_npm has a single dep which is a directory artifact but the contents of that dep are not re-rooted. This is the expected behaviour as the only way to create a valid npm package with a package.json at the root is to re-root to the output directory of the dep. The fix is actually to not copy anything but to just forward the directory artifact dep onto the default info of pkg_npm. Some refactoring done so that the npm scripts are generated in a separate action from the package.js which is not run in the single directory artifact dep case.
1 parent b459d6d commit 5a7c1a7

6 files changed

Lines changed: 190 additions & 63 deletions

File tree

internal/pkg_npm/BUILD.bazel

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,15 @@ exports_files(["pkg_npm.bzl"])
1414

1515
nodejs_binary(
1616
name = "packager",
17-
data = [
18-
"packager.js",
19-
"//third_party/github.com/gjtorikian/isBinaryFile",
20-
"@nodejs//:run_npm.sh.template",
21-
],
17+
data = ["//third_party/github.com/gjtorikian/isBinaryFile"],
2218
entry_point = ":packager.js",
2319
install_source_map_support = False,
24-
visibility = ["//visibility:public"],
20+
)
21+
22+
nodejs_binary(
23+
name = "npm_script_generator",
24+
entry_point = ":npm_script_generator.js",
25+
install_source_map_support = False,
2526
)
2627

2728
filegroup(
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/**
2+
* @license
3+
* Copyright 2018 The Bazel Authors. All rights reserved.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
*
8+
* You may obtain a copy of the License at
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
/**
18+
* @fileoverview This script generates npm pack & publish shell scripts from
19+
* a template.
20+
*/
21+
'use strict';
22+
23+
const fs = require('fs');
24+
25+
function main(args) {
26+
const [outDir, packPath, publishPath, runNpmTemplatePath] = args;
27+
const npmTemplate = fs.readFileSync(runNpmTemplatePath, {encoding: 'utf-8'});
28+
const cwd = process.cwd();
29+
if (/[\//]sandbox[\//]/.test(cwd)) {
30+
console.error('Error: npm_script_generator must be run with no sandbox');
31+
process.exit(1);
32+
}
33+
fs.writeFileSync(packPath, npmTemplate.replace('TMPL_args', `pack "${cwd}/${outDir}"`));
34+
fs.writeFileSync(publishPath, npmTemplate.replace('TMPL_args', `publish "${cwd}/${outDir}"`));
35+
}
36+
37+
if (require.main === module) {
38+
process.exitCode = main(process.argv.slice(2));
39+
}

internal/pkg_npm/packager.js

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@
1414
* See the License for the specific language governing permissions and
1515
* limitations under the License.
1616
*/
17+
/**
18+
* @fileoverview This script copies files and nested packages into the output
19+
* directory of the pkg_npm rule.
20+
*/
21+
'use strict';
22+
1723
const fs = require('fs');
1824
const path = require('path');
1925
const isBinary = require('isbinaryfile').isBinaryFileSync;
@@ -66,17 +72,16 @@ function unquoteArgs(s) {
6672
function main(args) {
6773
args = fs.readFileSync(args[0], {encoding: 'utf-8'}).split('\n').map(unquoteArgs);
6874
const
69-
[outDir, baseDir, srcsArg, binDir, genDir, depsArg, packagesArg, substitutionsArg, packPath,
70-
publishPath, replaceWithVersion, stampFile, vendorExternalArg, hideBuildFilesArg,
71-
runNpmTemplatePath] = args;
75+
[outDir, baseDir, srcsArg, binDir, genDir, depsArg, packagesArg, substitutionsArg,
76+
replaceWithVersion, stampFile, vendorExternalArg, hideBuildFilesArg] = args;
7277
const renameBuildFiles = parseInt(hideBuildFilesArg);
7378

7479
const substitutions = [
7580
// Strip content between BEGIN-INTERNAL / END-INTERNAL comments
7681
[/(#|\/\/)\s+BEGIN-INTERNAL[\w\W]+?END-INTERNAL/g, ''],
7782
];
7883
const rawReplacements = JSON.parse(substitutionsArg);
79-
for (let key of Object.keys(rawReplacements)) {
84+
for (const key of Object.keys(rawReplacements)) {
8085
substitutions.push([new RegExp(key, 'g'), rawReplacements[key]])
8186
}
8287
// Replace version last so that earlier substitutions can add
@@ -123,7 +128,7 @@ function main(args) {
123128
}
124129

125130
function outPath(f) {
126-
for (ext of vendorExternalArg.split(',').filter(s => !!s)) {
131+
for (const ext of vendorExternalArg.split(',').filter(s => !!s)) {
127132
const candidate = path.join(binDir, 'external', ext);
128133
if (!path.relative(candidate, f).startsWith('..')) {
129134
return path.join(outDir, path.relative(candidate, f));
@@ -145,7 +150,7 @@ function main(args) {
145150
}
146151

147152
// Deps like bazel-bin/baseDir/my/path is copied to outDir/my/path.
148-
for (dep of depsArg.split(',').filter(s => !!s)) {
153+
for (const dep of depsArg.split(',').filter(s => !!s)) {
149154
try {
150155
copyWithReplace(dep, outPath(dep), substitutions, renameBuildFiles);
151156
} catch (e) {
@@ -156,7 +161,7 @@ function main(args) {
156161

157162
// package contents like bazel-bin/baseDir/my/directory/* is
158163
// recursively copied to outDir/my/*
159-
for (pkg of packagesArg.split(',').filter(s => !!s)) {
164+
for (const pkg of packagesArg.split(',').filter(s => !!s)) {
160165
const outDir = outPath(path.dirname(pkg));
161166
function copyRecursive(base, file) {
162167
file = file.replace(/\\/g, '/');
@@ -167,7 +172,7 @@ function main(args) {
167172
});
168173
} else {
169174
function outFile() {
170-
for (ext of vendorExternalArg.split(',').filter(s => !!s)) {
175+
for (const ext of vendorExternalArg.split(',').filter(s => !!s)) {
171176
if (file.startsWith(`external/${ext}`)) {
172177
return file.substr(`external/${ext}`.length);
173178
}
@@ -182,10 +187,6 @@ function main(args) {
182187
copyRecursive(pkg, f);
183188
});
184189
}
185-
186-
const npmTemplate = fs.readFileSync(require.resolve(runNpmTemplatePath), {encoding: 'utf-8'});
187-
fs.writeFileSync(packPath, npmTemplate.replace('TMPL_args', `pack "${outDir}"`));
188-
fs.writeFileSync(publishPath, npmTemplate.replace('TMPL_args', `publish "${outDir}"`));
189190
}
190191

191192
if (require.main === module) {

internal/pkg_npm/pkg_npm.bzl

Lines changed: 62 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ to the `deps` of one of their targets.
77
"""
88

99
load("//:providers.bzl", "DeclarationInfo", "JSNamedModuleInfo", "LinkablePackageInfo", "NodeContextInfo")
10-
load("//internal/common:path_utils.bzl", "strip_external")
1110

1211
_DOC = """The pkg_npm rule creates a directory containing a publishable npm artifact.
1312
@@ -68,14 +67,12 @@ $ bazel run @nodejs//:npm_node_repositories who
6867
$ bazel run :my_package.publish
6968
```
7069
71-
> Note that the `.pack` and `.publish` commands require that the `bazel-out` symlink exists in your project.
72-
> Also, you must run the command from the workspace root directory containing the `bazel-out` symlink.
73-
7470
You can pass arguments to npm by escaping them from Bazel using a double-hyphen, for example:
7571
7672
`bazel run my_package.publish -- --tag=next`
7773
"""
7874

75+
# Used in angular/angular /packages/bazel/src/ng_package/ng_package.bzl
7976
PKG_NPM_ATTRS = {
8077
"package_name": attr.string(
8178
doc = """Optional package_name that this npm package may be imported as.""",
@@ -119,6 +116,11 @@ PKG_NPM_ATTRS = {
119116
doc = """Other targets which produce files that should be included in the package, such as `rollup_bundle`""",
120117
allow_files = True,
121118
),
119+
"_npm_script_generator": attr.label(
120+
default = Label("//internal/pkg_npm:npm_script_generator"),
121+
cfg = "host",
122+
executable = True,
123+
),
122124
"_packager": attr.label(
123125
default = Label("//internal/pkg_npm:packager"),
124126
cfg = "host",
@@ -130,6 +132,7 @@ PKG_NPM_ATTRS = {
130132
),
131133
}
132134

135+
# Used in angular/angular /packages/bazel/src/ng_package/ng_package.bzl
133136
PKG_NPM_OUTPUTS = {
134137
"pack": "%{name}.pack",
135138
"publish": "%{name}.publish",
@@ -141,6 +144,7 @@ PKG_NPM_OUTPUTS = {
141144
def _filter_out_external_files(ctx, files, package_path):
142145
result = []
143146
for file in files:
147+
# NB: package_path may be an empty string
144148
if file.short_path.startswith(package_path) and not file.short_path.startswith("../"):
145149
result.append(file.path)
146150
else:
@@ -149,16 +153,17 @@ def _filter_out_external_files(ctx, files, package_path):
149153
result.append(file.path)
150154
return result
151155

152-
def create_package(ctx, deps_sources, nested_packages):
156+
# Used in angular/angular /packages/bazel/src/ng_package/ng_package.bzl
157+
def create_package(ctx, deps_files, nested_packages):
153158
"""Creates an action that produces the npm package.
154159
155160
It copies srcs and deps into the artifact and produces the .pack and .publish
156161
scripts.
157162
158163
Args:
159164
ctx: the skylark rule context
160-
deps_sources: Files which have been specified as dependencies. Usually ".js" or ".d.ts"
161-
generated files.
165+
deps_files: list of files to include in the package which have been
166+
specified as dependencies
162167
nested_packages: list of TreeArtifact outputs from other actions which are
163168
to be nested inside this package
164169
@@ -167,14 +172,27 @@ def create_package(ctx, deps_sources, nested_packages):
167172
"""
168173

169174
stamp = ctx.attr.node_context_data[NodeContextInfo].stamp
175+
176+
all_files = deps_files + ctx.files.srcs
177+
178+
if not stamp and len(all_files) == 1 and all_files[0].is_directory and len(ctx.files.nested_packages) == 0:
179+
# Special case where these is a single dep that is a directory artifact and there are no
180+
# source files or nested_packages; in that case we assume the package is contained within
181+
# that single directory and there is no work to do
182+
package_dir = all_files[0]
183+
184+
_create_npm_scripts(ctx, package_dir)
185+
186+
return package_dir
187+
170188
package_dir = ctx.actions.declare_directory(ctx.label.name)
171189
package_path = ctx.label.package
172190

173191
# List of dependency sources which are local to the package that defines the current
174192
# target. Also include files from external repositories that explicitly specified in
175193
# the vendor_external list. We only want to package deps files which are inside of the
176194
# current package unless explicitely specified.
177-
filtered_deps_sources = _filter_out_external_files(ctx, deps_sources, package_path)
195+
filtered_deps_sources = _filter_out_external_files(ctx, deps_files, package_path)
178196

179197
args = ctx.actions.args()
180198
args.use_param_file("%s", use_always = True)
@@ -186,17 +204,12 @@ def create_package(ctx, deps_sources, nested_packages):
186204
args.add_joined(filtered_deps_sources, join_with = ",", omit_if_empty = False)
187205
args.add_joined([p.path for p in nested_packages], join_with = ",", omit_if_empty = False)
188206
args.add(ctx.attr.substitutions)
189-
args.add_all([ctx.outputs.pack.path, ctx.outputs.publish.path])
190207
args.add(ctx.attr.replace_with_version)
191208
args.add(ctx.version_file.path if stamp else "")
192209
args.add_joined(ctx.attr.vendor_external, join_with = ",", omit_if_empty = False)
193210
args.add("1" if ctx.attr.hide_build_files else "0")
194211

195-
# require.resolve expects the path to start with the workspace name and not "external"
196-
run_npm_template_path = strip_external(ctx.file._run_npm_template.path)
197-
args.add(run_npm_template_path)
198-
199-
inputs = ctx.files.srcs + deps_sources + nested_packages + [ctx.file._run_npm_template]
212+
inputs = ctx.files.srcs + deps_files + nested_packages
200213

201214
# The version_file is an undocumented attribute of the ctx that lets us read the volatile-status.txt file
202215
# produced by the --workspace_status_command. That command will be executed whenever
@@ -210,34 +223,60 @@ def create_package(ctx, deps_sources, nested_packages):
210223
mnemonic = "AssembleNpmPackage",
211224
executable = ctx.executable._packager,
212225
inputs = inputs,
213-
outputs = [package_dir, ctx.outputs.pack, ctx.outputs.publish],
226+
outputs = [package_dir],
214227
arguments = [args],
215228
)
229+
230+
_create_npm_scripts(ctx, package_dir)
231+
216232
return package_dir
217233

234+
def _create_npm_scripts(ctx, package_dir):
235+
args = ctx.actions.args()
236+
args.add_all([
237+
package_dir.path,
238+
ctx.outputs.pack.path,
239+
ctx.outputs.publish.path,
240+
ctx.file._run_npm_template.path,
241+
])
242+
243+
ctx.actions.run(
244+
progress_message = "Generating npm pack & publish scripts",
245+
mnemonic = "GenerateNpmScripts",
246+
executable = ctx.executable._npm_script_generator,
247+
inputs = [ctx.file._run_npm_template, package_dir],
248+
outputs = [ctx.outputs.pack, ctx.outputs.publish],
249+
arguments = [args],
250+
# Must be run local (no sandbox) so that the pwd is the actual execroot
251+
# in the script which is used to generate the path in the pack & publish
252+
# scripts.
253+
execution_requirements = {"local": "1"},
254+
)
255+
218256
def _pkg_npm(ctx):
219-
sources_depsets = []
257+
deps_files_depsets = []
220258

221259
for dep in ctx.attr.deps:
222260
# Collect whatever is in the "data"
223-
sources_depsets.append(dep.data_runfiles.files)
261+
deps_files_depsets.append(dep.data_runfiles.files)
224262

225263
# Only collect DefaultInfo files (not transitive)
226-
sources_depsets.append(dep.files)
264+
deps_files_depsets.append(dep.files)
227265

228266
# All direct & transitive JavaScript-producing deps
229267
# TODO: switch to JSModuleInfo when it is available
230268
if JSNamedModuleInfo in dep:
231-
sources_depsets.append(dep[JSNamedModuleInfo].sources)
269+
deps_files_depsets.append(dep[JSNamedModuleInfo].sources)
232270

233271
# Include all transitive declerations
234272
if DeclarationInfo in dep:
235-
sources_depsets.append(dep[DeclarationInfo].transitive_declarations)
236-
237-
sources = depset(transitive = sources_depsets)
273+
deps_files_depsets.append(dep[DeclarationInfo].transitive_declarations)
238274

239275
# Note: to_list() should be called once per rule!
240-
package_dir = create_package(ctx, sources.to_list(), ctx.files.nested_packages)
276+
deps_files = depset(transitive = deps_files_depsets).to_list()
277+
278+
package_dir = create_package(ctx, deps_files, ctx.files.nested_packages)
279+
241280
package_dir_depset = depset([package_dir])
242281

243282
result = [

internal/pkg_npm/test/BUILD.bazel

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,35 @@ pkg_npm(
6060
],
6161
)
6262

63+
pkg_npm(
64+
name = "test_noop_pkg",
65+
# Special case where these is a single dep that is a directory artifact
66+
# then we assume the package is contained within that single directory
67+
# and the pkg_npm rules does not need to copy any files
68+
deps = [":rollup/bundle/subdirectory"],
69+
)
70+
71+
pkg_npm(
72+
name = "test_noop2_pkg",
73+
# Special case where these is a single dep that is a directory artifact
74+
# then we assume the package is contained within that single directory
75+
# and the pkg_npm rules does not need to copy any files
76+
srcs = [":rollup/bundle/subdirectory"],
77+
)
78+
6379
jasmine_node_test(
6480
name = "test",
6581
srcs = ["pkg_npm.spec.js"],
66-
data = [":test_pkg"],
82+
data = [
83+
":test_noop2_pkg",
84+
":test_noop_pkg",
85+
":test_pkg",
86+
],
87+
templated_args = [
88+
"$(rootpath :test_pkg)",
89+
"$(rootpath :test_noop_pkg)",
90+
"$(rootpath :test_noop2_pkg)",
91+
],
6792
)
6893

6994
genrule(

0 commit comments

Comments
 (0)