Skip to content

Commit 1f679db

Browse files
authored
build: emit to dist/ and ship generated declarations (#6092)
1 parent 1b5cabd commit 1f679db

11 files changed

Lines changed: 232 additions & 44 deletions

File tree

.github/workflows/npm_release_cli.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ jobs:
105105
echo IS_RELEASE=$IS_RELEASE >> $GITHUB_OUTPUT
106106
107107
- name: Build nativescript
108-
run: npm pack
108+
run: npm run pack
109109

110110
- name: Upload npm package artifact
111111
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,4 +87,6 @@ lib/common/test-reports.xml
8787
!lib/common/test-scripts/**
8888
!lib/common/scripts/**
8989
config/test-deps-versions-generated.json
90-
!scripts/*.js
90+
!scripts/*.js
91+
# build output
92+
/dist

lib/services/project-backup-service.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export class ProjectBackupService implements IProjectBackupService {
88
constructor(
99
protected $fs: IFileSystem,
1010
protected $logger: ILogger,
11-
protected $projectHelper: IProjectHelper
11+
protected $projectHelper: IProjectHelper,
1212
) {}
1313

1414
getBackup(backupName: string): IBackup {
@@ -25,12 +25,19 @@ export class ProjectBackupService implements IProjectBackupService {
2525
return backup.restore();
2626
}
2727

28-
static Backup = class Backup implements IBackup {
28+
// annotated so declaration emit has a nameable type: an anonymous class
29+
// expression with private members cannot be described in a .d.ts
30+
static Backup: new (
31+
$super: ProjectBackupService,
32+
name: string,
33+
pathsToBackup?: string[],
34+
basePath?: string,
35+
) => IBackup = class Backup implements IBackup {
2936
constructor(
3037
private $super: ProjectBackupService,
3138
private name: string,
3239
private pathsToBackup: string[] = [],
33-
private basePath: string = $super.$projectHelper.projectDir
40+
private basePath: string = $super.$projectHelper.projectDir,
3441
) {}
3542

3643
get backupDir() {
@@ -49,7 +56,7 @@ export class ProjectBackupService implements IProjectBackupService {
4956
const targetPath = path.resolve(this.backupDir, pathToBackup);
5057
if (this.$super.$fs.exists(sourcePath)) {
5158
this.$super.$logger.trace(
52-
`BACKING UP ${color.cyan(sourcePath)} -> ${color.green(targetPath)}`
59+
`BACKING UP ${color.cyan(sourcePath)} -> ${color.green(targetPath)}`,
5360
);
5461
this.$super.$fs.copyFile(sourcePath, targetPath);
5562
backedUpPaths.push(pathToBackup);
@@ -76,7 +83,7 @@ export class ProjectBackupService implements IProjectBackupService {
7683
const sourcePath = path.resolve(this.backupDir, pathToBackup);
7784
const targetPath = path.resolve(this.basePath, pathToBackup);
7885
this.$super.$logger.trace(
79-
`RESTORING ${color.green(sourcePath)} -> ${color.cyan(targetPath)}`
86+
`RESTORING ${color.green(sourcePath)} -> ${color.cyan(targetPath)}`,
8087
);
8188
if (this.$super.$fs.exists(sourcePath)) {
8289
this.$super.$fs.copyFile(sourcePath, targetPath);
@@ -110,7 +117,7 @@ export class ProjectBackupService implements IProjectBackupService {
110117
remove() {
111118
if (!this.$super.$fs.exists(this.backupDir)) {
112119
this.$super.$logger.trace(
113-
`No backup named ${this.name} could be found.`
120+
`No backup named ${this.name} could be found.`,
114121
);
115122
return;
116123
}
@@ -135,15 +142,15 @@ export class ProjectBackupService implements IProjectBackupService {
135142
private getBackupData(): { name: string; paths: string[] } {
136143
if (!this.$super.$fs.exists(this.backupDir)) {
137144
this.$super.$logger.trace(
138-
`No backup named ${this.name} could be found.`
145+
`No backup named ${this.name} could be found.`,
139146
);
140147
return;
141148
}
142149
const backupJSONPath = path.resolve(this.backupDir, "_backup.json");
143150

144151
if (!this.$super.$fs.exists(backupJSONPath)) {
145152
this.$super.$logger.trace(
146-
`The backup ${this.name} does not contain a _backup.json.`
153+
`The backup ${this.name} does not contain a _backup.json.`,
147154
);
148155
return;
149156
}

package.json

Lines changed: 8 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "nativescript",
3-
"main": "./lib/nativescript-cli-lib.js",
3+
"main": "./dist/lib/nativescript-cli-lib.js",
44
"version": "9.1.0-alpha.15",
55
"author": "NativeScript <oss@nativescript.org>",
66
"description": "Command-line interface for building NativeScript projects",
@@ -11,40 +11,27 @@
1111
"ns": "./bin/tns"
1212
},
1313
"files": [
14-
"bin/*",
15-
"config",
16-
"docs",
17-
"!docs/html",
18-
"lib",
19-
"!lib/**/*.ts",
20-
"lib/**/*.d.ts",
21-
"!lib/**/*.js.map",
22-
"!lib/common/test",
23-
"!lib/common/docs/fonts",
24-
"resources",
25-
"setup",
26-
"vendor",
27-
"postinstall.js",
28-
"preuninstall.js"
14+
"dist"
2915
],
3016
"scripts": {
3117
"clean": "npx rimraf node_modules package-lock.json && npm run setup",
3218
"clean.build": "node scripts/clean.js",
33-
"build": "npm run tsc && node scripts/generate-test-deps.js",
19+
"build": "node scripts/clean.js --dist-only && npm run tsc && node scripts/generate-test-deps.js && node scripts/copy-assets.js",
3420
"build.all": "npm test",
3521
"dev": "tsc --watch",
3622
"setup": "npm i --ignore-scripts && npx husky",
37-
"test": "npm run tsc && mocha --config=test/.mocharc.yml",
23+
"test": "npm run build && mocha --config=test/.mocharc.yml",
3824
"postinstall": "node postinstall.js",
3925
"preuninstall": "node preuninstall.js",
40-
"prepack": "npm run clean.build && node scripts/generate-test-deps.js && npm test && tsc -p tsconfig.release.json && node scripts/set-ga-id.js live && node scripts/set-ga-id.js verify",
41-
"postpack": "node scripts/set-ga-id.js dev",
26+
"prepack": "node scripts/guard-root-pack.js",
4227
"docs-jekyll": "node scripts/build-docs.js",
4328
"mocha": "mocha",
4429
"tsc": "tsc",
4530
"test-watch": "node ./dev/tsc-to-mocha-watch.js",
4631
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
47-
"prettier": "prettier --write ./lib/**/*{.ts,.d.ts} ./test/**/*{.ts,.d.ts}"
32+
"prettier": "prettier --write ./lib/**/*{.ts,.d.ts} ./test/**/*{.ts,.d.ts}",
33+
"build.release": "npm run clean.build && tsc -p tsconfig.release.json && node scripts/generate-test-deps.js && node scripts/copy-assets.js --release && node scripts/set-ga-id.js live --dir dist && node scripts/set-ga-id.js verify --dir dist",
34+
"pack": "npm run build.release && npm pack ./dist"
4835
},
4936
"repository": {
5037
"type": "git",

scripts/clean.js

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,19 @@ const path = require("path");
44

55
const rootDir = path.join(__dirname, "..");
66

7-
// .gitignore is the source of truth for which files under lib/ and test/ are
8-
// compiler output: its negations already protect the vendored, hook and fixture
9-
// .js files that must survive a clean.
7+
fs.rmSync(path.join(rootDir, "dist"), { recursive: true, force: true });
8+
9+
// tsc never removes output whose source is gone, so every build starts from an
10+
// empty dist - otherwise a deleted file keeps being compiled-in and tested
11+
// against, which is the failure this whole layout exists to prevent.
12+
if (process.argv.includes("--dist-only")) {
13+
process.exit(0);
14+
}
15+
16+
// Builds used to emit next to each source file, so a tree that predates dist/
17+
// still has hundreds of stale .js lying around. .gitignore is the source of
18+
// truth for which of those are compiler output - its negations protect the
19+
// vendored, hook and fixture .js that must survive.
1020
const result = child_process.spawnSync("git", ["clean", "-Xdf", "lib", "test"], {
1121
cwd: rootDir,
1222
stdio: "inherit",

scripts/copy-assets.js

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
const fs = require("fs");
2+
const path = require("path");
3+
4+
// dist/ is assembled as a complete package root, not just compiled output:
5+
// lib/ resolves siblings through __dirname (../package.json, ../docs/helpers,
6+
// ../../vendor/gradle-plugin, ...), so those directories have to sit next to it
7+
// exactly as they do in the repo.
8+
9+
const rootDir = path.join(__dirname, "..");
10+
const distDir = path.join(rootDir, "dist");
11+
const release = process.argv.includes("--release");
12+
13+
// bundleDependencies are resolved from node_modules next to the manifest being
14+
// packed, so they have to be mirrored into dist for `npm pack` to bundle them
15+
const BUNDLED = ["universal-analytics", "debug", "ms", "uuid"];
16+
17+
const SIBLING_DIRS = ["resources", "docs", "config", "vendor", "bin", "setup"];
18+
// npm picks README/LICENSE/CHANGELOG up from the directory being packed, so
19+
// they have to exist inside dist or they silently drop out of the tarball
20+
const ROOT_FILES = [
21+
"postinstall.js",
22+
"preuninstall.js",
23+
"README.md",
24+
"LICENSE",
25+
"CHANGELOG.md",
26+
];
27+
28+
// paths (relative to the repo root) that never ship
29+
const RELEASE_EXCLUDES = [
30+
path.join("docs", "html"),
31+
path.join("lib", "common", "docs", "fonts"),
32+
path.join("lib", "common", "test"),
33+
];
34+
35+
function isExcluded(relPath) {
36+
if (!release) {
37+
return false;
38+
}
39+
return RELEASE_EXCLUDES.some(
40+
(excluded) => relPath === excluded || relPath.startsWith(excluded + path.sep)
41+
);
42+
}
43+
44+
let copied = 0;
45+
let skipped = 0;
46+
47+
function copyFile(sourcePath, targetPath) {
48+
const source = fs.statSync(sourcePath);
49+
if (fs.existsSync(targetPath)) {
50+
const target = fs.statSync(targetPath);
51+
// vendor/ alone is ~31MB; re-copying it on every build is pure waste
52+
if (target.mtimeMs >= source.mtimeMs && target.size === source.size) {
53+
skipped++;
54+
return;
55+
}
56+
}
57+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
58+
fs.copyFileSync(sourcePath, targetPath);
59+
copied++;
60+
}
61+
62+
function copyTree(relDir, filter) {
63+
const sourceDir = path.join(rootDir, relDir);
64+
if (!fs.existsSync(sourceDir)) {
65+
return;
66+
}
67+
68+
for (const entry of fs.readdirSync(sourceDir, { withFileTypes: true })) {
69+
const relPath = path.join(relDir, entry.name);
70+
if (isExcluded(relPath)) {
71+
continue;
72+
}
73+
if (entry.isDirectory()) {
74+
copyTree(relPath, filter);
75+
} else if (!filter || filter(relPath)) {
76+
copyFile(path.join(rootDir, relPath), path.join(distDir, relPath));
77+
}
78+
}
79+
}
80+
81+
// Everything under lib/ that is not TypeScript is an asset: vendored scripts,
82+
// hooks, platform-tools binaries, docs helpers and test fixtures. A .js with a
83+
// sibling .ts is compiler output instead - either left over from the old
84+
// in-place build or freshly emitted into dist - and copying it would overwrite
85+
// what tsc just produced.
86+
function isCompilerOutput(relPath) {
87+
const stem = relPath.replace(/\.js\.map$/, "").replace(/\.js$/, "");
88+
return (
89+
(relPath.endsWith(".js") || relPath.endsWith(".js.map")) &&
90+
fs.existsSync(path.join(rootDir, stem + ".ts"))
91+
);
92+
}
93+
94+
// Hand-written .d.ts come along too. tsc treats them as inputs and never emits
95+
// them, but the generated declarations import from them, so leaving them behind
96+
// ships types with dangling references.
97+
copyTree(
98+
"lib",
99+
(relPath) =>
100+
(!relPath.endsWith(".ts") || relPath.endsWith(".d.ts")) &&
101+
!isCompilerOutput(relPath)
102+
);
103+
104+
for (const dir of SIBLING_DIRS) {
105+
copyTree(dir);
106+
}
107+
108+
if (!release) {
109+
// fixtures the compiled tests read relative to their own location
110+
copyTree(path.join("test", "files"));
111+
}
112+
113+
for (const file of ROOT_FILES) {
114+
copyFile(path.join(rootDir, file), path.join(distDir, file));
115+
}
116+
117+
for (const dep of BUNDLED) {
118+
copyTree(path.join("node_modules", dep));
119+
}
120+
121+
writeManifest();
122+
123+
function writeManifest() {
124+
const pkg = JSON.parse(
125+
fs.readFileSync(path.join(rootDir, "package.json"), "utf8")
126+
);
127+
128+
// dist is the package root once published, so entrypoints lose the dist/
129+
// prefix they carry in the source manifest
130+
pkg.main = pkg.main.replace(/^\.\/dist\//, "./");
131+
132+
delete pkg.devDependencies;
133+
delete pkg.files;
134+
delete pkg.overrides;
135+
delete pkg["lint-staged"];
136+
137+
pkg.scripts = {
138+
postinstall: pkg.scripts.postinstall,
139+
preuninstall: pkg.scripts.preuninstall,
140+
};
141+
142+
fs.writeFileSync(
143+
path.join(distDir, "package.json"),
144+
JSON.stringify(pkg, null, 2) + "\n"
145+
);
146+
}
147+
148+
console.log(
149+
`assets: ${copied} copied, ${skipped} up to date${release ? " (release)" : ""}`
150+
);

scripts/guard-root-pack.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// The published package is assembled in dist/ and packed from there, so packing
2+
// the repository root would produce a tarball with everything nested one level
3+
// deeper - every path the CLI resolves through __dirname would break, silently.
4+
// npm pack ./dist runs dist's own manifest, so this guard does not fire for it.
5+
console.error(
6+
[
7+
"Refusing to pack the repository root.",
8+
"",
9+
"The published package is assembled in dist/. Use:",
10+
" npm run pack",
11+
"",
12+
"which builds dist/ and runs `npm pack ./dist`.",
13+
].join("\n")
14+
);
15+
16+
process.exit(1);

scripts/set-ga-id.js

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,14 @@ const GA_TRACKING_IDS = {
88
};
99

1010
const GA_KEY = "GA_TRACKING_ID";
11-
const configPath = path.join(__dirname, "..", "config", "config.json");
11+
const rootDir = path.join(__dirname, "..");
12+
13+
// Releases flip the id inside dist/ rather than in the working tree, so a failed
14+
// pack cannot leave a checkout configured to report as production.
15+
const dirIndex = process.argv.indexOf("--dir");
16+
const baseDir =
17+
dirIndex === -1 ? rootDir : path.resolve(rootDir, process.argv[dirIndex + 1]);
18+
const configPath = path.join(baseDir, "config", "config.json");
1219

1320
function readConfig() {
1421
return JSON.parse(fs.readFileSync(configPath, "utf8"));
@@ -18,13 +25,15 @@ const mode = process.argv[2];
1825

1926
if (mode === "verify") {
2027
if (readConfig()[GA_KEY] !== GA_TRACKING_IDS.live) {
21-
throw new Error("Google Analytics id is not configured correctly.");
28+
throw new Error(`Google Analytics id is not configured correctly in ${configPath}`);
2229
}
2330
} else if (mode === "live" || mode === "dev") {
2431
const config = readConfig();
2532
config[GA_KEY] = GA_TRACKING_IDS[mode];
2633
fs.writeFileSync(configPath, JSON.stringify(config, null, "\t") + EOL);
2734
} else {
28-
console.error("Usage: node scripts/set-ga-id.js <live|dev|verify>");
35+
console.error(
36+
"Usage: node scripts/set-ga-id.js <live|dev|verify> [--dir <path>]"
37+
);
2938
process.exit(1);
3039
}

0 commit comments

Comments
 (0)