Skip to content

Commit 25265f1

Browse files
committed
build: emit to dist/ and ship generated declarations
Compiling next to the sources has cost us repeatedly: a deleted spec left its .js behind and kept running for ten months, prepack silently undid its own release compile because both wrote to the same place, and .gitignore needs a blanket *.js plus a growing list of negations to tell source from output apart. dist/ is assembled as a complete package root rather than just compiled output. lib/ resolves its siblings through __dirname - ../package.json, ../docs/helpers, ../../config, ../../vendor/gradle-plugin - so resources, docs, config, vendor, bin and setup are mirrored alongside it and all 46 of those paths keep working untouched. The published tarball has the same internal layout as before; only where it is built from changed. - tsconfig gains rootDir/outDir; tsconfig.release.json builds lib only, with declarations, and is what gets packed - scripts/copy-assets.js mirrors assets and writes dist/package.json - packing is npm run pack (npm pack ./dist); prepack/postpack are gone and a guard refuses to pack the root, which would nest everything a level deeper - the GA id is now set inside dist, so a failed pack can no longer leave a checkout configured to report as production A .js with a sibling .ts is compiler output, so the copy step skips it - without that it would overwrite what tsc just emitted with whatever the old in-place build left behind. Hand-written .d.ts are copied too: tsc treats them as inputs and never emits them, and 115 generated declarations import from them. ProjectBackupService.Backup is annotated because declaration emit cannot describe an anonymous class expression that has private members. 1513 passing, unchanged. Verified by packing and installing the tarball into a clean consumer: the CLI runs, help renders, the sibling paths resolve, and the shipped config carries the live GA id while the working tree keeps dev.
1 parent 513415a commit 25265f1

11 files changed

Lines changed: 225 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": "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: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@ 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+
// Builds used to emit next to each source file, so a tree that predates dist/
10+
// still has hundreds of stale .js lying around. .gitignore is the source of
11+
// truth for which of those are compiler output - its negations protect the
12+
// vendored, hook and fixture .js that must survive.
1013
const result = child_process.spawnSync("git", ["clean", "-Xdf", "lib", "test"], {
1114
cwd: rootDir,
1215
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
}

test/.mocharc.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,13 @@ inline-diffs: false
3939
recursive: true
4040
reporter: 'spec'
4141
require:
42-
- 'test/test-bootstrap.js'
42+
- 'dist/test/test-bootstrap.js'
4343
retries: 1
4444
slow: 500
4545
sort: false
4646
spec:
47-
- 'test/**/*.js'
48-
- 'lib/common/test/unit-tests/**/*.js'
47+
- 'dist/test/**/*.js'
48+
- 'dist/lib/common/test/unit-tests/**/*.js'
4949
timeout: 150000 # same as "no-timeout: true" or "timeout: 0"
5050

5151
# node flags
@@ -55,7 +55,7 @@ ui: 'bdd'
5555
v8-stack-trace-limit: 100 # V8 flags are prepended with "v8-"
5656
watch: false
5757
watch-files:
58-
- 'test/**/*.js'
59-
- 'lib/common/test/unit-tests/**/*.js'
58+
- 'dist/test/**/*.js'
59+
- 'dist/lib/common/test/unit-tests/**/*.js'
6060
# watch-ignore:
6161
# - 'lib/vendor'

0 commit comments

Comments
 (0)