Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/nervous-islands-taste.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"modular-scripts": minor
---

Generate dependency manifest (package.json) for apps. This includes all the dependencies, either installed via the package's `package.json` or hoisted to the root's `package.json`.
If a dependency is imported in code but not specified in the `package.json`s, the app will not build anymore.
2 changes: 1 addition & 1 deletion packages/modular-scripts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@
"terser-webpack-plugin": "4.2.3",
"tmp": "^0.2.1",
"ts-jest": "26.5.6",
"ts-morph": "^11.0.3",
"ts-morph": "^13.0.2",
"update-notifier": "5.1.0",
"url-loader": "4.1.1",
"webpack": "4.46.0",
Expand Down
1 change: 1 addition & 0 deletions packages/modular-scripts/src/__tests__/app.esbuild.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ describe('when working with an app', () => {
├─ logo192.png #1nez7vk
├─ logo512.png #1hwqvcc
├─ manifest.json #19gah8o
├─ package.json
Comment thread
LukeSheard marked this conversation as resolved.
├─ robots.txt #1sjb8b3
└─ static
├─ css
Expand Down
20 changes: 20 additions & 0 deletions packages/modular-scripts/src/__tests__/app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import getModularRoot from '../utils/getModularRoot';
import puppeteer from 'puppeteer';

import { startApp, DevServer } from './start-app';
import type { CoreProperties } from '@schemastore/package';

// eslint-disable-next-line @typescript-eslint/unbound-method
const { getNodeText } = queries;
Expand Down Expand Up @@ -83,6 +84,7 @@ describe('when working with a NODE_ENV app', () => {
├─ logo192.png #1nez7vk
├─ logo512.png #1hwqvcc
├─ manifest.json #19gah8o
├─ package.json
├─ robots.txt #1sjb8b3
└─ static
└─ js
Expand Down Expand Up @@ -138,6 +140,7 @@ describe('When working with a nested app', () => {
├─ logo192.png #1nez7vk
├─ logo512.png #1hwqvcc
├─ manifest.json #19gah8o
├─ package.json
├─ robots.txt #1sjb8b3
└─ static
├─ css
Expand Down Expand Up @@ -355,6 +358,7 @@ describe('when working with an app', () => {
├─ logo192.png #1nez7vk
├─ logo512.png #1hwqvcc
├─ manifest.json #19gah8o
├─ package.json
├─ robots.txt #1sjb8b3
└─ static
├─ css
Expand Down Expand Up @@ -391,6 +395,22 @@ describe('when working with an app', () => {
).toMatchSnapshot();
});

it('can generate a package.json', async () => {
const packageJson = JSON.parse(
String(
await fs.readFile(
path.join(modularRoot, 'dist', 'sample-app', 'package.json'),
),
),
) as CoreProperties;

expect(packageJson.name).toBe('sample-app');
expect(packageJson.version).toBe('0.1.0');
expect(packageJson.modular).toStrictEqual({ type: 'app' });
expect(packageJson.dependencies?.react).toBeTruthy();
expect(packageJson.dependencies?.['react-dom']).toBeTruthy();
});

it('can generate a index.html', async () => {
expect(
prettier.format(
Expand Down
23 changes: 22 additions & 1 deletion packages/modular-scripts/src/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import {
createEsbuildAssets,
esbuildMeasureFileSizesBeforeBuild,
} from './esbuildFileSizeReporter';
import { getPackageDependencies } from '../utils/getPackageDependencies';
import type { CoreProperties } from '@schemastore/package';

async function buildApp(target: string) {
// True if there's no preference set - or the preference is for webpack.
Expand Down Expand Up @@ -81,7 +83,6 @@ async function buildApp(target: string) {
'../esbuild-scripts/build'
);
const result = await buildEsbuildApp(target, paths);

assets = createEsbuildAssets(paths, result);
} else {
// create-react-app doesn't support plain module outputs yet,
Expand Down Expand Up @@ -129,6 +130,26 @@ async function buildApp(target: string) {
}
}

// Add dependencies from source and bundled dependencies to target package.json
const packageDependencies = await getPackageDependencies(target);
const targetPackageJson = (await fs.readJSON(
Comment thread
LukeSheard marked this conversation as resolved.
path.join(targetDirectory, 'package.json'),
)) as CoreProperties;
targetPackageJson.dependencies = packageDependencies;
targetPackageJson.bundledDependencies = Object.keys(packageDependencies);
// Copy selected fields of package.json over
await fs.writeJSON(
path.join(paths.appBuild, 'package.json'),
{
name: targetPackageJson.name,
version: targetPackageJson.version,
license: targetPackageJson.license,
modular: targetPackageJson.modular,
dependencies: targetPackageJson.dependencies,
},
{ spaces: 2 },
);

printFileSizesAfterBuild(assets, previousFileSizes);

printHostingInstructions(
Expand Down
83 changes: 83 additions & 0 deletions packages/modular-scripts/src/utils/getPackageDependencies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import * as path from 'path';
import * as fs from 'fs-extra';
import { Project } from 'ts-morph';
import type { CoreProperties } from '@schemastore/package';
import getModularRoot from './getModularRoot';
import getLocation from './getLocation';
import getWorkspaceInfo from './getWorkspaceInfo';

type DependencyManifest = NonNullable<CoreProperties['dependencies']>;

const npmPackageMatcher =
/^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*/;

/* Get dependencies from import / require declarations, since they could be hoisted to the root workspace. Exclude test files. */
function getDependenciesFromSource(workspaceLocation: string) {
const project = new Project();
Comment thread
LukeSheard marked this conversation as resolved.
project.addSourceFilesAtPaths(
path.join(workspaceLocation, 'src/**/!(*.test){.d.ts,.ts,.js,.jsx,.tsx}'),
);

const dependencySet = new Set(
project
.getSourceFiles()
.flatMap((sourceFile) =>
sourceFile
.getImportDeclarations()
.map(
(declaration) =>
npmPackageMatcher.exec(
declaration.getModuleSpecifierValue(),
)?.[0],
),
)
.filter(Boolean),
) as Set<string>;

return Array.from(dependencySet);
}

export async function getPackageDependencies(
target: string,
): Promise<DependencyManifest> {
/* This function is based on the assumption that nested package are not supported, so dependencies can be either declared in the
* target's package.json or hoisted up to the workspace root.
*/
const targetLocation = await getLocation(target);
const workspaceInfo = getWorkspaceInfo();

const rootPackageJsonDependencies =
(
fs.readJSONSync(
path.join(getModularRoot(), 'package.json'),
) as CoreProperties
).dependencies || {};

const targetPackageJsonDependencies =
(
fs.readJSONSync(
path.join(targetLocation, 'package.json'),
) as CoreProperties
).dependencies || {};

/* Get regular dependencies from package.json (regular) or root package.json (hoisted)
* Exclude workspace dependencies. Error if a dependency is imported in the source code
* but not specified in any of the package.jsons
*/
const manifest = getDependenciesFromSource(targetLocation)
.filter((depName) => !(depName in workspaceInfo))
.reduce<DependencyManifest>((manifest, depName) => {
const depVersion =
targetPackageJsonDependencies[depName] ??
rootPackageJsonDependencies[depName];
if (!depVersion) {
throw new Error(
`Package ${depName} imported in ${target} source but not found in package dependencies or hoisted dependencies`,
);
}
manifest[depName] = depVersion;
return manifest;
}, {});

return manifest;
}
44 changes: 23 additions & 21 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2206,12 +2206,12 @@
resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad"
integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==

"@ts-morph/common@~0.10.1":
version "0.10.1"
resolved "https://registry.yarnpkg.com/@ts-morph/common/-/common-0.10.1.tgz#be15b9ab13a32bbc1f6a6bd7dc056b2247b272eb"
integrity sha512-rKN/VtZUUlW4M+6vjLFSaFc1Z9sK+1hh0832ucPtPkXqOw/mSWE80Lau4z2zTPNTqtxAjfZbvKpQcEwJy0KIEg==
"@ts-morph/common@~0.12.2":
version "0.12.2"
resolved "https://registry.yarnpkg.com/@ts-morph/common/-/common-0.12.2.tgz#61d07a47d622d231e833c44471ab306faaa41aed"
integrity sha512-m5KjptpIf1K0t0QL38uE+ol1n+aNn9MgRq++G3Zym1FlqfN+rThsXlp3cAgib14pIeXF7jk3UtJQOviwawFyYg==
dependencies:
fast-glob "^3.2.5"
fast-glob "^3.2.7"
minimatch "^3.0.4"
mkdirp "^1.0.4"
path-browserify "^1.0.1"
Expand Down Expand Up @@ -4323,10 +4323,12 @@ coa@^2.0.2:
chalk "^2.4.1"
q "^1.1.2"

code-block-writer@^10.1.1:
version "10.1.1"
resolved "https://registry.yarnpkg.com/code-block-writer/-/code-block-writer-10.1.1.tgz#ad5684ed4bfb2b0783c8b131281ae84ee640a42f"
integrity sha512-67ueh2IRGst/51p0n6FvPrnRjAGHY5F8xdjkgrYE7DDzpJe6qA07RYQ9VcoUeo5ATOjSOiWpSL3SWBRRbempMw==
code-block-writer@^11.0.0:
version "11.0.0"
resolved "https://registry.yarnpkg.com/code-block-writer/-/code-block-writer-11.0.0.tgz#5956fb186617f6740e2c3257757fea79315dd7d4"
integrity sha512-GEqWvEWWsOvER+g9keO4ohFoD3ymwyCnqY3hoTr7GZipYFwEhMHJw+TtV0rfgRhNImM6QWZGO2XYjlJVyYT62w==
dependencies:
tslib "2.3.1"

collect-v8-coverage@^1.0.0:
version "1.0.1"
Expand Down Expand Up @@ -6536,7 +6538,7 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==

fast-glob@^3.1.1, fast-glob@^3.2.5, fast-glob@^3.2.9:
fast-glob@^3.1.1, fast-glob@^3.2.7, fast-glob@^3.2.9:
version "3.2.11"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.11.tgz#a1172ad95ceb8a16e20caa5c5e56480e5129c1d9"
integrity sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==
Expand Down Expand Up @@ -12717,13 +12719,13 @@ ts-jest@26.5.6:
semver "7.x"
yargs-parser "20.x"

ts-morph@^11.0.3:
version "11.0.3"
resolved "https://registry.yarnpkg.com/ts-morph/-/ts-morph-11.0.3.tgz#01a92b3c2b5a48ccdf318ec90864229b8061d056"
integrity sha512-ymuPkndv9rzqTLiHWMkVrFXWcN4nBiBGhRP/kTC9F5amAAl7BNLfyrsTzMD1o9A0zishKoF1KQT/0yyFhJnPgA==
ts-morph@^13.0.2:
version "13.0.2"
resolved "https://registry.yarnpkg.com/ts-morph/-/ts-morph-13.0.2.tgz#55546023493ef82389d9e4f28848a556c784bac4"
integrity sha512-SjeeHaRf/mFsNeR3KTJnx39JyEOzT4e+DX28gQx5zjzEOuFs2eGrqeN2PLKs/+AibSxPmzV7RD8nJVKmFJqtLA==
dependencies:
"@ts-morph/common" "~0.10.1"
code-block-writer "^10.1.1"
"@ts-morph/common" "~0.12.2"
code-block-writer "^11.0.0"

ts-node@10.4.0:
version "10.4.0"
Expand Down Expand Up @@ -12758,16 +12760,16 @@ tsconfig-paths@^3.11.0:
minimist "^1.2.0"
strip-bom "^3.0.0"

tslib@2.3.1, tslib@^2.0.3, tslib@^2.1.0:
version "2.3.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"
integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==

tslib@^1.8.1, tslib@^1.9.3:
version "1.14.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==

tslib@^2.0.3, tslib@^2.1.0:
version "2.3.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"
integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==

tsutils@^3.21.0:
version "3.21.0"
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"
Expand Down